sarj-python-lint 0.80.8__tar.gz → 0.80.11__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 (124) hide show
  1. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_fixed_record.py +3 -8
  4. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_imports.py +2 -6
  5. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_prose_budget.py +7 -1
  6. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +2 -3
  7. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +3 -12
  8. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/mock_without_spec.py +3 -7
  9. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +2 -3
  10. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +7 -14
  11. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +26 -15
  12. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_redundant_literal_description.py +1 -3
  13. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_repeated_string_literal.py +12 -4
  14. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +4 -4
  15. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/over_mocked_test.py +1 -3
  16. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/phase_label_comment.py +29 -16
  17. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_class_row.py +1 -3
  18. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +8 -7
  19. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +1 -3
  20. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +21 -5
  21. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_library_fake.py +7 -12
  22. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_match_assert_never.py +7 -17
  23. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +5 -15
  24. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +4 -7
  25. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +10 -10
  26. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +5 -9
  27. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +12 -15
  28. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +10 -13
  29. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_str_enum.py +18 -21
  30. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +2 -4
  31. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +3 -1
  32. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +5 -1
  33. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/production_derived_test_cases.py +19 -8
  34. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +8 -22
  35. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/redundant_class_docstring.py +1 -6
  36. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/redundant_docstring.py +2 -10
  37. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +3 -2
  38. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/restated_test_docstring.py +6 -3
  39. sarj_python_lint-0.80.11/src/sarj_python_lint/rules/trailing_value_narration.py +295 -0
  40. sarj_python_lint-0.80.8/src/sarj_python_lint/rules/trailing_value_narration.py +0 -197
  41. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/.gitignore +0 -0
  42. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/LICENSE +0 -0
  43. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/README.md +0 -0
  44. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/__init__.py +0 -0
  45. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/__main__.py +0 -0
  46. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/_analysis_session.py +0 -0
  47. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/_filesystem.py +0 -0
  48. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  49. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/_secret_names.py +0 -0
  50. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/_version.py +0 -0
  51. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/py.typed +0 -0
  52. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/ratchet.py +0 -0
  53. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rule_base.py +0 -0
  54. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/__init__.py +0 -0
  55. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  56. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_comments.py +0 -0
  57. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  58. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  59. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_first_party.py +0 -0
  60. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_logging.py +0 -0
  61. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_paths.py +0 -0
  62. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_project_index.py +0 -0
  63. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_pytest.py +0 -0
  64. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_registry.py +0 -0
  65. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_sql.py +0 -0
  66. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  67. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  68. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  69. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  70. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  71. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  72. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  73. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  74. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  75. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  76. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  77. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  78. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  79. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  80. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  81. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  82. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  83. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  84. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  85. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  86. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  87. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  88. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  89. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  90. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  91. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  92. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  93. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  94. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  95. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  96. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  97. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  98. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  99. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  100. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  101. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  102. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  103. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  104. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  105. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  106. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  107. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  108. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  109. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  110. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  111. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  112. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  113. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  114. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  115. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  116. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  117. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/stepdown.py +0 -0
  118. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  119. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  120. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  121. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  122. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  123. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  124. {sarj_python_lint-0.80.8 → sarj_python_lint-0.80.11}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.80.8
3
+ Version: 0.80.11
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.80.8"
3
+ version = "0.80.11"
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" }]
@@ -48,9 +48,7 @@ def builds_fixed_record(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
48
48
  else:
49
49
  aliases[target.id] = current.value.id
50
50
  else:
51
- invalidated_names.update(
52
- target.id for target in current.targets if isinstance(target, ast.Name)
53
- )
51
+ invalidated_names.update(target.id for target in current.targets if isinstance(target, ast.Name))
54
52
  for target in current.targets:
55
53
  if isinstance(target, ast.Subscript):
56
54
  invalidated_names.update(_mutated_record_roots(target))
@@ -115,8 +113,7 @@ def builds_fixed_record(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
115
113
  ):
116
114
  return False
117
115
  return any(
118
- _is_record_literal(value)
119
- or (isinstance(value, ast.Name) and original_name(value.id) in intact_record_names)
116
+ _is_record_literal(value) or (isinstance(value, ast.Name) and original_name(value.id) in intact_record_names)
120
117
  for value in returned
121
118
  )
122
119
 
@@ -140,9 +137,7 @@ def _is_record_literal(node: ast.expr) -> bool:
140
137
  return _is_record_literal(node.elt)
141
138
  if not isinstance(node, ast.Dict):
142
139
  return False
143
- return bool(node.keys) and all(
144
- isinstance(key, ast.Constant) and isinstance(key.value, str) for key in node.keys
145
- )
140
+ return bool(node.keys) and all(isinstance(key, ast.Constant) and isinstance(key.value, str) for key in node.keys)
146
141
 
147
142
 
148
143
  def _mutated_record_roots(target: ast.AST) -> set[str]:
@@ -39,9 +39,7 @@ class ImportIndex:
39
39
  pass
40
40
 
41
41
  imported_names = frozenset(candidates)
42
- non_import_bindings = (
43
- _module_non_import_bindings(tree) if module_scope_only else _non_import_bindings(tree)
44
- )
42
+ non_import_bindings = _module_non_import_bindings(tree) if module_scope_only else _non_import_bindings(tree)
45
43
  rebound = non_import_bindings & imported_names
46
44
  bindings = {
47
45
  local: next(iter(targets))
@@ -166,9 +164,7 @@ def _module_import_statements(tree: ast.Module) -> list[ast.stmt]:
166
164
  for statement in tree.body:
167
165
  if not isinstance(statement, ast.If) or not _is_type_checking_guard(statement.test):
168
166
  continue
169
- statements.extend(
170
- child for child in statement.body if isinstance(child, (ast.Import, ast.ImportFrom))
171
- )
167
+ statements.extend(child for child in statement.body if isinstance(child, (ast.Import, ast.ImportFrom)))
172
168
  return statements
173
169
 
174
170
 
@@ -428,7 +428,13 @@ def _yielded_type(node: ast.expr) -> str | None:
428
428
  node = parsed
429
429
  if not isinstance(node, ast.Subscript):
430
430
  return None
431
- name = node.value.attr if isinstance(node.value, ast.Attribute) else node.value.id if isinstance(node.value, ast.Name) else ""
431
+ name = (
432
+ node.value.attr
433
+ if isinstance(node.value, ast.Attribute)
434
+ else node.value.id
435
+ if isinstance(node.value, ast.Name)
436
+ else ""
437
+ )
432
438
  if name not in {"AsyncGenerator", "AsyncIterable", "AsyncIterator", "Generator", "Iterable", "Iterator"}:
433
439
  return None
434
440
  members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
@@ -430,9 +430,8 @@ def _check_return(
430
430
  continue
431
431
  response_model = route.keywords.get("response_model")
432
432
  status = _status_code(route.keywords.get("status_code"), index) or HTTPStatus.OK
433
- if (
434
- _concrete_response_model(response_model, index)
435
- or any(_documents_response_model(node, status, index) for node in _route_response_nodes(route))
433
+ if _concrete_response_model(response_model, index) or any(
434
+ _documents_response_model(node, status, index) for node in _route_response_nodes(route)
436
435
  ):
437
436
  continue
438
437
  return [
@@ -181,9 +181,7 @@ class InvalidPydanticFieldDefault(Rule):
181
181
  files=(
182
182
  ExampleFile.python(
183
183
  "app/models.py",
184
- "from pydantic import BaseModel\n\n"
185
- "class User(BaseModel):\n"
186
- " display_name: str = None\n",
184
+ "from pydantic import BaseModel\n\nclass User(BaseModel):\n display_name: str = None\n",
187
185
  ),
188
186
  ),
189
187
  focus_path=PurePosixPath("app/models.py"),
@@ -231,11 +229,7 @@ class InvalidPydanticFieldDefault(Rule):
231
229
  if statement.target.id.startswith("_"):
232
230
  continue
233
231
  contract = _annotation_contract(path, tree, statement.annotation)
234
- if (
235
- contract.transforms_default
236
- or transformers.all_fields
237
- or statement.target.id in transformers.fields
238
- ):
232
+ if contract.transforms_default or transformers.all_fields or statement.target.id in transformers.fields:
239
233
  continue
240
234
  assignment_field = (
241
235
  statement.value
@@ -276,10 +270,7 @@ def _is_direct_base_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
276
270
  return False
277
271
  return all(
278
272
  imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel")
279
- or (
280
- isinstance(base, ast.Subscript)
281
- and imports.resolves(base.value, sources=_TYPING_SOURCES, symbol="Generic")
282
- )
273
+ or (isinstance(base, ast.Subscript) and imports.resolves(base.value, sources=_TYPING_SOURCES, symbol="Generic"))
283
274
  for base in node.bases
284
275
  )
285
276
 
@@ -333,9 +333,7 @@ class _MockNames:
333
333
 
334
334
  def _is_shadowed(self, name: str, node: ast.AST) -> bool:
335
335
  scope = self.scopes.get(id(node))
336
- return name in self.shadowed or (
337
- scope is not None and name in self.local_shadowed.get(scope, frozenset())
338
- )
336
+ return name in self.shadowed or (scope is not None and name in self.local_shadowed.get(scope, frozenset()))
339
337
 
340
338
 
341
339
  class _FileFacts:
@@ -559,14 +557,12 @@ def _escaped_names(argument: ast.expr) -> set[str]:
559
557
 
560
558
  def _mock_add_spec_has_contract(node: ast.Call) -> bool:
561
559
  if any(
562
- not isinstance(argument, ast.Starred)
563
- and not (isinstance(argument, ast.Constant) and argument.value is None)
560
+ not isinstance(argument, ast.Starred) and not (isinstance(argument, ast.Constant) and argument.value is None)
564
561
  for argument in node.args
565
562
  ):
566
563
  return True
567
564
  return any(
568
- keyword.arg == "spec"
569
- and not (isinstance(keyword.value, ast.Constant) and keyword.value.value is None)
565
+ keyword.arg == "spec" and not (isinstance(keyword.value, ast.Constant) and keyword.value.value is None)
570
566
  for keyword in node.keywords
571
567
  )
572
568
 
@@ -112,7 +112,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
112
112
  ExampleFile.python(
113
113
  "app/event_store.py",
114
114
  "import psycopg\n\n"
115
- 'QUERY = "SELECT DATE_TRUNC(\'day\', occurred_at), COUNT(*), AVG(latency_ms) '
115
+ "QUERY = \"SELECT DATE_TRUNC('day', occurred_at), COUNT(*), AVG(latency_ms) "
116
116
  'FROM event GROUP BY 1"\n',
117
117
  ),
118
118
  ),
@@ -127,8 +127,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
127
127
  files=(
128
128
  ExampleFile.python(
129
129
  "app/account_store.py",
130
- "import psycopg\n\n"
131
- 'QUERY = "SELECT SUM(amount) FROM ledger WHERE account_id = %s FOR UPDATE"\n',
130
+ 'import psycopg\n\nQUERY = "SELECT SUM(amount) FROM ledger WHERE account_id = %s FOR UPDATE"\n',
132
131
  ),
133
132
  ),
134
133
  focus_path=PurePosixPath("app/account_store.py"),
@@ -23,9 +23,7 @@ if TYPE_CHECKING:
23
23
  from pathlib import Path
24
24
 
25
25
 
26
- _PYDANTIC_BASE_MODEL_SOURCES = frozenset(
27
- {"pydantic", "pydantic.main", "pydantic.v1", "pydantic.v1.main"}
28
- )
26
+ _PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main", "pydantic.v1", "pydantic.v1.main"})
29
27
  _PYDANTIC_SETTINGS_SOURCES = frozenset({"pydantic", "pydantic.v1", "pydantic_settings"})
30
28
  _FIELD_VALIDATOR_SOURCES = frozenset({"pydantic", "pydantic.functional_validators"})
31
29
  _LEGACY_VALIDATOR_SOURCES = frozenset(
@@ -199,9 +197,7 @@ def _is_class_var(annotation: ast.expr, imports: ImportIndex) -> bool:
199
197
 
200
198
  def _literal_check_fields_false(decorator: ast.Call) -> bool:
201
199
  return any(
202
- keyword.arg == "check_fields"
203
- and isinstance(keyword.value, ast.Constant)
204
- and keyword.value.value is False
200
+ keyword.arg == "check_fields" and isinstance(keyword.value, ast.Constant) and keyword.value.value is False
205
201
  for keyword in decorator.keywords
206
202
  )
207
203
 
@@ -235,9 +231,7 @@ def _shadowed_in_enclosing_function(
235
231
  return False
236
232
  current = parents.get(owner)
237
233
  while current is not None:
238
- if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)) and _function_binds(
239
- current, root
240
- ):
234
+ if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)) and _function_binds(current, root):
241
235
  return True
242
236
  current = parents.get(current)
243
237
  return False
@@ -251,11 +245,10 @@ def _root_name(node: ast.expr) -> str | None:
251
245
 
252
246
  def _function_binds(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda, name: str) -> bool:
253
247
  arguments = node.args
254
- if any(
255
- argument.arg == name
256
- for argument in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs)
257
- ) or (arguments.vararg is not None and arguments.vararg.arg == name) or (
258
- arguments.kwarg is not None and arguments.kwarg.arg == name
248
+ if (
249
+ any(argument.arg == name for argument in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs))
250
+ or (arguments.vararg is not None and arguments.vararg.arg == name)
251
+ or (arguments.kwarg is not None and arguments.kwarg.arg == name)
259
252
  ):
260
253
  return True
261
254
  body = node.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) else [node.body]
@@ -272,9 +272,7 @@ def _cursors(
272
272
  )
273
273
 
274
274
 
275
- def _initial_bindings(
276
- function: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex
277
- ) -> dict[str, _Shape]:
275
+ def _initial_bindings(function: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex) -> dict[str, _Shape]:
278
276
  result: dict[str, _Shape] = {}
279
277
  for name, annotation in _parameter_annotations(function).items():
280
278
  shape = _connection_annotation_shape(annotation, imports)
@@ -480,7 +478,11 @@ def _connection_context_shape(
480
478
  or _resolves_connection_connect(call.func, imports)
481
479
  ):
482
480
  return _connect_call_shape(call, imports, shadowed)
483
- if isinstance(call.func, ast.Attribute) and call.func.attr == "connection" and isinstance(call.func.value, ast.Name):
481
+ if (
482
+ isinstance(call.func, ast.Attribute)
483
+ and call.func.attr == "connection"
484
+ and isinstance(call.func.value, ast.Name)
485
+ ):
484
486
  return visible.get(call.func.value.id)
485
487
  return None
486
488
 
@@ -509,7 +511,12 @@ def _receiver_shape(
509
511
  ) -> _Shape:
510
512
  if isinstance(receiver, ast.Name):
511
513
  return visible.get(receiver.id, _Shape.UNKNOWN)
512
- if owner is not None and isinstance(receiver, ast.Attribute) and isinstance(receiver.value, ast.Name) and receiver.value.id == "self":
514
+ if (
515
+ owner is not None
516
+ and isinstance(receiver, ast.Attribute)
517
+ and isinstance(receiver.value, ast.Name)
518
+ and receiver.value.id == "self"
519
+ ):
513
520
  if receiver.attr in invalidated_attrs:
514
521
  return _Shape.UNKNOWN
515
522
  return class_attrs.get((owner.lineno, receiver.attr), _Shape.UNKNOWN)
@@ -587,7 +594,9 @@ def _cursor_shape(cursor: _Cursor, fetch: ast.Call, imports: ImportIndex, shadow
587
594
  if _position(statement) >= _position(fetch):
588
595
  break
589
596
  match statement:
590
- case ast.Assign(targets=[ast.Attribute(value=ast.Name(id=name), attr="row_factory")], value=value) if name == cursor.name:
597
+ case ast.Assign(targets=[ast.Attribute(value=ast.Name(id=name), attr="row_factory")], value=value) if (
598
+ name == cursor.name
599
+ ):
591
600
  assignments.append(value)
592
601
  case _ if _assigns_cursor_factory(statement, cursor.name):
593
602
  ambiguous = True
@@ -755,11 +764,7 @@ def _merge_origins(states: Iterable[dict[str, _OriginSet]]) -> dict[str, _Origin
755
764
  materialized = tuple(states)
756
765
  names = {name for state in materialized for name in state}
757
766
  return {
758
- name: frozenset(
759
- origin
760
- for state in materialized
761
- for origin in state.get(name, frozenset({None}))
762
- )
767
+ name: frozenset(origin for state in materialized for origin in state.get(name, frozenset({None})))
763
768
  for name in names
764
769
  }
765
770
 
@@ -789,7 +794,9 @@ def _statement_mutation_roots(statement: ast.stmt) -> frozenset[str]:
789
794
  root: str | None = None
790
795
  if isinstance(node, ast.Attribute | ast.Subscript) and isinstance(node.ctx, ast.Store | ast.Del):
791
796
  root = _root_name(node)
792
- elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in _MUTATING_METHODS:
797
+ elif (
798
+ isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in _MUTATING_METHODS
799
+ ):
793
800
  root = _root_name(node.func.value)
794
801
  if root is not None:
795
802
  roots.add(root)
@@ -894,9 +901,13 @@ def _scope_nodes(statements: Iterable[ast.stmt], *, stop_rebinding: str | None =
894
901
  yield node
895
902
  if isinstance(node, _SCOPE_NODES):
896
903
  continue
897
- if stop_rebinding is not None and isinstance(node, ast.With | ast.AsyncWith) and any(
898
- isinstance(item.optional_vars, ast.Name) and item.optional_vars.id == stop_rebinding
899
- for item in node.items
904
+ if (
905
+ stop_rebinding is not None
906
+ and isinstance(node, ast.With | ast.AsyncWith)
907
+ and any(
908
+ isinstance(item.optional_vars, ast.Name) and item.optional_vars.id == stop_rebinding
909
+ for item in node.items
910
+ )
900
911
  ):
901
912
  continue
902
913
  pending.extend(reversed(list(ast.iter_child_nodes(node))))
@@ -149,9 +149,7 @@ class NoRedundantLiteralDescription(Rule):
149
149
 
150
150
 
151
151
  def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
152
- return any(
153
- imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel") for base in node.bases
154
- )
152
+ return any(imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel") for base in node.bases)
155
153
 
156
154
 
157
155
  def _local_closed_domains(tree: ast.Module, imports: ImportIndex) -> dict[str, frozenset[str]]:
@@ -163,9 +163,7 @@ class NoRepeatedStringLiteral(Rule):
163
163
  function_scopes = {scope for n in nodes if (scope := scope_of.get(id(n), _MODULE_SCOPE)) != _MODULE_SCOPE}
164
164
  canonical = canonical_constants.get(value, ())
165
165
  eligible_canonical = tuple(
166
- name
167
- for name, line in canonical
168
- if all(line < scope_line_of.get(scope, 0) for scope in function_scopes)
166
+ name for name, line in canonical if all(line < scope_line_of.get(scope, 0) for scope in function_scopes)
169
167
  )
170
168
  if len(eligible_canonical) == 1 and function_scopes:
171
169
  (constant_name,) = eligible_canonical
@@ -282,6 +280,16 @@ def _preview(value: str) -> str:
282
280
 
283
281
  def _is_skipped_path(path: Path) -> bool:
284
282
  excluded_parts = frozenset(
285
- {"benchmark", "benchmarks", "example", "examples", "fixture", "fixtures", "migration", "migrations", "snapshots"}
283
+ {
284
+ "benchmark",
285
+ "benchmarks",
286
+ "example",
287
+ "examples",
288
+ "fixture",
289
+ "fixtures",
290
+ "migration",
291
+ "migrations",
292
+ "snapshots",
293
+ }
286
294
  )
287
295
  return is_test_path(path) or bool(excluded_parts.intersection(part.lower() for part in path.parts))
@@ -164,9 +164,7 @@ def _module_import_index(tree: ast.Module) -> ImportIndex:
164
164
  if regular_names:
165
165
  body.append(ast.ImportFrom(module=statement.module, names=regular_names, level=statement.level))
166
166
  body.extend(
167
- ast.Import(
168
- names=[ast.alias(name=f"{statement.module}.errors", asname=alias.asname or alias.name)]
169
- )
167
+ ast.Import(names=[ast.alias(name=f"{statement.module}.errors", asname=alias.asname or alias.name)])
170
168
  for alias in statement.names
171
169
  if alias.name == "errors"
172
170
  )
@@ -311,7 +309,9 @@ def _classification_matches(statements: list[ast.stmt], context: _HandlerContext
311
309
 
312
310
 
313
311
  def _branch_translates(statements: list[ast.stmt]) -> bool:
314
- return any(isinstance(node, (ast.Raise, ast.Return)) for statement in statements for node in _walk_same_scope(statement))
312
+ return any(
313
+ isinstance(node, (ast.Raise, ast.Return)) for statement in statements for node in _walk_same_scope(statement)
314
+ )
315
315
 
316
316
 
317
317
  def _message_matches(node: ast.expr, context: _HandlerContext) -> list[ast.expr]:
@@ -762,9 +762,7 @@ def _parametrized_arguments(
762
762
  continue
763
763
  argnames = _call_argument(decorator, 0, "argnames")
764
764
  if isinstance(argnames, ast.Constant) and isinstance(argnames.value, str):
765
- parametrized.update(
766
- stripped_name for name in argnames.value.split(",") if (stripped_name := name.strip())
767
- )
765
+ parametrized.update(stripped_name for name in argnames.value.split(",") if (stripped_name := name.strip()))
768
766
  elif isinstance(argnames, (ast.List, ast.Tuple)):
769
767
  parametrized.update(
770
768
  element.value
@@ -15,6 +15,8 @@ from sarj_python_lint.rule_base import (
15
15
  RuleCategory,
16
16
  RuleDocumentation,
17
17
  RuleExample,
18
+ Severity,
19
+ parse_or_none,
18
20
  )
19
21
  from sarj_python_lint.rules._comments import nested_comment_lines, standalone_comments
20
22
  from sarj_python_lint.rules._paths import is_generated, is_test_path
@@ -26,44 +28,49 @@ if TYPE_CHECKING:
26
28
 
27
29
  # A bounded whole-body grammar: decoration and joins are ceremony only when
28
30
  # every substantive token is itself a phase label.
29
- _PHASE_WORD = (
30
- r"arrange|act|assert(?:ion)?s?|given|when|then|exercise|execute|"
31
- r"verif(?:y|ication)|cleanup|prepare|sanity(?:\s+check)?"
32
- )
31
+ _PHASE_WORD = r"(?:arrange|act|assert(?:ion)?s?|given|when|then)(?:\s+phase)?"
33
32
  _PHASE_RE = re.compile(
34
- rf"^[-=~*_#.\s]{{0,40}}(?:{_PHASE_WORD})"
35
- rf"(?:\s*(?:[/&+,|]|->|and)\s*(?:{_PHASE_WORD}))*"
33
+ rf"^(?:\d+[.)]\s*)?[-=~*_#.\s]{{0,40}}(?:{_PHASE_WORD})"
34
+ rf"(?:\s*(?:[/&+,|]|-|->|→|and)\s*(?:{_PHASE_WORD}))*"
36
35
  rf"[-=~*_#.\s:;!–—]{{0,40}}$",
37
36
  re.IGNORECASE,
38
37
  )
39
38
 
40
39
 
41
40
  class TestPhaseLabelComment(Rule):
42
- id: str = "test-phase-label-comment"
41
+ id: str = "no-bare-test-phase-comments"
43
42
  code: str = "SARJ089"
44
43
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
45
- summary="Tests must not use bare Arrange, Act, Assert, Given, When, or Then phase comments.",
44
+ summary="Test files must not use bare phase comments such as Arrange, Act, Assert, Given, When, or Then.",
46
45
  rationale="Phase labels narrate test structure without explaining behavior and often indicate that a test needs clearer names or smaller units.",
47
46
  remediation="Delete the label; if the phases remain hard to follow, extract a named helper or split the test.",
48
47
  category=RuleCategory.TESTING,
49
48
  autofix=AutofixPolicy.NONE,
49
+ aliases=("test-phase-label-comment",),
50
50
  limitations=(
51
- "Only standalone comments in recognized test files are checked; nested literal comments and trailing comments are excluded.",
51
+ "Standalone comments across recognized test and test-support files are checked, including fixtures, helpers, and module sections.",
52
+ "Trailing comments and comments inside bracketed expressions are excluded.",
52
53
  "Comments containing words beyond the bounded phase-label grammar are preserved.",
53
54
  ),
54
55
  examples=(
55
56
  RuleExample(
56
- example_id="bare-phase-label",
57
- title="Bare phase label",
57
+ example_id="bare-test-phase-comments",
58
+ title="Bare Arrange, Act, and Assert labels",
58
59
  outcome=ExampleOutcome.MATCH,
59
60
  files=(
60
61
  ExampleFile.python(
61
62
  "tests/test_widget.py",
62
- "def test_widget():\n # Arrange\n widget = make_widget()\n assert widget\n",
63
+ "def test_widget():\n"
64
+ " # Arrange\n"
65
+ " widget = make_widget()\n"
66
+ " # Act\n"
67
+ " result = widget.run()\n"
68
+ " # Assert\n"
69
+ " assert result.ok\n",
63
70
  ),
64
71
  ),
65
72
  focus_path=PurePosixPath("tests/test_widget.py"),
66
- expected_count=1,
73
+ expected_count=3,
67
74
  public=True,
68
75
  ),
69
76
  RuleExample(
@@ -73,7 +80,9 @@ class TestPhaseLabelComment(Rule):
73
80
  files=(
74
81
  ExampleFile.python(
75
82
  "tests/test_widget.py",
76
- "def test_widget():\n # Then the retry loop would spin forever.\n assert works()\n",
83
+ "def test_widget():\n"
84
+ " # A stale token here would authorize the wrong tenant.\n"
85
+ " assert request_is_rejected()\n",
77
86
  ),
78
87
  ),
79
88
  focus_path=PurePosixPath("tests/test_widget.py"),
@@ -86,7 +95,7 @@ class TestPhaseLabelComment(Rule):
86
95
 
87
96
  @override
88
97
  def check(self, path: Path, source: str) -> list[Diagnostic]:
89
- if is_generated(path, source) or not is_test_path(path):
98
+ if is_generated(path, source) or not is_test_path(path) or parse_or_none(path, source) is None:
90
99
  return []
91
100
  try:
92
101
  standalone, _ = standalone_comments(source)
@@ -99,7 +108,11 @@ class TestPhaseLabelComment(Rule):
99
108
  line=line,
100
109
  col=col + 1,
101
110
  code=self.code,
102
- message=self.description,
111
+ message=(
112
+ f"Bare test phase comment {body.strip()!r} narrates structure; delete it or replace it "
113
+ "with non-obvious rationale."
114
+ ),
115
+ severity=Severity.WARNING,
103
116
  column_encoding=ColumnEncoding.CODEPOINTS,
104
117
  )
105
118
  for line, col, body in standalone
@@ -401,9 +401,7 @@ def _model_name(node: ast.expr) -> str | None:
401
401
 
402
402
  def _is_rebound_between(scoped_nodes: list[ast.AST], name: str, start: int, end: int) -> bool:
403
403
  return any(
404
- start < node.lineno < end and _writes_name(node, name)
405
- for node in scoped_nodes
406
- if isinstance(node, ast.stmt)
404
+ start < node.lineno < end and _writes_name(node, name) for node in scoped_nodes if isinstance(node, ast.stmt)
407
405
  )
408
406
 
409
407
 
@@ -50,7 +50,9 @@ _CATEGORY_WORDS = frozenset({"kind", "kinds", "type", "types"})
50
50
  _DESCRIPTOR_WORDS = frozenset(
51
51
  {"id", "ids", "kind", "kinds", "name", "names", "permission", "permissions", "scope", "scopes", "type", "types"}
52
52
  )
53
- _EXTERNAL_PREFIXES = frozenset({"given", "incoming", "presented", "provided", "received", "request", "submitted", "supplied"})
53
+ _EXTERNAL_PREFIXES = frozenset(
54
+ {"given", "incoming", "presented", "provided", "received", "request", "submitted", "supplied"}
55
+ )
54
56
  _EXPECTED_PREFIXES = frozenset({"configured", "current", "expected", "known", "stored"})
55
57
  _AUTH_MAPPING_WORDS = frozenset({"authorization", "cookies", "headers", "path_params", "query_params"})
56
58
  _STORED_OWNER_WORDS = frozenset({"config", "configuration", "self", "session", "settings"})
@@ -340,8 +342,10 @@ def _is_auth_mapping(node: ast.expr) -> bool:
340
342
 
341
343
 
342
344
  def _is_auth_key(node: ast.expr) -> bool:
343
- return isinstance(node, ast.Constant) and isinstance(node.value, str) and (
344
- node.value.lower() == "token" or _is_authenticator_name(node.value)
345
+ return (
346
+ isinstance(node, ast.Constant)
347
+ and isinstance(node.value, str)
348
+ and (node.value.lower() == "token" or _is_authenticator_name(node.value))
345
349
  )
346
350
 
347
351
 
@@ -409,10 +413,7 @@ def _environment_names(module: ast.Module) -> frozenset[str]:
409
413
 
410
414
 
411
415
  def _bound_names(scope: ast.FunctionDef | ast.AsyncFunctionDef) -> frozenset[str]:
412
- names = {
413
- argument.arg
414
- for argument in (*scope.args.posonlyargs, *scope.args.args, *scope.args.kwonlyargs)
415
- }
416
+ names = {argument.arg for argument in (*scope.args.posonlyargs, *scope.args.args, *scope.args.kwonlyargs)}
416
417
  for assignment in _assignments(scope.body):
417
418
  target, _ = _assignment_parts(assignment)
418
419
  if target is not None:
@@ -559,9 +559,7 @@ def _shadowed_producers(statements: list[ast.stmt], before_line: int | None) ->
559
559
  bound = _bound_names(statement)
560
560
  if isinstance(statement, ast.Import):
561
561
  bound.difference_update(
562
- alias.asname or alias.name
563
- for alias in statement.names
564
- if alias.name in {"json", "re"}
562
+ alias.asname or alias.name for alias in statement.names if alias.name in {"json", "re"}
565
563
  )
566
564
  shadowed.update(bound & _PRODUCER_NAMES)
567
565
  return frozenset(shadowed)
@@ -49,7 +49,23 @@ _MUTATING_METHODS = frozenset(
49
49
  )
50
50
  _CONCRETE_COLLECTION_METHODS = _MUTATING_METHODS | {"copy"}
51
51
  _NON_ESCAPING_CALLS = frozenset(
52
- {"all", "any", "dict", "enumerate", "frozenset", "iter", "len", "list", "max", "min", "reversed", "set", "sorted", "sum", "tuple"}
52
+ {
53
+ "all",
54
+ "any",
55
+ "dict",
56
+ "enumerate",
57
+ "frozenset",
58
+ "iter",
59
+ "len",
60
+ "list",
61
+ "max",
62
+ "min",
63
+ "reversed",
64
+ "set",
65
+ "sorted",
66
+ "sum",
67
+ "tuple",
68
+ }
53
69
  )
54
70
 
55
71
 
@@ -57,9 +73,7 @@ class PreferImmutableModuleConstant(Rule):
57
73
  id: str = "prefer-immutable-module-constant"
58
74
  code: str = "SARJ096"
59
75
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
60
- summary=(
61
- "Nonempty uppercase module collections allow top-level membership or keys to change at runtime."
62
- ),
76
+ summary=("Nonempty uppercase module collections allow top-level membership or keys to change at runtime."),
63
77
  rationale=(
64
78
  "A constant-looking collection can expose process-wide top-level mutation even when callers intend it as a "
65
79
  "read-only lookup table."
@@ -259,7 +273,9 @@ class _MutationVisitor(ast.NodeVisitor):
259
273
  self.visit(node.value)
260
274
 
261
275
  def visit_AugAssign(self, node: ast.AugAssign) -> None:
262
- self._record_name(_mutated_target(node.target) or (node.target.id if isinstance(node.target, ast.Name) else None))
276
+ self._record_name(
277
+ _mutated_target(node.target) or (node.target.id if isinstance(node.target, ast.Name) else None)
278
+ )
263
279
  self.visit(node.value)
264
280
 
265
281
  def visit_Delete(self, node: ast.Delete) -> None: