sarj-python-lint 0.25.0__tar.gz → 0.26.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.26.0}/.gitignore +1 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/PKG-INFO +6 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/README.md +5 -5
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/ratchet.py +8 -8
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rule_base.py +1 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_comments.py +128 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_first_party.py +23 -17
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_registry.py +20 -28
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +2 -2
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/duplicate_test_body.py +9 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +8 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/mock_without_spec.py +1 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_comment_cruft.py +67 -15
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +1 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +2 -2
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/no_implicit_attribute_access.py +106 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +2 -2
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_restated_comment.py +2 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +2 -2
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/over_mocked_test.py +65 -17
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +33 -6
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +3 -3
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +72 -12
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +275 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +3 -6
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_pattern_matching.py +340 -0
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_self_type_annotation.py +159 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_str_enum.py +1 -4
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +125 -0
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +143 -0
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +133 -0
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/primary_export_file_name.py +149 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/redundant_docstring.py +97 -17
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/require_port_for_service.py +6 -2
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/stepdown.py +4 -4
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/trailing_value_narration.py +65 -14
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +1 -1
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/zero_assertion_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.26.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
|
|
|
@@ -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.26.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
|
|
|
@@ -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"]
|
|
@@ -901,9 +901,9 @@ def _parametrized_nonempty(node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[
|
|
|
901
901
|
def _parametrize_names(dec: ast.Call) -> list[str]:
|
|
902
902
|
argnames = dec.args[0] if dec.args else None
|
|
903
903
|
if isinstance(argnames, ast.Constant) and isinstance(argnames.value, str):
|
|
904
|
-
return [
|
|
904
|
+
return [p for part in argnames.value.split(",") if (p := part.strip())]
|
|
905
905
|
if isinstance(argnames, (ast.List, ast.Tuple)):
|
|
906
|
-
return [
|
|
906
|
+
return [val for elt in argnames.elts if isinstance(elt, ast.Constant) and isinstance(val := elt.value, str)]
|
|
907
907
|
return []
|
|
908
908
|
|
|
909
909
|
|
|
@@ -294,6 +294,15 @@ class DuplicateTestBody(Rule):
|
|
|
294
294
|
groups = _duplicate_groups(tree, source)
|
|
295
295
|
except tokenize.TokenError, IndentationError, SyntaxError:
|
|
296
296
|
return []
|
|
297
|
+
except RecursionError:
|
|
298
|
+
# Normalisation deep-copies each body and `ast.dump`s it, both of
|
|
299
|
+
# which recurse once per AST level. A left-nested `+` spine of a few
|
|
300
|
+
# thousand terms — generated or minified source, never hand-written;
|
|
301
|
+
# the deepest real test-file AST measured across 43k files is 33 —
|
|
302
|
+
# exhausts the stack. Returning nothing skips this one file, where
|
|
303
|
+
# letting it propagate would abort the whole lint run: `check()` is
|
|
304
|
+
# called per file by a CLI that does not catch it.
|
|
305
|
+
return []
|
|
297
306
|
|
|
298
307
|
diags = [
|
|
299
308
|
Diagnostic(
|
|
@@ -301,9 +301,9 @@ def _calls_super_same_name(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool
|
|
|
301
301
|
|
|
302
302
|
"""
|
|
303
303
|
return any(
|
|
304
|
-
isinstance(call.func, ast.Attribute)
|
|
305
|
-
and
|
|
306
|
-
and isinstance(inner :=
|
|
304
|
+
isinstance(func := call.func, ast.Attribute)
|
|
305
|
+
and func.attr == node.name
|
|
306
|
+
and isinstance(inner := func.value, ast.Call)
|
|
307
307
|
and isinstance(inner.func, ast.Name)
|
|
308
308
|
and inner.func.id == "super"
|
|
309
309
|
for call in ast.walk(node)
|
|
@@ -362,9 +362,11 @@ def _swap_prone_annotation(args: ast.arguments) -> str | None:
|
|
|
362
362
|
if isinstance(ann := p.annotation, ast.Name) and ann.id in _PRIMITIVES:
|
|
363
363
|
groups.setdefault(ann.id, []).append(p.arg)
|
|
364
364
|
for name, arg_names in sorted(groups.items(), key=lambda kv: -len(kv[1])):
|
|
365
|
-
if
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
if (
|
|
366
|
+
len(arg_names) >= _MIN_SAME_TYPE
|
|
367
|
+
and not (_is_symmetric_numbering(arg_names) or _is_conventional_order(arg_names))
|
|
368
|
+
and _is_high_value_group(name, arg_names)
|
|
369
|
+
):
|
|
368
370
|
return name
|
|
369
371
|
return None
|
|
370
372
|
|
{sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
@@ -126,6 +126,7 @@ _PATCHERS = frozenset({"patch"})
|
|
|
126
126
|
# Any one of these gives the double a real contract to honour.
|
|
127
127
|
_SPEC_KEYWORDS = frozenset({"spec", "spec_set", "autospec", "new", "new_callable", "wraps"})
|
|
128
128
|
|
|
129
|
+
|
|
129
130
|
# How many positional arguments a callee must receive before the spec/new
|
|
130
131
|
# parameter is filled. `Mock(spec, wraps, ...)`, `patch(target, new, ...)` and
|
|
131
132
|
# `patch.object(target, attribute, new, ...)` all take it positionally, and that
|
{sarj_python_lint-0.25.0 → sarj_python_lint-0.26.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
@@ -178,8 +178,6 @@ _DIRECTIVE_PREFIXES = (
|
|
|
178
178
|
"flake8:",
|
|
179
179
|
"nosec",
|
|
180
180
|
"nosemgrep",
|
|
181
|
-
"todo",
|
|
182
|
-
"fixme",
|
|
183
181
|
"hack",
|
|
184
182
|
"xxx:",
|
|
185
183
|
"-*-",
|
|
@@ -249,7 +247,7 @@ _SENTENCE_END_RE = re.compile(r"""[.!?:;)\]}"'`]$""")
|
|
|
249
247
|
# holds") aren't mistaken for an enumeration marker.
|
|
250
248
|
_STEP_NARRATION_RE = re.compile(
|
|
251
249
|
r"^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|after(?:wards| that)?"
|
|
252
|
-
r"|finally|lastly|now)\s*[,:]\s*\S
|
|
250
|
+
r"|finally|lastly|now)\s*[,:]\s*\S",
|
|
253
251
|
re.IGNORECASE,
|
|
254
252
|
)
|
|
255
253
|
|
|
@@ -274,14 +272,49 @@ _META_COMMENTARY_RE = re.compile(
|
|
|
274
272
|
# demo-gateway, 12 of 12 sampled were true positives. Closed vocabulary on
|
|
275
273
|
# purpose: a one-word comment outside this list ("# Riyadh") is far more likely
|
|
276
274
|
# to be a genuine label for a value.
|
|
277
|
-
_SECTION_LABEL_WORDS = frozenset(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
275
|
+
_SECTION_LABEL_WORDS = frozenset(
|
|
276
|
+
{
|
|
277
|
+
"actions",
|
|
278
|
+
"components",
|
|
279
|
+
"config",
|
|
280
|
+
"configuration",
|
|
281
|
+
"constant",
|
|
282
|
+
"constants",
|
|
283
|
+
"enums",
|
|
284
|
+
"exports",
|
|
285
|
+
"fixtures",
|
|
286
|
+
"getters",
|
|
287
|
+
"globals",
|
|
288
|
+
"handler",
|
|
289
|
+
"handlers",
|
|
290
|
+
"helper",
|
|
291
|
+
"helpers",
|
|
292
|
+
"hook",
|
|
293
|
+
"hooks",
|
|
294
|
+
"imports",
|
|
295
|
+
"interfaces",
|
|
296
|
+
"main",
|
|
297
|
+
"mocks",
|
|
298
|
+
"models",
|
|
299
|
+
"mutations",
|
|
300
|
+
"props",
|
|
301
|
+
"queries",
|
|
302
|
+
"reducers",
|
|
303
|
+
"routes",
|
|
304
|
+
"schemas",
|
|
305
|
+
"selectors",
|
|
306
|
+
"setters",
|
|
307
|
+
"setup",
|
|
308
|
+
"state",
|
|
309
|
+
"styles",
|
|
310
|
+
"teardown",
|
|
311
|
+
"type",
|
|
312
|
+
"types",
|
|
313
|
+
"util",
|
|
314
|
+
"utilities",
|
|
315
|
+
"utils",
|
|
316
|
+
}
|
|
317
|
+
)
|
|
285
318
|
_SECTION_LABEL_RE = re.compile(r"^([A-Za-z]+)\s*:?\s*$")
|
|
286
319
|
|
|
287
320
|
# "Helper function to check if a path is active" — the opener announces the
|
|
@@ -315,7 +348,13 @@ _LETS_RE = re.compile(
|
|
|
315
348
|
# `# Phase 2: reconcile`. Flagged only when the file carries exactly one of
|
|
316
349
|
# them — a *run* of them is a documented algorithm walkthrough, which is the
|
|
317
350
|
# kind of comment this rule exists to protect.
|
|
318
|
-
_ENUMERATION_RE = re.compile(r"^(?:\d+[.)]\s+\S|phase\s+\d+\b)", re.IGNORECASE)
|
|
351
|
+
_ENUMERATION_RE = re.compile(r"^(?:\d+[.)]\s+\S|(?:phase|step)\s+\d+\b)", re.IGNORECASE)
|
|
352
|
+
|
|
353
|
+
# Dummy translational comments: ultra-short comments that just restate the code.
|
|
354
|
+
_DUMMY_TRANSLATION_RE = re.compile(
|
|
355
|
+
r"^(?:increment|return|returns|get|gets|set\b(?! up\b)|sets\b(?! up\b)|function to|method to)\b",
|
|
356
|
+
re.IGNORECASE,
|
|
357
|
+
)
|
|
319
358
|
|
|
320
359
|
|
|
321
360
|
def _is_word_char(ch: str) -> bool:
|
|
@@ -493,6 +532,14 @@ def _is_redundant_narration(
|
|
|
493
532
|
return True
|
|
494
533
|
if _HELPER_OPENER_RE.match(c) or _LETS_RE.match(c):
|
|
495
534
|
return True
|
|
535
|
+
words = c.split()
|
|
536
|
+
if len(words) <= 4 and _DUMMY_TRANSLATION_RE.match(c) and not any(ch in c for ch in "():="):
|
|
537
|
+
# Exclude common rationale words or test markers
|
|
538
|
+
lower_c = c.lower()
|
|
539
|
+
if not any(w in lower_c for w in ("when", "because", "if", "so that", "due to", "for", "instead of", "to prevent", "to avoid", "only")):
|
|
540
|
+
# Also exclude single-word labels like `# get` which are often group labels in tests
|
|
541
|
+
if len(words) > 1:
|
|
542
|
+
return True
|
|
496
543
|
if not nested and _is_section_label(c):
|
|
497
544
|
return True
|
|
498
545
|
return isolated_enumeration and bool(_ENUMERATION_RE.match(c))
|
|
@@ -559,6 +606,9 @@ def _looks_like_code(body: str) -> bool:
|
|
|
559
606
|
if c.startswith("@"):
|
|
560
607
|
return _compiles(c + "\ndef _f():\n pass")
|
|
561
608
|
if _ASSIGN_OR_CALL_RE.match(c):
|
|
609
|
+
# Implicit type condition documentation (e.g. under `else:`)
|
|
610
|
+
if c.startswith("isinstance(") or c.startswith("issubclass("):
|
|
611
|
+
return False
|
|
562
612
|
return _is_assign_or_call(c)
|
|
563
613
|
return False
|
|
564
614
|
|
|
@@ -660,6 +710,10 @@ class NoCommentCruft(Rule):
|
|
|
660
710
|
) -> str | None:
|
|
661
711
|
if _CODE_REGEN_CALL_RE.match(body):
|
|
662
712
|
return None
|
|
713
|
+
if re.match(r"^(?:todo|fixme)\b", body, re.IGNORECASE):
|
|
714
|
+
if not narration_protected:
|
|
715
|
+
return "Untracked TODO/FIXME marker — add an issue ticket or context link."
|
|
716
|
+
return None
|
|
663
717
|
if _is_banner(body):
|
|
664
718
|
if _is_heading_underline(body, prev_body):
|
|
665
719
|
return None
|
|
@@ -670,9 +724,7 @@ class NoCommentCruft(Rule):
|
|
|
670
724
|
return "Commented-out code — delete it; git history remembers."
|
|
671
725
|
if narration_protected:
|
|
672
726
|
return None
|
|
673
|
-
if _is_redundant_narration(
|
|
674
|
-
body, prev_body, isolated_enumeration=isolated_enumeration, nested=nested
|
|
675
|
-
):
|
|
727
|
+
if _is_redundant_narration(body, prev_body, isolated_enumeration=isolated_enumeration, nested=nested):
|
|
676
728
|
return "Comment narrates the code — delete it or say why, not what. Code is self-documenting."
|
|
677
729
|
return None
|
|
678
730
|
|
|
@@ -48,7 +48,7 @@ class NoCorsWildcardWithCredentials(Rule):
|
|
|
48
48
|
for node in ast.walk(tree):
|
|
49
49
|
if not isinstance(node, ast.Call):
|
|
50
50
|
continue
|
|
51
|
-
keywords = {
|
|
51
|
+
keywords = {arg: kw.value for kw in node.keywords if (arg := kw.arg) is not None}
|
|
52
52
|
credentials = keywords.get("allow_credentials")
|
|
53
53
|
origins = keywords.get("allow_origins")
|
|
54
54
|
if credentials is None or origins is None:
|
|
@@ -148,9 +148,9 @@ def _from_import_hits(node: ast.ImportFrom) -> list[tuple[int, int, str, str]]:
|
|
|
148
148
|
if private_segment is not None:
|
|
149
149
|
return [(node.lineno, node.col_offset + 1, node.module, private_segment)]
|
|
150
150
|
return [
|
|
151
|
-
(alias.lineno, alias.col_offset + 1, node.module,
|
|
151
|
+
(alias.lineno, alias.col_offset + 1, node.module, name)
|
|
152
152
|
for alias in node.names
|
|
153
|
-
if _is_private_name(alias.name)
|
|
153
|
+
if _is_private_name(name := alias.name)
|
|
154
154
|
]
|
|
155
155
|
|
|
156
156
|
|