Datagrunt 4.5.7: Rust Formatting Parity, Ruff 0.16 Compatibility, and CI Supply-Chain Hardening

July 30, 2026by Martin Graham

Datagrunt 4.5.7 is a toolchain-integrity release. There is no new CSV or PDF behavior, no API surface change, and nothing to migrate — every commit here targets the machinery that builds, checks, and ships Datagrunt itself. We closed a blind spot in the Rust formatting gate, absorbed a ruff upgrade that broke a blocking CI check on code nobody had touched, and hardened the release pipeline against breaking changes arriving from GitHub Actions and Dependabot. If 4.5.5 and 4.5.6 hardened what ships, 4.5.7 hardens how it ships.


Closing the Rust Formatting Gap

Datagrunt’s CI gated Python formatting (ruff format --check .) and Rust lints (cargo clippy --all-targets -- -D warnings) as blocking checks. It never gated Rust formatting. Nothing was running cargo fmt --check, so drift accumulated silently. Issue #310 first flagged four unformatted call sites in delimiter.rs and dialect.rs; running cargo fmt across the whole crate normalized seven files in total: delimiter.rs, dialect.rs, io.rs, lib.rs, normalize.rs, rows.rs, and the PyO3 binding layer’s lib.rs.

This matters more for Datagrunt’s Rust crate than for most codebases: it carries the parity-critical delimiter and dialect-sniffing logic that has to stay diffable against the pure-Python oracle. Unformatted noise in a diff makes it harder to tell whether a change altered behavior or just rewrapped a line.

PR #311 ran the reformat — line-wrapping only, with one incidental non-semantic change: rustfmt alphabetizes pub mod declarations, so lib.rs now declares delimiter before dialect. Then it added the missing gate, right beside the existing clippy step:

      - name: cargo clippy (blocking)
        working-directory: rust
        env:
          CARGO_NET_RETRY: "10"
        run: cargo clippy --all-targets -- -D warnings

      # The Rust counterpart to the `ruff format --check` gate below. rustfmt
      # needs no dependency resolution, so no CARGO_NET_RETRY here.
      - name: cargo fmt check (blocking)
        working-directory: rust
        run: cargo fmt --check
  graph LR
    PR[Pull Request] --> RuffFmt[ruff format --check]
    RuffFmt --> Clippy[cargo clippy -D warnings]
    Clippy --> CargoFmt[cargo fmt --check — NEW]
    CargoFmt --> PyTests[pytest: Rust + Python backends]
    PyTests --> Parity[differential parity suite]
    Parity --> Merge[Merge to main]

    style CargoFmt fill:#4CAF50,stroke:#388E3C,color:#fff

Every gate stayed green after the reformat:

Gate Result
cargo fmt --check clean
cargo clippy --all-targets -- -D warnings clean
cargo test 58 passed
pytest tests/ -q (Rust backend) 1,429 passed
DATAGRUNT_DISABLE_RUST=1 pytest tests/ -q (Python oracle) 1,429 passed
pytest tests/parity/ -q 622 passed
ruff format --check . clean, 135 files

Closes #310.


When the Formatter Breaks Code Nobody Touched

That same day, main saw a second CI regression — this one with no source changes involved at all. ruff format --check ., a blocking gate, started failing on completely untouched code.

The cause was upstream: ruff 0.16 began formatting Python code blocks embedded in Markdown fences. Datagrunt’s README.md deliberately aligns trailing comments across its example lines for readability:

reader_polars = CSVReader(csv_file, engine='polars')    # String path - fast DataFrame ops
reader_duckdb = CSVReader(csv_path, engine='duckdb')    # Path object - best for SQL queries

ruff 0.16 wanted to collapse that alignment and normalize the quote style. pyproject.toml pinned ruff>=0.15.17 with no upper bound, and CI installs fresh on every run, so it floated straight to the newest release:

ruff==0.15.17  ->  135 files already formatted
ruff==0.16.1   ->  1 file would be reformatted, 141 files already formatted

PR #313 fixed both the symptom and the exposure. First, scope the formatter to source:

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
# ruff 0.16 formats Python inside Markdown fences. Docs are hand-authored:
# the README aligns trailing comments across example lines for readability,
# which the formatter collapses. The gate exists to keep *source* consistent,
# so it stops at source. See #312.
exclude = ["*.md"]

Second, bound the dev dependency itself: ruff>=0.15.17,<0.17. A formatter’s output is a moving target by design — left unbounded in a blocking gate, it’s a standing outage waiting for the next release. That’s a deliberate asymmetry with how Datagrunt treats its own runtime dependencies: library floors stay unbounded for downstream consumers (more on that below), but dev tooling gets pinned, because nobody downstream depends on our formatter’s opinions. Verified clean at 135 files with README.md byte-identical — no docs churn, no logic touched. Closes #312.


Supply-Chain Integrity: Pinned Action Bumps

The remainder of 4.5.7 is about the dependencies behind Datagrunt’s dependencies — two routine, pinned-SHA Action bumps that Dependabot proposed and CI verified without incident:

  • actions/setup-python: 5.6.0 → 7.0.0 (#297, used in build.yml and bump-version.yml)
  • actions/upload-artifact: 4.6.2 → 7.0.1 (#308, wheel and sdist uploads in build.yml)

Both are semver-major bumps, and both flowed through normally because the Dependabot floor-raise policy introduced alongside them is scoped to the pip ecosystem alone — GitHub Actions and Cargo updates were never part of the problem it fixed. That policy, and the setup-uv v9 regression that broke the release-blog workflow, are covered in the 4.5.6 release notes; both landed within a minute of the 4.5.6 version bump and so fall inside this release’s commit range, but they were documented there first.


Keeping the Docs Honest

Two small documentation fixes round out the release. CLAUDE.md’s architecture map named DocumentAssembler.extract_page as the PDF per-page entry point — that method doesn’t exist on the assembler. The real entry point is DocumentAssembler.parse_page (pdfcomponents.py:51); extract_page is the name of the backend method (pymupdf, pdfium, OCR) that parse_page calls once per page. Both names are now spelled out explicitly so the two can’t be mistaken for each other.

Separately, the PyPI version badge in README.md was stuck showing 4.5.4 as the latest version. shields.io serves badges with s-maxage=10800, and GitHub’s Camo image proxy held a copy cached from when 4.5.4 was current; a new query parameter on the badge URL gave both caching layers a fresh key without changing how the badge renders.


Upgrading to v4.5.7

This release changes no runtime behavior — upgrading is a formality, not a migration:

# Upgrade via uv
uv pip install --upgrade datagrunt

# Verify your installation
python -c "import datagrunt; print(datagrunt.__version__)"
# Output: 4.5.7

The value of 4.5.7 is entirely upstream of the package you install: a cleaner Rust diff history, a CI pipeline that can’t be broken by a formatter’s own release cadence, and a dependency graph that only moves when something actually requires it to. Read more on our CSV Engines & Rust Acceleration page or browse the full Datagrunt Documentation Index.