sarj-python-lint 0.80.13__tar.gz → 0.80.15__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 (125) hide show
  1. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/pyproject.toml +1 -1
  3. sarj_python_lint-0.80.15/src/sarj_python_lint/__init__.py +2 -0
  4. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_ratchet_cli.py +3 -0
  5. sarj_python_lint-0.80.15/src/sarj_python_lint/rules/__init__.py +3 -0
  6. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_registry.py +2 -2
  7. sarj_python_lint-0.80.15/src/sarj_python_lint/rules/no_dunder_all.py +364 -0
  8. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/unused_mock_setup.py +202 -58
  9. sarj_python_lint-0.80.13/src/sarj_python_lint/__init__.py +0 -5
  10. sarj_python_lint-0.80.13/src/sarj_python_lint/rules/__init__.py +0 -6
  11. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/.gitignore +0 -0
  12. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/LICENSE +0 -0
  13. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/README.md +0 -0
  14. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/__main__.py +0 -0
  15. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/py.typed +0 -0
  20. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/ratchet.py +0 -0
  21. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rule_base.py +0 -0
  22. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  23. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_comments.py +0 -0
  24. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  25. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  26. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_first_party.py +0 -0
  27. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  28. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_imports.py +0 -0
  29. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_logging.py +0 -0
  30. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_paths.py +0 -0
  31. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_project_index.py +0 -0
  32. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  33. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_pytest.py +0 -0
  34. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_sql.py +0 -0
  35. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  36. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  37. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  38. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  39. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  40. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  41. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  42. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  43. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  44. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  45. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  46. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  47. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  48. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  49. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  50. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  51. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  52. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  53. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  54. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  55. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  56. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  57. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  58. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  59. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  60. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  61. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  62. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  63. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  64. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  65. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  66. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  67. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  68. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  69. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  70. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  71. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  72. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  73. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  74. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  75. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  76. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  77. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  78. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  79. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  80. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  81. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  82. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  83. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  84. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  85. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  86. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  87. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  88. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  89. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  90. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  91. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  92. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  93. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  94. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  95. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  96. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  97. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  98. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  99. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  100. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  101. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  102. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  103. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  104. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  105. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  106. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  107. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  108. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  109. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  110. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  111. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  112. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  113. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  114. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  115. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  116. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  117. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  118. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  119. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/stepdown.py +0 -0
  120. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  121. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  122. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  123. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  124. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  125. {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.80.13
3
+ Version: 0.80.15
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.13"
3
+ version = "0.80.15"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -0,0 +1,2 @@
1
+ from sarj_python_lint._ratchet_cli import run_ratchet as run_ratchet
2
+ from sarj_python_lint._version import __version__ as __version__
@@ -249,5 +249,8 @@ def _update(
249
249
  return 0
250
250
 
251
251
 
252
+ run_ratchet = main
253
+
254
+
252
255
  if __name__ == "__main__":
253
256
  sys.exit(main())
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ from sarj_python_lint.rules._registry import REGISTRY as REGISTRY
@@ -33,6 +33,7 @@ from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInherit
33
33
  from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
34
34
  NoCorsWildcardWithCredentials,
35
35
  )
36
+ from sarj_python_lint.rules.no_dunder_all import NoDunderAll
36
37
  from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
37
38
  from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
38
39
  from sarj_python_lint.rules.no_file_level_escape_hatch_suppression import (
@@ -224,6 +225,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
224
225
  PreferSetIsdisjoint.id: PreferSetIsdisjoint,
225
226
  PreferSelfDocumentingConstant.id: PreferSelfDocumentingConstant,
226
227
  NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
228
+ NoDunderAll.id: NoDunderAll,
227
229
  NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
228
230
  RedundantClassDocstring.id: RedundantClassDocstring,
229
231
  RedundantModuleDocstring.id: RedundantModuleDocstring,
@@ -250,5 +252,3 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
250
252
  TypedErrorReasons.id: TypedErrorReasons,
251
253
  }
252
254
  )
253
-
254
- __all__ = ["REGISTRY"]
@@ -0,0 +1,364 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, NamedTuple, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ parse_or_none,
17
+ )
18
+ from sarj_python_lint.rules._paths import is_generated
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from pathlib import Path
23
+
24
+
25
+ class _Location(NamedTuple):
26
+ line: int
27
+ column: int
28
+
29
+
30
+ @final
31
+ class NoDunderAll(Rule):
32
+ id = "no-dunder-all"
33
+ code = "SARJ438"
34
+ documentation = RuleDocumentation(
35
+ summary="modules should not define or mutate `__all__`",
36
+ rationale=(
37
+ "`__all__` creates a second, mutable wildcard-import surface that can drift from runtime bindings; "
38
+ "Sarj code uses explicit imports and private-name conventions instead."
39
+ ),
40
+ remediation=(
41
+ "Before deleting `__all__`, replace wildcard-import consumers with explicit imports, retain each intended "
42
+ "binding, and prefix implementation-only bindings with `_`. Use self-aliased imports to mark intentional "
43
+ "re-exports to static tooling."
44
+ ),
45
+ category=RuleCategory.MAINTAINABILITY,
46
+ autofix=AutofixPolicy.NONE,
47
+ limitations=(
48
+ (
49
+ "Reads, attributes on other objects, indirect mutation through arbitrary calls, and ordinary local "
50
+ "variables are not reported; the rule targets module-owned bindings and provable mutations."
51
+ ),
52
+ "Writes inside a function or class are reported only when that lexical scope declares `global __all__`.",
53
+ "Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
54
+ "Dynamic construction through `globals()`, `setattr()`, or `exec()` is not inferred.",
55
+ (
56
+ "No autofix is offered because removal can change `module.__all__` introspection, wildcard imports, "
57
+ "underscore-name exports, ordering, and dynamic or conditional export behavior."
58
+ ),
59
+ ),
60
+ examples=(
61
+ RuleExample(
62
+ example_id="duplicated-export-list",
63
+ title="Do not repeat an explicit re-export list",
64
+ outcome=ExampleOutcome.MATCH,
65
+ files=(
66
+ ExampleFile.python(
67
+ "diagnostics/__init__.py",
68
+ 'from .models import Diagnostic as Diagnostic\n\n__all__ = ["Diagnostic"]\n',
69
+ ),
70
+ ),
71
+ focus_path=PurePosixPath("diagnostics/__init__.py"),
72
+ expected_count=1,
73
+ public=True,
74
+ ),
75
+ RuleExample(
76
+ example_id="explicit-re-export",
77
+ title="Make the re-export explicit in the import",
78
+ outcome=ExampleOutcome.NO_MATCH,
79
+ files=(
80
+ ExampleFile.python(
81
+ "diagnostics/__init__.py",
82
+ "from .models import Diagnostic as Diagnostic\n",
83
+ ),
84
+ ),
85
+ focus_path=PurePosixPath("diagnostics/__init__.py"),
86
+ expected_count=0,
87
+ public=True,
88
+ ),
89
+ ),
90
+ )
91
+ description = documentation.summary
92
+
93
+ @override
94
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
95
+ if path.suffix != ".py" or "__all__" not in source or is_generated(path, source):
96
+ return []
97
+ tree = parse_or_none(path, source)
98
+ if tree is None:
99
+ return []
100
+
101
+ findings: list[Diagnostic] = []
102
+ for statement in _module_statements(tree.body):
103
+ if (location := _dunder_all_location(statement)) is None:
104
+ continue
105
+ findings.append(
106
+ Diagnostic(
107
+ path=path,
108
+ line=location.line,
109
+ col=location.column,
110
+ code=self.code,
111
+ message="Remove the module-owned `__all__` binding or mutation; use explicit imports instead.",
112
+ )
113
+ )
114
+ return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.code))
115
+
116
+
117
+ def _module_statements(statements: list[ast.stmt]) -> list[ast.stmt]:
118
+ result: list[ast.stmt] = []
119
+ pending = list(reversed(statements))
120
+ while pending:
121
+ statement = pending.pop()
122
+ result.append(statement)
123
+ nested = _module_control_flow_bodies(statement)
124
+ pending.extend(reversed(nested))
125
+ return result
126
+
127
+
128
+ def _module_control_flow_bodies(statement: ast.stmt) -> list[ast.stmt]:
129
+ match statement:
130
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
131
+ return [*statement.body, *statement.orelse]
132
+ case ast.With() | ast.AsyncWith():
133
+ return list(statement.body)
134
+ case ast.Try() | ast.TryStar():
135
+ return [
136
+ *statement.body,
137
+ *(nested for handler in statement.handlers for nested in handler.body),
138
+ *statement.orelse,
139
+ *statement.finalbody,
140
+ ]
141
+ case ast.Match():
142
+ return [nested for match_case in statement.cases for nested in match_case.body]
143
+ case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() if _declares_global_dunder_all(statement.body):
144
+ return list(statement.body)
145
+ case _:
146
+ return []
147
+
148
+
149
+ def _dunder_all_location(statement: ast.stmt) -> _Location | None:
150
+ visitor = _DunderAllVisitor()
151
+ visitor.visit(statement)
152
+ return min(visitor.locations, default=None)
153
+
154
+
155
+ _MUTATING_METHODS = frozenset({"append", "clear", "extend", "insert", "pop", "remove", "reverse", "sort"})
156
+
157
+
158
+ class _ScopePruningVisitor(ast.NodeVisitor):
159
+ def __init__(self) -> None:
160
+ self.locations: list[_Location] = []
161
+
162
+ def _record(self, node: ast.expr | ast.stmt | ast.alias | ast.ExceptHandler | ast.pattern) -> None:
163
+ self.locations.append(_Location(node.lineno, node.col_offset + 1))
164
+
165
+ @override
166
+ def visit_Name(self, node: ast.Name) -> None:
167
+ if node.id == "__all__" and isinstance(node.ctx, (ast.Store, ast.Del)):
168
+ self._record(node)
169
+
170
+ @override
171
+ def visit_alias(self, node: ast.alias) -> None:
172
+ bound_name = node.asname or node.name.split(".", maxsplit=1)[0]
173
+ if bound_name == "__all__":
174
+ self._record(node)
175
+
176
+ @override
177
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
178
+ if node.name == "__all__":
179
+ self._record(node)
180
+ self._visit_definition_header(node)
181
+
182
+ @override
183
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
184
+ if node.name == "__all__":
185
+ self._record(node)
186
+ self._visit_definition_header(node)
187
+
188
+ def _visit_definition_header(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
189
+ for decorator in node.decorator_list:
190
+ self.visit(decorator)
191
+ for default in (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None)):
192
+ self.visit(default)
193
+ for argument in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs):
194
+ if argument.annotation is not None:
195
+ self.visit(argument.annotation)
196
+ if node.args.vararg is not None and node.args.vararg.annotation is not None:
197
+ self.visit(node.args.vararg.annotation)
198
+ if node.args.kwarg is not None and node.args.kwarg.annotation is not None:
199
+ self.visit(node.args.kwarg.annotation)
200
+ if node.returns is not None:
201
+ self.visit(node.returns)
202
+ for type_param in node.type_params:
203
+ self.visit(type_param)
204
+
205
+ @override
206
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
207
+ if node.name == "__all__":
208
+ self._record(node)
209
+ for decorator in node.decorator_list:
210
+ self.visit(decorator)
211
+ for base in node.bases:
212
+ self.visit(base)
213
+ for keyword in node.keywords:
214
+ self.visit(keyword.value)
215
+ for type_param in node.type_params:
216
+ self.visit(type_param)
217
+
218
+ @override
219
+ def visit_Lambda(self, node: ast.Lambda) -> None:
220
+ del node
221
+
222
+ @override
223
+ def visit_If(self, node: ast.If) -> None:
224
+ self.visit(node.test)
225
+
226
+ @override
227
+ def visit_For(self, node: ast.For) -> None:
228
+ self.visit(node.target)
229
+ self.visit(node.iter)
230
+
231
+ @override
232
+ def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
233
+ self.visit(node.target)
234
+ self.visit(node.iter)
235
+
236
+ @override
237
+ def visit_While(self, node: ast.While) -> None:
238
+ self.visit(node.test)
239
+
240
+ @override
241
+ def visit_With(self, node: ast.With) -> None:
242
+ self._visit_with_items(node.items)
243
+
244
+ def _visit_with_items(self, items: list[ast.withitem]) -> None:
245
+ for item in items:
246
+ self.visit(item.context_expr)
247
+ if item.optional_vars is not None:
248
+ self.visit(item.optional_vars)
249
+
250
+ @override
251
+ def visit_AsyncWith(self, node: ast.AsyncWith) -> None:
252
+ self._visit_with_items(node.items)
253
+
254
+ @override
255
+ def visit_Try(self, node: ast.Try) -> None:
256
+ self._visit_handlers(node.handlers)
257
+
258
+ @override
259
+ def visit_TryStar(self, node: ast.TryStar) -> None:
260
+ self._visit_handlers(node.handlers)
261
+
262
+ def _visit_handlers(self, handlers: list[ast.ExceptHandler]) -> None:
263
+ for handler in handlers:
264
+ if handler.type is not None:
265
+ self.visit(handler.type)
266
+ if handler.name == "__all__":
267
+ self._record(handler)
268
+
269
+
270
+ class _DunderAllVisitor(_ScopePruningVisitor):
271
+ @override
272
+ def visit_Match(self, node: ast.Match) -> None:
273
+ self.visit(node.subject)
274
+ for match_case in node.cases:
275
+ self.visit(match_case.pattern)
276
+ if match_case.guard is not None:
277
+ self.visit(match_case.guard)
278
+
279
+ @override
280
+ def visit_MatchAs(self, node: ast.MatchAs) -> None:
281
+ if node.pattern is not None:
282
+ self.visit(node.pattern)
283
+ if node.name == "__all__":
284
+ self._record(node)
285
+
286
+ @override
287
+ def visit_MatchStar(self, node: ast.MatchStar) -> None:
288
+ if node.name == "__all__":
289
+ self._record(node)
290
+
291
+ @override
292
+ def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
293
+ for key in node.keys:
294
+ self.visit(key)
295
+ for pattern in node.patterns:
296
+ self.visit(pattern)
297
+ if node.rest == "__all__":
298
+ self._record(node)
299
+
300
+ @override
301
+ def visit_Subscript(self, node: ast.Subscript) -> None:
302
+ if isinstance(node.ctx, (ast.Store, ast.Del)) and _is_dunder_all_root(node.value):
303
+ self._record(node.value)
304
+ self.generic_visit(node)
305
+
306
+ @override
307
+ def visit_Attribute(self, node: ast.Attribute) -> None:
308
+ if isinstance(node.ctx, (ast.Store, ast.Del)) and _is_dunder_all_root(node.value):
309
+ self._record(node.value)
310
+ self.generic_visit(node)
311
+
312
+ @override
313
+ def visit_Call(self, node: ast.Call) -> None:
314
+ match node.func:
315
+ case ast.Attribute(value=ast.Name(id="__all__") as owner, attr=method) if method in _MUTATING_METHODS:
316
+ self._record(owner)
317
+ case _:
318
+ pass
319
+ self.generic_visit(node)
320
+
321
+ def _visit_comprehension(self, node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp) -> None:
322
+ if isinstance(node, ast.DictComp):
323
+ self.visit(node.key)
324
+ self.visit(node.value)
325
+ else:
326
+ self.visit(node.elt)
327
+ for generator in node.generators:
328
+ self.visit(generator.iter)
329
+ for condition in generator.ifs:
330
+ self.visit(condition)
331
+
332
+ @override
333
+ def visit_ListComp(self, node: ast.ListComp) -> None:
334
+ self._visit_comprehension(node)
335
+
336
+ @override
337
+ def visit_SetComp(self, node: ast.SetComp) -> None:
338
+ self._visit_comprehension(node)
339
+
340
+ @override
341
+ def visit_DictComp(self, node: ast.DictComp) -> None:
342
+ self._visit_comprehension(node)
343
+
344
+ @override
345
+ def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
346
+ self._visit_comprehension(node)
347
+
348
+
349
+ def _is_dunder_all_root(node: ast.expr) -> bool:
350
+ while isinstance(node, (ast.Attribute, ast.Subscript)):
351
+ node = node.value
352
+ return isinstance(node, ast.Name) and node.id == "__all__"
353
+
354
+
355
+ def _declares_global_dunder_all(statements: list[ast.stmt]) -> bool:
356
+ pending: list[ast.AST] = list(statements)
357
+ while pending:
358
+ node = pending.pop()
359
+ if isinstance(node, ast.Global) and "__all__" in node.names:
360
+ return True
361
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
362
+ continue
363
+ pending.extend(ast.iter_child_nodes(node))
364
+ return False