sarj-python-lint 0.81.1__tar.gz → 0.81.3__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 (126) hide show
  1. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_dunder_all.py +2 -1
  5. sarj_python_lint-0.81.3/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +203 -0
  6. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/.gitignore +0 -0
  7. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/LICENSE +0 -0
  8. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/README.md +0 -0
  9. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_version.py +0 -0
  17. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/py.typed +0 -0
  18. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/ratchet.py +0 -0
  19. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rule_base.py +0 -0
  20. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/__init__.py +0 -0
  21. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  22. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_comments.py +0 -0
  23. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  24. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  25. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
  26. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  27. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_imports.py +0 -0
  28. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_logging.py +0 -0
  29. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_paths.py +0 -0
  30. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
  31. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  32. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
  33. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_sql.py +0 -0
  34. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  35. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  36. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  37. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  38. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  39. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  40. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  41. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  42. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  43. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  44. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  45. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  46. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  47. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  48. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  49. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  50. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  51. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  52. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  53. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  54. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  55. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  56. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  57. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  58. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  59. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  60. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  61. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  62. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  63. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  64. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  65. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  66. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  67. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  68. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  69. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  70. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  71. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  72. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  73. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  74. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  75. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  76. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  77. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  78. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  79. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  80. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  81. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  82. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  83. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  84. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  85. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  86. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  87. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  88. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  89. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  90. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  91. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  92. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  93. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  94. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  95. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  96. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  97. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  98. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  99. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  100. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  101. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  102. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  103. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  104. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  105. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  106. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  107. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  108. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  109. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  110. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  111. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  112. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  113. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  114. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  115. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  116. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  117. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  118. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  119. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
  120. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  121. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  122. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  123. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  124. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  125. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  126. {sarj_python_lint-0.81.1 → sarj_python_lint-0.81.3}/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.81.1
3
+ Version: 0.81.3
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.81.1"
3
+ version = "0.81.3"
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" }]
@@ -59,6 +59,7 @@ from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
59
59
  from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
60
60
  from sarj_python_lint.rules.no_raw_source_text_test_oracle import NoRawSourceTextTestOracle
61
61
  from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
62
+ from sarj_python_lint.rules.no_redundant_module_alias_exports import NoRedundantModuleAliasExports
62
63
  from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
63
64
  from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
64
65
  from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
@@ -204,6 +205,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
204
205
  RedundantDocstring.id: RedundantDocstring,
205
206
  TrailingValueNarration.id: TrailingValueNarration,
206
207
  NoRandomUuidInSql.id: NoRandomUuidInSql,
208
+ NoRedundantModuleAliasExports.id: NoRedundantModuleAliasExports,
207
209
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
208
210
  NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
209
211
  NoFastapiOnEvent.id: NoFastapiOnEvent,
@@ -40,7 +40,8 @@ class NoDunderAll(Rule):
40
40
  remediation=(
41
41
  "Before deleting `__all__`, replace wildcard-import consumers with explicit imports, retain each intended "
42
42
  "binding, and prefix implementation-only bindings with `_`. Use self-aliased imports to mark intentional "
43
- "re-exports to static tooling."
43
+ "facade re-exports to static tooling. For a whole-module compatibility alias, replace the entry in "
44
+ "`sys.modules` and migrate maintained consumers to the canonical path instead of copying every name."
44
45
  ),
45
46
  category=RuleCategory.MAINTAINABILITY,
46
47
  autofix=AutofixPolicy.NONE,
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, 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._imports import ImportIndex
19
+ from sarj_python_lint.rules._paths import is_generated
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
25
+
26
+ _SYS_SOURCES = frozenset({"sys"})
27
+
28
+
29
+ @final
30
+ class NoRedundantModuleAliasExports(Rule):
31
+ id: str = "no-redundant-module-alias-exports"
32
+ code: str = "SARJ440"
33
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
+ summary="Do not replace the current module through `sys.modules`.",
35
+ rationale=(
36
+ "Replacing the current module mutates import identity at runtime in a way static tools cannot model. "
37
+ "It makes introspection, reloads, circular imports, state, and monkeypatch behavior surprising."
38
+ ),
39
+ remediation=(
40
+ "Import the canonical path inside maintained code. If a compatibility module is still required, expose "
41
+ "its supported names with explicit same-name imports grouped by source module."
42
+ ),
43
+ category=RuleCategory.MAINTAINABILITY,
44
+ autofix=AutofixPolicy.NONE,
45
+ limitations=(
46
+ "Only module-scope assignments through an unconditional, unshadowed stdlib `sys` import are checked.",
47
+ "Assignments inside functions, classes, and `TYPE_CHECKING` branches are excluded.",
48
+ "Literal-key registrations, child-module registrations, method calls, reads, and arbitrary registries are excluded.",
49
+ "Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
50
+ "No autofix is offered because the intended compatibility surface and identity requirements need review.",
51
+ ),
52
+ examples=(
53
+ RuleExample(
54
+ example_id="current-module-replacement",
55
+ title="Do not replace a compatibility module at runtime",
56
+ outcome=ExampleOutcome.MATCH,
57
+ files=(
58
+ ExampleFile.python(
59
+ "legacy/settings.py",
60
+ "import sys\n\n"
61
+ "from canonical import settings as _canonical\n\n\n"
62
+ "sys.modules[__name__] = _canonical\n",
63
+ ),
64
+ ),
65
+ focus_path=PurePosixPath("legacy/settings.py"),
66
+ expected_count=1,
67
+ public=True,
68
+ ),
69
+ RuleExample(
70
+ example_id="explicit-compatibility-exports",
71
+ title="Expose the supported compatibility surface explicitly",
72
+ outcome=ExampleOutcome.NO_MATCH,
73
+ files=(
74
+ ExampleFile.python(
75
+ "legacy/settings.py",
76
+ "from canonical.settings import (\n Settings as Settings,\n load as load,\n)\n",
77
+ ),
78
+ ),
79
+ focus_path=PurePosixPath("legacy/settings.py"),
80
+ expected_count=0,
81
+ public=True,
82
+ ),
83
+ ),
84
+ )
85
+ description: str = documentation.summary
86
+
87
+ @override
88
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
89
+ if path.suffix != ".py" or "modules" not in source or "__name__" not in source or is_generated(path, source):
90
+ return []
91
+ tree = parse_or_none(path, source)
92
+ if tree is None:
93
+ return []
94
+ imports = ImportIndex.from_tree(tree, module_scope_only=True)
95
+ if not imports.builtin_is_unshadowed("__name__"):
96
+ return []
97
+ direct_sys_bindings = _direct_sys_bindings(tree)
98
+ findings = [
99
+ Diagnostic(
100
+ path=path,
101
+ line=target.lineno,
102
+ col=target.col_offset + 1,
103
+ code=self.code,
104
+ message=(
105
+ "Do not replace the current module through `sys.modules[__name__]`; expose compatibility names "
106
+ "with explicit imports and update maintained callers to the canonical module."
107
+ ),
108
+ )
109
+ for statement in _module_statements(tree.body)
110
+ for target in _assignment_targets(statement)
111
+ if _is_current_module_target(target, imports, direct_sys_bindings)
112
+ ]
113
+ return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.code))
114
+
115
+
116
+ def _direct_sys_bindings(tree: ast.Module) -> frozenset[str]:
117
+ bindings: set[str] = set()
118
+ for statement in tree.body:
119
+ match statement:
120
+ case ast.Import(names=names):
121
+ bindings.update(alias.asname or "sys" for alias in names if alias.name == "sys")
122
+ case ast.ImportFrom(module="sys", level=0, names=names):
123
+ bindings.update(alias.asname or "modules" for alias in names if alias.name == "modules")
124
+ case _:
125
+ pass
126
+ return frozenset(bindings)
127
+
128
+
129
+ def _module_statements(statements: list[ast.stmt]) -> list[ast.stmt]:
130
+ result: list[ast.stmt] = []
131
+ pending = list(reversed(statements))
132
+ while pending:
133
+ statement = pending.pop()
134
+ result.append(statement)
135
+ pending.extend(reversed(_module_control_flow_bodies(statement)))
136
+ return result
137
+
138
+
139
+ def _module_control_flow_bodies(statement: ast.stmt) -> list[ast.stmt]:
140
+ match statement:
141
+ case ast.If() if _is_type_checking_guard(statement.test):
142
+ return list(statement.orelse)
143
+ case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
144
+ return [*statement.body, *statement.orelse]
145
+ case ast.With() | ast.AsyncWith():
146
+ return list(statement.body)
147
+ case ast.Try() | ast.TryStar():
148
+ return [
149
+ *statement.body,
150
+ *(nested for handler in statement.handlers for nested in handler.body),
151
+ *statement.orelse,
152
+ *statement.finalbody,
153
+ ]
154
+ case ast.Match():
155
+ return [nested for match_case in statement.cases for nested in match_case.body]
156
+ case _:
157
+ return []
158
+
159
+
160
+ def _assignment_targets(statement: ast.stmt) -> list[ast.expr]:
161
+ match statement:
162
+ case ast.Assign(targets=targets):
163
+ return [nested for target in targets for nested in _nested_targets(target)]
164
+ case ast.AnnAssign(target=target):
165
+ return _nested_targets(target)
166
+ case _:
167
+ return []
168
+
169
+
170
+ def _nested_targets(target: ast.expr) -> list[ast.expr]:
171
+ if isinstance(target, (ast.Tuple, ast.List)):
172
+ return [nested for element in target.elts for nested in _nested_targets(element)]
173
+ return [target]
174
+
175
+
176
+ def _is_current_module_target(
177
+ target: ast.expr,
178
+ imports: ImportIndex,
179
+ direct_sys_bindings: frozenset[str],
180
+ ) -> bool:
181
+ match target:
182
+ case ast.Subscript(
183
+ value=value,
184
+ slice=ast.Name(id="__name__", ctx=ast.Load()),
185
+ ctx=ast.Store(),
186
+ ):
187
+ root = value.value if isinstance(value, ast.Attribute) else value
188
+ return (
189
+ isinstance(root, ast.Name)
190
+ and root.id in direct_sys_bindings
191
+ and imports.resolves(value, sources=_SYS_SOURCES, symbol="modules")
192
+ )
193
+ case _:
194
+ return False
195
+
196
+
197
+ def _is_type_checking_guard(node: ast.expr) -> bool:
198
+ return (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or (
199
+ isinstance(node, ast.Attribute)
200
+ and isinstance(node.value, ast.Name)
201
+ and node.value.id in {"typing", "typing_extensions"}
202
+ and node.attr == "TYPE_CHECKING"
203
+ )