[package] name = "rvgsrust-xlsxwriter" version = "0.3.5" edition = "2021" rust-version = "1.88" license = "MIT" description = "Python bindings for rust_xlsxwriter -- a Rust-powered XLSX writer" repository = "https://github.com/Godse4ever/rvgsrust-xlsxwriter" [lib] name = "rvgsrust_xlsxwriter" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"] } # Same version rust_xlsxwriter itself already pulls in (see its own # Cargo.toml), so this resolves to the one already in the dependency # tree rather than pulling a second copy. Used only for the row-height # exact-value patch below -- reads the just-written xlsx back out of # memory, rewrites a handful of `ht="..."` attributes, re-zips. Never # touches any entry this crate isn't specifically patching (those are # copied verbatim via raw_copy_file(), unchanged compression and all). zip = { version = "8.3", default-features = false, features = ["deflate"] } # rust_xlsxwriter 0.99.0 (upgraded from 0.98.2 -- breaking rename # XlsxError::TableNameReused -> XlsxError::NameReused does not affect this # binding: src/lib.rs only pattern-matches XlsxError::IoError and falls # through everything else to a generic .to_string() conversion. 0.99.0 also # adds stricter validation that table names and defined names must be # globally unique to each other, and fixes quote_sheet_name() / defined-name # splitting for quoted sheet names -- see upstream CHANGELOG for details. # (See below for what changed going from 0.96 to 0.98.2, and why 0.97.x was # never installed as an intermediate step), with: # - zlib: compression backend # - zmij: ~10% faster numeric-write backend (drop-in, no API change) # - constant_memory: enables Workbook.add_worksheet(constant_memory=True) # in src/lib.rs, which streams rows to a temp file instead of # buffering the whole sheet. Restriction (confirmed from # rust_xlsxwriter's own src/performance.rs): rows must be written in # non-decreasing order. rust_xlsxwriter itself does NOT raise a clean # error if that's violated (confirmed from worksheet.rs's # check_dimensions/store_string -- it silently proceeds rather than # returning an XlsxError, which would produce a corrupt/incomplete # .xlsx with no signal to the caller), so this binding layer enforces # it itself: every Worksheet method that writes cells checks and # tracks a per-worksheet row high-water mark (Worksheet:: # check_row_order()/check_row_order_range() in src/lib.rs) and raises # a clear ValueError instead of letting a backward write through. # write_records()/write_dataframe() both already write row-by-row in # increasing order, so they're unaffected by this in normal use; the # check exists for direct write()/write_row()/merge_range()/etc calls # that might not maintain that invariant on their own. # # 0.96 -> 0.98.2 upgrade notes (read from the real upstream CHANGELOG.md # at each version tag, not taken on faith from any secondhand summary): # # - MSRV: 0.97.0 bumped its own zip dependency to v8.3, which requires # rustc 1.88.0. This is a real floor from that specific release, not # an estimate -- rust-version above is bumped to match. 0.97.x itself # was never installed as an intermediate step; this Cargo.toml jumped # straight from 0.96 to 0.98.2, so 0.97.0's MSRV bump is inherited # but 0.97.0's own code was never built against by this project. # # - Bug relevant to this project, fixed in 0.97.1: calling # Worksheet::set_tab_color() and Worksheet::set_print_fit_to_pages() # on the same worksheet produced an invalid Excel file due to an XML # element ordering issue -- both are exposed here (set_tab_color(), # set_print_fit_to_pages()) and nothing in this project's own test # suite exercised that combination before this upgrade, so this was # a real, previously-unverified risk for any caller using both. Now # covered by a regression test. # # - Bug relevant to this project, fixed in 0.98.1: sparklines and a # conditional-format data bar on the same worksheet caused an XML # error. Both features are exposed here and, same as above, no test # combined them before this upgrade. Now covered by a regression # test. # # - Fixed in 0.98.2, relevant to this project's exposed API: # * Worksheet::define_name() with an empty sheet or variable name # created an invalid Excel file rather than erroring. define_name() # is exposed here with no validation of its own, passing the # caller's string straight through -- now covered by a regression # test expecting a clean error instead. # * A truncated PNG/JPEG image, or a BMP with a height of i32::MIN, # caused a panic while reading image dimensions. insert_image() # here takes an arbitrary caller-supplied file path straight into # rust_xlsxwriter::Image::new(). Because this crate builds with # panic = "unwind" (see the [profile.release] comment below), a # pre-0.98.2 panic here would have been caught by PyO3 at the FFI # boundary and surfaced as a Python exception rather than crashing # the process -- so this was never a process-crash risk for # callers of this binding, just an opaque PanicException instead # of the clean ValueError xlsx_err_to_pyerr() already produces for # every other XlsxError. Now covered by a regression test # confirming the clean-error path. # # - `ryu` feature: not enabled, and this upgrade doesn't change that. # 0.95.0 already deprecated `ryu` in favour of `zmij` (a faster # drop-in replacement by the same author, same interface), and this # project already has `zmij` enabled above. Nothing to reconsider. # # - `enhanced_autofit` feature: considered, not enabled. It improves # Worksheet::autofit() accuracy for formatted numbers/dates (e.g. # "$1,000,000.00" sizing correctly instead of using the unformatted # width) via an additional `ssfmt` dependency. autofit() is exposed # here, so this would be a real, user-visible quality improvement -- # but it's a pure enhancement, not a bug fix or something this # project's users have reported an issue with, and it adds a new # transitive dependency. Left as a candidate for its own follow-up # rather than bundled into a version-bump PR. # # - `rust_decimal` feature: considered, not enabled. It would let # Worksheet::write() accept a rust_decimal::Decimal directly. This # project's classify() function (src/lib.rs) has no handling for # Python's decimal.Decimal today -- such a value currently falls # through to classify()'s str() fallback, becoming a text cell, not a # numeric one. Enabling the Cargo feature alone would do nothing # without also adding Decimal detection and conversion on the Python # value side, which is new work beyond a version bump. Worth a # MISSING.md entry of its own rather than folding into this upgrade. # # MSRV history: rust-version was 1.85, not 1.83, before this upgrade. # Verified empirically in CI at the time, not inferred: with no # Cargo.lock committed, transitive dependencies resolved to # latest-compatible, and indexmap 2.14.0 (pulled in via the arrow # stack) declares edition2024. Cargo 1.83 cannot parse that manifest at # all ("feature `edition2024` is required"), so the build failed during # resolution rather than compilation. edition2024 was stabilised in # Rust 1.85, which was therefore the real floor at that time -- now # superseded by the 1.88.0 floor above, which comes directly from # rust_xlsxwriter 0.97.0's own zip bump rather than from a transitive # dependency this project doesn't control. # # Because dependencies float, this floor can rise again on a transitive # bump. The msrv job in CI exists to make that visible rather than # letting the declared value quietly become fiction, which is what had # already happened once before with 1.83. # # Toolchain note: this exact pin could not be compiled in the sandbox # this was developed in (rustc 1.75, apt-installed) -- confirmed via # actual build attempts, not assumption. Every API signature this # project calls against 0.98.2 needs the same direct diff-against-source # verification the 0.96 pin got; that verification could not be run # locally for the same toolchain reason, so this upgrade is validated # by CI (which has the real toolchain), not by a local build. rust_xlsxwriter = { version = "0.99.0", features = ["zlib", "zmij", "constant_memory"] } # Native Arrow zero-copy DataFrame support (write_dataframe()). Reads # via the Arrow PyCapsule Interface (__arrow_c_stream__) -- no per-cell # Python object extraction. Supports pyarrow.Table, pandas.DataFrame # (2.x+, exposes the capsule interface directly), and polars.DataFrame # (via .to_arrow()). Deliberately not using arrow's own "pyarrow" # feature, which requires pyo3 ^0.23 and would conflict with the pyo3 # 0.22 pinned above -- reads the PyCapsule directly instead via pyo3's # own PyCapsule type. See src/lib.rs's record_batches_from_arrow(). arrow = { version = "54.0", features = ["ffi"] } [profile.release] # Aggressive optimization for maximum performance. lto = true codegen-units = 1 opt-level = 3 strip = true # target-cpu = "native" is intentionally NOT set in the committed Cargo.toml. # It would make manylinux wheels fail on machines without the same CPU features # as the build machine. For local benchmarks and source installs where you want # maximum performance on your own hardware, you can override it: # RUSTFLAGS="-C target-cpu=native" maturin develop --release # NOT setting panic = "abort" here: for a PyO3 extension module this is # a reliability problem, not a pure perf win. With the default # panic = "unwind", a Rust panic (e.g. an unexpected unwrap() failure # anywhere in this crate or a dependency) gets caught by PyO3 at the # FFI boundary and surfaces as a normal, recoverable Python exception. # With panic = "abort", the same panic takes down the entire Python # process -- there's no way for calling code to catch it, log it, or # continue. The binary-size/panic-speed benefit isn't worth trading # away Python's normal exception-handling model for every caller of # this library. # Clippy lint configuration. These are configured via Cargo's [lints] table # rather than via #![allow(...)] in lib.rs because the #[pymethods] and # #[pyclass] proc-macros expand into wrapper code that doesn't inherit inner # attributes from the source file -- Cargo-level lint config propagates # through the entire compilation including macro-expanded items. # # - useless_conversion: pyo3's #[pymethods] macro generates .into() calls on # PyResult return types as part of its expansion. Clippy sees them as # useless (PyResult -> PyResult) and flags every method returning PyResult. # This is a well-known pyo3+clippy interaction, not a bug in our code. # # - too_many_arguments: PyO3 method wrappers necessarily mirror the underlying # rust_xlsxwriter signatures, several of which take 8+ params (write_records, # write_datetime, write_url with text/tip, merge_range, etc.). Splitting # these into builder-style intermediate structs just to satisfy the linter # would make the Python-facing API worse. #[pyo3(signature = ...)] already # enforces named-argument use from Python. # # - new_without_default: All #[pyclass] structs have `fn new() -> Self` for # construction from Rust. Adding Default impls is redundant boilerplate for # types whose only intended construction path is either Python (via #[new]) # or the internal fn new(). [lints.clippy] useless_conversion = "allow" too_many_arguments = "allow" new_without_default = "allow"