sarj-python-lint 1.70.2__tar.gz → 1.72.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 (254) hide show
  1. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/__init__.py +2 -0
  4. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_file_context.py +2 -2
  5. sarj_python_lint-1.72.1/src/sarj_python_lint/_fixes.py +67 -0
  6. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_parallel.py +2 -1
  7. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_source.py +6 -0
  8. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rule_base.py +23 -0
  9. sarj_python_lint-1.72.1/src/sarj_python_lint/rules/_local_alias_fix.py +161 -0
  10. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_redundant_local_alias.py +49 -7
  11. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/.gitignore +0 -0
  12. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/LICENSE +0 -0
  13. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/README.md +0 -0
  14. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/__main__.py +0 -0
  15. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_python_target.py +0 -0
  18. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  19. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_secret_names.py +0 -0
  20. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/cast_access.py +0 -0
  23. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  24. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/json_boundary.py +0 -0
  25. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/py.typed +0 -0
  26. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/ratchet.py +0 -0
  27. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  29. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  30. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  31. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  32. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  38. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  39. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  40. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  41. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  42. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  43. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  44. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  45. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  46. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  47. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  48. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  49. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  50. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  51. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  52. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  53. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  54. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  55. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  56. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  57. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  58. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  59. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  60. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  61. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  62. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  63. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  64. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  65. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  66. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  67. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  68. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  69. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  70. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  71. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  72. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  73. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  74. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  75. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  76. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  77. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  78. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  79. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  80. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  81. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  82. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  83. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  84. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  85. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  86. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  87. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  88. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  89. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  90. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  91. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  92. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  93. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  94. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  95. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  96. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  97. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  98. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  99. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  100. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  101. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  102. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  103. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  104. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  105. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  106. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  107. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  108. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  109. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  110. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  111. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  112. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  113. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  114. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  115. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  116. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  117. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  118. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  119. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  120. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  121. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  122. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  123. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  124. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  125. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  126. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  127. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  128. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  129. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  130. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  131. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  132. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  133. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  134. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  135. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  136. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  137. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  138. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  139. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  140. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  141. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  142. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  143. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  144. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  145. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  146. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  147. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  148. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  149. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  150. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  151. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  152. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  153. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  154. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  155. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  156. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  157. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  158. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  159. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  160. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  161. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  162. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  163. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  164. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  165. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  166. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  167. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  168. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  169. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  170. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  171. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  172. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  173. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  174. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  175. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  176. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  177. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  178. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  179. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  180. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  181. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  182. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  183. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  184. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  185. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  186. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  187. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  188. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  189. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  190. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  191. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  192. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  193. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  194. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  195. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  196. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  197. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  198. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  199. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  200. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  201. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  202. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  203. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  204. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  205. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  206. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  207. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  208. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  209. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  210. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  211. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  212. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  213. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  214. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  215. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  216. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  217. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  218. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  219. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  220. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  221. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  222. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  223. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  224. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  225. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  226. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  227. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  228. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  229. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  230. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  231. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  232. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  233. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  234. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  235. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  236. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  237. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  238. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  239. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  240. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  241. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  242. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  243. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  244. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  245. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  246. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  247. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  248. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  249. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  250. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  251. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  252. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  253. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  254. {sarj_python_lint-1.70.2 → sarj_python_lint-1.72.1}/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.70.2
3
+ Version: 1.72.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 = "1.70.2"
3
+ version = "1.72.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" }]
@@ -1,3 +1,5 @@
1
+ from sarj_python_lint._analysis_session import AnalysisSession as AnalysisSession
2
+ from sarj_python_lint._fixes import apply_fixes as apply_fixes, python_source_hash as python_source_hash
1
3
  from sarj_python_lint._ratchet_cli import run_ratchet as run_ratchet
2
4
  from sarj_python_lint._source import python_source_encoding as python_source_encoding
3
5
  from sarj_python_lint._version import __version__ as __version__
@@ -5,7 +5,7 @@ from functools import cached_property
5
5
  from typing import TYPE_CHECKING, final
6
6
 
7
7
  from sarj_python_lint._analysis_session import AnalysisSession
8
- from sarj_python_lint._source import symbol_prefilter_source
8
+ from sarj_python_lint._source import python_source_lines, symbol_prefilter_source
9
9
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
10
10
  from sarj_python_lint.rules._ast_index import NodeIndex
11
11
  from sarj_python_lint.rules._fastapi import FastapiIndex
@@ -47,7 +47,7 @@ class PythonFileContext:
47
47
 
48
48
  @cached_property
49
49
  def source_lines(self) -> list[str]:
50
- return self.source.splitlines()
50
+ return python_source_lines(self.source)
51
51
 
52
52
  @cached_property
53
53
  def generated(self) -> bool:
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from hashlib import sha256
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sarj_python_lint.rule_base import AutofixPolicy
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
13
+ from sarj_python_lint.rule_base import Fix
14
+
15
+
16
+ def python_source_hash(source: str) -> str:
17
+ canonical = source.removeprefix("\ufeff").replace("\r\n", "\n").replace("\r", "\n")
18
+ return sha256(canonical.encode("utf-8")).hexdigest()
19
+
20
+
21
+ def apply_fixes(source: str, fixes: Sequence[Fix]) -> str:
22
+ if not fixes:
23
+ return source
24
+ digest = python_source_hash(source)
25
+ prefix = "\ufeff" if source.startswith("\ufeff") else ""
26
+ raw = source[len(prefix) :].encode("utf-8")
27
+ lines = raw.splitlines(keepends=True)
28
+ offsets = [0]
29
+ for line in lines:
30
+ offsets.append(offsets[-1] + len(line))
31
+ if not lines or lines[-1].endswith((b"\n", b"\r")):
32
+ lines.append(b"")
33
+
34
+ def offset(line: int, col: int) -> int:
35
+ if line < 1 or line > len(lines) or col < 1 or col > len(lines[line - 1].rstrip(b"\r\n")) + 1:
36
+ msg = "fix position is outside the source"
37
+ raise ValueError(msg)
38
+ prefix = lines[line - 1][: col - 1]
39
+ prefix.decode("utf-8")
40
+ return offsets[line - 1] + len(prefix)
41
+
42
+ edits: list[tuple[int, int, bytes]] = []
43
+ for fix in fixes:
44
+ if fix.safety is not AutofixPolicy.SAFE or fix.source_hash != digest or not fix.edits:
45
+ msg = "fix must be safe, nonempty and match the complete source revision"
46
+ raise ValueError(msg)
47
+ for edit in fix.edits:
48
+ start = offset(edit.start_line, edit.start_col)
49
+ end = offset(edit.end_line, edit.end_col)
50
+ if end < start:
51
+ msg = "fix range runs backwards"
52
+ raise ValueError(msg)
53
+ edits.append((start, end, edit.replacement.encode("utf-8")))
54
+ edits.sort()
55
+ result: list[bytes] = []
56
+ previous = 0
57
+ for start, end, replacement in edits:
58
+ if start < previous:
59
+ msg = "fix groups overlap"
60
+ raise ValueError(msg)
61
+ result.extend((raw[previous:start], replacement))
62
+ previous = end
63
+ result.append(raw[previous:])
64
+ fixed = b"".join(result).decode("utf-8")
65
+ tree = ast.parse(fixed)
66
+ compile(tree, "<native-fix>", "exec")
67
+ return prefix + fixed
@@ -87,7 +87,8 @@ def check_parallel(
87
87
  resolutions: set[_ResolutionKey] = set()
88
88
  modules: set[Path] = set()
89
89
  try:
90
- for result in pool.map(_check_file, loaded.items(), chunksize=1, buffersize=workers):
90
+ # Keep workers busy while an earlier file delays ordered results.
91
+ for result in pool.map(_check_file, loaded.items(), chunksize=1, buffersize=workers * 4):
91
92
  resolutions.update(result.resolutions)
92
93
  modules.update(result.modules)
93
94
  # A worker cannot hit either order-dependent limit when the whole
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from io import BytesIO
4
+ import re
4
5
  import tokenize
5
6
  from typing import TYPE_CHECKING
6
7
  import unicodedata
@@ -19,6 +20,11 @@ def read_python_source(path: Path) -> str:
19
20
  return stream.read()
20
21
 
21
22
 
23
+ def python_source_lines(source: str) -> list[str]:
24
+ lines = re.split(r"\r\n?|\n", source)
25
+ return lines[:-1] if not lines[-1] else lines
26
+
27
+
22
28
  def symbol_prefilter_source(source: str) -> str:
23
29
  # Only necessary symbol gates may use this view; parse and locate original source.
24
30
  return source if source.isascii() else unicodedata.normalize("NFKC", source)
@@ -75,6 +75,28 @@ class ColumnEncoding(StrEnum):
75
75
  CODEPOINTS = "codepoints"
76
76
 
77
77
 
78
+ @dataclass(frozen=True, slots=True)
79
+ class TextEdit:
80
+ """An exact, half-open range of one-based lines and UTF-8 byte columns.
81
+
82
+ Columns refer to decoded source without its original leading BOM.
83
+ """
84
+
85
+ start_line: int
86
+ start_col: int
87
+ end_line: int
88
+ end_col: int
89
+ replacement: str
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class Fix:
94
+ title: str
95
+ safety: AutofixPolicy
96
+ source_hash: str
97
+ edits: tuple[TextEdit, ...]
98
+
99
+
78
100
  @dataclass(frozen=True, slots=True)
79
101
  class Diagnostic:
80
102
  path: Path
@@ -84,6 +106,7 @@ class Diagnostic:
84
106
  message: str
85
107
  severity: Severity = Severity.ERROR
86
108
  column_encoding: ColumnEncoding = ColumnEncoding.UTF8_BYTES
109
+ fixes: tuple[Fix, ...] = ()
87
110
 
88
111
  def format(self) -> str:
89
112
  label = "warning: " if self.severity is Severity.WARNING else ""
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from functools import cached_property
5
+ from typing import TYPE_CHECKING
6
+
7
+ from sarj_python_lint._fixes import python_source_hash
8
+ from sarj_python_lint._source import python_source_encoding
9
+ from sarj_python_lint.rule_base import AutofixPolicy, Fix, TextEdit
10
+ from sarj_python_lint.rules._suppression_comments import comment_lines
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Mapping
15
+
16
+ from sarj_python_lint._file_context import PythonFileContext
17
+
18
+
19
+ _NAMESPACE_MUTATORS = frozenset(
20
+ {
21
+ "setattr",
22
+ "delattr",
23
+ "setitem",
24
+ "delitem",
25
+ "__setattr__",
26
+ "__delattr__",
27
+ "__setitem__",
28
+ "__delitem__",
29
+ "__init__",
30
+ "__ior__",
31
+ "ior",
32
+ "methodcaller",
33
+ "patch",
34
+ "clear",
35
+ "pop",
36
+ "popitem",
37
+ "setdefault",
38
+ "update",
39
+ }
40
+ )
41
+
42
+
43
+ class LocalAliasFixes:
44
+ def __init__(self, context: PythonFileContext) -> None:
45
+ self.context = context
46
+
47
+ @cached_property
48
+ def _lines(self) -> tuple[bytes, ...]:
49
+ return tuple(line.encode("utf-8") for line in self.context.source_lines)
50
+
51
+ def spelling(self, node: ast.Name) -> str:
52
+ return self._lines[node.lineno - 1][node.col_offset : node.end_col_offset].decode("utf-8")
53
+
54
+ @cached_property
55
+ def _available(self) -> bool:
56
+ context = self.context
57
+ return (
58
+ python_source_encoding(context.source.encode("utf-8")) in {"utf-8", "utf-8-sig"}
59
+ and context.imports.builtin_is_unshadowed("dict")
60
+ and not any("dict" in bindings for bindings in context.resource_provenance.bindings.values())
61
+ and context.resource_provenance.builtin_namespace_untouched
62
+ and not any(node.attr == "__code__" for node in context.nodes(ast.Attribute))
63
+ and not _visible_namespace_mutation(context)
64
+ )
65
+
66
+ @cached_property
67
+ def _comments(self) -> Mapping[int, str]:
68
+ return comment_lines(tuple(self.context.source_lines))
69
+
70
+ @cached_property
71
+ def _digest(self) -> str:
72
+ return python_source_hash(self.context.source)
73
+
74
+ def for_alias(self, assignment: ast.Assign, use: ast.Name) -> tuple[Fix, ...]:
75
+ context = self.context
76
+ owner = context.parents.get(assignment)
77
+ if not isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef):
78
+ return ()
79
+ if not isinstance(context.parents.get(owner), ast.Module) or owner.decorator_list or owner.type_params:
80
+ return ()
81
+ args = owner.args
82
+ parameters = [*args.posonlyargs, *args.args, *args.kwonlyargs]
83
+ if len(parameters) != 1 or args.vararg or args.kwarg or args.defaults or any(args.kw_defaults):
84
+ return ()
85
+ source = assignment.value
86
+ if not isinstance(source, ast.Name) or parameters[0].arg != source.id:
87
+ return ()
88
+ body = owner.body[1:] if ast.get_docstring(owner) is not None else owner.body
89
+ if body[:1] != [assignment] or len(body[1:]) != 1 or not isinstance(body[-1], ast.Return):
90
+ return ()
91
+ call = body[-1].value
92
+ if not isinstance(call, ast.Call) or not isinstance(call.func, ast.Name) or call.func.id != "dict":
93
+ return ()
94
+ if call.args or len(call.keywords) != 1 or call.keywords[0].value is not use or not self._available:
95
+ return ()
96
+ edits = self._edits(assignment, use, source=source, owner=owner)
97
+ if not edits:
98
+ return ()
99
+ return (Fix("Pass the original local directly", AutofixPolicy.SAFE, self._digest, edits),)
100
+
101
+ def _edits(
102
+ self,
103
+ assignment: ast.Assign,
104
+ use: ast.Name,
105
+ *,
106
+ source: ast.Name,
107
+ owner: ast.FunctionDef | ast.AsyncFunctionDef,
108
+ ) -> tuple[TextEdit, ...]:
109
+ if any(line in self._comments for line in range(owner.lineno, (owner.end_lineno or owner.lineno) + 1)):
110
+ return ()
111
+ if assignment.end_lineno != assignment.lineno:
112
+ return ()
113
+ line = self.context.source_lines[assignment.lineno - 1]
114
+ raw = self._lines[assignment.lineno - 1]
115
+ if (
116
+ raw[: assignment.col_offset].strip()
117
+ or raw[assignment.end_col_offset :].strip()
118
+ or ";" in line
119
+ or "\\" in line
120
+ ):
121
+ return ()
122
+ return (
123
+ TextEdit(assignment.lineno, 1, assignment.lineno + 1, 1, ""),
124
+ TextEdit(
125
+ use.lineno,
126
+ use.col_offset + 1,
127
+ use.end_lineno or use.lineno,
128
+ (use.end_col_offset or 0) + 1,
129
+ self.spelling(source),
130
+ ),
131
+ )
132
+
133
+
134
+ def _visible_namespace_mutation(context: PythonFileContext) -> bool:
135
+ if _imports_unittest(context):
136
+ return True
137
+ for node in context.nodes(ast.Name, ast.alias, ast.Attribute, ast.Subscript, ast.AugAssign):
138
+ match node:
139
+ case ast.Name(id=name) | ast.alias(name=name):
140
+ if name in _NAMESPACE_MUTATORS:
141
+ return True
142
+ case ast.Attribute(attr=name, ctx=ctx):
143
+ if name in _NAMESPACE_MUTATORS or not isinstance(ctx, ast.Load):
144
+ return True
145
+ case ast.Subscript(ctx=ctx):
146
+ if not isinstance(ctx, ast.Load):
147
+ return True
148
+ case ast.AugAssign(op=op):
149
+ if isinstance(op, ast.BitOr):
150
+ return True
151
+ return False
152
+
153
+
154
+ def _imports_unittest(context: PythonFileContext) -> bool:
155
+ for node in context.nodes(ast.Import):
156
+ if any(alias.name.partition(".")[0] == "unittest" for alias in node.names):
157
+ return True
158
+ return any(
159
+ node.module is not None and node.module.partition(".")[0] == "unittest"
160
+ for node in context.nodes(ast.ImportFrom)
161
+ )
@@ -19,6 +19,7 @@ from sarj_python_lint.rule_base import (
19
19
  Severity,
20
20
  )
21
21
  from sarj_python_lint.rules._ast_index import children
22
+ from sarj_python_lint.rules._local_alias_fix import LocalAliasFixes
22
23
  from sarj_python_lint.rules._suppression_comments import comment_lines
23
24
 
24
25
 
@@ -82,9 +83,9 @@ class NoRedundantLocalAlias(Rule):
82
83
  default_level=Severity.WARNING,
83
84
  summary="Remove a local alias used only for its matching keyword argument.",
84
85
  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
+ remediation="Apply the safe fix where offered; otherwise pass the original local to the matching keyword and remove the unannotated alias after review.",
86
87
  category=RuleCategory.MAINTAINABILITY,
87
- autofix=AutofixPolicy.NONE,
88
+ autofix=AutofixPolicy.SAFE,
88
89
  limitations=(
89
90
  "Only unannotated, single-target `alias = original` assignments with one binding and one read at `alias=alias` are checked.",
90
91
  "The original must be a parameter or an earlier initialized local with one binding; reassignment, deletion, imports, alias chains and uncertain initialization are excluded.",
@@ -93,10 +94,35 @@ class NoRedundantLocalAlias(Rule):
93
94
  "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
95
  "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
96
  "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.",
97
+ "Generated source, stubs and malformed input are excluded; files must compile with normal module execution flags. Arbitrary external frame introspection is not modeled.",
98
+ "Safe fixes are limited to undecorated module functions with one required parameter, an optional existing docstring, a clean single-line alias and a tail return of unshadowed builtin dict with exactly the matching keyword. Recorded dict bindings, visible attribute/subscript writes, setter, mapping-mutator, ior, methodcaller or patch references, unittest imports, `|=` assignments, comments, non-UTF8 encodings, type parameters and visible code-object inspection prevent fixes. Mutation guards also exclude unrelated ordinary mappings, nonmutating methodcaller uses, harmless unittest imports and integer `|=` assignments. Other calls remain diagnostic-only; arbitrary external tracing, reflection and monkeypatching are not modeled.",
99
+ "Readability judgments, multiple reads, positional forwarding and more complex control flow remain outside the contract.",
98
100
  ),
99
101
  examples=(
102
+ RuleExample(
103
+ example_id="safe-builtin-tail",
104
+ scenario="safe-fix",
105
+ title="A safe builtin mapping return",
106
+ outcome=ExampleOutcome.MATCH,
107
+ files=(
108
+ ExampleFile.python(
109
+ "case.py", "def render(item):\n payload = item\n return dict(payload=payload)\n"
110
+ ),
111
+ ),
112
+ focus_path=PurePosixPath("case.py"),
113
+ expected_count=1,
114
+ public=True,
115
+ ),
116
+ RuleExample(
117
+ example_id="direct-builtin-tail",
118
+ scenario="safe-fix",
119
+ title="Return the original local in the mapping",
120
+ outcome=ExampleOutcome.NO_MATCH,
121
+ files=(ExampleFile.python("case.py", "def render(item):\n return dict(payload=item)\n"),),
122
+ focus_path=PurePosixPath("case.py"),
123
+ expected_count=0,
124
+ public=True,
125
+ ),
100
126
  RuleExample(
101
127
  example_id="keyword-supplies-role",
102
128
  title="The keyword supplies the role",
@@ -138,6 +164,7 @@ class NoRedundantLocalAlias(Rule):
138
164
  quoted = _quoted_references(context, provenance, names)
139
165
  positions = {statement: i for owner in context.nodes(*_SUITES) for i, statement in enumerate(owner.body)}
140
166
  findings: list[Diagnostic] = []
167
+ fixes = LocalAliasFixes(context)
141
168
  for assignment in candidates:
142
169
  target = assignment.targets[0]
143
170
  source = assignment.value
@@ -152,14 +179,17 @@ class NoRedundantLocalAlias(Rule):
152
179
  continue
153
180
  if not _stable_source(source, assignment, provenance, positions, conditional_sources):
154
181
  continue
182
+ target_spelling = fixes.spelling(target)
183
+ source_spelling = fixes.spelling(source)
155
184
  findings.append(
156
185
  Diagnostic(
157
186
  path=context.path,
158
187
  line=target.lineno,
159
188
  col=target.col_offset + 1,
160
189
  code=self.code,
161
- message=f"Local `{target.id}` only forwards `{source.id}` to `{target.id}=`; pass `{source.id}` directly and remove the alias.",
190
+ message=f"Local `{target_spelling}` only forwards `{source_spelling}` to `{target_spelling}=`; pass `{source_spelling}` directly and remove the alias.",
162
191
  severity=Severity.WARNING,
192
+ fixes=fixes.for_alias(assignment, use),
163
193
  )
164
194
  )
165
195
  return findings
@@ -204,7 +234,7 @@ def _local_assignments(context: PythonFileContext, names: set[str]) -> list[ast.
204
234
  and isinstance(node.value, ast.Name)
205
235
  and node.targets[0].id != node.value.id
206
236
  ]
207
- if not candidates or context.has_wildcard_import or context.dynamic_namespace or _observes_references(context):
237
+ if not candidates or context.has_wildcard_import:
208
238
  return []
209
239
  provenance = context.resource_provenance
210
240
  blocked = {provenance.scope(node) for node in context.nodes(*_UNCERTAIN)}
@@ -214,11 +244,23 @@ def _local_assignments(context: PythonFileContext, names: set[str]) -> list[ast.
214
244
  eligible = [
215
245
  node for node in candidates if isinstance(scope := provenance.scope(node), _FUNCTIONS) and scope not in blocked
216
246
  ]
217
- if not eligible or context.generated or _has_type_comments(context):
247
+ if not eligible or context.dynamic_namespace or _observes_references(context):
248
+ return []
249
+ if context.generated or _has_type_comments(context) or not _compiles(context):
218
250
  return []
219
251
  return eligible
220
252
 
221
253
 
254
+ def _compiles(context: PythonFileContext) -> bool:
255
+ if context.tree is None:
256
+ return False
257
+ try:
258
+ compile(context.tree, str(context.path), "exec")
259
+ except SyntaxError:
260
+ return False
261
+ return True
262
+
263
+
222
264
  def _has_type_comments(context: PythonFileContext) -> bool:
223
265
  return any(_TYPE_COMMENT.search(comment) for comment in comment_lines(tuple(context.source_lines)).values())
224
266