Skip to content

Commit a1a1e7d

Browse files
fix: the generated ruff config runs the rules it selects (#8)
`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
1 parent ffb60a1 commit a1a1e7d

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ jobs:
3838
make test-unit
3939
uv build
4040
41+
# A rule that is selected and then ignored looks exactly like a rule that
42+
# works: the gate above stays green either way. Hand the generated project
43+
# the mistakes the selected rules exist to catch and make it say so, and an
44+
# assert in tests/ that has to stay accepted.
45+
- name: The generated config runs the rules it selects
46+
working-directory: ${{ runner.temp }}/demo-lib
47+
run: |
48+
cat > demo_lib/probe.py <<'PY'
49+
import datetime
50+
51+
52+
def stamp(offset):
53+
return datetime.datetime.now() + offset
54+
PY
55+
cat > tests/unit/test_probe.py <<'PY'
56+
def test__assert__still_reads_as_a_test() -> None:
57+
assert True
58+
PY
59+
report=$(uv run ruff check --output-format concise demo_lib/probe.py || true)
60+
echo "$report"
61+
for rule in DTZ005 ANN201 ANN001; do
62+
case "$report" in
63+
*"$rule"*) ;;
64+
*) echo "::error::$rule never fired -- the config selects the rule and then ignores it"; exit 1 ;;
65+
esac
66+
done
67+
uv run ruff check tests/unit/test_probe.py
68+
rm demo_lib/probe.py tests/unit/test_probe.py
69+
4170
all-checks-passed:
4271
name: All checks passed
4372
if: always()

template/CONTRIBUTING.md.jinja

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ make test # full suite with 90% coverage threshold
2626
- **Docstrings** on public API only — Google style
2727
- **Line length** — 120 characters (ruff enforced)
2828
- **Quotes** — double quotes (ruff enforced)
29+
- **Timezone-aware datetimes** — `datetime.now(tz=...)`, never `utcnow()` (ruff `DTZ`)
30+
- **`assert` in tests only** — library code raises instead (ruff `S101`)
2931
- **No comments** unless the *why* is non-obvious
3032

3133
## Commit messages

template/pyproject.toml.jinja

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,18 @@ target-version = "py{{ python_min_version | replace('.', '') }}"
8686
extend-exclude = ["*.md"]
8787

8888
[tool.ruff.lint]
89+
# Everything selected here runs. An entry below names one rule the house style
90+
# disagrees with -- never a whole family that `select` has just asked for, which
91+
# reads as a promise the linter does not keep.
8992
select = ["F", "E", "W", "I", "B", "N", "S", "C4", "DTZ", "SIM", "TRY", "PERF", "RUF", "UP", "ANN", "T20", "PTH", "PLC", "PLE", "PLW"]
90-
ignore = ["TRY003", "S101", "ANN401", "RUF012", "S104", "S105", "S106", "ANN204", "DTZ", "ANN", "N802", "PERF401", "SIM105", "S607"]
93+
ignore = ["TRY003", "ANN401", "RUF012", "S104", "ANN204", "N802", "PERF401", "SIM105", "S607"]
94+
95+
# Tests are held to a different standard from the library they exercise:
96+
# `assert` is what a test is made of, and a credential in a fixture is a literal
97+
# rather than a leak. Annotations are not on this list -- mypy reads
98+
# {{ package_name }}/ and never tests/, so ruff is the only thing holding that line here.
99+
[tool.ruff.lint.per-file-ignores]
100+
"tests/**/*.py" = ["S101", "S105", "S106"]
91101

92102
[tool.ruff.format]
93103
quote-style = "double"

0 commit comments

Comments
 (0)