sarj-python-lint 1.70.1__tar.gz → 1.72.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 (254) hide show
  1. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/__init__.py +2 -0
  4. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_file_context.py +2 -2
  5. sarj_python_lint-1.72.0/src/sarj_python_lint/_fixes.py +67 -0
  6. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_source.py +6 -0
  7. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rule_base.py +23 -0
  8. sarj_python_lint-1.72.0/src/sarj_python_lint/rules/_annotation_origins.py +74 -0
  9. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -2
  10. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_ast_index.py +26 -1
  11. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_declared_contracts.py +38 -26
  12. sarj_python_lint-1.72.0/src/sarj_python_lint/rules/_local_alias_fix.py +161 -0
  13. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_test_composition.py +20 -14
  14. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/excessive_commentary.py +13 -34
  15. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +49 -7
  16. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +16 -2
  17. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +21 -12
  18. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +38 -40
  19. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +4 -21
  20. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +16 -17
  21. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +19 -13
  22. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +18 -67
  23. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/.gitignore +0 -0
  24. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/LICENSE +0 -0
  25. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/README.md +0 -0
  26. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/__main__.py +0 -0
  27. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  28. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_filesystem.py +0 -0
  29. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_parallel.py +0 -0
  30. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_python_target.py +0 -0
  31. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  32. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_secret_names.py +0 -0
  33. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/_version.py +0 -0
  34. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/benchmark.py +0 -0
  35. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/cast_access.py +0 -0
  36. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  37. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/json_boundary.py +0 -0
  38. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/py.typed +0 -0
  39. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/ratchet.py +0 -0
  40. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  41. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  42. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  43. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  44. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  45. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  46. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  47. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  48. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  49. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  50. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  51. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  52. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  53. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  54. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  55. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  56. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  57. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  58. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  59. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  60. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  61. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  62. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  63. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  64. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  65. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  66. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  67. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  68. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  69. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  70. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  71. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  72. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  73. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  74. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  75. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  76. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  77. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  78. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  79. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  80. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  81. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  82. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  83. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  84. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  85. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  86. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  87. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  88. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  89. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  90. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  91. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  92. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  93. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  94. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  95. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  96. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  97. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  98. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  99. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  100. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  101. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  102. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  103. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  104. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  105. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  106. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  107. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  108. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  109. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  110. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  111. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  112. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  113. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  114. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  115. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  116. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  117. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  118. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  119. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  120. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  121. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  122. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  123. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  124. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  125. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  126. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  127. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  128. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  129. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  130. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  131. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  132. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  133. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  134. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  135. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  136. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  137. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  138. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  139. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  140. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  141. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  142. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  143. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  144. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  145. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  146. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  147. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  148. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  149. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  150. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  151. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  152. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  153. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  154. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  155. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  156. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  157. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  158. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  159. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  160. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  161. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  162. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  163. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  164. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  165. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  166. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  167. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  168. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  169. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  170. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  171. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  172. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  173. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  174. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  175. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  176. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  177. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  178. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  179. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  180. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  181. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  182. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  183. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  184. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  185. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  186. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  187. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  188. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  189. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  190. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  191. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  192. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  193. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  194. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  195. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  196. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  197. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  198. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  199. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  200. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  201. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  202. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  203. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  204. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  205. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  206. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  207. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  208. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  209. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  210. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  211. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  212. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  213. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  214. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  215. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  216. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  217. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  218. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  219. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  220. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  221. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  222. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  223. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  224. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  225. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  226. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  227. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  228. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  229. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  230. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  231. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  232. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  233. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  234. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  235. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  236. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  237. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  238. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  239. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  240. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  241. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  242. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  243. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  244. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  245. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  246. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  247. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  248. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  249. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  250. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  251. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  252. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  253. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  254. {sarj_python_lint-1.70.1 → sarj_python_lint-1.72.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.70.1
3
+ Version: 1.72.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.70.1"
3
+ version = "1.72.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" }]
@@ -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
@@ -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,74 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sarj_python_lint.rules._imports import ImportIndex
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from sarj_python_lint._file_context import PythonFileContext
11
+
12
+
13
+ class AnnotationOrigins:
14
+ def __init__(self, context: PythonFileContext) -> None:
15
+ self.context = context
16
+ self.provenance = context.static_resource_provenance
17
+ self.imports = {
18
+ node: ImportIndex.from_tree(ast.Module(body=[node], type_ignores=[]))
19
+ for node in context.nodes(ast.Import, ast.ImportFrom)
20
+ }
21
+
22
+ def shadowed(self, name: str, at: ast.AST) -> bool:
23
+ return type_parameter_shadow(name, at, self.context)
24
+
25
+ def qualified(self, node: ast.expr, at: ast.AST) -> str | None:
26
+ root = node
27
+ while isinstance(root, ast.Attribute):
28
+ root = root.value
29
+ if (
30
+ not isinstance(root, ast.Name)
31
+ or root.id in self.provenance.mutated | self.provenance.external_bindings
32
+ or type_parameter_shadow(root.id, at, self.context)
33
+ ):
34
+ return None
35
+ binding = self.provenance.binding(root.id, at)
36
+ parent = self.context.parents.get(binding) if binding is not None else None
37
+ index = self.imports.get(parent) if isinstance(parent, ast.Import | ast.ImportFrom) else None
38
+ return index.resolved_qualified_name(node) if index is not None else None
39
+
40
+ def builtin(self, node: ast.expr, name: str, at: ast.AST) -> bool:
41
+ return (
42
+ self.provenance.builtin_namespace_untouched
43
+ and isinstance(node, ast.Name)
44
+ and node.id == name
45
+ and not type_parameter_shadow(name, at, self.context)
46
+ and self.provenance.binding(name, at) is None
47
+ )
48
+
49
+
50
+ def type_parameter_shadow(name: str, at: ast.AST, context: PythonFileContext) -> bool:
51
+ outside: set[ast.AST] = set()
52
+ current = at
53
+ while (parent := context.parents.get(current)) is not None:
54
+ if (
55
+ isinstance(parent, ast.arguments)
56
+ and (current in parent.defaults or current in parent.kw_defaults)
57
+ and (owner := context.parents.get(parent)) is not None
58
+ ):
59
+ outside.add(owner)
60
+ if (
61
+ isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias)
62
+ and parent not in outside
63
+ and not (
64
+ isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
65
+ and current in parent.decorator_list
66
+ )
67
+ and any(
68
+ isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name
69
+ for parameter in parent.type_params
70
+ )
71
+ ):
72
+ return True
73
+ current = parent
74
+ return False
@@ -63,7 +63,7 @@ class AnnotationSemantics:
63
63
  candidates[name] = value
64
64
  if not candidates:
65
65
  return cls(imports, MappingProxyType({}))
66
- rebound = _module_rebindings(tree)
66
+ rebound = module_rebindings(tree)
67
67
  aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
68
68
  return cls(imports, MappingProxyType(aliases))
69
69
 
@@ -139,7 +139,7 @@ class AnnotationSemantics:
139
139
  return [node.slice]
140
140
 
141
141
 
142
- def _module_rebindings(tree: ast.Module) -> set[str]:
142
+ def module_rebindings(tree: ast.Module) -> set[str]:
143
143
  counts: dict[str, int] = {}
144
144
  for statement in tree.body:
145
145
  for name in _bound_names(statement):
@@ -15,7 +15,32 @@ if TYPE_CHECKING:
15
15
  _AST = ast.AST
16
16
 
17
17
 
18
- def object_list(value: object) -> TypeIs[list[object]]:
18
+ def statement_blocks(statements: list[ast.stmt]) -> Iterator[list[ast.stmt]]:
19
+ pending = [statements]
20
+ while pending:
21
+ block = pending.pop()
22
+ yield block
23
+ for statement in reversed(block):
24
+ match statement:
25
+ case ast.For() | ast.AsyncFor() | ast.While() | ast.If():
26
+ nested = [statement.body, statement.orelse]
27
+ case ast.With() | ast.AsyncWith():
28
+ nested = [statement.body]
29
+ case ast.Try() | ast.TryStar():
30
+ nested = [
31
+ statement.body,
32
+ *(handler.body for handler in statement.handlers),
33
+ statement.orelse,
34
+ statement.finalbody,
35
+ ]
36
+ case ast.Match():
37
+ nested = [case.body for case in statement.cases]
38
+ case _:
39
+ continue
40
+ pending.extend(reversed(nested))
41
+
42
+
43
+ def object_list(value: object) -> TypeIs[list[object]]: # sarj-noqa: SARJ528 -- narrows opaque AST fields.
19
44
  return isinstance(value, list)
20
45
 
21
46
 
@@ -28,27 +28,30 @@ class DeclaredClass:
28
28
 
29
29
 
30
30
  def _class_metadata_annotation(annotation: ast.expr, semantics: AnnotationSemantics) -> bool:
31
- pending = [annotation]
32
31
  seen: set[str] = set()
33
- while pending:
34
- node = semantics.parse(pending.pop())
35
- if node is None:
32
+ node = semantics.parse(annotation)
33
+ while node is not None:
34
+ if isinstance(node, ast.Name) and node.id in semantics.aliases and node.id not in seen:
35
+ seen.add(node.id)
36
+ node = semantics.parse(semantics.aliases[node.id])
37
+ continue
38
+ if not isinstance(node, ast.Subscript):
39
+ return False
40
+ wrapper = semantics.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
41
+ if wrapper in {"ClassVar", "Final"}:
42
+ return True
43
+ if wrapper != "Annotated":
44
+ return False
45
+ if isinstance(node.slice, ast.Tuple) and not node.slice.elts:
36
46
  return True
37
- for member in ast.walk(node):
38
- if isinstance(member, ast.Subscript) and semantics.imports.resolved_symbol(
39
- member.value, sources=_TYPING_SOURCES
40
- ) in {"ClassVar", "Final"}:
41
- return True
42
- if isinstance(member, ast.Name) and member.id in semantics.aliases and member.id not in seen:
43
- seen.add(member.id)
44
- pending.append(semantics.aliases[member.id])
45
- return False
47
+ node = semantics.parse(node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice)
48
+ return True
46
49
 
47
50
 
48
51
  class DeclaredContracts:
49
52
  def __init__(self, context: PythonFileContext) -> None:
50
53
  self.context = context
51
- self._field_cache: dict[ast.ClassDef, dict[str, ast.AnnAssign]] = {}
54
+ self._field_cache: dict[tuple[ast.ClassDef, bool], dict[str, ast.AnnAssign]] = {}
52
55
  self._module_annotations: dict[Path, AnnotationSemantics] = {}
53
56
  self.project = context.session.project or ProjectIndexSet.single(context.path, context.source)
54
57
  self.unit = self.project.unit(context.path)
@@ -130,23 +133,32 @@ class DeclaredContracts:
130
133
  def inherits(self, model: DeclaredClass, bases: frozenset[str]) -> bool:
131
134
  return self.project.class_inherits_from(model.unit, model.declaration.name, bases)
132
135
 
133
- def model_fields(self, model: DeclaredClass) -> dict[str, ast.AnnAssign]:
136
+ def model_fields(
137
+ self, model: DeclaredClass, *, inherited: bool = False, seen: frozenset[ast.ClassDef] = frozenset()
138
+ ) -> dict[str, ast.AnnAssign]:
134
139
  if model.unit.tree is None:
135
140
  return {}
136
- cached = self._field_cache.get(model.declaration)
141
+ key = (model.declaration, inherited)
142
+ cached = self._field_cache.get(key)
137
143
  if cached is not None:
138
144
  return cached
145
+ if model.declaration in seen or len(seen) >= _MAX_DEPTH:
146
+ return {}
139
147
  semantics = self._module_semantics(model.unit)
140
- fields = {
141
- node.target.id: node
142
- for node in model.declaration.body
143
- if isinstance(node, ast.AnnAssign)
144
- and isinstance(node.target, ast.Name)
145
- and not node.target.id.startswith("_")
146
- and node.target.id != "model_config"
147
- and not _class_metadata_annotation(node.annotation, semantics)
148
- }
149
- self._field_cache[model.declaration] = fields
148
+ fields: dict[str, ast.AnnAssign] = {}
149
+ if inherited and len(model.declaration.bases) == 1:
150
+ parent = self.class_type(model.declaration.bases[0], model.unit)
151
+ if parent is not None:
152
+ fields.update(self.model_fields(parent, inherited=True, seen=seen | {model.declaration}))
153
+ for node in model.declaration.body:
154
+ if not isinstance(node, ast.AnnAssign) or not isinstance(node.target, ast.Name):
155
+ continue
156
+ name = node.target.id
157
+ if name.startswith("_") or name == "model_config" or _class_metadata_annotation(node.annotation, semantics):
158
+ fields.pop(name, None)
159
+ else:
160
+ fields[name] = node
161
+ self._field_cache[key] = fields
150
162
  return fields
151
163
 
152
164
  def _module_semantics(self, unit: SourceUnit) -> AnnotationSemantics:
@@ -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
+ )
@@ -5,6 +5,7 @@ from collections import Counter
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from sarj_python_lint.rules._declared_contracts import DeclaredClass
8
9
  from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
9
10
 
10
11
 
@@ -40,9 +41,7 @@ def direct_nodes(node: ast.AST) -> Iterator[ast.AST]:
40
41
  yield from direct_nodes(child)
41
42
 
42
43
 
43
- def _class_owner(
44
- context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet
45
- ) -> tuple[SourceUnit, ast.ClassDef] | None:
44
+ def _class_owner(context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet) -> DeclaredClass | None:
46
45
  qualified = context.module_imports.resolved_qualified_name(expression)
47
46
  unit = project.unit(context.path)
48
47
  if qualified is not None:
@@ -62,7 +61,7 @@ def _class_owner(
62
61
  for node in direct_nodes(unit.tree)
63
62
  ):
64
63
  return None
65
- return unit, owner
64
+ return DeclaredClass(unit, owner)
66
65
 
67
66
 
68
67
  def constructor(
@@ -75,7 +74,7 @@ def constructor(
75
74
  resolved = _class_owner(context, expression, project)
76
75
  if resolved is None:
77
76
  return None
78
- unit, owner = resolved
77
+ unit, owner = resolved.unit, resolved.declaration
79
78
  if unit.module is None:
80
79
  return None
81
80
  initializer = next(
@@ -240,15 +239,9 @@ def wiring(
240
239
  spec: Constructor,
241
240
  scope: WiringScope,
242
241
  ) -> tuple[tuple[str, str], ...] | None:
243
- if any(isinstance(arg, ast.Starred) for arg in call.args) or any(keyword.arg is None for keyword in call.keywords):
244
- return None
245
- if len(call.args) > len(spec.positional):
242
+ values = call_arguments(call, spec.positional)
243
+ if values is None:
246
244
  return None
247
- values = dict(zip(spec.positional, call.args, strict=False))
248
- for keyword in call.keywords:
249
- if keyword.arg is None or keyword.arg in values:
250
- return None
251
- values[keyword.arg] = keyword.value
252
245
  result: list[tuple[str, str]] = []
253
246
  for name in sorted(spec.collaborators):
254
247
  value = values.get(name)
@@ -261,6 +254,19 @@ def wiring(
261
254
  return tuple(result)
262
255
 
263
256
 
257
+ def call_arguments(call: ast.Call, positional: tuple[str, ...]) -> dict[str, ast.expr] | None:
258
+ if any(isinstance(arg, ast.Starred) for arg in call.args) or any(keyword.arg is None for keyword in call.keywords):
259
+ return None
260
+ if len(call.args) > len(positional):
261
+ return None
262
+ values = dict(zip(positional, call.args, strict=False))
263
+ for keyword in call.keywords:
264
+ if keyword.arg is None or keyword.arg in values:
265
+ return None
266
+ values[keyword.arg] = keyword.value
267
+ return values
268
+
269
+
264
270
  def _value_key(
265
271
  context: PythonFileContext,
266
272
  project: ProjectIndexSet,
@@ -300,4 +306,4 @@ def _value_key(
300
306
  ]
301
307
  if any(key is None for key in positional) or any(key is None for _, key in keywords):
302
308
  return None
303
- return f"{owner[0].module}.{owner[1].name}:{positional!r}:{sorted(keywords)!r}"
309
+ return f"{owner.unit.module}.{owner.declaration.name}:{positional!r}:{sorted(keywords)!r}"