sarj-python-lint 0.25.0__tar.gz → 0.27.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.25.0 → sarj_python_lint-0.27.0}/.gitignore +1 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/PKG-INFO +6 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/README.md +5 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/ratchet.py +8 -8
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rule_base.py +1 -1
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/_ast_index.py +180 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_comments.py +128 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_first_party.py +23 -17
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/_logging.py +54 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_pytest.py +3 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_registry.py +20 -28
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_suppression_comments.py +18 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +17 -18
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/duplicate_test_body.py +11 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +4 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +5 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/interaction_only_test.py +6 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +2 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +27 -24
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/mock_without_spec.py +8 -8
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_comment_cruft.py +83 -15
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +4 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +3 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +5 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +5 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +3 -2
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/no_implicit_attribute_access.py +126 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_offset_pagination.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +5 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +72 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +2 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +4 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_restated_comment.py +2 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_secret_in_log.py +2 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_select_star.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +14 -10
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_sequential_await.py +74 -51
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +2 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +34 -19
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_tautological_expect.py +37 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +15 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/over_mocked_test.py +69 -20
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +2 -7
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_class_row.py +2 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +36 -26
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +36 -8
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_library_fake.py +4 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +10 -13
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +75 -14
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +275 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +4 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +7 -9
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +3 -3
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_pattern_matching.py +343 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +5 -4
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_self_type_annotation.py +159 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_str_enum.py +11 -17
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +3 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +4 -3
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +125 -0
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +144 -0
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +129 -0
- sarj_python_lint-0.27.0/src/sarj_python_lint/rules/primary_export_file_name.py +149 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +4 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/redundant_docstring.py +99 -18
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/require_port_for_service.py +27 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/stepdown.py +6 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +3 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +183 -10
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +3 -2
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/trailing_value_narration.py +65 -14
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +48 -172
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/unused_mock_setup.py +8 -9
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +4 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/zero_assertion_test.py +5 -5
- sarj_python_lint-0.25.0/src/sarj_python_lint/rules/_logging.py +0 -39
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.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.27.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
|
|
@@ -240,7 +240,7 @@ disappears — and the query still runs — whenever the filter is empty:
|
|
|
240
240
|
|
|
241
241
|
```python
|
|
242
242
|
where_conditions = []
|
|
243
|
-
if args.organization_ids:
|
|
243
|
+
if args.organization_ids: # ← optional
|
|
244
244
|
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
245
245
|
...
|
|
246
246
|
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
@@ -282,12 +282,12 @@ is the assertion whose real condition slid out of the condition slot, because it
|
|
|
282
282
|
was a working assertion when it was typed:
|
|
283
283
|
|
|
284
284
|
```python
|
|
285
|
-
assert {
|
|
285
|
+
assert { # ← braces, not parentheses
|
|
286
286
|
"referencing a non existing `via_device` " in caplog.text
|
|
287
|
-
}
|
|
287
|
+
} # one-element SET, always truthy
|
|
288
288
|
|
|
289
|
-
assert [f"No logs found on hdfs for ti={ti}"]
|
|
290
|
-
assert True, cover_result_json[0]["success"][...]
|
|
289
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
290
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
291
291
|
```
|
|
292
292
|
|
|
293
293
|
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
@@ -222,7 +222,7 @@ disappears — and the query still runs — whenever the filter is empty:
|
|
|
222
222
|
|
|
223
223
|
```python
|
|
224
224
|
where_conditions = []
|
|
225
|
-
if args.organization_ids:
|
|
225
|
+
if args.organization_ids: # ← optional
|
|
226
226
|
where_conditions.append(SQL("organization_id = ANY(%s::uuid[])"))
|
|
227
227
|
...
|
|
228
228
|
where_clause = SQL(" AND ").join(where_conditions) if where_conditions else SQL("1=1")
|
|
@@ -264,12 +264,12 @@ is the assertion whose real condition slid out of the condition slot, because it
|
|
|
264
264
|
was a working assertion when it was typed:
|
|
265
265
|
|
|
266
266
|
```python
|
|
267
|
-
assert {
|
|
267
|
+
assert { # ← braces, not parentheses
|
|
268
268
|
"referencing a non existing `via_device` " in caplog.text
|
|
269
|
-
}
|
|
269
|
+
} # one-element SET, always truthy
|
|
270
270
|
|
|
271
|
-
assert [f"No logs found on hdfs for ti={ti}"]
|
|
272
|
-
assert True, cover_result_json[0]["success"][...]
|
|
271
|
+
assert [f"No logs found on hdfs for ti={ti}"] # the `== messages` was lost
|
|
272
|
+
assert True, cover_result_json[0]["success"][...] # slid into the MESSAGE slot
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
**The narrowness is the rule.** The obvious generalisation — "flag a comparison
|
|
@@ -210,14 +210,14 @@ def gate(measurement: Measurement, baseline: Baseline) -> list[Failure]:
|
|
|
210
210
|
|
|
211
211
|
"""
|
|
212
212
|
failures = [
|
|
213
|
-
Failure(dimension="code", key=key, ceiling=
|
|
213
|
+
Failure(dimension="code", key=key, ceiling=c, actual=n)
|
|
214
214
|
for key, n in sorted(measurement.codes.items())
|
|
215
|
-
if n > baseline.codes.get(key, 0)
|
|
215
|
+
if n > (c := baseline.codes.get(key, 0))
|
|
216
216
|
]
|
|
217
217
|
failures += [
|
|
218
|
-
Failure(dimension="package", key=key, ceiling=
|
|
218
|
+
Failure(dimension="package", key=key, ceiling=c, actual=n)
|
|
219
219
|
for key, n in sorted(measurement.packages.items())
|
|
220
|
-
if n > baseline.packages.get(key, 0)
|
|
220
|
+
if n > (c := baseline.packages.get(key, 0))
|
|
221
221
|
]
|
|
222
222
|
failures += [
|
|
223
223
|
Failure(dimension="file", key=key, ceiling=ceiling, actual=n)
|
|
@@ -337,11 +337,11 @@ def discover_packages(root: Path, excluded_dir_names: frozenset[str] = DEFAULT_E
|
|
|
337
337
|
|
|
338
338
|
"""
|
|
339
339
|
return sorted(
|
|
340
|
-
|
|
340
|
+
name
|
|
341
341
|
for child in root.iterdir()
|
|
342
|
-
if child.
|
|
343
|
-
and
|
|
344
|
-
and
|
|
342
|
+
if (name := child.name) not in excluded_dir_names
|
|
343
|
+
and not name.startswith(".")
|
|
344
|
+
and child.is_dir()
|
|
345
345
|
and any(True for _ in _python_files(child, excluded_dir_names))
|
|
346
346
|
)
|
|
347
347
|
|
|
@@ -46,7 +46,7 @@ def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
|
|
|
46
46
|
if not codes_str:
|
|
47
47
|
# Bare `# sarj-noqa` suppresses every SARJ code on the line
|
|
48
48
|
return True
|
|
49
|
-
codes = {
|
|
49
|
+
codes = {val.upper() for c in codes_str.split(",") if (val := c.strip())}
|
|
50
50
|
return code.upper() in codes
|
|
51
51
|
|
|
52
52
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""One tree traversal per file, shared by every rule that needs node lookups.
|
|
2
|
+
|
|
3
|
+
Profiling the whole registry over a 1,920-line module showed `ast.walk` and its
|
|
4
|
+
helpers (`iter_child_nodes` / `iter_fields`) accounting for ~60% of all rule
|
|
5
|
+
time. The cause is structural rather than any one rule's fault: the dominant
|
|
6
|
+
shape of a rule is
|
|
7
|
+
|
|
8
|
+
for node in ast.walk(tree):
|
|
9
|
+
if isinstance(node, ast.Something):
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
so a file with N nodes and R rules asking that question pays R full traversals,
|
|
13
|
+
each one re-deriving the same partition of the tree by node type.
|
|
14
|
+
|
|
15
|
+
`nodes()` derives that partition **once per file** and answers every subsequent
|
|
16
|
+
type query from it. The index is memoized in a single slot keyed on tree
|
|
17
|
+
identity, mirroring `parse_or_none`: the CLI iterates files on the outer loop
|
|
18
|
+
and rules on the inner one, so one slot is all that is ever live. The slot holds
|
|
19
|
+
a strong reference to the tree, so a recycled `id()` can never alias a stale
|
|
20
|
+
index.
|
|
21
|
+
|
|
22
|
+
Two invariants make the substitution finding-preserving, not merely faster:
|
|
23
|
+
|
|
24
|
+
* **Order.** The index is built by breadth-first traversal, the same order
|
|
25
|
+
`ast.walk` yields, and every query preserves it. `nodes(tree, T)` is therefore
|
|
26
|
+
elementwise equal to `[n for n in ast.walk(tree) if isinstance(n, T)]`, so a
|
|
27
|
+
rule that takes the *first* match, or reports in discovery order, is
|
|
28
|
+
unaffected.
|
|
29
|
+
* **Subclasses.** Queries match by `isinstance`, not by exact type: the buckets
|
|
30
|
+
are keyed on exact class, and a query resolves to the set of present classes
|
|
31
|
+
that are subclasses of the requested types. Asking for `ast.stmt` still
|
|
32
|
+
returns every statement.
|
|
33
|
+
|
|
34
|
+
`walk()` is the same breadth-first order for the cases the index cannot serve —
|
|
35
|
+
traversing a *subtree* rather than the module, where containment is not
|
|
36
|
+
something a whole-module partition can answer.
|
|
37
|
+
|
|
38
|
+
No rule may mutate the tree. That was already required — every rule shares one
|
|
39
|
+
memoized parse, and `duplicate_test_body` deep-copies before normalizing — and
|
|
40
|
+
the index inherits the requirement, since it holds references into the tree it
|
|
41
|
+
was built from.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import ast
|
|
47
|
+
from typing import TYPE_CHECKING, final
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Iterator
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_AST = ast.AST
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def children(node: ast.AST) -> list[ast.AST]:
|
|
58
|
+
"""`node`'s direct children, in the same order as `ast.iter_child_nodes`.
|
|
59
|
+
|
|
60
|
+
A list rather than a generator: every caller either iterates it once or
|
|
61
|
+
extends a stack with it, and the hand-rolled walkers in this package call
|
|
62
|
+
this once per node, where the generator's setup cost dominates the work.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The child nodes, in field-declaration order.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
out: list[ast.AST] = []
|
|
69
|
+
for name in node._fields:
|
|
70
|
+
value: object = getattr(node, name, None)
|
|
71
|
+
if isinstance(value, list):
|
|
72
|
+
out += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
73
|
+
elif isinstance(value, _AST):
|
|
74
|
+
out.append(value)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
79
|
+
"""Yield `node` and every descendant, breadth-first.
|
|
80
|
+
|
|
81
|
+
Identical in order to `ast.walk`, but reads each node's fields directly
|
|
82
|
+
instead of routing every node through two intermediate generators.
|
|
83
|
+
|
|
84
|
+
Yields:
|
|
85
|
+
`node` first, then its descendants breadth-first.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
queue: list[ast.AST] = [node]
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(queue):
|
|
91
|
+
current = queue[i]
|
|
92
|
+
i += 1
|
|
93
|
+
for name in current._fields:
|
|
94
|
+
value: object = getattr(current, name, None)
|
|
95
|
+
if isinstance(value, list):
|
|
96
|
+
queue += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
97
|
+
elif isinstance(value, _AST):
|
|
98
|
+
queue.append(value)
|
|
99
|
+
yield current
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@final
|
|
103
|
+
class _NodeIndex:
|
|
104
|
+
"""A module's nodes partitioned by exact class, in breadth-first order."""
|
|
105
|
+
|
|
106
|
+
__slots__ = ("_buckets", "_flat", "_queries")
|
|
107
|
+
|
|
108
|
+
def __init__(self, tree: ast.AST) -> None:
|
|
109
|
+
buckets: dict[type[ast.AST], list[ast.AST]] = {}
|
|
110
|
+
flat: list[ast.AST] = [tree]
|
|
111
|
+
i = 0
|
|
112
|
+
while i < len(flat):
|
|
113
|
+
current = flat[i]
|
|
114
|
+
i += 1
|
|
115
|
+
cls = current.__class__
|
|
116
|
+
bucket = buckets.get(cls)
|
|
117
|
+
if bucket is None:
|
|
118
|
+
buckets[cls] = [current]
|
|
119
|
+
else:
|
|
120
|
+
bucket.append(current)
|
|
121
|
+
for name in cls._fields:
|
|
122
|
+
value: object = getattr(current, name, None)
|
|
123
|
+
if isinstance(value, list):
|
|
124
|
+
flat += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
125
|
+
elif isinstance(value, _AST):
|
|
126
|
+
flat.append(value)
|
|
127
|
+
self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
|
|
128
|
+
self._flat: list[ast.AST] = flat
|
|
129
|
+
self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
|
|
130
|
+
|
|
131
|
+
def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
|
|
132
|
+
"""Return every node matching `isinstance(node, types)`, breadth-first.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The match list, cached per `types` and shared — treat as read-only.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
hit = self._queries.get(types)
|
|
139
|
+
if hit is not None:
|
|
140
|
+
return hit
|
|
141
|
+
matched = frozenset(cls for cls in self._buckets if issubclass(cls, types))
|
|
142
|
+
if len(matched) == 1:
|
|
143
|
+
# The single bucket is already the answer, in breadth-first order.
|
|
144
|
+
result = self._buckets[next(iter(matched))]
|
|
145
|
+
elif len(matched) == len(self._buckets):
|
|
146
|
+
# Every class present matches — e.g. `nodes(tree, ast.AST)`, asked by
|
|
147
|
+
# a rule whose loop body inspects every node rather than a few types.
|
|
148
|
+
result = self._flat
|
|
149
|
+
elif matched:
|
|
150
|
+
result = [node for node in self._flat if node.__class__ in matched]
|
|
151
|
+
else:
|
|
152
|
+
result = []
|
|
153
|
+
self._queries[types] = result
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_last_index: tuple[ast.AST, _NodeIndex] | None = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT]) -> list[NodeT]:
|
|
161
|
+
"""Every node of `tree` matching `types`, in `ast.walk` order.
|
|
162
|
+
|
|
163
|
+
A drop-in, memoized replacement for the ubiquitous
|
|
164
|
+
`[n for n in ast.walk(tree) if isinstance(n, types)]`. Pass the *module*
|
|
165
|
+
tree: the index is per-file, so calling this with a subtree builds a fresh
|
|
166
|
+
index and evicts the module's. Use `walk` for subtrees.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The matching nodes, breadth-first. A fresh list per call; the partition
|
|
170
|
+
it is drawn from is what gets cached.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
global _last_index # ruff: ignore[global-statement] — single-slot memo, mirroring `parse_or_none`
|
|
174
|
+
if _last_index is None or _last_index[0] is not tree:
|
|
175
|
+
_last_index = (tree, _NodeIndex(tree))
|
|
176
|
+
# The `isinstance` pass is what narrows `list[ast.AST]` to `list[NodeT]`
|
|
177
|
+
# honestly, rather than asserting it with a `cast` the house style bans. It
|
|
178
|
+
# runs over the matches only — never the tree — and every element passes by
|
|
179
|
+
# construction, so it costs one type check per node actually returned.
|
|
180
|
+
return [node for node in _last_index[1].query(types) if isinstance(node, types)]
|
|
@@ -180,7 +180,134 @@ _CAMEL_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+")
|
|
|
180
180
|
# the prototype's list: shrinking it costs recall, growing it costs precision by
|
|
181
181
|
# letting a genuinely novel word be discounted.
|
|
182
182
|
STOPWORDS: frozenset[str] = frozenset(
|
|
183
|
-
[
|
|
183
|
+
[
|
|
184
|
+
"a",
|
|
185
|
+
"an",
|
|
186
|
+
"the",
|
|
187
|
+
"this",
|
|
188
|
+
"that",
|
|
189
|
+
"these",
|
|
190
|
+
"those",
|
|
191
|
+
"it",
|
|
192
|
+
"its",
|
|
193
|
+
"their",
|
|
194
|
+
"his",
|
|
195
|
+
"her",
|
|
196
|
+
"our",
|
|
197
|
+
"your",
|
|
198
|
+
"my",
|
|
199
|
+
"is",
|
|
200
|
+
"are",
|
|
201
|
+
"was",
|
|
202
|
+
"were",
|
|
203
|
+
"be",
|
|
204
|
+
"been",
|
|
205
|
+
"being",
|
|
206
|
+
"am",
|
|
207
|
+
"do",
|
|
208
|
+
"does",
|
|
209
|
+
"did",
|
|
210
|
+
"done",
|
|
211
|
+
"doing",
|
|
212
|
+
"has",
|
|
213
|
+
"have",
|
|
214
|
+
"had",
|
|
215
|
+
"having",
|
|
216
|
+
"will",
|
|
217
|
+
"would",
|
|
218
|
+
"shall",
|
|
219
|
+
"should",
|
|
220
|
+
"can",
|
|
221
|
+
"could",
|
|
222
|
+
"may",
|
|
223
|
+
"might",
|
|
224
|
+
"must",
|
|
225
|
+
"and",
|
|
226
|
+
"or",
|
|
227
|
+
"but",
|
|
228
|
+
"nor",
|
|
229
|
+
"so",
|
|
230
|
+
"yet",
|
|
231
|
+
"not",
|
|
232
|
+
"no",
|
|
233
|
+
"none",
|
|
234
|
+
"to",
|
|
235
|
+
"of",
|
|
236
|
+
"for",
|
|
237
|
+
"in",
|
|
238
|
+
"on",
|
|
239
|
+
"at",
|
|
240
|
+
"by",
|
|
241
|
+
"with",
|
|
242
|
+
"from",
|
|
243
|
+
"into",
|
|
244
|
+
"onto",
|
|
245
|
+
"out",
|
|
246
|
+
"up",
|
|
247
|
+
"down",
|
|
248
|
+
"over",
|
|
249
|
+
"under",
|
|
250
|
+
"about",
|
|
251
|
+
"as",
|
|
252
|
+
"if",
|
|
253
|
+
"then",
|
|
254
|
+
"than",
|
|
255
|
+
"when",
|
|
256
|
+
"where",
|
|
257
|
+
"which",
|
|
258
|
+
"who",
|
|
259
|
+
"whom",
|
|
260
|
+
"whose",
|
|
261
|
+
"what",
|
|
262
|
+
"how",
|
|
263
|
+
"why",
|
|
264
|
+
"while",
|
|
265
|
+
"we",
|
|
266
|
+
"you",
|
|
267
|
+
"they",
|
|
268
|
+
"i",
|
|
269
|
+
"he",
|
|
270
|
+
"she",
|
|
271
|
+
"them",
|
|
272
|
+
"him",
|
|
273
|
+
"us",
|
|
274
|
+
"me",
|
|
275
|
+
"also",
|
|
276
|
+
"just",
|
|
277
|
+
"only",
|
|
278
|
+
"even",
|
|
279
|
+
"still",
|
|
280
|
+
"already",
|
|
281
|
+
"again",
|
|
282
|
+
"there",
|
|
283
|
+
"here",
|
|
284
|
+
"all",
|
|
285
|
+
"any",
|
|
286
|
+
"each",
|
|
287
|
+
"every",
|
|
288
|
+
"some",
|
|
289
|
+
"via",
|
|
290
|
+
"per",
|
|
291
|
+
"etc",
|
|
292
|
+
"eg",
|
|
293
|
+
"ie",
|
|
294
|
+
"vs",
|
|
295
|
+
"need",
|
|
296
|
+
"needs",
|
|
297
|
+
"needed",
|
|
298
|
+
"want",
|
|
299
|
+
"wants",
|
|
300
|
+
"make",
|
|
301
|
+
"makes",
|
|
302
|
+
"making",
|
|
303
|
+
"let",
|
|
304
|
+
"lets",
|
|
305
|
+
"please",
|
|
306
|
+
"note",
|
|
307
|
+
"see",
|
|
308
|
+
"above",
|
|
309
|
+
"below",
|
|
310
|
+
]
|
|
184
311
|
)
|
|
185
312
|
|
|
186
313
|
|
{sarj_python_lint-0.25.0 → sarj_python_lint-0.27.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
@@ -50,23 +50,25 @@ _PROJECT_MARKER = "pyproject.toml"
|
|
|
50
50
|
|
|
51
51
|
# Never descend into these: a virtualenv or vendored tree holds *third-party*
|
|
52
52
|
# packages, and collecting their names would classify every dependency as ours.
|
|
53
|
-
_SKIP_DIR_NAMES = frozenset(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
_SKIP_DIR_NAMES = frozenset(
|
|
54
|
+
{
|
|
55
|
+
".git",
|
|
56
|
+
".mypy_cache",
|
|
57
|
+
".next",
|
|
58
|
+
".pytest_cache",
|
|
59
|
+
".ruff_cache",
|
|
60
|
+
".tox",
|
|
61
|
+
".turbo",
|
|
62
|
+
".venv",
|
|
63
|
+
"__pycache__",
|
|
64
|
+
"build",
|
|
65
|
+
"coverage",
|
|
66
|
+
"dist",
|
|
67
|
+
"node_modules",
|
|
68
|
+
"site-packages",
|
|
69
|
+
"venv",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
70
72
|
|
|
71
73
|
# Deep enough for a uv/pnpm-style monorepo (`<root>/packages/python/src/<pkg>`),
|
|
72
74
|
# shallow enough that the scan stays a few hundred `iterdir()` calls.
|
|
@@ -119,6 +121,10 @@ def own_top_package(path: Path) -> str | None:
|
|
|
119
121
|
top = ancestor.name
|
|
120
122
|
if ancestor == root:
|
|
121
123
|
break
|
|
124
|
+
if top is None and root is not None:
|
|
125
|
+
roots = _first_party_roots(root)
|
|
126
|
+
if len(roots) == 1:
|
|
127
|
+
return next(iter(roots))
|
|
122
128
|
return top
|
|
123
129
|
|
|
124
130
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared logging-receiver detection for SARJ012/SARJ017.
|
|
2
|
+
|
|
3
|
+
A single resolver for "does this receiver expression evaluate to a logger?",
|
|
4
|
+
used by both the secret-in-log and f-string-in-log rules so they recognise the
|
|
5
|
+
same factory/builder forms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_LOGGER_NAMES = frozenset({"logger", "log", "logging", "loguru", "_logger", "_log"})
|
|
14
|
+
|
|
15
|
+
_LOGGER_FACTORIES = frozenset({"getlogger", "get_logger"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_logger_expr(expr: ast.expr) -> bool:
|
|
19
|
+
"""Report whether `expr` evaluates to a logger.
|
|
20
|
+
|
|
21
|
+
Resolves the whole receiver chain so adapter/builder/factory calls are
|
|
22
|
+
caught: `logger.bind(...).info(...)`, `logger.opt(lazy=True).debug(...)`,
|
|
23
|
+
`logging.getLogger(__name__).info(...)`, `self.logger.error(...)`, and the
|
|
24
|
+
bare-name factory `get_logger().info(...)`.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
True when `expr` resolves to a logger receiver.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(expr, ast.Name):
|
|
31
|
+
return expr.id.lower() in _LOGGER_NAMES
|
|
32
|
+
if isinstance(expr, ast.Attribute):
|
|
33
|
+
if expr.attr.lower() in _LOGGER_NAMES or expr.attr.lower() in _LOGGER_FACTORIES:
|
|
34
|
+
return True
|
|
35
|
+
return is_logger_expr(expr.value)
|
|
36
|
+
if isinstance(expr, ast.Call):
|
|
37
|
+
# A factory names a logger only when it is *called*: `get_logger()` is a
|
|
38
|
+
# logger, the bare name `get_logger` is a function — which is why the
|
|
39
|
+
# factories live in their own set and not in `_LOGGER_NAMES`. Both the
|
|
40
|
+
# dotted spelling (`structlog.get_logger()`, `logging.getLogger()`) and
|
|
41
|
+
# the bare one (`from structlog import get_logger`, then `get_logger()`)
|
|
42
|
+
# have to be recognised right here: a bare callee is an `ast.Name`, and
|
|
43
|
+
# recursing on it lands in the `_LOGGER_NAMES` branch, which by that
|
|
44
|
+
# design does not carry the factory names. Omitting this second check is
|
|
45
|
+
# a silent false negative rather than a crash, so it went unnoticed —
|
|
46
|
+
# `get_logger().info("auth", token=token)`, structlog's own documented
|
|
47
|
+
# module-level idiom, was unlinted by both SARJ012 and SARJ017.
|
|
48
|
+
callee = expr.func
|
|
49
|
+
if isinstance(callee, ast.Attribute) and callee.attr.lower() in _LOGGER_FACTORIES:
|
|
50
|
+
return True
|
|
51
|
+
if isinstance(callee, ast.Name) and callee.id.lower() in _LOGGER_FACTORIES:
|
|
52
|
+
return True
|
|
53
|
+
return is_logger_expr(callee)
|
|
54
|
+
return False
|
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import ast
|
|
15
15
|
|
|
16
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
# The pytest-benchmark fixture: the test measures time, it does not verify.
|
|
18
20
|
_BENCHMARK = "benchmark"
|
|
@@ -33,7 +35,7 @@ def uses_benchmark_fixture(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool
|
|
|
33
35
|
"""
|
|
34
36
|
args = node.args
|
|
35
37
|
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
|
|
38
|
+
return declared and any(isinstance(child, ast.Name) and child.id == _BENCHMARK for child in walk(node))
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def has_benchmark_marker(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
@@ -27,6 +27,7 @@ from sarj_python_lint.rules.no_first_party_private_import import (
|
|
|
27
27
|
)
|
|
28
28
|
from sarj_python_lint.rules.no_fstring_in_log import NoFstringInLog
|
|
29
29
|
from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
|
|
30
|
+
from sarj_python_lint.rules.no_implicit_attribute_access import NoImplicitAttributeAccess
|
|
30
31
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
31
32
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
32
33
|
from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
@@ -57,6 +58,7 @@ from sarj_python_lint.rules.prefer_fstring_over_concat import PreferFstringOverC
|
|
|
57
58
|
from sarj_python_lint.rules.prefer_library_fake import PreferLibraryFake
|
|
58
59
|
from sarj_python_lint.rules.prefer_match_assert_never import PreferMatchAssertNever
|
|
59
60
|
from sarj_python_lint.rules.prefer_match_pattern_destructuring import PreferMatchPatternDestructuring
|
|
61
|
+
from sarj_python_lint.rules.prefer_match_type_dispatch import PreferMatchTypeDispatch
|
|
60
62
|
from sarj_python_lint.rules.prefer_module_level_constant import (
|
|
61
63
|
PreferModuleLevelConstant,
|
|
62
64
|
)
|
|
@@ -67,7 +69,9 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
|
67
69
|
PreferNonNullableCollection,
|
|
68
70
|
)
|
|
69
71
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
72
|
+
from sarj_python_lint.rules.prefer_pattern_matching import PreferPatternMatching
|
|
70
73
|
from sarj_python_lint.rules.prefer_real_store_in_tests import PreferRealStoreInTests
|
|
74
|
+
from sarj_python_lint.rules.prefer_self_type_annotation import PreferSelfTypeAnnotation
|
|
71
75
|
from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
72
76
|
from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
73
77
|
PreferStructOverNamedtuple,
|
|
@@ -75,6 +79,14 @@ from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
|
75
79
|
from sarj_python_lint.rules.prefer_timedelta_for_durations import (
|
|
76
80
|
PreferTimedeltaForDurations,
|
|
77
81
|
)
|
|
82
|
+
from sarj_python_lint.rules.prefer_walrus_comprehension_filter import (
|
|
83
|
+
PreferWalrusComprehensionFilter,
|
|
84
|
+
)
|
|
85
|
+
from sarj_python_lint.rules.prefer_walrus_regex_match import PreferWalrusRegexMatch
|
|
86
|
+
from sarj_python_lint.rules.prefer_walrus_stream_loop import PreferWalrusStreamLoop
|
|
87
|
+
from sarj_python_lint.rules.primary_export_file_name import (
|
|
88
|
+
PrimaryExportFileName,
|
|
89
|
+
)
|
|
78
90
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
79
91
|
from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
80
92
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
@@ -98,34 +110,6 @@ from sarj_python_lint.rules.zero_assertion_test import ZeroAssertionTest
|
|
|
98
110
|
if TYPE_CHECKING:
|
|
99
111
|
from sarj_python_lint.rule_base import Rule
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
# Retired codes — never reuse these for new rules:
|
|
103
|
-
# SARJ004, SARJ005 (retired before the standards merge),
|
|
104
|
-
# SARJ027, SARJ029, SARJ030 (dropped in 0.11.1 as too noisy),
|
|
105
|
-
# SARJ033 httpx-client-requires-timeout, SARJ035 no-import-time-settings
|
|
106
|
-
# (dropped by user veto after the 0.13.x mined-rules review),
|
|
107
|
-
# SARJ055 no-filler-success-adverb (built and corpus-validated, then dropped
|
|
108
|
-
# — but not for the reason first recorded. The headline "4.7% of Airflow's
|
|
109
|
-
# info/debug logs" is a composition artifact: 237 of those 248 hits are in
|
|
110
|
-
# `providers/` (contributed vendor operators, heavily copy-pasted — the
|
|
111
|
-
# template "%s completed successfully." appears 22 times verbatim). Airflow's
|
|
112
|
-
# maintainer-owned `airflow-core/` sits at 1.15% and Home Assistant at 1.24%,
|
|
113
|
-
# so the real external baseline is ~1.2%, not 4.7%. The rule was re-measured
|
|
114
|
-
# and dropped on a stronger basis: the narrow variant — fire only when the
|
|
115
|
-
# adverb is the sole content beyond a bare verb — has 12 external hits and
|
|
116
|
-
# ZERO internal ones across all six repos, so it would govern nothing we
|
|
117
|
-
# write. The broad rule remains opt-in house style, not a defect check.
|
|
118
|
-
# (Noted for any future revisit: noura-be measures 7.92%, a genuine outlier
|
|
119
|
-
# against every corpus; that, not the narrow variant, is the case to make.),
|
|
120
|
-
# SARJ037 no-trivial-single-use-helper (prototyped and dropped for FP rate;
|
|
121
|
-
# see the 0.13.1 inlining commit for the corpus analysis).
|
|
122
|
-
# SARJ072 unbound-mock-assertion and SARJ073 raises-needs-specific-error
|
|
123
|
-
# (built, corpus-measured and dropped before ever being registered, in the
|
|
124
|
-
# wave that added SARJ058-071). SARJ072 found 0 hits across 16,130 files in
|
|
125
|
-
# 9 repos, including 0 against 8,322 real mock-assertion API uses; ruff B018
|
|
126
|
-
# covers its bare-statement shape. SARJ073 was a strict superset of ruff
|
|
127
|
-
# PT011, which this standard already enables — its one real gap,
|
|
128
|
-
# RuntimeError, is closed by `lint.flake8-pytest-style.raises-require-match-for`.
|
|
129
113
|
REGISTRY: dict[str, type[Rule]] = {
|
|
130
114
|
NoSequentialAwait.id: NoSequentialAwait,
|
|
131
115
|
InefficientStringConcatInLoop.id: InefficientStringConcatInLoop,
|
|
@@ -133,6 +117,7 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
133
117
|
PreferStrEnum.id: PreferStrEnum,
|
|
134
118
|
NoFatTryBlocks.id: NoFatTryBlocks,
|
|
135
119
|
NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
|
|
120
|
+
NoImplicitAttributeAccess.id: NoImplicitAttributeAccess,
|
|
136
121
|
NoOffsetPagination.id: NoOffsetPagination,
|
|
137
122
|
PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
|
|
138
123
|
NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
|
|
@@ -188,8 +173,15 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
188
173
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
189
174
|
PreferMatchPatternDestructuring.id: PreferMatchPatternDestructuring,
|
|
190
175
|
PreferOrPattern.id: PreferOrPattern,
|
|
176
|
+
PreferPatternMatching.id: PreferPatternMatching,
|
|
191
177
|
RequirePortForService.id: RequirePortForService,
|
|
192
178
|
PreferNonNullableCollection.id: PreferNonNullableCollection,
|
|
179
|
+
PreferMatchTypeDispatch.id: PreferMatchTypeDispatch,
|
|
180
|
+
PrimaryExportFileName.id: PrimaryExportFileName,
|
|
181
|
+
PreferWalrusRegexMatch.id: PreferWalrusRegexMatch,
|
|
182
|
+
PreferWalrusComprehensionFilter.id: PreferWalrusComprehensionFilter,
|
|
183
|
+
PreferWalrusStreamLoop.id: PreferWalrusStreamLoop,
|
|
184
|
+
PreferSelfTypeAnnotation.id: PreferSelfTypeAnnotation,
|
|
193
185
|
}
|
|
194
186
|
|
|
195
187
|
__all__ = ["REGISTRY"]
|