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.
Files changed (84) hide show
  1. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/PKG-INFO +119 -2
  2. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/README.md +118 -1
  3. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/pyproject.toml +1 -1
  4. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/_pytest.py +61 -0
  5. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_registry.py +54 -0
  6. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/conditional_assertion_in_test.py +1265 -0
  7. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/duplicate_test_body.py +701 -0
  8. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/interaction_only_test.py +712 -0
  9. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_comment_cruft.py +44 -2
  10. {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
  11. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_offset_pagination.py +35 -4
  12. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +192 -0
  13. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_patching_system_under_test.py +734 -0
  14. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +14 -0
  15. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_restated_comment.py +52 -6
  16. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +35 -9
  17. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/no_tautological_expect.py +431 -0
  18. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/over_mocked_test.py +1006 -0
  19. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +546 -0
  20. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_library_fake.py +631 -0
  21. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +427 -0
  22. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_or_pattern.py +370 -0
  23. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +613 -0
  24. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/require_port_for_service.py +759 -0
  25. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +151 -0
  26. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/tautological_mock_assertion.py +531 -0
  27. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/trailing_value_narration.py +25 -2
  28. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/trivially_true_assertion.py +772 -0
  29. sarj_python_lint-0.24.0/src/sarj_python_lint/rules/unused_mock_setup.py +491 -0
  30. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/zero_assertion_test.py +2 -13
  31. sarj_python_lint-0.21.0/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -93
  32. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/.gitignore +0 -0
  33. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/__init__.py +0 -0
  34. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/__main__.py +0 -0
  35. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  36. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_secret_names.py +0 -0
  37. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/_version.py +0 -0
  38. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/py.typed +0 -0
  39. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/ratchet.py +0 -0
  40. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rule_base.py +0 -0
  41. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  42. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  43. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  44. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  46. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  47. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  48. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  49. {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
  50. {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
  51. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  52. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  53. {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
  54. {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
  55. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  56. {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
  57. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  58. {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
  59. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  60. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  61. {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
  62. {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
  63. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  64. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  65. {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
  66. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  67. {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
  68. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  69. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  70. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  71. {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
  72. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  73. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  74. {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
  75. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  76. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  77. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  78. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  79. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  80. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  81. {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
  82. {sarj_python_lint-0.21.0 → sarj_python_lint-0.24.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  83. {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
  84. {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.21.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.2.0
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.2.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.21.0"
3
+ version = "0.24.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" }]
@@ -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"]