sarj-python-lint 1.69.0__tar.gz → 1.70.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-1.70.0/src/sarj_python_lint/rules/no_redundant_local_alias.py +397 -0
  5. sarj_python_lint-1.70.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +113 -0
  6. sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -83
  7. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/.gitignore +0 -0
  8. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/LICENSE +0 -0
  9. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/README.md +0 -0
  10. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_parallel.py +0 -0
  16. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/cast_access.py +0 -0
  23. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  24. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/json_boundary.py +0 -0
  25. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/py.typed +0 -0
  26. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/ratchet.py +0 -0
  27. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rule_base.py +0 -0
  28. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  29. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  30. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  32. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  38. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  39. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  40. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  41. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  42. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  43. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  44. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  45. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  46. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  47. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  48. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  49. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  50. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  51. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  52. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  53. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  54. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  55. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  56. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  57. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  58. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  59. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  60. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  61. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  62. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  63. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  64. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  65. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  66. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  67. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  68. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  69. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  70. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  71. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  72. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  73. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  74. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  75. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  76. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  77. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  78. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  79. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  80. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  81. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  82. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  83. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  84. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  85. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  86. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  87. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  88. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  89. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  90. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  91. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  92. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  93. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  94. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  95. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  96. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  97. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  98. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  99. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  100. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  101. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  102. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  103. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  104. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  105. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  106. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  107. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  108. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  109. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  110. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  111. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  112. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  113. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  114. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  115. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  116. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  117. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  118. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  119. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  120. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  121. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  122. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  123. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  124. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  125. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  126. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  127. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  128. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  129. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  130. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  131. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  132. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  133. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  134. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  135. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  136. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  137. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  138. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  139. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  140. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  141. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  142. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  143. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  144. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  145. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  146. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  147. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  148. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  149. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  150. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  151. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  152. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  153. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  154. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  155. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  156. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  157. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  158. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  159. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  160. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  161. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  162. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  163. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  164. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  165. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  166. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  167. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  168. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  169. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  170. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  171. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  172. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  173. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  174. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  175. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  176. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  177. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  178. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  179. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  180. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  181. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  182. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  183. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  184. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  185. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  186. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  187. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  188. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  189. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  190. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  191. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  192. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  193. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  194. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  195. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  196. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  197. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  198. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  199. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  200. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  201. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  202. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  203. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  204. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  205. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  206. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  207. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  208. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  209. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  210. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  211. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  212. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  213. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  214. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  215. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  216. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  217. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  218. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  219. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  220. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  221. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  222. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  223. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  224. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  225. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  226. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  227. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  228. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  229. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  230. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  231. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  232. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  233. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  234. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  235. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  236. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  237. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  238. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  239. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  240. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  241. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  242. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  243. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  244. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  245. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  246. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  247. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  248. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  249. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  250. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  251. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  252. {sarj_python_lint-1.69.0 → sarj_python_lint-1.70.0}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.69.0
3
+ Version: 1.70.0
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 = "1.69.0"
3
+ version = "1.70.0"
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" }]
@@ -232,6 +232,7 @@ from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossi
232
232
  from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
233
233
  from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
234
234
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
235
+ from sarj_python_lint.rules.no_redundant_local_alias import NoRedundantLocalAlias
235
236
  from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
236
237
  from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlashCollectionRootUnderPrefix
237
238
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
@@ -254,6 +255,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
254
255
 
255
256
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
256
257
  {
258
+ NoRedundantLocalAlias.id: NoRedundantLocalAlias,
257
259
  PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
258
260
  DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
259
261
  PreferTypedSettingsField.id: PreferTypedSettingsField,
@@ -0,0 +1,397 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ from pathlib import PurePosixPath
6
+ import re
7
+ from typing import TYPE_CHECKING, ClassVar, final, override
8
+ import unicodedata
9
+
10
+ from sarj_python_lint.rule_base import (
11
+ AutofixPolicy,
12
+ Diagnostic,
13
+ ExampleFile,
14
+ ExampleOutcome,
15
+ Rule,
16
+ RuleCategory,
17
+ RuleDocumentation,
18
+ RuleExample,
19
+ Severity,
20
+ )
21
+ from sarj_python_lint.rules._ast_index import children
22
+ from sarj_python_lint.rules._suppression_comments import comment_lines
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Mapping
27
+
28
+ from sarj_python_lint._file_context import PythonFileContext
29
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
30
+
31
+
32
+ _FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
33
+ _SUITES = (*_FUNCTIONS, ast.With, ast.AsyncWith)
34
+ _UNCERTAIN = (
35
+ *_FUNCTIONS,
36
+ ast.ClassDef,
37
+ ast.Lambda,
38
+ ast.ListComp,
39
+ ast.SetComp,
40
+ ast.DictComp,
41
+ ast.GeneratorExp,
42
+ ast.If,
43
+ ast.For,
44
+ ast.AsyncFor,
45
+ ast.While,
46
+ ast.Try,
47
+ ast.TryStar,
48
+ ast.Match,
49
+ ast.Assert,
50
+ ast.BoolOp,
51
+ ast.Global,
52
+ ast.Nonlocal,
53
+ ast.TypeAlias,
54
+ )
55
+ _INITIALIZERS = (ast.Call, ast.Await, ast.Constant, ast.List, ast.Tuple, ast.Dict, ast.Set)
56
+ _REFERENCE_OBSERVERS = frozenset(
57
+ {
58
+ "getrefcount",
59
+ "get_referrers",
60
+ "getattr",
61
+ "attrgetter",
62
+ "TypeAdapter",
63
+ "BaseModel",
64
+ "RootModel",
65
+ "create_model",
66
+ "validate_call",
67
+ "model_rebuild",
68
+ "rebuild_dataclass",
69
+ "new_class",
70
+ }
71
+ )
72
+ _WORDS = re.compile(r"\w+")
73
+ _TYPE_COMMENT = re.compile(r"#\s*type\s*:")
74
+ _NAME_PAIR = re.compile(r"\b(?P<target>[A-Za-z_]\w*)[\s)\\]*=[\s(\\]*(?P<source>[A-Za-z_]\w*)\b(?=[\s),;\\]|$)")
75
+
76
+
77
+ @final
78
+ class NoRedundantLocalAlias(Rule):
79
+ id = "no-redundant-local-alias"
80
+ code = "SARJ531"
81
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
82
+ default_level=Severity.WARNING,
83
+ summary="Remove a local alias used only for its matching keyword argument.",
84
+ rationale="An extra local binding makes readers trace two names for one value when the keyword already communicates its role.",
85
+ remediation="Pass the original local directly to the matching keyword and remove the unannotated alias assignment after review.",
86
+ category=RuleCategory.MAINTAINABILITY,
87
+ autofix=AutofixPolicy.NONE,
88
+ limitations=(
89
+ "Only unannotated, single-target `alias = original` assignments with one binding and one read at `alias=alias` are checked.",
90
+ "The original must be a parameter or an earlier initialized local with one binding; reassignment, deletion, imports, alias chains and uncertain initialization are excluded.",
91
+ "Functions with if statements, loops, try statements, asserts, Boolean short-circuiting, nested scopes, global/nonlocal declarations or type aliases are excluded. Ordinary and async with bodies require ordered enclosing-suite binding evidence.",
92
+ "A conditional expression is admitted only when its test is a bare name bound once as a parameter, and neither local is read within the expression.",
93
+ "Typed aliases, files with type comments, same-scope quoted references and conditional reads of either local are excluded. Captured bindings and module/class aliases are preserved.",
94
+ "Files with wildcard imports, Pydantic imports or references to known namespace readers, dynamic attribute getters or reference-count observers are excluded. Wildcard exports cannot establish builtin identity; computed type names can consume caller-local bindings without a Name read in the AST.",
95
+ "Dynamic class construction and class-valued references to type are excluded. Direct, unshadowed unary type(value) queries and standard-library dataclasses remain supported.",
96
+ "Generated source, stubs and malformed input are excluded. Arbitrary external frame introspection is not modeled.",
97
+ "No autofix is offered; readability judgments, multiple reads, positional forwarding and more complex control flow remain outside the contract.",
98
+ ),
99
+ examples=(
100
+ RuleExample(
101
+ example_id="keyword-supplies-role",
102
+ title="The keyword supplies the role",
103
+ outcome=ExampleOutcome.MATCH,
104
+ files=(
105
+ ExampleFile.python(
106
+ "case.py", "def render(item):\n payload = item\n consume(payload=payload)\n"
107
+ ),
108
+ ),
109
+ focus_path=PurePosixPath("case.py"),
110
+ expected_count=1,
111
+ public=True,
112
+ ),
113
+ RuleExample(
114
+ example_id="direct-keyword",
115
+ title="Pass the original local",
116
+ outcome=ExampleOutcome.NO_MATCH,
117
+ files=(ExampleFile.python("case.py", "def render(item):\n consume(payload=item)\n"),),
118
+ focus_path=PurePosixPath("case.py"),
119
+ expected_count=0,
120
+ public=True,
121
+ ),
122
+ ),
123
+ )
124
+ description = documentation.summary
125
+
126
+ @override
127
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
128
+ if _excluded_file(context):
129
+ return []
130
+ keywords = _matching_keywords(context)
131
+ candidates = _local_assignments(context, set(keywords.values()))
132
+ if not candidates:
133
+ return []
134
+ provenance = context.resource_provenance
135
+ names = {node.targets[0].id for node in candidates if isinstance(node.targets[0], ast.Name)}
136
+ reads = _scope_reads(provenance, names)
137
+ conditional_sources = _conditional_sources(context)
138
+ quoted = _quoted_references(context, provenance, names)
139
+ positions = {statement: i for owner in context.nodes(*_SUITES) for i, statement in enumerate(owner.body)}
140
+ findings: list[Diagnostic] = []
141
+ for assignment in candidates:
142
+ target = assignment.targets[0]
143
+ source = assignment.value
144
+ if not isinstance(target, ast.Name) or not isinstance(source, ast.Name):
145
+ continue
146
+ if (provenance.scope(assignment), target.id) in quoted:
147
+ continue
148
+ use = _single_keyword_read(target, provenance, reads, keywords)
149
+ if use is None:
150
+ continue
151
+ if not _dominates(assignment, use, context.parents, positions):
152
+ continue
153
+ if not _stable_source(source, assignment, provenance, positions, conditional_sources):
154
+ continue
155
+ findings.append(
156
+ Diagnostic(
157
+ path=context.path,
158
+ line=target.lineno,
159
+ col=target.col_offset + 1,
160
+ code=self.code,
161
+ message=f"Local `{target.id}` only forwards `{source.id}` to `{target.id}=`; pass `{source.id}` directly and remove the alias.",
162
+ severity=Severity.WARNING,
163
+ )
164
+ )
165
+ return findings
166
+
167
+
168
+ def _excluded_file(context: PythonFileContext) -> bool:
169
+ if context.path.suffix != ".py" or "=" not in context.source:
170
+ return True
171
+ # This is only a necessary ASCII signal; Unicode identifiers use the AST.
172
+ if context.source.isascii() and not _possible_alias(context.source):
173
+ return True
174
+ return context.tree is None
175
+
176
+
177
+ def _possible_alias(source: str) -> bool:
178
+ # Comments may separate tokens; bypass the text gate instead of rescanning them.
179
+ if "#" in source:
180
+ return True
181
+ forwarded: set[str] = set()
182
+ assigned: set[str] = set()
183
+ for match in _NAME_PAIR.finditer(source):
184
+ target = match.group("target")
185
+ (forwarded if target == match.group("source") else assigned).add(target)
186
+ return not forwarded.isdisjoint(assigned)
187
+
188
+
189
+ def _matching_keywords(context: PythonFileContext) -> dict[ast.Name, str]:
190
+ return {
191
+ node.value: node.value.id
192
+ for node in context.nodes(ast.keyword)
193
+ if isinstance(node.value, ast.Name) and node.arg == node.value.id
194
+ }
195
+
196
+
197
+ def _local_assignments(context: PythonFileContext, names: set[str]) -> list[ast.Assign]:
198
+ candidates = [
199
+ node
200
+ for node in context.nodes(ast.Assign)
201
+ if len(node.targets) == 1
202
+ and isinstance(node.targets[0], ast.Name)
203
+ and node.targets[0].id in names
204
+ and isinstance(node.value, ast.Name)
205
+ and node.targets[0].id != node.value.id
206
+ ]
207
+ if not candidates or context.has_wildcard_import or context.dynamic_namespace or _observes_references(context):
208
+ return []
209
+ provenance = context.resource_provenance
210
+ blocked = {provenance.scope(node) for node in context.nodes(*_UNCERTAIN)}
211
+ blocked.update(
212
+ provenance.scope(node) for node in context.nodes(ast.IfExp) if not _parameter_choice(node, provenance)
213
+ )
214
+ eligible = [
215
+ node for node in candidates if isinstance(scope := provenance.scope(node), _FUNCTIONS) and scope not in blocked
216
+ ]
217
+ if not eligible or context.generated or _has_type_comments(context):
218
+ return []
219
+ return eligible
220
+
221
+
222
+ def _has_type_comments(context: PythonFileContext) -> bool:
223
+ return any(_TYPE_COMMENT.search(comment) for comment in comment_lines(tuple(context.source_lines)).values())
224
+
225
+
226
+ def _parameter_choice(node: ast.IfExp, provenance: ResourceProvenance) -> bool:
227
+ if not isinstance(node.test, ast.Name):
228
+ return False
229
+ bindings = provenance.bindings.get(provenance.scope(node), {}).get(node.test.id, ())
230
+ return len(bindings) == 1 and isinstance(bindings[0], ast.arg)
231
+
232
+
233
+ def _scope_reads(provenance: ResourceProvenance, names: set[str]) -> dict[tuple[ast.AST | None, str], list[ast.Name]]:
234
+ reads: dict[tuple[ast.AST | None, str], list[ast.Name]] = defaultdict(list)
235
+ for name in names:
236
+ for read in provenance.reads.get(name, ()):
237
+ reads[provenance.scope(read), name].append(read)
238
+ return reads
239
+
240
+
241
+ def _conditional_sources(context: PythonFileContext) -> set[tuple[ast.AST | None, str]]:
242
+ provenance = context.resource_provenance
243
+ pending: list[ast.AST] = list(context.nodes(ast.IfExp))
244
+ seen: set[ast.AST] = set()
245
+ names: set[tuple[ast.AST | None, str]] = set()
246
+ while pending:
247
+ node = pending.pop()
248
+ if node in seen:
249
+ continue
250
+ seen.add(node)
251
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
252
+ names.add((provenance.scope(node), node.id))
253
+ pending.extend(children(node))
254
+ return names
255
+
256
+
257
+ def _single_keyword_read(
258
+ target: ast.Name,
259
+ provenance: ResourceProvenance,
260
+ reads: Mapping[tuple[ast.AST | None, str], list[ast.Name]],
261
+ keywords: Mapping[ast.Name, str],
262
+ ) -> ast.Name | None:
263
+ scope = provenance.scope(target)
264
+ uses = reads.get((scope, target.id), ())
265
+ if provenance.bindings.get(scope, {}).get(target.id) != [target] or len(uses) != 1:
266
+ return None
267
+ [use] = uses
268
+ if keywords.get(use) != target.id or _statement(use, provenance.parents) is None:
269
+ return None
270
+ return use
271
+
272
+
273
+ def _quoted_references(
274
+ context: PythonFileContext,
275
+ provenance: ResourceProvenance,
276
+ names: set[str],
277
+ ) -> set[tuple[ast.AST | None, str]]:
278
+ references: set[tuple[ast.AST | None, str]] = set()
279
+ for node in context.nodes(ast.Constant):
280
+ if not isinstance(node.value, str):
281
+ continue
282
+ mentioned = _quoted_words(node.value) & names
283
+ if mentioned:
284
+ scope = provenance.scope(node)
285
+ references.update((scope, name) for name in mentioned)
286
+ return references
287
+
288
+
289
+ def _quoted_words(value: str) -> set[str]:
290
+ normalized = unicodedata.normalize("NFKC", value)
291
+ if normalized.isascii():
292
+ return set(_WORDS.findall(normalized))
293
+ words: set[str] = set()
294
+ word: list[str] = []
295
+ for character in normalized:
296
+ # A leading letter tests identifier continuation, including combining marks.
297
+ if ("a" + character).isidentifier():
298
+ word.append(character)
299
+ elif word:
300
+ words.add("".join(word))
301
+ word.clear()
302
+ if word:
303
+ words.add("".join(word))
304
+ return words
305
+
306
+
307
+ def _observes_references(context: PythonFileContext) -> bool:
308
+ if _imports_model_namespace(context):
309
+ return True
310
+ for node in context.nodes(ast.Name, ast.Attribute, ast.alias):
311
+ name = _reference_name(node)
312
+ if name in _REFERENCE_OBSERVERS or (name == "type" and not _plain_type_query(node, context)):
313
+ return True
314
+ return False
315
+
316
+
317
+ def _imports_model_namespace(context: PythonFileContext) -> bool:
318
+ for node in context.nodes(ast.Import, ast.ImportFrom):
319
+ if isinstance(node, ast.ImportFrom):
320
+ if node.level == 0 and (node.module or "").partition(".")[0] == "pydantic":
321
+ return True
322
+ elif any(alias.name.partition(".")[0] == "pydantic" for alias in node.names):
323
+ return True
324
+ return False
325
+
326
+
327
+ def _reference_name(node: ast.AST) -> str | None:
328
+ if isinstance(node, ast.Name):
329
+ return node.id
330
+ if isinstance(node, ast.Attribute):
331
+ return node.attr
332
+ return node.name if isinstance(node, ast.alias) else None
333
+
334
+
335
+ def _plain_type_query(node: ast.AST, context: PythonFileContext) -> bool:
336
+ parent = context.parents.get(node)
337
+ return (
338
+ isinstance(node, ast.Name)
339
+ and context.imports.builtin_is_unshadowed("type")
340
+ and isinstance(parent, ast.Call)
341
+ and parent.func is node
342
+ and len(parent.args) == 1
343
+ and not parent.keywords
344
+ and not isinstance(parent.args[0], ast.Starred)
345
+ )
346
+
347
+
348
+ def _statement(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> ast.stmt | None:
349
+ while not isinstance(node, ast.stmt):
350
+ if isinstance(node, (ast.IfExp, ast.BoolOp)) or (parent := parents.get(node)) is None:
351
+ return None
352
+ node = parent
353
+ return node
354
+
355
+
356
+ def _dominates(
357
+ before: ast.stmt,
358
+ at: ast.AST,
359
+ parents: Mapping[ast.AST, ast.AST],
360
+ positions: Mapping[ast.stmt, int],
361
+ ) -> bool:
362
+ owner = parents.get(before)
363
+ if before not in positions or not isinstance(owner, _SUITES):
364
+ return False
365
+ current = at
366
+ while parents.get(current) is not owner:
367
+ parent = parents.get(current)
368
+ if parent is None:
369
+ return False
370
+ current = parent
371
+ # Exiting a with body can suppress an exception and skip its assignments.
372
+ # Only the same suite or an earlier enclosing-suite statement dominates.
373
+ return isinstance(current, ast.stmt) and current in positions and positions[before] < positions[current]
374
+
375
+
376
+ def _stable_source(
377
+ source: ast.Name,
378
+ at: ast.Assign,
379
+ provenance: ResourceProvenance,
380
+ positions: Mapping[ast.stmt, int],
381
+ conditional_sources: set[tuple[ast.AST | None, str]],
382
+ ) -> bool:
383
+ scope = provenance.scope(at)
384
+ if (scope, source.id) in conditional_sources:
385
+ return False
386
+ bindings = provenance.bindings.get(scope, {}).get(source.id, ())
387
+ if len(bindings) != 1:
388
+ return False
389
+ [binding] = bindings
390
+ if isinstance(binding, ast.arg):
391
+ return True
392
+ parent = provenance.parents.get(binding)
393
+ if not isinstance(binding, ast.Name) or not isinstance(parent, ast.Assign | ast.AnnAssign):
394
+ return False
395
+ if isinstance(parent, ast.Assign) and (len(parent.targets) != 1 or parent.targets[0] is not binding):
396
+ return False
397
+ return isinstance(parent.value, _INITIALIZERS) and _dominates(parent, at, provenance.parents, positions)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from keyword import iskeyword
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, final, override
7
+ from unicodedata import is_normalized
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ )
20
+ from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ @final
28
+ class PreferDirectImportModule(Rule):
29
+ id = "prefer-direct-import-module"
30
+ code = "SARJ529"
31
+ documentation = RuleDocumentation(
32
+ default_level=Severity.WARNING,
33
+ summary="Prefer static imports for eager module loads with a literal module name.",
34
+ rationale="Static imports expose known dependencies to readers and tooling without dynamic loader indirection.",
35
+ remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Keep dynamic loading when module selection or import timing requires it.",
36
+ category=RuleCategory.STYLE,
37
+ autofix=AutofixPolicy.NONE,
38
+ limitations=(
39
+ "Checks direct module-level assignments and expression statements calling the proven stdlib import_module function, through qualified or directly imported names, with one literal absolute module name.",
40
+ "Function/class bodies, conditional or guarded imports, variable module names, relative imports, callback references, shadowed or mutated providers and generated code are excluded. Static import syntax must be valid for every module-name segment.",
41
+ "This is a warning without automatic rewriting: moving an import can change execution order, optional-dependency behavior or cycle handling. The rule does not prefer one spelling of genuinely dynamic import_module calls.",
42
+ ),
43
+ examples=(
44
+ RuleExample(
45
+ example_id="literal-eager-loader",
46
+ title="A known eager dependency is loaded dynamically",
47
+ outcome=ExampleOutcome.MATCH,
48
+ files=(
49
+ ExampleFile.python(
50
+ "app.py", "import importlib\nplugin = importlib.import_module('plugins.billing')\n"
51
+ ),
52
+ ),
53
+ focus_path=PurePosixPath("app.py"),
54
+ expected_count=1,
55
+ public=True,
56
+ ),
57
+ RuleExample(
58
+ example_id="static-module-import",
59
+ title="A static import preserves the module binding",
60
+ outcome=ExampleOutcome.NO_MATCH,
61
+ files=(ExampleFile.python("app.py", "import plugins.billing as plugin\n"),),
62
+ focus_path=PurePosixPath("app.py"),
63
+ expected_count=0,
64
+ public=True,
65
+ ),
66
+ ),
67
+ )
68
+ description = documentation.summary
69
+
70
+ @override
71
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
72
+ if context.generated or context.tree is None or "import_module" not in context.symbol_source:
73
+ return []
74
+ loader_references = {
75
+ reference
76
+ for call in context.nodes(ast.Call)
77
+ if context.imports.resolved_qualified_name(call.func) == "importlib.import_module"
78
+ for reference in ast.walk(call.func)
79
+ }
80
+ if dynamic_namespace_access(
81
+ (node for node in context.nodes(ast.AST) if node not in loader_references),
82
+ context.imports,
83
+ context.parents,
84
+ ):
85
+ return []
86
+ return [
87
+ Diagnostic(
88
+ context.path,
89
+ node.lineno,
90
+ node.col_offset + 1,
91
+ self.code,
92
+ f"Use a module-level static import for the known module '{module}'; preserve the binding with 'import {module} as plugin' when needed.",
93
+ severity=Severity.WARNING,
94
+ )
95
+ for statement in context.tree.body
96
+ if isinstance(statement, ast.Assign | ast.AnnAssign | ast.Expr)
97
+ and isinstance(node := statement.value, ast.Call)
98
+ and (module := _literal_module(node)) is not None
99
+ and context.resource_provenance.runtime_import_name(node.func) == "importlib.import_module"
100
+ ]
101
+
102
+
103
+ def _literal_module(call: ast.Call) -> str | None:
104
+ match call:
105
+ case ast.Call(args=[ast.Constant(value=str() as module)], keywords=[]):
106
+ pass
107
+ case ast.Call(args=[], keywords=[ast.keyword(arg="name", value=ast.Constant(value=str() as module))]):
108
+ pass
109
+ case _:
110
+ return None
111
+ if not is_normalized("NFKC", module):
112
+ return None
113
+ return module if all(part.isidentifier() and not iskeyword(part) for part in module.split(".")) else None
@@ -1,83 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- from pathlib import PurePosixPath
5
- from typing import TYPE_CHECKING, 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
- Severity,
17
- )
18
-
19
-
20
- if TYPE_CHECKING:
21
- from sarj_python_lint._file_context import PythonFileContext
22
-
23
-
24
- @final
25
- class PreferDirectImportModule(Rule):
26
- id = "prefer-direct-import-module"
27
- code = "SARJ529"
28
- documentation = RuleDocumentation(
29
- default_level=Severity.WARNING,
30
- summary="Import import_module directly instead of accessing importlib.import_module.",
31
- rationale="A focused import makes the loader dependency explicit and removes repeated module qualification.",
32
- remediation="Use 'from importlib import import_module' and annotate importer callables with 'Callable[[str], ModuleType]'. Keep intentional module patching behind an explained local exception.",
33
- category=RuleCategory.STYLE,
34
- autofix=AutofixPolicy.NONE,
35
- limitations=(
36
- "Only provenance-resolved importlib.import_module attribute reads are checked. Other importlib APIs, direct imports, shadowed or mutated namespaces and generated code are preserved. No automatic rewrite because changing the lookup can change monkeypatch behavior.",
37
- ),
38
- examples=(
39
- RuleExample(
40
- example_id="qualified-loader",
41
- title="The module loader is qualified at each use",
42
- outcome=ExampleOutcome.MATCH,
43
- files=(ExampleFile.python("app.py", "import importlib\nloader = importlib.import_module\n"),),
44
- focus_path=PurePosixPath("app.py"),
45
- expected_count=1,
46
- public=True,
47
- ),
48
- RuleExample(
49
- example_id="focused-loader",
50
- title="Import only the loader",
51
- outcome=ExampleOutcome.NO_MATCH,
52
- files=(ExampleFile.python("app.py", "from importlib import import_module\nloader = import_module\n"),),
53
- focus_path=PurePosixPath("app.py"),
54
- expected_count=0,
55
- public=True,
56
- ),
57
- ),
58
- )
59
- description = documentation.summary
60
-
61
- @override
62
- def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
63
- if context.generated or context.tree is None or "import_module" not in context.symbol_source:
64
- return []
65
- provenance = context.static_resource_provenance
66
- return [
67
- Diagnostic(
68
- context.path,
69
- node.lineno,
70
- node.col_offset + 1,
71
- self.code,
72
- "Use 'from importlib import import_module' and call 'import_module(...)' directly.",
73
- severity=Severity.WARNING,
74
- )
75
- for node in context.nodes(ast.Attribute)
76
- if node.attr == "import_module"
77
- and isinstance(node.ctx, ast.Load)
78
- and context.imports.resolved_qualified_name(node) == "importlib.import_module"
79
- and isinstance(node.value, ast.Name)
80
- and isinstance(provenance.binding(node.value.id, node), ast.alias)
81
- and node.value.id not in provenance.mutated
82
- and not provenance.wildcard
83
- ]