sarj-python-lint 0.21.0__tar.gz → 0.24.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.21.0 → sarj_python_lint-0.24.0}/PKG-INFO +119 -2
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/README.md +118 -1
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/pyproject.toml +1 -1
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/_pytest.py +61 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_registry.py +54 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/conditional_assertion_in_test.py +1265 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/duplicate_test_body.py +701 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/interaction_only_test.py +712 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_comment_cruft.py +44 -2
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +28 -2
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_offset_pagination.py +35 -4
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +192 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_patching_system_under_test.py +734 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +14 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_restated_comment.py +52 -6
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +35 -9
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_tautological_expect.py +431 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/over_mocked_test.py +1006 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +546 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_library_fake.py +631 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +427 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_or_pattern.py +370 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +613 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/require_port_for_service.py +759 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +151 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/tautological_mock_assertion.py +531 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/trailing_value_narration.py +25 -2
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/trivially_true_assertion.py +772 -0
- sarj_python_lint-0.24.0/src/sarj_python_lint/rules/unused_mock_setup.py +491 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/zero_assertion_test.py +2 -13
- sarj_python_lint-0.21.0/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -93
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/.gitignore +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
|
|
6
6
|
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
@@ -28,7 +28,7 @@ uv tool install sarj-python-lint
|
|
|
28
28
|
|
|
29
29
|
```yaml
|
|
30
30
|
- repo: https://github.com/sarj-ai/standards
|
|
31
|
-
rev: python-v0.
|
|
31
|
+
rev: python-v0.24.0
|
|
32
32
|
hooks:
|
|
33
33
|
- id: sarj-no-sequential-await
|
|
34
34
|
- id: sarj-inefficient-string-concat-in-loop
|
|
@@ -146,6 +146,37 @@ decisions into one unreviewable one and pre-authorizes every mock added later.
|
|
|
146
146
|
Scoped exemptions for mechanical codes (`E501`, `F401`, `UP035`) are never
|
|
147
147
|
flagged — measured across five repos those are the entire population.
|
|
148
148
|
|
|
149
|
+
|
|
150
|
+
### Mock-quality and real-dependency rules (0.24.0)
|
|
151
|
+
|
|
152
|
+
The second test-quality wave. Where the 0.15.0 family asks "does this test assert
|
|
153
|
+
anything?", this one asks "does it exercise anything real?" — it pushes suites off
|
|
154
|
+
hand-rolled doubles and onto the real store, the real database and a maintained fake
|
|
155
|
+
library. Measured against bulbul, noura-be, five other first-party repos and fourteen
|
|
156
|
+
OSS corpora; two candidates were dropped outright when the corpus showed they only
|
|
157
|
+
duplicated ruff.
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
- id: sarj-prefer-real-store-in-tests # SARJ058
|
|
161
|
+
- id: sarj-prefer-library-fake # SARJ059
|
|
162
|
+
- id: sarj-tautological-mock-assertion # SARJ060
|
|
163
|
+
- id: sarj-no-patching-system-under-test # SARJ061
|
|
164
|
+
- id: sarj-over-mocked-test # SARJ062
|
|
165
|
+
- id: sarj-interaction-only-test # SARJ063
|
|
166
|
+
- id: sarj-trivially-true-assertion # SARJ064
|
|
167
|
+
- id: sarj-conditional-assertion-in-test # SARJ065
|
|
168
|
+
- id: sarj-duplicate-test-body # SARJ066
|
|
169
|
+
- id: sarj-unused-mock-setup # SARJ067
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Expressiveness rules (0.24.0)
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
- id: sarj-prefer-fstring-over-concat # SARJ068
|
|
176
|
+
- id: sarj-prefer-match-pattern-destructuring # SARJ069
|
|
177
|
+
- id: sarj-prefer-or-pattern # SARJ070
|
|
178
|
+
- id: sarj-require-port-for-service # SARJ071
|
|
179
|
+
```
|
|
149
180
|
### Suppression ratchet (`sarj-ratchet`, 0.21.0)
|
|
150
181
|
|
|
151
182
|
```yaml
|
|
@@ -196,6 +227,92 @@ sarj-python-lint check --rule mock-without-spec --update-baseline test-quality-b
|
|
|
196
227
|
sarj-python-lint check --rule mock-without-spec --baseline test-quality-baseline.json python/
|
|
197
228
|
```
|
|
198
229
|
|
|
230
|
+
### Multi-tenant scoping (0.24.0)
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
- id: sarj-no-optional-tenant-predicate # SARJ056
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`SARJ056` fires when every WHERE-fragment mentioning a tenant column
|
|
237
|
+
(`organization_id` and friends) sits inside a conditional, so the predicate
|
|
238
|
+
disappears — and the query still runs — whenever the filter is empty:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
where_conditions = []
|
|
242
|
+
if args.organization_ids: # ← optional
|
|
243
|
+
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
244
|
+
...
|
|
245
|
+
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The safe idiom seeds the list, so scoping always applies and the rule stays
|
|
249
|
+
quiet:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
conditions: list[Composable] = [SQL("organization_id = %s")]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A function with **no** tenant predicate at all never fires — an intentionally
|
|
256
|
+
cross-tenant admin query is out of scope; only *attempted-but-optional* scoping
|
|
257
|
+
is a finding. Where a caller genuinely wants the all-tenant query, that
|
|
258
|
+
intent belongs in an explicit method (or an inline `sarj-noqa`) rather than in
|
|
259
|
+
an omitted filter.
|
|
260
|
+
|
|
261
|
+
Measured before shipping: **0 findings across 26,345 files** of pydantic, trio,
|
|
262
|
+
attrs, Airflow and Home Assistant — single-tenant codebases have no tenant
|
|
263
|
+
column, so the rule is silent by construction — and 0 in noura-be, ai, kpi-hub
|
|
264
|
+
and demo-gateway. In bulbul it finds 10 sites, all genuine fail-open
|
|
265
|
+
compositions, two of which were reachable cross-tenant reads at the time of
|
|
266
|
+
writing (`POST /v1/calls/list` and `POST /v1/calls/batch/list`, both of which
|
|
267
|
+
composed `WHERE 1=1` for a user whose `organization_id` was NULL).
|
|
268
|
+
|
|
269
|
+
### Assertions that can never fail (0.23.0)
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
- id: sarj-no-tautological-expect # SARJ057
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`SARJ057` fires when an assertion's operands are all literals, so its outcome is
|
|
276
|
+
fixed before the code runs. `SARJ043` already catches the test with *no*
|
|
277
|
+
assertion; this is the test whose assertion is decorative.
|
|
278
|
+
|
|
279
|
+
The placeholder spelling (`assert True`) is the obvious half. The expensive half
|
|
280
|
+
is the assertion whose real condition slid out of the condition slot, because it
|
|
281
|
+
was a working assertion when it was typed:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
assert { # ← braces, not parentheses
|
|
285
|
+
"referencing a non existing `via_device` " in caplog.text
|
|
286
|
+
} # one-element SET, always truthy
|
|
287
|
+
|
|
288
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
289
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
293
|
+
of a thing with itself" — measures ~95% false positives: `assert i == i`,
|
|
294
|
+
`assert x is x`, `expect(hash([o])).toEqual(hash([o]))` are reflexivity,
|
|
295
|
+
determinism and memoization tests, and for a type with custom `__eq__`/`__hash__`
|
|
296
|
+
they can genuinely fail. So an identifier, attribute or call operand is never
|
|
297
|
+
enough; both sides must be literals, and textually identical ones. `assert True`
|
|
298
|
+
as the sole statement of an `except` handler is exempt — it asserts *which branch
|
|
299
|
+
ran* — as is anything inside a pytest-benchmark test.
|
|
300
|
+
|
|
301
|
+
Measured before shipping: **4 findings across 28,608 files** — 26,346 of
|
|
302
|
+
pydantic, trio, attrs, Airflow and Home Assistant plus 2,262 first-party files
|
|
303
|
+
in bulbul, noura-be, kpi-hub, ai and demo-gateway. All 4 are true positives
|
|
304
|
+
(Home Assistant `tests/helpers/test_device_registry.py:3711` and `:3777`,
|
|
305
|
+
`tests/components/emulated_hue/test_hue_api.py:1078`, Airflow
|
|
306
|
+
`providers/apache/hdfs/.../log/test_hdfs_task_handler.py:170`); 0 false
|
|
307
|
+
positives. The two `except ...: assert True` markers that a carve-out-free
|
|
308
|
+
version does flag — `pydantic-core/tests/benchmarks/test_micro_benchmarks.py:716`
|
|
309
|
+
and `core/tests/components/mqtt/test_client.py:1353` — were verified silent.
|
|
310
|
+
|
|
311
|
+
The TypeScript half of the same rule ships as `@sarj/no-tautological-expect` in
|
|
312
|
+
`@sarj/eslint-plugin` ≥ 2.14.0; until now there was no TS counterpart at all,
|
|
313
|
+
which is how `expect(true).toBe(true); // placeholder` survived in a suite named
|
|
314
|
+
for the behaviour it was supposed to check.
|
|
315
|
+
|
|
199
316
|
## CLI
|
|
200
317
|
|
|
201
318
|
```bash
|
|
@@ -10,7 +10,7 @@ uv tool install sarj-python-lint
|
|
|
10
10
|
|
|
11
11
|
```yaml
|
|
12
12
|
- repo: https://github.com/sarj-ai/standards
|
|
13
|
-
rev: python-v0.
|
|
13
|
+
rev: python-v0.24.0
|
|
14
14
|
hooks:
|
|
15
15
|
- id: sarj-no-sequential-await
|
|
16
16
|
- id: sarj-inefficient-string-concat-in-loop
|
|
@@ -128,6 +128,37 @@ decisions into one unreviewable one and pre-authorizes every mock added later.
|
|
|
128
128
|
Scoped exemptions for mechanical codes (`E501`, `F401`, `UP035`) are never
|
|
129
129
|
flagged — measured across five repos those are the entire population.
|
|
130
130
|
|
|
131
|
+
|
|
132
|
+
### Mock-quality and real-dependency rules (0.24.0)
|
|
133
|
+
|
|
134
|
+
The second test-quality wave. Where the 0.15.0 family asks "does this test assert
|
|
135
|
+
anything?", this one asks "does it exercise anything real?" — it pushes suites off
|
|
136
|
+
hand-rolled doubles and onto the real store, the real database and a maintained fake
|
|
137
|
+
library. Measured against bulbul, noura-be, five other first-party repos and fourteen
|
|
138
|
+
OSS corpora; two candidates were dropped outright when the corpus showed they only
|
|
139
|
+
duplicated ruff.
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
- id: sarj-prefer-real-store-in-tests # SARJ058
|
|
143
|
+
- id: sarj-prefer-library-fake # SARJ059
|
|
144
|
+
- id: sarj-tautological-mock-assertion # SARJ060
|
|
145
|
+
- id: sarj-no-patching-system-under-test # SARJ061
|
|
146
|
+
- id: sarj-over-mocked-test # SARJ062
|
|
147
|
+
- id: sarj-interaction-only-test # SARJ063
|
|
148
|
+
- id: sarj-trivially-true-assertion # SARJ064
|
|
149
|
+
- id: sarj-conditional-assertion-in-test # SARJ065
|
|
150
|
+
- id: sarj-duplicate-test-body # SARJ066
|
|
151
|
+
- id: sarj-unused-mock-setup # SARJ067
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Expressiveness rules (0.24.0)
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- id: sarj-prefer-fstring-over-concat # SARJ068
|
|
158
|
+
- id: sarj-prefer-match-pattern-destructuring # SARJ069
|
|
159
|
+
- id: sarj-prefer-or-pattern # SARJ070
|
|
160
|
+
- id: sarj-require-port-for-service # SARJ071
|
|
161
|
+
```
|
|
131
162
|
### Suppression ratchet (`sarj-ratchet`, 0.21.0)
|
|
132
163
|
|
|
133
164
|
```yaml
|
|
@@ -178,6 +209,92 @@ sarj-python-lint check --rule mock-without-spec --update-baseline test-quality-b
|
|
|
178
209
|
sarj-python-lint check --rule mock-without-spec --baseline test-quality-baseline.json python/
|
|
179
210
|
```
|
|
180
211
|
|
|
212
|
+
### Multi-tenant scoping (0.24.0)
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
- id: sarj-no-optional-tenant-predicate # SARJ056
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`SARJ056` fires when every WHERE-fragment mentioning a tenant column
|
|
219
|
+
(`organization_id` and friends) sits inside a conditional, so the predicate
|
|
220
|
+
disappears — and the query still runs — whenever the filter is empty:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
where_conditions = []
|
|
224
|
+
if args.organization_ids: # ← optional
|
|
225
|
+
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
226
|
+
...
|
|
227
|
+
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The safe idiom seeds the list, so scoping always applies and the rule stays
|
|
231
|
+
quiet:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
conditions: list[Composable] = [SQL("organization_id = %s")]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A function with **no** tenant predicate at all never fires — an intentionally
|
|
238
|
+
cross-tenant admin query is out of scope; only *attempted-but-optional* scoping
|
|
239
|
+
is a finding. Where a caller genuinely wants the all-tenant query, that
|
|
240
|
+
intent belongs in an explicit method (or an inline `sarj-noqa`) rather than in
|
|
241
|
+
an omitted filter.
|
|
242
|
+
|
|
243
|
+
Measured before shipping: **0 findings across 26,345 files** of pydantic, trio,
|
|
244
|
+
attrs, Airflow and Home Assistant — single-tenant codebases have no tenant
|
|
245
|
+
column, so the rule is silent by construction — and 0 in noura-be, ai, kpi-hub
|
|
246
|
+
and demo-gateway. In bulbul it finds 10 sites, all genuine fail-open
|
|
247
|
+
compositions, two of which were reachable cross-tenant reads at the time of
|
|
248
|
+
writing (`POST /v1/calls/list` and `POST /v1/calls/batch/list`, both of which
|
|
249
|
+
composed `WHERE 1=1` for a user whose `organization_id` was NULL).
|
|
250
|
+
|
|
251
|
+
### Assertions that can never fail (0.23.0)
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
- id: sarj-no-tautological-expect # SARJ057
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`SARJ057` fires when an assertion's operands are all literals, so its outcome is
|
|
258
|
+
fixed before the code runs. `SARJ043` already catches the test with *no*
|
|
259
|
+
assertion; this is the test whose assertion is decorative.
|
|
260
|
+
|
|
261
|
+
The placeholder spelling (`assert True`) is the obvious half. The expensive half
|
|
262
|
+
is the assertion whose real condition slid out of the condition slot, because it
|
|
263
|
+
was a working assertion when it was typed:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
assert { # ← braces, not parentheses
|
|
267
|
+
"referencing a non existing `via_device` " in caplog.text
|
|
268
|
+
} # one-element SET, always truthy
|
|
269
|
+
|
|
270
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
271
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
275
|
+
of a thing with itself" — measures ~95% false positives: `assert i == i`,
|
|
276
|
+
`assert x is x`, `expect(hash([o])).toEqual(hash([o]))` are reflexivity,
|
|
277
|
+
determinism and memoization tests, and for a type with custom `__eq__`/`__hash__`
|
|
278
|
+
they can genuinely fail. So an identifier, attribute or call operand is never
|
|
279
|
+
enough; both sides must be literals, and textually identical ones. `assert True`
|
|
280
|
+
as the sole statement of an `except` handler is exempt — it asserts *which branch
|
|
281
|
+
ran* — as is anything inside a pytest-benchmark test.
|
|
282
|
+
|
|
283
|
+
Measured before shipping: **4 findings across 28,608 files** — 26,346 of
|
|
284
|
+
pydantic, trio, attrs, Airflow and Home Assistant plus 2,262 first-party files
|
|
285
|
+
in bulbul, noura-be, kpi-hub, ai and demo-gateway. All 4 are true positives
|
|
286
|
+
(Home Assistant `tests/helpers/test_device_registry.py:3711` and `:3777`,
|
|
287
|
+
`tests/components/emulated_hue/test_hue_api.py:1078`, Airflow
|
|
288
|
+
`providers/apache/hdfs/.../log/test_hdfs_task_handler.py:170`); 0 false
|
|
289
|
+
positives. The two `except ...: assert True` markers that a carve-out-free
|
|
290
|
+
version does flag — `pydantic-core/tests/benchmarks/test_micro_benchmarks.py:716`
|
|
291
|
+
and `core/tests/components/mqtt/test_client.py:1353` — were verified silent.
|
|
292
|
+
|
|
293
|
+
The TypeScript half of the same rule ships as `@sarj/no-tautological-expect` in
|
|
294
|
+
`@sarj/eslint-plugin` ≥ 2.14.0; until now there was no TS counterpart at all,
|
|
295
|
+
which is how `expect(true).toBe(true); // placeholder` survived in a suite named
|
|
296
|
+
for the behaviour it was supposed to check.
|
|
297
|
+
|
|
181
298
|
## CLI
|
|
182
299
|
|
|
183
300
|
```bash
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Shared pytest-shape predicates for the assertion-quality rules.
|
|
2
|
+
|
|
3
|
+
A pytest-benchmark test measures wall-clock time; verifying is not its job, and
|
|
4
|
+
every rule that judges a test's assertions needs the same carve-out for it —
|
|
5
|
+
SARJ043 (`zero-assertion-test`) because a benchmark asserts nothing, SARJ057
|
|
6
|
+
(`no-tautological-expect`) because the idiomatic way to benchmark a *failing*
|
|
7
|
+
path is `try: ...; assert False; except Err: assert True` inside the timed
|
|
8
|
+
callable. Keeping the predicate in one module is what stops the two copies from
|
|
9
|
+
drifting apart.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# The pytest-benchmark fixture: the test measures time, it does not verify.
|
|
18
|
+
_BENCHMARK = "benchmark"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def uses_benchmark_fixture(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
22
|
+
"""Report whether `node` both declares and uses the pytest-benchmark fixture.
|
|
23
|
+
|
|
24
|
+
`def test_x(benchmark)` then `benchmark(fn, arg)` or `@benchmark` on a nested
|
|
25
|
+
function. The fixture times the callable; a benchmark that asserted on the
|
|
26
|
+
result would be measuring the assertion. Requiring the name to be *used*, not
|
|
27
|
+
just declared, keeps an unrelated parameter that happens to be called
|
|
28
|
+
`benchmark` from silencing the rule.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True when the function is a pytest-benchmark test.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
args = node.args
|
|
35
|
+
declared = any(arg.arg == _BENCHMARK for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs))
|
|
36
|
+
return declared and any(isinstance(child, ast.Name) and child.id == _BENCHMARK for child in ast.walk(node))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_benchmark_marker(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
40
|
+
"""Report whether `node` carries `@pytest.mark.benchmark`, called or bare.
|
|
41
|
+
|
|
42
|
+
The marker form is the other half of pytest-benchmark's surface: a test can
|
|
43
|
+
take the fixture, wear the marker, or both.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True when a `pytest.mark.benchmark` decorator is present.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
return any(_decorator_attr(dec) == _BENCHMARK for dec in node.decorator_list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _decorator_attr(dec: ast.expr) -> str | None:
|
|
53
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
54
|
+
if not isinstance(target, ast.Attribute):
|
|
55
|
+
return None
|
|
56
|
+
# Only `*.mark.<name>`, so a `@benchmark.something` helper is not mistaken
|
|
57
|
+
# for a pytest marker.
|
|
58
|
+
parent = target.value
|
|
59
|
+
if isinstance(parent, ast.Attribute) and parent.attr == "mark":
|
|
60
|
+
return target.attr
|
|
61
|
+
return None
|
|
@@ -2,10 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from sarj_python_lint.rules.conditional_assertion_in_test import ConditionalAssertionInTest
|
|
6
|
+
from sarj_python_lint.rules.duplicate_test_body import DuplicateTestBody
|
|
5
7
|
from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
|
|
6
8
|
from sarj_python_lint.rules.inefficient_string_concat_in_loop import (
|
|
7
9
|
InefficientStringConcatInLoop,
|
|
8
10
|
)
|
|
11
|
+
from sarj_python_lint.rules.interaction_only_test import InteractionOnlyTest
|
|
9
12
|
from sarj_python_lint.rules.kwarg_heavy_construction_in_test import KwargHeavyConstructionInTest
|
|
10
13
|
from sarj_python_lint.rules.kwonly_same_type_params import KwonlySameTypeParams
|
|
11
14
|
from sarj_python_lint.rules.mock_without_spec import MockWithoutSpec
|
|
@@ -26,6 +29,10 @@ from sarj_python_lint.rules.no_fstring_in_log import NoFstringInLog
|
|
|
26
29
|
from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
|
|
27
30
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
28
31
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
32
|
+
from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
33
|
+
NoOptionalTenantPredicate,
|
|
34
|
+
)
|
|
35
|
+
from sarj_python_lint.rules.no_patching_system_under_test import NoPatchingSystemUnderTest
|
|
29
36
|
from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
|
|
30
37
|
from sarj_python_lint.rules.no_raw_sql_in_tests import NoRawSqlInTests
|
|
31
38
|
from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
|
|
@@ -36,21 +43,28 @@ from sarj_python_lint.rules.no_sentinel_return_on_except import NoSentinelReturn
|
|
|
36
43
|
from sarj_python_lint.rules.no_sequential_await import NoSequentialAwait
|
|
37
44
|
from sarj_python_lint.rules.no_sleep_in_test_body import NoSleepInTestBody
|
|
38
45
|
from sarj_python_lint.rules.no_stdlib_logging import NoStdlibLogging
|
|
46
|
+
from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
|
|
39
47
|
from sarj_python_lint.rules.no_unreachable_after_terminal import (
|
|
40
48
|
NoUnreachableAfterTerminal,
|
|
41
49
|
)
|
|
50
|
+
from sarj_python_lint.rules.over_mocked_test import OverMockedTest
|
|
42
51
|
from sarj_python_lint.rules.parametrize_case_needs_id import ParametrizeCaseNeedsId
|
|
43
52
|
from sarj_python_lint.rules.prefer_class_row import PreferClassRow
|
|
44
53
|
from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
|
|
45
54
|
PreferConstantTimeSecretCompare,
|
|
46
55
|
)
|
|
56
|
+
from sarj_python_lint.rules.prefer_fstring_over_concat import PreferFstringOverConcat
|
|
57
|
+
from sarj_python_lint.rules.prefer_library_fake import PreferLibraryFake
|
|
47
58
|
from sarj_python_lint.rules.prefer_match_assert_never import PreferMatchAssertNever
|
|
59
|
+
from sarj_python_lint.rules.prefer_match_pattern_destructuring import PreferMatchPatternDestructuring
|
|
48
60
|
from sarj_python_lint.rules.prefer_module_level_constant import (
|
|
49
61
|
PreferModuleLevelConstant,
|
|
50
62
|
)
|
|
51
63
|
from sarj_python_lint.rules.prefer_namedtuple_over_tuple_return import (
|
|
52
64
|
PreferNamedtupleOverTupleReturn,
|
|
53
65
|
)
|
|
66
|
+
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
67
|
+
from sarj_python_lint.rules.prefer_real_store_in_tests import PreferRealStoreInTests
|
|
54
68
|
from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
55
69
|
from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
56
70
|
PreferStructOverNamedtuple,
|
|
@@ -60,16 +74,20 @@ from sarj_python_lint.rules.prefer_timedelta_for_durations import (
|
|
|
60
74
|
)
|
|
61
75
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
62
76
|
from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
77
|
+
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
63
78
|
from sarj_python_lint.rules.single_public_export import SinglePublicExport
|
|
64
79
|
from sarj_python_lint.rules.sleep_with_computed_arg_in_test import SleepWithComputedArgInTest
|
|
65
80
|
from sarj_python_lint.rules.stepdown import Stepdown
|
|
66
81
|
from sarj_python_lint.rules.store_insert_requires_on_conflict import (
|
|
67
82
|
StoreInsertRequiresOnConflict,
|
|
68
83
|
)
|
|
84
|
+
from sarj_python_lint.rules.tautological_mock_assertion import TautologicalMockAssertion
|
|
69
85
|
from sarj_python_lint.rules.test_loops_over_literal_cases import (
|
|
70
86
|
TestLoopsOverLiteralCases,
|
|
71
87
|
)
|
|
72
88
|
from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
|
|
89
|
+
from sarj_python_lint.rules.trivially_true_assertion import TriviallyTrueAssertion
|
|
90
|
+
from sarj_python_lint.rules.unused_mock_setup import UnusedMockSetup
|
|
73
91
|
from sarj_python_lint.rules.xfail_requires_strict import XfailRequiresStrict
|
|
74
92
|
from sarj_python_lint.rules.zero_assertion_test import ZeroAssertionTest
|
|
75
93
|
|
|
@@ -83,8 +101,28 @@ if TYPE_CHECKING:
|
|
|
83
101
|
# SARJ027, SARJ029, SARJ030 (dropped in 0.11.1 as too noisy),
|
|
84
102
|
# SARJ033 httpx-client-requires-timeout, SARJ035 no-import-time-settings
|
|
85
103
|
# (dropped by user veto after the 0.13.x mined-rules review),
|
|
104
|
+
# SARJ055 no-filler-success-adverb (built and corpus-validated, then dropped
|
|
105
|
+
# — but not for the reason first recorded. The headline "4.7% of Airflow's
|
|
106
|
+
# info/debug logs" is a composition artifact: 237 of those 248 hits are in
|
|
107
|
+
# `providers/` (contributed vendor operators, heavily copy-pasted — the
|
|
108
|
+
# template "%s completed successfully." appears 22 times verbatim). Airflow's
|
|
109
|
+
# maintainer-owned `airflow-core/` sits at 1.15% and Home Assistant at 1.24%,
|
|
110
|
+
# so the real external baseline is ~1.2%, not 4.7%. The rule was re-measured
|
|
111
|
+
# and dropped on a stronger basis: the narrow variant — fire only when the
|
|
112
|
+
# adverb is the sole content beyond a bare verb — has 12 external hits and
|
|
113
|
+
# ZERO internal ones across all six repos, so it would govern nothing we
|
|
114
|
+
# write. The broad rule remains opt-in house style, not a defect check.
|
|
115
|
+
# (Noted for any future revisit: noura-be measures 7.92%, a genuine outlier
|
|
116
|
+
# against every corpus; that, not the narrow variant, is the case to make.),
|
|
86
117
|
# SARJ037 no-trivial-single-use-helper (prototyped and dropped for FP rate;
|
|
87
118
|
# see the 0.13.1 inlining commit for the corpus analysis).
|
|
119
|
+
# SARJ072 unbound-mock-assertion and SARJ073 raises-needs-specific-error
|
|
120
|
+
# (built, corpus-measured and dropped before ever being registered, in the
|
|
121
|
+
# wave that added SARJ058-071). SARJ072 found 0 hits across 16,130 files in
|
|
122
|
+
# 9 repos, including 0 against 8,322 real mock-assertion API uses; ruff B018
|
|
123
|
+
# covers its bare-statement shape. SARJ073 was a strict superset of ruff
|
|
124
|
+
# PT011, which this standard already enables — its one real gap,
|
|
125
|
+
# RuntimeError, is closed by `lint.flake8-pytest-style.raises-require-match-for`.
|
|
88
126
|
REGISTRY: dict[str, type[Rule]] = {
|
|
89
127
|
NoSequentialAwait.id: NoSequentialAwait,
|
|
90
128
|
InefficientStringConcatInLoop.id: InefficientStringConcatInLoop,
|
|
@@ -132,6 +170,22 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
132
170
|
NoStdlibLogging.id: NoStdlibLogging,
|
|
133
171
|
NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
|
|
134
172
|
NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
|
|
173
|
+
NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
|
|
174
|
+
NoTautologicalExpect.id: NoTautologicalExpect,
|
|
175
|
+
PreferRealStoreInTests.id: PreferRealStoreInTests,
|
|
176
|
+
PreferLibraryFake.id: PreferLibraryFake,
|
|
177
|
+
TautologicalMockAssertion.id: TautologicalMockAssertion,
|
|
178
|
+
NoPatchingSystemUnderTest.id: NoPatchingSystemUnderTest,
|
|
179
|
+
OverMockedTest.id: OverMockedTest,
|
|
180
|
+
InteractionOnlyTest.id: InteractionOnlyTest,
|
|
181
|
+
TriviallyTrueAssertion.id: TriviallyTrueAssertion,
|
|
182
|
+
ConditionalAssertionInTest.id: ConditionalAssertionInTest,
|
|
183
|
+
DuplicateTestBody.id: DuplicateTestBody,
|
|
184
|
+
UnusedMockSetup.id: UnusedMockSetup,
|
|
185
|
+
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
186
|
+
PreferMatchPatternDestructuring.id: PreferMatchPatternDestructuring,
|
|
187
|
+
PreferOrPattern.id: PreferOrPattern,
|
|
188
|
+
RequirePortForService.id: RequirePortForService,
|
|
135
189
|
}
|
|
136
190
|
|
|
137
191
|
__all__ = ["REGISTRY"]
|