sarj-python-lint 1.70.1__tar.gz → 1.70.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/pyproject.toml +1 -1
  3. sarj_python_lint-1.70.2/src/sarj_python_lint/rules/_annotation_origins.py +74 -0
  4. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -2
  5. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_index.py +26 -1
  6. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_declared_contracts.py +38 -26
  7. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_composition.py +20 -14
  8. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/excessive_commentary.py +13 -34
  9. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_direct_import_module.py +16 -2
  10. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +21 -12
  11. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +38 -40
  12. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +4 -21
  13. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +16 -17
  14. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +19 -13
  15. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +18 -67
  16. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/.gitignore +0 -0
  17. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/LICENSE +0 -0
  18. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/README.md +0 -0
  19. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/__init__.py +0 -0
  20. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/__main__.py +0 -0
  21. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  22. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_file_context.py +0 -0
  23. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_filesystem.py +0 -0
  24. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_parallel.py +0 -0
  25. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_python_target.py +0 -0
  26. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  27. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_secret_names.py +0 -0
  28. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_source.py +0 -0
  29. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/_version.py +0 -0
  30. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/benchmark.py +0 -0
  31. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/cast_access.py +0 -0
  32. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  33. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/json_boundary.py +0 -0
  34. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/py.typed +0 -0
  35. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/ratchet.py +0 -0
  36. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rule_base.py +0 -0
  37. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  38. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  39. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  40. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  41. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  42. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  43. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  44. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  45. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  46. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  47. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  48. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  49. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  50. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  51. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  52. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  53. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  54. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  55. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  56. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  57. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  58. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  59. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  60. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  61. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  62. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  63. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  64. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  65. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  66. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  67. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  68. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  69. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  70. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  71. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  72. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  73. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  74. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  75. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  76. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  77. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  78. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  79. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  80. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  81. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  82. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  83. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  84. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  85. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  86. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  87. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  88. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  89. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  90. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  91. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  92. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  93. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  94. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  95. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  96. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  97. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  98. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  99. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  100. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  101. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  102. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  103. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  104. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  105. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  106. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  107. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  108. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  109. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  110. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  111. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  112. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  113. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  114. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  115. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  116. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  117. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  118. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  119. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  120. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  121. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  122. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  123. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  124. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  125. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  126. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  127. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  128. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  129. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  130. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  131. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  132. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  133. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  134. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  135. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  136. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  137. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  138. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  139. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  140. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  141. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  142. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  143. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  144. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  145. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  146. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  147. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  148. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  149. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  150. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  151. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  152. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  153. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  154. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  155. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  156. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  157. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  158. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  159. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  160. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  161. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  162. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  163. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  164. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  165. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  166. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  167. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  168. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  169. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  170. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  171. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  172. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  173. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  174. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  175. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  176. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  177. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  178. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  179. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  180. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  181. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  182. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  183. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  184. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  185. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  186. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  187. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  188. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  189. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  190. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  191. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  192. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  193. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  194. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  195. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  196. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  197. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  198. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  199. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  200. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  201. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  202. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  203. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  204. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  205. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  206. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  207. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  208. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  209. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  210. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  211. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  212. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  213. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  214. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  215. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  216. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  217. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  218. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  219. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  220. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  221. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  222. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  223. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  224. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  225. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  226. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  227. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  228. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  229. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  230. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  231. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  232. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  233. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  234. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  235. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  236. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  237. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  238. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  239. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  240. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  241. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  242. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  243. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  244. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  245. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  246. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  247. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  248. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  249. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  250. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  251. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  252. {sarj_python_lint-1.70.1 → sarj_python_lint-1.70.2}/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.70.2
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.70.2"
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" }]
@@ -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:
@@ -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}"
@@ -16,7 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleExample,
17
17
  Severity,
18
18
  )
19
- from sarj_python_lint.rules._ast_index import walk as walk_ast
19
+ from sarj_python_lint.rules._ast_index import statement_blocks
20
20
  from sarj_python_lint.rules._comments import has_external_reference
21
21
  from sarj_python_lint.rules._prose_budget import (
22
22
  groups,
@@ -26,7 +26,6 @@ from sarj_python_lint.rules._prose_budget import (
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  from sarj_python_lint._file_context import PythonFileContext
29
- from sarj_python_lint.rules._ast_index import NodeIndex
30
29
 
31
30
 
32
31
  _MIN_LINES = 3
@@ -64,7 +63,7 @@ class ExcessiveCommentary(Rule):
64
63
  limitations=(
65
64
  "Contiguous standalone implementation comment blocks with at least three non-empty lines and 20 words are inspected.",
66
65
  "The paragraph must immediately precede executable implementation code; narrative comments in tests and module setup are included.",
67
- "The previously shipped accumulator pattern remains an error; expanded implementation commentary is a warning requiring review, with no automatic deletion.",
66
+ "The previously shipped accumulator pattern with a proven builtin collection remains an error; expanded implementation commentary is a warning requiring review, with no automatic deletion.",
68
67
  "Rationale, technical vocabulary and external citations do not exempt a long paragraph from warning-level review. Their existing protection against accumulator errors remains intact.",
69
68
  "Generated files, directives, licenses, structured lists, docstrings, inline comments and short constraints are excluded.",
70
69
  ),
@@ -123,7 +122,7 @@ class ExcessiveCommentary(Rule):
123
122
  for node in context.nodes(ast.stmt)
124
123
  if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom))
125
124
  }
126
- accumulator_lines = _narrated_accumulator_lines(tree, node_index=context.node_index)
125
+ accumulator_lines = _narrated_accumulator_lines(context)
127
126
  findings: list[Diagnostic] = []
128
127
  for group in groups(path, source):
129
128
  lines = tuple(stripped for line in group.text.splitlines() if (stripped := line.strip()))
@@ -164,18 +163,17 @@ class ExcessiveCommentary(Rule):
164
163
  return findings
165
164
 
166
165
 
167
- def _narrated_accumulator_lines(tree: ast.Module, *, node_index: NodeIndex | None = None) -> set[int]:
166
+ def _narrated_accumulator_lines(context: PythonFileContext) -> set[int]:
168
167
  lines: set[int] = set()
169
- for function in walk_ast(tree, index=node_index):
170
- if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef)):
171
- continue
172
- _collect_accumulators(function.body, lines)
168
+ for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
169
+ for statements in statement_blocks(function.body):
170
+ _collect_accumulators(statements, lines, context)
173
171
  return lines
174
172
 
175
173
 
176
- def _collect_accumulators(statements: list[ast.stmt], lines: set[int]) -> None:
174
+ def _collect_accumulators(statements: list[ast.stmt], lines: set[int], context: PythonFileContext) -> None:
177
175
  for index, statement in enumerate(statements):
178
- name = _empty_collection_name(statement)
176
+ name = _empty_collection_name(statement, context)
179
177
  if name is not None:
180
178
  mutations = 0
181
179
  for following in statements[index + 1 :]:
@@ -184,11 +182,9 @@ def _collect_accumulators(statements: list[ast.stmt], lines: set[int]) -> None:
184
182
  mutations += 1
185
183
  if mutations >= _MIN_ACCUMULATOR_CALLS:
186
184
  lines.add(statement.lineno)
187
- for child_statements in _nested_statement_lists(statement):
188
- _collect_accumulators(child_statements, lines)
189
185
 
190
186
 
191
- def _empty_collection_name(statement: ast.stmt) -> str | None:
187
+ def _empty_collection_name(statement: ast.stmt, context: PythonFileContext) -> str | None:
192
188
  target: ast.expr | None = None
193
189
  value: ast.expr | None = None
194
190
  if isinstance(statement, ast.AnnAssign):
@@ -208,7 +204,9 @@ def _empty_collection_name(statement: ast.stmt) -> str | None:
208
204
  and not value.args
209
205
  and not value.keywords
210
206
  ):
211
- return target.id
207
+ provenance = context.static_resource_provenance
208
+ if provenance.builtin_namespace_untouched and provenance.binding(value.func.id, value.func) is None:
209
+ return target.id
212
210
  return None
213
211
 
214
212
 
@@ -222,22 +220,3 @@ def _is_accumulator_call(statement: ast.stmt, name: str) -> bool:
222
220
  and isinstance(function.value, ast.Name)
223
221
  and function.value.id == name
224
222
  )
225
-
226
-
227
- def _nested_statement_lists(statement: ast.stmt) -> list[list[ast.stmt]]:
228
- match statement:
229
- case ast.For() | ast.AsyncFor() | ast.While() | ast.If():
230
- return [statement.body, statement.orelse]
231
- case ast.With() | ast.AsyncWith():
232
- return [statement.body]
233
- case ast.Try() | ast.TryStar():
234
- return [
235
- statement.body,
236
- statement.orelse,
237
- statement.finalbody,
238
- *(handler.body for handler in statement.handlers),
239
- ]
240
- case ast.Match():
241
- return [case.body for case in statement.cases]
242
- case _:
243
- return []
@@ -32,7 +32,7 @@ class PreferDirectImportModule(Rule):
32
32
  default_level=Severity.WARNING,
33
33
  summary="Prefer static imports for eager module loads with a literal module name.",
34
34
  rationale="Static imports expose known dependencies to readers and tooling without dynamic loader indirection.",
35
- remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Keep dynamic loading when module selection or import timing requires it.",
35
+ remediation="Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Use import plugins.billing when callers use the qualified module name; avoid a redundant alias for a single-component module. Keep dynamic loading when module selection or import timing requires it.",
36
36
  category=RuleCategory.STYLE,
37
37
  autofix=AutofixPolicy.NONE,
38
38
  limitations=(
@@ -89,7 +89,7 @@ class PreferDirectImportModule(Rule):
89
89
  node.lineno,
90
90
  node.col_offset + 1,
91
91
  self.code,
92
- f"Use a module-level static import for the known module '{module}'; preserve the binding with 'import {module} as plugin' when needed.",
92
+ _recommendation(statement, module),
93
93
  severity=Severity.WARNING,
94
94
  )
95
95
  for statement in context.tree.body
@@ -100,6 +100,20 @@ class PreferDirectImportModule(Rule):
100
100
  ]
101
101
 
102
102
 
103
+ def _recommendation(statement: ast.Assign | ast.AnnAssign | ast.Expr, module: str) -> str:
104
+ target = statement.target if isinstance(statement, ast.AnnAssign) else None
105
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
106
+ target = statement.targets[0]
107
+ replacement = f"import {module}"
108
+ if isinstance(target, ast.Name):
109
+ if target.id != module:
110
+ replacement += f" as {target.id}"
111
+ return f"Use the module-level static import '{replacement}' to preserve the '{target.id}' binding."
112
+ return (
113
+ f"Use the module-level static import '{replacement}'; update references to the qualified module where needed."
114
+ )
115
+
116
+
103
117
  def _literal_module(call: ast.Call) -> str | None:
104
118
  match call:
105
119
  case ast.Call(args=[ast.Constant(value=str() as module)], keywords=[]):
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
28
28
  _MIN_FIELDS = 2
29
29
  _DOCUMENTATION_DIR_NAMES = frozenset({"docs", "docs_src"})
30
30
  _TUPLE_SOURCES = frozenset({"builtins"})
31
- _TYPING_SOURCES = frozenset({"typing"})
31
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
32
32
 
33
33
  _MSG = (
34
34
  "function returns a fixed positional tuple record — return a dataclass or validation model; "
@@ -55,7 +55,7 @@ class PreferNamedtupleOverTupleReturn(Rule):
55
55
  limitations=(
56
56
  "Generated files and documentation examples are excluded.",
57
57
  "Provenance-resolved builtin or typing fixed tuple return annotations with at least two slots are reported, including stable local aliases, quoted annotations, Annotated wrappers and optional unions in test helpers and fixtures; variadic tuples remain valid collections.",
58
- "Existing direct tuple records remain errors; newly resolved local aliases and optional-union records start as warnings for adoption review.",
58
+ "Existing direct tuple records remain errors; newly resolved typing_extensions providers, local aliases, application types named Unpack and optional-union records start as warnings for adoption review.",
59
59
  "Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible.",
60
60
  ),
61
61
  examples=(
@@ -124,7 +124,9 @@ class PreferNamedtupleOverTupleReturn(Rule):
124
124
  col=node.col_offset + 1,
125
125
  code=self.code,
126
126
  message=_MSG,
127
- severity=Severity.ERROR if _is_public_record_tuple(node.returns, imports) else Severity.WARNING,
127
+ severity=Severity.ERROR
128
+ if _is_public_record_tuple(node.returns, imports, legacy=True)
129
+ else Severity.WARNING,
128
130
  )
129
131
  )
130
132
  return diagnostics
@@ -134,40 +136,47 @@ def _is_documentation_path(path: Path) -> bool:
134
136
  return any(part.lower() in _DOCUMENTATION_DIR_NAMES for part in path.parts)
135
137
 
136
138
 
137
- def _is_public_record_tuple(annotation: ast.expr, imports: ImportIndex) -> bool:
138
- annotation = _unwrap_return_annotation(annotation, imports)
139
+ def _is_public_record_tuple(annotation: ast.expr, imports: ImportIndex, *, legacy: bool = False) -> bool:
140
+ typing_sources = frozenset({"typing"}) if legacy else _TYPING_SOURCES
141
+ annotation = _unwrap_return_annotation(annotation, imports, typing_sources)
139
142
  if not isinstance(annotation, ast.Subscript) or not isinstance(annotation.slice, ast.Tuple):
140
143
  return False
141
144
  target = annotation.value
142
145
  is_tuple = (
143
146
  (isinstance(target, ast.Name) and target.id == "tuple" and imports.builtin_is_unshadowed("tuple"))
144
147
  or imports.resolves(target, sources=_TUPLE_SOURCES, symbol="tuple")
145
- or imports.resolves(target, sources=_TYPING_SOURCES, symbol="Tuple")
148
+ or imports.resolves(target, sources=typing_sources, symbol="Tuple")
146
149
  )
147
150
  if not is_tuple:
148
151
  return False
149
152
  fields = annotation.slice.elts
150
- return len(fields) >= _MIN_FIELDS and not any(_is_variadic_field(field) for field in fields)
153
+ return len(fields) >= _MIN_FIELDS and not any(_is_variadic_field(field, imports, legacy=legacy) for field in fields)
151
154
 
152
155
 
153
- def _unwrap_return_annotation(annotation: ast.expr, imports: ImportIndex) -> ast.expr:
156
+ def _unwrap_return_annotation(annotation: ast.expr, imports: ImportIndex, typing_sources: frozenset[str]) -> ast.expr:
154
157
  if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
155
158
  parsed = parse_or_none(Path("annotation.py"), f"value: {annotation.value}")
156
159
  if parsed is not None and isinstance(parsed.body[0], ast.AnnAssign):
157
160
  return parsed.body[0].annotation
158
161
  if (
159
162
  isinstance(annotation, ast.Subscript)
160
- and imports.resolves(annotation.value, sources=_TYPING_SOURCES, symbol="Annotated")
163
+ and imports.resolves(annotation.value, sources=typing_sources, symbol="Annotated")
161
164
  and isinstance(annotation.slice, ast.Tuple)
162
165
  and annotation.slice.elts
163
166
  ):
164
- return _unwrap_return_annotation(annotation.slice.elts[0], imports)
167
+ return _unwrap_return_annotation(annotation.slice.elts[0], imports, typing_sources)
165
168
  return annotation
166
169
 
167
170
 
168
- def _is_variadic_field(node: ast.expr) -> bool:
171
+ def _is_variadic_field(node: ast.expr, imports: ImportIndex, *, legacy: bool) -> bool:
169
172
  return (
170
173
  isinstance(node, ast.Starred)
171
- or (isinstance(node, ast.Subscript) and _leaf_name(node.value) == "Unpack")
174
+ or (
175
+ isinstance(node, ast.Subscript)
176
+ and (
177
+ imports.resolves(node.value, sources=frozenset({"typing", "typing_extensions"}), symbol="Unpack")
178
+ or (legacy and _leaf_name(node.value) == "Unpack")
179
+ )
180
+ )
172
181
  or (isinstance(node, ast.Constant) and node.value is Ellipsis)
173
182
  )