Skip to content

fix: the generated ruff config runs the rules it selects - #8

Merged
AlexeyShalaev merged 1 commit into
masterfrom
fix/ruff-config-runs-what-it-selects
Sep 7, 2026
Merged

fix: the generated ruff config runs the rules it selects#8
AlexeyShalaev merged 1 commit into
masterfrom
fix/ruff-config-runs-what-it-selects

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Contributor

The problem

[tool.ruff.lint] selected DTZ and ANN and then ignored both families. Generating a project and dropping a file into the package shows what that costs:

def stamp():
    return datetime.datetime.now()


def utc_stamp():
    return datetime.datetime.utcnow()


def from_epoch(seconds):
    return datetime.datetime.fromtimestamp(seconds)

make check before this change: All checks passed!. After it: seven findings — DTZ005, DTZ003, DTZ006, three ANN202, one ANN001. The select list was making a promise the linter did not keep, and every library generated from the template carries the same two lines.

The decision

DTZ comes on, not out of select. It is the family nothing else covers, and it catches naive now(), utcnow() and naive fromtimestamp in an org whose libraries compute deadlines, retention windows, partition boundaries and idempotency TTLs from timestamps.

ANN comes on too. mypy's disallow_untyped_defs already enforces annotations for the package, so ANN adds nothing there — but files = ["<package>"] means mypy never reads tests/, and ruff is the only thing that can hold that line where CONTRIBUTING.md already claims it ("type hints on all functions and methods, including tests").

The entries that only ever meant except in tests now say so:

ignore = ["TRY003", "ANN401", "RUF012", "S104", "ANN204", "N802", "PERF401", "SIM105", "S607"]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "S105", "S106"]

S104 stays global — servicewright binds 0.0.0.0 in library code in four places, so scoping it to tests would be a lie in the other direction. TRY003, RUF012, N802, PERF401 and SIM105 all fire in library code too. ANN401 and ANN204 stay and finally mean something.

What the org's libraries say about it

Measured before writing anything, with ruff 0.16.6 --isolated over deadline-budget, clientwright, servicewright and sqlalchemy-foundation-kit (read-only; nothing changed in those repositories):

DTZ ANN in library code new findings under this config
deadline-budget clean clean none
clientwright clean clean 5 × S101, 1 × ANN201 in tests
servicewright clean clean 1 × ANN201 in tests
sqlalchemy-foundation-kit clean clean 13 × ANN001/ANN202 in tests

DTZ reports nothing anywhere — library code, tests, scripts/, examples/ — and it is a real pass, not a vacuous one: clientwright/core/engine/base.py:32 already computes (when - datetime.datetime.now(datetime.UTC)).total_seconds() and sqlalchemy_foundation_kit/base/models.py:30 maps datetime.datetime to TIMESTAMP(timezone=True). Nothing to file against those repositories.

The five S101 in clientwright are all type narrowing rather than validation (assert isinstance(response, requests.Response), assert response is not None). That is a decision for that repository when it takes the update, not something to pre-empt here.

What I rejected

  • Dropping DTZ and ANN from select. It would make the file honest and throw away the one family nothing else covers.
  • Ignoring ANN in tests/, which the issue suggests. I measured it instead of assuming: fifteen findings across three repositories, against a contributing guide that already asks for annotations in tests. Fifteen annotations is a fair price for the config and the guide agreeing.
  • Moving more of the ignore list into the tests block. S104, TRY003, RUF012, N802, PERF401 and SIM105 all fire in library code in at least one library, so they belong where they are.

The test

The failure mode is a config that claims a rule it does not run, and the generated gate stays green either way — which is how this survived. CI now feeds the rendered project a naive datetime.now() and an unannotated def and fails if ruff stays quiet, plus a bare assert in tests/ that has to stay accepted so the per-file-ignores block is checked too.

Against the pre-change config that step fails on the first probe (::error::DTZ005 never fired); against this one it passes.

Verification

Rendered widget-kit from this branch with copier copy --defaults:

uv run ruff check .          All checks passed!
uv run ruff format --check . 8 files already formatted
uv run mypy widget_kit       Success: no issues found in 2 source files
pytest --cov ...             1 passed, coverage 100.00%
uv build                     Successfully built widget_kit-0.0.0.tar.gz and the wheel

Closes #7

`DTZ` and `ANN` sat in `select` and in `ignore` at once, so a generated
project shipped a lint file that promised three timezone rules and the
whole annotation family and ran neither. A naive `datetime.now()`, a
`utcnow()` and an unannotated def passed `make check` without a word.

`DTZ` is the family nothing else covers, and this org computes deadlines,
retention windows and TTLs from timestamps, so it comes on rather than out
of `select`. Annotations mypy already enforces for the package; keeping
`ANN` on adds the coverage mypy has no view of, since it reads the package
and never `tests/`.

The entries that only ever meant "except in tests" now say so: `S101`,
`S105` and `S106` move into a `per-file-ignores` block for `tests/**/*.py`,
which leaves library code held to them. `S104` stays global -- library code
does bind all interfaces. `ANN401` and `ANN204` stay and finally mean
something.

Measured against the org's libraries before changing anything: `DTZ` reports
nothing in `deadline-budget`, `clientwright`, `servicewright` or
`sqlalchemy-foundation-kit`, in library code or tests. The whole cost of the
new set is five narrowing asserts in `clientwright` and fifteen missing test
annotations across three repositories.

CI gets the check that was missing: a rule selected and then ignored looks
exactly like a rule that works, so the rendered project is now fed the
mistakes those rules exist to catch and has to report them.

Closes #7
@AlexeyShalaev
AlexeyShalaev merged commit a1a1e7d into master Sep 7, 2026
2 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the fix/ruff-config-runs-what-it-selects branch September 7, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The generated ruff config selects DTZ and ANN and then ignores both families

1 participant