sarj-python-lint 0.20.0__tar.gz → 0.23.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sarj_python_lint-0.23.0/PKG-INFO +298 -0
- sarj_python_lint-0.23.0/README.md +280 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/pyproject.toml +2 -1
- sarj_python_lint-0.23.0/src/sarj_python_lint/_ratchet_cli.py +185 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/ratchet.py +423 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/_pytest.py +61 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_registry.py +25 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/_suppression_comments.py +78 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +150 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +3 -64
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +118 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +192 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/no_stdlib_logging.py +195 -0
- sarj_python_lint-0.23.0/src/sarj_python_lint/rules/no_tautological_expect.py +374 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/zero_assertion_test.py +2 -13
- sarj_python_lint-0.20.0/PKG-INFO +0 -125
- sarj_python_lint-0.20.0/README.md +0 -107
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/.gitignore +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.20.0 → sarj_python_lint-0.23.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sarj-python-lint
|
|
3
|
+
Version: 0.23.0
|
|
4
|
+
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
|
+
Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
|
|
6
|
+
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
7
|
+
Project-URL: Issues, https://github.com/sarj-ai/standards/issues
|
|
8
|
+
Author: sarj-ai
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# sarj-python-lint
|
|
20
|
+
|
|
21
|
+
Custom Python lint rules via stdlib `ast`. Designed for pre-commit. For SQL rules see [`sarj-sql-lint`](../sql/).
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install sarj-python-lint
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Pre-commit
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- repo: https://github.com/sarj-ai/standards
|
|
31
|
+
rev: python-v0.2.0
|
|
32
|
+
hooks:
|
|
33
|
+
- id: sarj-no-sequential-await
|
|
34
|
+
- id: sarj-inefficient-string-concat-in-loop
|
|
35
|
+
- id: sarj-prefer-str-enum
|
|
36
|
+
- id: sarj-no-fat-try-blocks
|
|
37
|
+
- id: sarj-pydantic-at-boundaries
|
|
38
|
+
- id: sarj-prefer-class-row
|
|
39
|
+
- id: sarj-prefer-timedelta-for-durations
|
|
40
|
+
- id: sarj-prefer-struct-over-namedtuple
|
|
41
|
+
- id: sarj-no-comment-cruft
|
|
42
|
+
- id: sarj-no-fstring-in-log
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Test-quality rules (0.15.0)
|
|
46
|
+
|
|
47
|
+
Mined from an AST audit of ~7,500 test functions across two production repos.
|
|
48
|
+
Every one is scoped to test files and carries the false-positive guard that made
|
|
49
|
+
it shippable; the module docstring for each records the population it was
|
|
50
|
+
measured against.
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- id: sarj-mock-without-spec # SARJ040
|
|
54
|
+
- id: sarj-test-loops-over-literal-cases # SARJ041
|
|
55
|
+
- id: sarj-parametrize-case-needs-id # SARJ042
|
|
56
|
+
- id: sarj-zero-assertion-test # SARJ043
|
|
57
|
+
- id: sarj-fixture-returns-bare-tuple # SARJ044
|
|
58
|
+
- id: sarj-kwarg-heavy-construction-in-test # SARJ045
|
|
59
|
+
- id: sarj-xfail-requires-strict # SARJ046
|
|
60
|
+
- id: sarj-sleep-with-computed-arg-in-test # SARJ047
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Private access, first-party only (0.19.0)
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- id: sarj-no-first-party-private-import # SARJ048
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Reaching past a module's public surface is a design finding when the module is
|
|
70
|
+
ours and an unavoidable fact of life when it is not: a dependency that moves an
|
|
71
|
+
API private in a minor release leaves no edit that satisfies the lint.
|
|
72
|
+
|
|
73
|
+
`SARJ048` fires only when the module declaring the private name resolves to a
|
|
74
|
+
package inside your own project. Third-party privates are never flagged.
|
|
75
|
+
|
|
76
|
+
**It replaces ruff's `PLC2701 import-private-name`,** whose only exemption is
|
|
77
|
+
*same top-level package* — a different question, and one that cannot separate
|
|
78
|
+
`from bulbul.stores.task_store import _row_to_task` (real; export it) from
|
|
79
|
+
`from livekit.agents.inference_runner import _InferenceRunner` (no fix exists).
|
|
80
|
+
`sarj-lint-configs` ≥ 0.8.0 ships `PLC2701` in its ignore list for exactly this
|
|
81
|
+
reason; if you take that config, turn this hook on, or you lose the check
|
|
82
|
+
entirely.
|
|
83
|
+
|
|
84
|
+
Attribute access (`session._stt`) is out of scope and stays with ruff's
|
|
85
|
+
`SLF001`, which cannot make the distinction either — see the rationale in
|
|
86
|
+
`ruff.strict.toml`.
|
|
87
|
+
|
|
88
|
+
### Comment-hygiene rules (0.20.0)
|
|
89
|
+
|
|
90
|
+
From a 37,918-comment, nine-repo measurement study. All three are
|
|
91
|
+
deletion-class, so each was validated against pydantic / trio / attrs as well as
|
|
92
|
+
the maintained repos before shipping — the counts and the false-positive classes
|
|
93
|
+
each guard was built from are recorded in the rule module docstrings.
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- id: sarj-no-restated-comment # SARJ049
|
|
97
|
+
- id: sarj-redundant-docstring # SARJ050
|
|
98
|
+
- id: sarj-trailing-value-narration # SARJ051
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`redundant-docstring` finds real volume on a codebase that has never had it
|
|
102
|
+
(105 in noura-be), so the same baseline ratchet applies.
|
|
103
|
+
|
|
104
|
+
### House conventions moved out of consumer repos (0.21.0)
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- id: sarj-no-stdlib-logging # SARJ052
|
|
108
|
+
- id: sarj-no-gen-random-uuid-in-sql # SARJ053
|
|
109
|
+
- id: sarj-no-file-level-escape-hatch-noqa # SARJ054
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`SARJ052` bans importing stdlib `logging` in application code, because the
|
|
113
|
+
house logger is loguru and two logger hierarchies mean two handler chains: the
|
|
114
|
+
records written to the one nobody configured skip the JSON formatter, the
|
|
115
|
+
redaction patcher and the error reporter, and — since the stdlib root defaults
|
|
116
|
+
to WARNING — usually vanish in production while looking fine locally.
|
|
117
|
+
|
|
118
|
+
The one legitimate reason to touch stdlib logging in a loguru house is to
|
|
119
|
+
*bridge* it, and the bridge cannot be written without naming both loggers, so a
|
|
120
|
+
module importing loguru is exempt. Measured across two production repos that
|
|
121
|
+
exemption is exact: all four sites that import stdlib logging
|
|
122
|
+
(`bulbul/__init__.py`, `bulbul/configure_logging.py`, `agent/main.py`,
|
|
123
|
+
noura-be's `common/logging.py`) are bridges, all four import loguru, and no
|
|
124
|
+
other module in either repo imports stdlib logging at all. Tests, `scripts/`,
|
|
125
|
+
`notebooks/`, generated files and `if TYPE_CHECKING:` imports are also exempt.
|
|
126
|
+
|
|
127
|
+
**This is a house-convention rule, not a universal one.** A *library* should log
|
|
128
|
+
through stdlib `logging` precisely so it does not impose a sink on its callers —
|
|
129
|
+
trio's three sites are correct for trio. Enable it in applications only.
|
|
130
|
+
|
|
131
|
+
`SARJ053` flags `gen_random_uuid()` in SQL embedded in a Python string literal:
|
|
132
|
+
UUIDv4 keys scatter B-tree inserts across every leaf page, where `uuidv7()`
|
|
133
|
+
(Postgres 18) is time-ordered and appends. It is the embedded-SQL third of a
|
|
134
|
+
policy the stack already states twice — `ruff.strict.toml` bans `uuid.uuid4`,
|
|
135
|
+
and `sarj-sql-lint`'s SARJ109 `prefer-uuidv7-default` covers `.sql` migration
|
|
136
|
+
files (41 sites in bulbul, 14 in noura-be, all of them a primary-key `DEFAULT`).
|
|
137
|
+
A literal only counts when it is SQL-shaped, so prose naming the function is not
|
|
138
|
+
a finding.
|
|
139
|
+
|
|
140
|
+
`SARJ054` is SARJ038's scoped sibling. SARJ038 bans the unscoped blanket
|
|
141
|
+
(`# ruff: noqa`); this bans a *scoped* file-level exemption that names an
|
|
142
|
+
escape-hatch code — a code whose remediation `ruff.strict.toml` spells as an
|
|
143
|
+
inline `# noqa: CODE — <reason>`, which today is `TID251` alone, ruff's only
|
|
144
|
+
banned-API code. Hoisting that to the top of a file turns N reviewed per-site
|
|
145
|
+
decisions into one unreviewable one and pre-authorizes every mock added later.
|
|
146
|
+
Scoped exemptions for mechanical codes (`E501`, `F401`, `UP035`) are never
|
|
147
|
+
flagged — measured across five repos those are the entire population.
|
|
148
|
+
|
|
149
|
+
### Suppression ratchet (`sarj-ratchet`, 0.21.0)
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
- id: sarj-suppression-ratchet
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
One tool replacing the per-repo ratchet scripts. It counts every escape hatch in
|
|
156
|
+
the tree and enforces three ceilings that may only shrink:
|
|
157
|
+
|
|
158
|
+
* **per code** — `noqa:TID251` going 40 → 41 is a regression even if the total falls
|
|
159
|
+
* **per package** — one package's headroom must not finance another's debt
|
|
160
|
+
* **per file** — a global cap so new suppressions cannot pile into one hot spot;
|
|
161
|
+
pre-existing hot spots are grandfathered at their then-current counts
|
|
162
|
+
|
|
163
|
+
All four dialects are counted under distinct key prefixes, so moving a
|
|
164
|
+
suppression between spellings can never hide it: `noqa:CODE`,
|
|
165
|
+
`sarj-noqa:CODE`, `pyright:CODE`, `type-ignore:CODE` / bare `type-ignore`, plus
|
|
166
|
+
the file-level `file-noqa:CODE` / `file-noqa:<blanket>` and `file-pyright:RULE`.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
sarj-ratchet --update python/ # seed (or lock in a drop)
|
|
170
|
+
sarj-ratchet python/ # gate
|
|
171
|
+
sarj-ratchet --update --allow-increase python/ # a reviewed ceiling raise
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--update` **refuses** to raise a ceiling unless `--allow-increase` says the
|
|
175
|
+
raise was reviewed, and it drops a per-file grandfather clause as soon as the
|
|
176
|
+
file falls back under the global cap, so an allowance cannot outlive its debt.
|
|
177
|
+
|
|
178
|
+
### Two conventions that stayed pygrep
|
|
179
|
+
|
|
180
|
+
`sarj-fakes-in-shared-location` and `sarj-no-raw-connection-in-tests` ship as
|
|
181
|
+
pygrep hooks, not SARJ rules, and both need a `files:`/`exclude:` from the
|
|
182
|
+
consumer. An AST port of each was built and measured, and the boundary each
|
|
183
|
+
encodes turned out to be repo-specific rather than shared: "shared fake" flagged
|
|
184
|
+
9/9 single-use test doubles in noura-be that are idiomatic where they sit, and
|
|
185
|
+
"raw connection in a test" flagged 46 sites in bulbul of which every one is
|
|
186
|
+
already an intentional exemption (store tests asserting DB state, pool-lifecycle
|
|
187
|
+
tests, retention tests where physical deletion is the subject). SARJ036
|
|
188
|
+
`no-raw-sql-in-tests` remains the corpus-validated shared rule for raw SQL in
|
|
189
|
+
tests.
|
|
190
|
+
|
|
191
|
+
Adopting these against an existing suite is easier through the baseline ratchet
|
|
192
|
+
than as a big-bang fix — snapshot the current counts, then let them only shrink:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
sarj-python-lint check --rule mock-without-spec --update-baseline test-quality-baseline.json python/
|
|
196
|
+
sarj-python-lint check --rule mock-without-spec --baseline test-quality-baseline.json python/
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Multi-tenant scoping (0.22.0)
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
- id: sarj-no-optional-tenant-predicate # SARJ056
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`SARJ056` fires when every WHERE-fragment mentioning a tenant column
|
|
206
|
+
(`organization_id` and friends) sits inside a conditional, so the predicate
|
|
207
|
+
disappears — and the query still runs — whenever the filter is empty:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
where_conditions = []
|
|
211
|
+
if args.organization_ids: # ← optional
|
|
212
|
+
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
213
|
+
...
|
|
214
|
+
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The safe idiom seeds the list, so scoping always applies and the rule stays
|
|
218
|
+
quiet:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
conditions: list[Composable] = [SQL("organization_id = %s")]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
A function with **no** tenant predicate at all never fires — an intentionally
|
|
225
|
+
cross-tenant admin query is out of scope; only *attempted-but-optional* scoping
|
|
226
|
+
is a finding. Where a caller genuinely wants the all-tenant query, that
|
|
227
|
+
intent belongs in an explicit method (or an inline `sarj-noqa`) rather than in
|
|
228
|
+
an omitted filter.
|
|
229
|
+
|
|
230
|
+
Measured before shipping: **0 findings across 26,345 files** of pydantic, trio,
|
|
231
|
+
attrs, Airflow and Home Assistant — single-tenant codebases have no tenant
|
|
232
|
+
column, so the rule is silent by construction — and 0 in noura-be, ai, kpi-hub
|
|
233
|
+
and demo-gateway. In bulbul it finds 10 sites, all genuine fail-open
|
|
234
|
+
compositions, two of which were reachable cross-tenant reads at the time of
|
|
235
|
+
writing (`POST /v1/calls/list` and `POST /v1/calls/batch/list`, both of which
|
|
236
|
+
composed `WHERE 1=1` for a user whose `organization_id` was NULL).
|
|
237
|
+
|
|
238
|
+
### Assertions that can never fail (0.23.0)
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
- id: sarj-no-tautological-expect # SARJ057
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`SARJ057` fires when an assertion's operands are all literals, so its outcome is
|
|
245
|
+
fixed before the code runs. `SARJ043` already catches the test with *no*
|
|
246
|
+
assertion; this is the test whose assertion is decorative.
|
|
247
|
+
|
|
248
|
+
The placeholder spelling (`assert True`) is the obvious half. The expensive half
|
|
249
|
+
is the assertion whose real condition slid out of the condition slot, because it
|
|
250
|
+
was a working assertion when it was typed:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
assert { # ← braces, not parentheses
|
|
254
|
+
"referencing a non existing `via_device` " in caplog.text
|
|
255
|
+
} # one-element SET, always truthy
|
|
256
|
+
|
|
257
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
258
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
262
|
+
of a thing with itself" — measures ~95% false positives: `assert i == i`,
|
|
263
|
+
`assert x is x`, `expect(hash([o])).toEqual(hash([o]))` are reflexivity,
|
|
264
|
+
determinism and memoization tests, and for a type with custom `__eq__`/`__hash__`
|
|
265
|
+
they can genuinely fail. So an identifier, attribute or call operand is never
|
|
266
|
+
enough; both sides must be literals, and textually identical ones. `assert True`
|
|
267
|
+
as the sole statement of an `except` handler is exempt — it asserts *which branch
|
|
268
|
+
ran* — as is anything inside a pytest-benchmark test.
|
|
269
|
+
|
|
270
|
+
Measured before shipping: **4 findings across 28,608 files** — 26,346 of
|
|
271
|
+
pydantic, trio, attrs, Airflow and Home Assistant plus 2,262 first-party files
|
|
272
|
+
in bulbul, noura-be, kpi-hub, ai and demo-gateway. All 4 are true positives
|
|
273
|
+
(Home Assistant `tests/helpers/test_device_registry.py:3711` and `:3777`,
|
|
274
|
+
`tests/components/emulated_hue/test_hue_api.py:1078`, Airflow
|
|
275
|
+
`providers/apache/hdfs/.../log/test_hdfs_task_handler.py:170`); 0 false
|
|
276
|
+
positives. The two `except ...: assert True` markers that a carve-out-free
|
|
277
|
+
version does flag — `pydantic-core/tests/benchmarks/test_micro_benchmarks.py:716`
|
|
278
|
+
and `core/tests/components/mqtt/test_client.py:1353` — were verified silent.
|
|
279
|
+
|
|
280
|
+
The TypeScript half of the same rule ships as `@sarj/no-tautological-expect` in
|
|
281
|
+
`@sarj/eslint-plugin` ≥ 2.14.0; until now there was no TS counterpart at all,
|
|
282
|
+
which is how `expect(true).toBe(true); // placeholder` survived in a suite named
|
|
283
|
+
for the behaviour it was supposed to check.
|
|
284
|
+
|
|
285
|
+
## CLI
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
sarj-python-lint check --rule no-sequential-await path/to/file.py
|
|
289
|
+
sarj-python-lint list-rules
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Diagnostic format is `path:line:col: CODE message` — Ruff-compatible.
|
|
293
|
+
|
|
294
|
+
## Suppression
|
|
295
|
+
|
|
296
|
+
Inline `# sarj-noqa: SARJ00X — <reason>` on the offending line.
|
|
297
|
+
|
|
298
|
+
Each rule's source under `src/sarj_python_lint/rules/` carries its own `description` and diagnostic message.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# sarj-python-lint
|
|
2
|
+
|
|
3
|
+
Custom Python lint rules via stdlib `ast`. Designed for pre-commit. For SQL rules see [`sarj-sql-lint`](../sql/).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv tool install sarj-python-lint
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Pre-commit
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
- repo: https://github.com/sarj-ai/standards
|
|
13
|
+
rev: python-v0.2.0
|
|
14
|
+
hooks:
|
|
15
|
+
- id: sarj-no-sequential-await
|
|
16
|
+
- id: sarj-inefficient-string-concat-in-loop
|
|
17
|
+
- id: sarj-prefer-str-enum
|
|
18
|
+
- id: sarj-no-fat-try-blocks
|
|
19
|
+
- id: sarj-pydantic-at-boundaries
|
|
20
|
+
- id: sarj-prefer-class-row
|
|
21
|
+
- id: sarj-prefer-timedelta-for-durations
|
|
22
|
+
- id: sarj-prefer-struct-over-namedtuple
|
|
23
|
+
- id: sarj-no-comment-cruft
|
|
24
|
+
- id: sarj-no-fstring-in-log
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Test-quality rules (0.15.0)
|
|
28
|
+
|
|
29
|
+
Mined from an AST audit of ~7,500 test functions across two production repos.
|
|
30
|
+
Every one is scoped to test files and carries the false-positive guard that made
|
|
31
|
+
it shippable; the module docstring for each records the population it was
|
|
32
|
+
measured against.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- id: sarj-mock-without-spec # SARJ040
|
|
36
|
+
- id: sarj-test-loops-over-literal-cases # SARJ041
|
|
37
|
+
- id: sarj-parametrize-case-needs-id # SARJ042
|
|
38
|
+
- id: sarj-zero-assertion-test # SARJ043
|
|
39
|
+
- id: sarj-fixture-returns-bare-tuple # SARJ044
|
|
40
|
+
- id: sarj-kwarg-heavy-construction-in-test # SARJ045
|
|
41
|
+
- id: sarj-xfail-requires-strict # SARJ046
|
|
42
|
+
- id: sarj-sleep-with-computed-arg-in-test # SARJ047
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Private access, first-party only (0.19.0)
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
- id: sarj-no-first-party-private-import # SARJ048
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Reaching past a module's public surface is a design finding when the module is
|
|
52
|
+
ours and an unavoidable fact of life when it is not: a dependency that moves an
|
|
53
|
+
API private in a minor release leaves no edit that satisfies the lint.
|
|
54
|
+
|
|
55
|
+
`SARJ048` fires only when the module declaring the private name resolves to a
|
|
56
|
+
package inside your own project. Third-party privates are never flagged.
|
|
57
|
+
|
|
58
|
+
**It replaces ruff's `PLC2701 import-private-name`,** whose only exemption is
|
|
59
|
+
*same top-level package* — a different question, and one that cannot separate
|
|
60
|
+
`from bulbul.stores.task_store import _row_to_task` (real; export it) from
|
|
61
|
+
`from livekit.agents.inference_runner import _InferenceRunner` (no fix exists).
|
|
62
|
+
`sarj-lint-configs` ≥ 0.8.0 ships `PLC2701` in its ignore list for exactly this
|
|
63
|
+
reason; if you take that config, turn this hook on, or you lose the check
|
|
64
|
+
entirely.
|
|
65
|
+
|
|
66
|
+
Attribute access (`session._stt`) is out of scope and stays with ruff's
|
|
67
|
+
`SLF001`, which cannot make the distinction either — see the rationale in
|
|
68
|
+
`ruff.strict.toml`.
|
|
69
|
+
|
|
70
|
+
### Comment-hygiene rules (0.20.0)
|
|
71
|
+
|
|
72
|
+
From a 37,918-comment, nine-repo measurement study. All three are
|
|
73
|
+
deletion-class, so each was validated against pydantic / trio / attrs as well as
|
|
74
|
+
the maintained repos before shipping — the counts and the false-positive classes
|
|
75
|
+
each guard was built from are recorded in the rule module docstrings.
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
- id: sarj-no-restated-comment # SARJ049
|
|
79
|
+
- id: sarj-redundant-docstring # SARJ050
|
|
80
|
+
- id: sarj-trailing-value-narration # SARJ051
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`redundant-docstring` finds real volume on a codebase that has never had it
|
|
84
|
+
(105 in noura-be), so the same baseline ratchet applies.
|
|
85
|
+
|
|
86
|
+
### House conventions moved out of consumer repos (0.21.0)
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
- id: sarj-no-stdlib-logging # SARJ052
|
|
90
|
+
- id: sarj-no-gen-random-uuid-in-sql # SARJ053
|
|
91
|
+
- id: sarj-no-file-level-escape-hatch-noqa # SARJ054
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`SARJ052` bans importing stdlib `logging` in application code, because the
|
|
95
|
+
house logger is loguru and two logger hierarchies mean two handler chains: the
|
|
96
|
+
records written to the one nobody configured skip the JSON formatter, the
|
|
97
|
+
redaction patcher and the error reporter, and — since the stdlib root defaults
|
|
98
|
+
to WARNING — usually vanish in production while looking fine locally.
|
|
99
|
+
|
|
100
|
+
The one legitimate reason to touch stdlib logging in a loguru house is to
|
|
101
|
+
*bridge* it, and the bridge cannot be written without naming both loggers, so a
|
|
102
|
+
module importing loguru is exempt. Measured across two production repos that
|
|
103
|
+
exemption is exact: all four sites that import stdlib logging
|
|
104
|
+
(`bulbul/__init__.py`, `bulbul/configure_logging.py`, `agent/main.py`,
|
|
105
|
+
noura-be's `common/logging.py`) are bridges, all four import loguru, and no
|
|
106
|
+
other module in either repo imports stdlib logging at all. Tests, `scripts/`,
|
|
107
|
+
`notebooks/`, generated files and `if TYPE_CHECKING:` imports are also exempt.
|
|
108
|
+
|
|
109
|
+
**This is a house-convention rule, not a universal one.** A *library* should log
|
|
110
|
+
through stdlib `logging` precisely so it does not impose a sink on its callers —
|
|
111
|
+
trio's three sites are correct for trio. Enable it in applications only.
|
|
112
|
+
|
|
113
|
+
`SARJ053` flags `gen_random_uuid()` in SQL embedded in a Python string literal:
|
|
114
|
+
UUIDv4 keys scatter B-tree inserts across every leaf page, where `uuidv7()`
|
|
115
|
+
(Postgres 18) is time-ordered and appends. It is the embedded-SQL third of a
|
|
116
|
+
policy the stack already states twice — `ruff.strict.toml` bans `uuid.uuid4`,
|
|
117
|
+
and `sarj-sql-lint`'s SARJ109 `prefer-uuidv7-default` covers `.sql` migration
|
|
118
|
+
files (41 sites in bulbul, 14 in noura-be, all of them a primary-key `DEFAULT`).
|
|
119
|
+
A literal only counts when it is SQL-shaped, so prose naming the function is not
|
|
120
|
+
a finding.
|
|
121
|
+
|
|
122
|
+
`SARJ054` is SARJ038's scoped sibling. SARJ038 bans the unscoped blanket
|
|
123
|
+
(`# ruff: noqa`); this bans a *scoped* file-level exemption that names an
|
|
124
|
+
escape-hatch code — a code whose remediation `ruff.strict.toml` spells as an
|
|
125
|
+
inline `# noqa: CODE — <reason>`, which today is `TID251` alone, ruff's only
|
|
126
|
+
banned-API code. Hoisting that to the top of a file turns N reviewed per-site
|
|
127
|
+
decisions into one unreviewable one and pre-authorizes every mock added later.
|
|
128
|
+
Scoped exemptions for mechanical codes (`E501`, `F401`, `UP035`) are never
|
|
129
|
+
flagged — measured across five repos those are the entire population.
|
|
130
|
+
|
|
131
|
+
### Suppression ratchet (`sarj-ratchet`, 0.21.0)
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
- id: sarj-suppression-ratchet
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
One tool replacing the per-repo ratchet scripts. It counts every escape hatch in
|
|
138
|
+
the tree and enforces three ceilings that may only shrink:
|
|
139
|
+
|
|
140
|
+
* **per code** — `noqa:TID251` going 40 → 41 is a regression even if the total falls
|
|
141
|
+
* **per package** — one package's headroom must not finance another's debt
|
|
142
|
+
* **per file** — a global cap so new suppressions cannot pile into one hot spot;
|
|
143
|
+
pre-existing hot spots are grandfathered at their then-current counts
|
|
144
|
+
|
|
145
|
+
All four dialects are counted under distinct key prefixes, so moving a
|
|
146
|
+
suppression between spellings can never hide it: `noqa:CODE`,
|
|
147
|
+
`sarj-noqa:CODE`, `pyright:CODE`, `type-ignore:CODE` / bare `type-ignore`, plus
|
|
148
|
+
the file-level `file-noqa:CODE` / `file-noqa:<blanket>` and `file-pyright:RULE`.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
sarj-ratchet --update python/ # seed (or lock in a drop)
|
|
152
|
+
sarj-ratchet python/ # gate
|
|
153
|
+
sarj-ratchet --update --allow-increase python/ # a reviewed ceiling raise
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`--update` **refuses** to raise a ceiling unless `--allow-increase` says the
|
|
157
|
+
raise was reviewed, and it drops a per-file grandfather clause as soon as the
|
|
158
|
+
file falls back under the global cap, so an allowance cannot outlive its debt.
|
|
159
|
+
|
|
160
|
+
### Two conventions that stayed pygrep
|
|
161
|
+
|
|
162
|
+
`sarj-fakes-in-shared-location` and `sarj-no-raw-connection-in-tests` ship as
|
|
163
|
+
pygrep hooks, not SARJ rules, and both need a `files:`/`exclude:` from the
|
|
164
|
+
consumer. An AST port of each was built and measured, and the boundary each
|
|
165
|
+
encodes turned out to be repo-specific rather than shared: "shared fake" flagged
|
|
166
|
+
9/9 single-use test doubles in noura-be that are idiomatic where they sit, and
|
|
167
|
+
"raw connection in a test" flagged 46 sites in bulbul of which every one is
|
|
168
|
+
already an intentional exemption (store tests asserting DB state, pool-lifecycle
|
|
169
|
+
tests, retention tests where physical deletion is the subject). SARJ036
|
|
170
|
+
`no-raw-sql-in-tests` remains the corpus-validated shared rule for raw SQL in
|
|
171
|
+
tests.
|
|
172
|
+
|
|
173
|
+
Adopting these against an existing suite is easier through the baseline ratchet
|
|
174
|
+
than as a big-bang fix — snapshot the current counts, then let them only shrink:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
sarj-python-lint check --rule mock-without-spec --update-baseline test-quality-baseline.json python/
|
|
178
|
+
sarj-python-lint check --rule mock-without-spec --baseline test-quality-baseline.json python/
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Multi-tenant scoping (0.22.0)
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
- id: sarj-no-optional-tenant-predicate # SARJ056
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`SARJ056` fires when every WHERE-fragment mentioning a tenant column
|
|
188
|
+
(`organization_id` and friends) sits inside a conditional, so the predicate
|
|
189
|
+
disappears — and the query still runs — whenever the filter is empty:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
where_conditions = []
|
|
193
|
+
if args.organization_ids: # ← optional
|
|
194
|
+
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
195
|
+
...
|
|
196
|
+
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The safe idiom seeds the list, so scoping always applies and the rule stays
|
|
200
|
+
quiet:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
conditions: list[Composable] = [SQL("organization_id = %s")]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A function with **no** tenant predicate at all never fires — an intentionally
|
|
207
|
+
cross-tenant admin query is out of scope; only *attempted-but-optional* scoping
|
|
208
|
+
is a finding. Where a caller genuinely wants the all-tenant query, that
|
|
209
|
+
intent belongs in an explicit method (or an inline `sarj-noqa`) rather than in
|
|
210
|
+
an omitted filter.
|
|
211
|
+
|
|
212
|
+
Measured before shipping: **0 findings across 26,345 files** of pydantic, trio,
|
|
213
|
+
attrs, Airflow and Home Assistant — single-tenant codebases have no tenant
|
|
214
|
+
column, so the rule is silent by construction — and 0 in noura-be, ai, kpi-hub
|
|
215
|
+
and demo-gateway. In bulbul it finds 10 sites, all genuine fail-open
|
|
216
|
+
compositions, two of which were reachable cross-tenant reads at the time of
|
|
217
|
+
writing (`POST /v1/calls/list` and `POST /v1/calls/batch/list`, both of which
|
|
218
|
+
composed `WHERE 1=1` for a user whose `organization_id` was NULL).
|
|
219
|
+
|
|
220
|
+
### Assertions that can never fail (0.23.0)
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
- id: sarj-no-tautological-expect # SARJ057
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`SARJ057` fires when an assertion's operands are all literals, so its outcome is
|
|
227
|
+
fixed before the code runs. `SARJ043` already catches the test with *no*
|
|
228
|
+
assertion; this is the test whose assertion is decorative.
|
|
229
|
+
|
|
230
|
+
The placeholder spelling (`assert True`) is the obvious half. The expensive half
|
|
231
|
+
is the assertion whose real condition slid out of the condition slot, because it
|
|
232
|
+
was a working assertion when it was typed:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
assert { # ← braces, not parentheses
|
|
236
|
+
"referencing a non existing `via_device` " in caplog.text
|
|
237
|
+
} # one-element SET, always truthy
|
|
238
|
+
|
|
239
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
240
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
244
|
+
of a thing with itself" — measures ~95% false positives: `assert i == i`,
|
|
245
|
+
`assert x is x`, `expect(hash([o])).toEqual(hash([o]))` are reflexivity,
|
|
246
|
+
determinism and memoization tests, and for a type with custom `__eq__`/`__hash__`
|
|
247
|
+
they can genuinely fail. So an identifier, attribute or call operand is never
|
|
248
|
+
enough; both sides must be literals, and textually identical ones. `assert True`
|
|
249
|
+
as the sole statement of an `except` handler is exempt — it asserts *which branch
|
|
250
|
+
ran* — as is anything inside a pytest-benchmark test.
|
|
251
|
+
|
|
252
|
+
Measured before shipping: **4 findings across 28,608 files** — 26,346 of
|
|
253
|
+
pydantic, trio, attrs, Airflow and Home Assistant plus 2,262 first-party files
|
|
254
|
+
in bulbul, noura-be, kpi-hub, ai and demo-gateway. All 4 are true positives
|
|
255
|
+
(Home Assistant `tests/helpers/test_device_registry.py:3711` and `:3777`,
|
|
256
|
+
`tests/components/emulated_hue/test_hue_api.py:1078`, Airflow
|
|
257
|
+
`providers/apache/hdfs/.../log/test_hdfs_task_handler.py:170`); 0 false
|
|
258
|
+
positives. The two `except ...: assert True` markers that a carve-out-free
|
|
259
|
+
version does flag — `pydantic-core/tests/benchmarks/test_micro_benchmarks.py:716`
|
|
260
|
+
and `core/tests/components/mqtt/test_client.py:1353` — were verified silent.
|
|
261
|
+
|
|
262
|
+
The TypeScript half of the same rule ships as `@sarj/no-tautological-expect` in
|
|
263
|
+
`@sarj/eslint-plugin` ≥ 2.14.0; until now there was no TS counterpart at all,
|
|
264
|
+
which is how `expect(true).toBe(true); // placeholder` survived in a suite named
|
|
265
|
+
for the behaviour it was supposed to check.
|
|
266
|
+
|
|
267
|
+
## CLI
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
sarj-python-lint check --rule no-sequential-await path/to/file.py
|
|
271
|
+
sarj-python-lint list-rules
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Diagnostic format is `path:line:col: CODE message` — Ruff-compatible.
|
|
275
|
+
|
|
276
|
+
## Suppression
|
|
277
|
+
|
|
278
|
+
Inline `# sarj-noqa: SARJ00X — <reason>` on the offending line.
|
|
279
|
+
|
|
280
|
+
Each rule's source under `src/sarj_python_lint/rules/` carries its own `description` and diagnostic message.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.23.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -18,6 +18,7 @@ dependencies = []
|
|
|
18
18
|
|
|
19
19
|
[project.scripts]
|
|
20
20
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
21
|
+
sarj-ratchet = "sarj_python_lint._ratchet_cli:main"
|
|
21
22
|
|
|
22
23
|
[project.urls]
|
|
23
24
|
Homepage = "https://github.com/sarj-ai/standards/tree/main/packages/python"
|