sarj-python-lint 0.82.0__tar.gz → 0.82.1__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 (129) hide show
  1. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +98 -15
  4. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/.gitignore +0 -0
  5. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/LICENSE +0 -0
  6. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/README.md +0 -0
  7. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/__init__.py +0 -0
  8. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/__main__.py +0 -0
  9. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  10. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_filesystem.py +0 -0
  11. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_python_target.py +0 -0
  12. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  13. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_secret_names.py +0 -0
  14. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/_version.py +0 -0
  15. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/py.typed +0 -0
  16. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/ratchet.py +0 -0
  17. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rule_base.py +0 -0
  18. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  19. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  20. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  21. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  22. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  23. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  24. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  25. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  26. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  27. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  28. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  29. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  30. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  31. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  32. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  33. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  34. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  35. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  36. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  37. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  38. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  39. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  40. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  41. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  42. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  43. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  44. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  45. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  46. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  47. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  48. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  49. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  50. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  51. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
  52. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  53. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  54. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  55. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  56. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  57. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  58. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  59. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  60. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  61. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  62. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  63. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  64. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  65. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  66. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  67. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  68. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  69. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  70. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  71. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  72. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  73. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  74. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  75. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  76. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  77. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  78. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  79. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  80. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  81. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  82. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  83. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  84. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  85. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  86. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  87. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  88. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  89. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  90. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  91. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  92. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  93. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  94. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  95. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  96. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  97. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  98. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  99. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  100. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  101. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  102. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  103. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  104. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  105. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  106. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  107. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  108. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  109. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  110. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  111. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  112. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  113. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  114. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  115. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  116. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  117. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  118. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  119. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  120. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  121. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  122. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  123. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  124. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  125. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  126. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  127. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  128. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  129. {sarj_python_lint-0.82.0 → sarj_python_lint-0.82.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.82.0
3
+ Version: 0.82.1
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.82.0"
3
+ version = "0.82.1"
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" }]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from pathlib import PurePosixPath
5
+ import re
5
6
  from typing import TYPE_CHECKING, ClassVar, final, override
6
7
 
7
8
  from sarj_python_lint.rule_base import (
@@ -24,6 +25,7 @@ if TYPE_CHECKING:
24
25
 
25
26
 
26
27
  _SYS_SOURCES = frozenset({"sys"})
28
+ _PRIVATE_ALIAS_CANDIDATE_RE = re.compile(r"(?m)^(?![\d_])\w+(?:\s*:[^=\n]+)?(?:\s*=\s*(?!_)\w+)*\s*=\s*_(?!_)\w")
27
29
 
28
30
 
29
31
  @final
@@ -31,25 +33,67 @@ class NoRedundantModuleAliasExports(Rule):
31
33
  id: str = "no-redundant-module-alias-exports"
32
34
  code: str = "SARJ440"
33
35
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
- summary="Do not replace the current module through `sys.modules`.",
36
+ summary="Do not manufacture public APIs from private names or replace the current module.",
35
37
  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
+ "Binding a public name to a private implementation creates two names for one API and disguises the "
39
+ "intended visibility boundary. Replacing the current module similarly mutates import identity at runtime "
40
+ "in a way static tools cannot model. Both forms make navigation, introspection, and compatibility ownership "
41
+ "surprising."
38
42
  ),
39
43
  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."
44
+ "Define maintained APIs under their public names and update local callers directly. Import the canonical "
45
+ "path inside maintained code. If a compatibility module is still required, expose its supported names with "
46
+ "explicit same-name imports grouped by source module."
42
47
  ),
43
48
  category=RuleCategory.MAINTAINABILITY,
44
49
  autofix=AutofixPolicy.NONE,
45
50
  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.",
51
+ (
52
+ "Public-to-private name aliases are checked only as direct module-body assignments; assignments "
53
+ "through an unconditional, unshadowed stdlib `sys` import include module-level control flow."
54
+ ),
55
+ "Aliases inside module control flow, functions, classes, and `TYPE_CHECKING` branches are excluded.",
56
+ (
57
+ "Private targets, dunder sources, attribute values, destructuring, imports, and same-visibility aliases "
58
+ "are excluded."
59
+ ),
48
60
  "Literal-key registrations, child-module registrations, method calls, reads, and arbitrary registries are excluded.",
49
61
  "Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
50
62
  "No autofix is offered because the intended compatibility surface and identity requirements need review.",
51
63
  ),
52
64
  examples=(
65
+ RuleExample(
66
+ example_id="public-alias-for-private-helper",
67
+ title="Define a public helper under its public name",
68
+ outcome=ExampleOutcome.MATCH,
69
+ files=(
70
+ ExampleFile.python(
71
+ "pagination.py",
72
+ "def _encode_cursor(value: str) -> str:\n"
73
+ " return value\n\n\n"
74
+ "encode_phone_number_cursor = _encode_cursor\n",
75
+ ),
76
+ ),
77
+ focus_path=PurePosixPath("pagination.py"),
78
+ expected_count=1,
79
+ public=True,
80
+ scenario="public-private-alias",
81
+ ),
82
+ RuleExample(
83
+ example_id="public-helper-definition",
84
+ title="Give the implementation its intended public name",
85
+ outcome=ExampleOutcome.NO_MATCH,
86
+ files=(
87
+ ExampleFile.python(
88
+ "pagination.py",
89
+ "def encode_phone_number_cursor(value: str) -> str:\n return value\n",
90
+ ),
91
+ ),
92
+ focus_path=PurePosixPath("pagination.py"),
93
+ expected_count=0,
94
+ public=True,
95
+ scenario="public-private-alias",
96
+ ),
53
97
  RuleExample(
54
98
  example_id="current-module-replacement",
55
99
  title="Do not replace a compatibility module at runtime",
@@ -65,6 +109,7 @@ class NoRedundantModuleAliasExports(Rule):
65
109
  focus_path=PurePosixPath("legacy/settings.py"),
66
110
  expected_count=1,
67
111
  public=True,
112
+ scenario="module-replacement",
68
113
  ),
69
114
  RuleExample(
70
115
  example_id="explicit-compatibility-exports",
@@ -79,6 +124,7 @@ class NoRedundantModuleAliasExports(Rule):
79
124
  focus_path=PurePosixPath("legacy/settings.py"),
80
125
  expected_count=0,
81
126
  public=True,
127
+ scenario="module-replacement",
82
128
  ),
83
129
  ),
84
130
  )
@@ -86,14 +132,17 @@ class NoRedundantModuleAliasExports(Rule):
86
132
 
87
133
  @override
88
134
  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):
135
+ has_module_replacement_candidate = "modules" in source and "__name__" in source
136
+ if (
137
+ path.suffix != ".py"
138
+ or (not has_module_replacement_candidate and _PRIVATE_ALIAS_CANDIDATE_RE.search(source) is None)
139
+ or is_generated(path, source)
140
+ ):
90
141
  return []
91
142
  tree = parse_or_none(path, source)
92
143
  if tree is None:
93
144
  return []
94
145
  imports = ImportIndex.from_tree(tree, module_scope_only=True)
95
- if not imports.builtin_is_unshadowed("__name__"):
96
- return []
97
146
  direct_sys_bindings = _direct_sys_bindings(tree)
98
147
  findings = [
99
148
  Diagnostic(
@@ -102,17 +151,51 @@ class NoRedundantModuleAliasExports(Rule):
102
151
  col=target.col_offset + 1,
103
152
  code=self.code,
104
153
  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."
154
+ f"Do not expose private `{source_name}` as public `{target.id}`; define the implementation under "
155
+ "its public name and update local callers directly."
107
156
  ),
108
157
  )
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)
158
+ for statement in tree.body
159
+ for target, source_name in _public_private_aliases(statement)
112
160
  ]
161
+ if imports.builtin_is_unshadowed("__name__"):
162
+ findings.extend(
163
+ Diagnostic(
164
+ path=path,
165
+ line=target.lineno,
166
+ col=target.col_offset + 1,
167
+ code=self.code,
168
+ message=(
169
+ "Do not replace the current module through `sys.modules[__name__]`; expose compatibility names "
170
+ "with explicit imports and update maintained callers to the canonical module."
171
+ ),
172
+ )
173
+ for statement in _module_statements(tree.body)
174
+ for target in _assignment_targets(statement)
175
+ if _is_current_module_target(target, imports, direct_sys_bindings)
176
+ )
113
177
  return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.code))
114
178
 
115
179
 
180
+ def _public_private_aliases(statement: ast.stmt) -> list[tuple[ast.Name, str]]:
181
+ match statement:
182
+ case ast.Assign(targets=targets, value=ast.Name(id=source_name)):
183
+ pass
184
+ case ast.AnnAssign(target=target, value=ast.Name(id=source_name)):
185
+ targets = [target]
186
+ case _:
187
+ return []
188
+ if not _is_private_name(source_name):
189
+ return []
190
+ return [
191
+ (target, source_name) for target in targets if isinstance(target, ast.Name) and not target.id.startswith("_")
192
+ ]
193
+
194
+
195
+ def _is_private_name(name: str) -> bool:
196
+ return len(name) > 1 and name.startswith("_") and not name.startswith("__")
197
+
198
+
116
199
  def _direct_sys_bindings(tree: ast.Module) -> frozenset[str]:
117
200
  bindings: set[str] = set()
118
201
  for statement in tree.body: