sarj-python-lint 1.29.1__tar.gz → 1.31.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 (200) hide show
  1. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_namespace_access.py +3 -0
  4. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  5. sarj_python_lint-1.31.0/src/sarj_python_lint/rules/_resource_provenance.py +357 -0
  6. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +5 -3
  7. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +305 -5
  8. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +2 -2
  9. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +2 -2
  10. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +2 -2
  11. sarj_python_lint-1.31.0/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +655 -0
  12. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +52 -5
  13. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_resource_provenance.py +0 -200
  14. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/.gitignore +0 -0
  15. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/LICENSE +0 -0
  16. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/README.md +0 -0
  17. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/__init__.py +0 -0
  18. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/__main__.py +0 -0
  19. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  20. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_file_context.py +0 -0
  21. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_filesystem.py +0 -0
  22. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_python_target.py +0 -0
  23. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  24. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_secret_names.py +0 -0
  25. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_source.py +0 -0
  26. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/_version.py +0 -0
  27. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/benchmark.py +0 -0
  28. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  29. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/json_boundary.py +0 -0
  30. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/py.typed +0 -0
  31. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/ratchet.py +0 -0
  32. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rule_base.py +0 -0
  33. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  34. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  35. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  36. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  37. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  38. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  39. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  40. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  41. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  42. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  43. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  44. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  45. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  46. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  47. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  48. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  49. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  50. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  51. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  52. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  53. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  54. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  55. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  56. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  59. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  60. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  61. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  62. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  63. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  64. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  65. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  66. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  67. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  68. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  69. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  70. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  71. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  72. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  73. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  74. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  75. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  76. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  77. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  78. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  79. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  80. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  81. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  83. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  84. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  85. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  86. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  87. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  88. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  89. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  90. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  91. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  92. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  93. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  94. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  95. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  96. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  97. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  98. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  99. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  100. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  101. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  102. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  103. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  104. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  105. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  106. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  107. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  108. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  109. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  110. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  111. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  112. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  113. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  114. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  115. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  116. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  117. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  118. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  119. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  120. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  121. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  122. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  123. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  124. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  125. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  126. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  127. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  128. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  129. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  130. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  131. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  132. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  133. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  134. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  135. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  136. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  137. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  138. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  139. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  140. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  141. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  142. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  143. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  144. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  145. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  146. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  147. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  148. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  149. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  150. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  151. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  152. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  153. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  154. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  155. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  156. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  157. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  158. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  159. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  160. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  161. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  162. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  163. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  164. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  165. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  166. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  167. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  168. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  169. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  170. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  171. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  172. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  173. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  174. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  175. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  176. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  177. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  178. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  179. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  180. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  181. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  182. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  183. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  184. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  185. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  186. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  187. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  188. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  189. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  190. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  191. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  192. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  193. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  194. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  195. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  196. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  197. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  198. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  199. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  200. {sarj_python_lint-1.29.1 → sarj_python_lint-1.31.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.29.1
3
+ Version: 1.31.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.29.1"
3
+ version = "1.31.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" }]
@@ -16,6 +16,9 @@ _DYNAMIC_MEMBERS = frozenset(
16
16
  "__dict__",
17
17
  "__globals__",
18
18
  "__code__",
19
+ "__origin__",
20
+ "__args__",
21
+ "__parameters__",
19
22
  "__setattr__",
20
23
  "__delattr__",
21
24
  "__getattribute__",
@@ -112,6 +112,7 @@ from sarj_python_lint.rules.prefer_collection_comprehension import PreferCollect
112
112
  from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
113
113
  PreferConstantTimeSecretCompare,
114
114
  )
115
+ from sarj_python_lint.rules.prefer_declarative_batch_validation import PreferDeclarativeBatchValidation
115
116
  from sarj_python_lint.rules.prefer_fstring_over_concat import PreferFstringOverConcat
116
117
  from sarj_python_lint.rules.prefer_immutable_module_constant import (
117
118
  PreferImmutableModuleConstant,
@@ -245,6 +246,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
245
246
  NoStringConcatInLoop.id: NoStringConcatInLoop,
246
247
  PreferClassRow.id: PreferClassRow,
247
248
  PreferCollectionComprehension.id: PreferCollectionComprehension,
249
+ PreferDeclarativeBatchValidation.id: PreferDeclarativeBatchValidation,
248
250
  PreferStrEnum.id: PreferStrEnum,
249
251
  NoOffsetPagination.id: NoOffsetPagination,
250
252
  NoExcessiveCognitiveComplexity.id: NoExcessiveCognitiveComplexity,
@@ -0,0 +1,357 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ from functools import cached_property
6
+ from types import MappingProxyType
7
+ from typing import TYPE_CHECKING, final
8
+
9
+ from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from sarj_python_lint._file_context import PythonFileContext
14
+
15
+
16
+ _SCOPES = (
17
+ ast.Module,
18
+ ast.FunctionDef,
19
+ ast.AsyncFunctionDef,
20
+ ast.ClassDef,
21
+ ast.Lambda,
22
+ ast.ListComp,
23
+ ast.SetComp,
24
+ ast.DictComp,
25
+ ast.GeneratorExp,
26
+ )
27
+ _RESOURCE_OWNERS = MappingProxyType(
28
+ {
29
+ "contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
30
+ "subprocess": frozenset({"Popen", "TimeoutExpired"}),
31
+ }
32
+ )
33
+
34
+
35
+ @final
36
+ class ResourceProvenance:
37
+ def __init__(self, context: PythonFileContext, *, require_static_namespace: bool = False) -> None:
38
+ self.context = context
39
+ self.require_static_namespace = require_static_namespace
40
+ self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
41
+ self.reads: dict[str, list[ast.Name]] = defaultdict(list)
42
+ self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
43
+ self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
44
+ self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
45
+ self.import_contract_cache: dict[str, bool] = {}
46
+ self.wildcard = False
47
+ self.mutated: set[str] = set()
48
+ self.external_bindings: set[str] = set()
49
+ for node in context.nodes(ast.AST):
50
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
51
+ self.reads[node.id].append(node)
52
+
53
+ for name in _bound_names(node):
54
+ self.bindings[self.scope(node)][name].append(node)
55
+ self.wildcard |= name == "*"
56
+ if isinstance(node, ast.Global | ast.Nonlocal):
57
+ self.external_bindings.update(node.names)
58
+ if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del):
59
+ root = _root_name(node)
60
+ if root is not None:
61
+ self.mutated.add(root)
62
+
63
+ @cached_property
64
+ def builtin_namespace_untouched(self) -> bool:
65
+ return not any(
66
+ (isinstance(node, ast.Import) and any(alias.name == "builtins" for alias in node.names))
67
+ or (isinstance(node, ast.ImportFrom) and node.module == "builtins")
68
+ or (isinstance(node, ast.Name) and node.id == "__builtins__")
69
+ for node in self.context.nodes(ast.Import, ast.ImportFrom, ast.Name)
70
+ )
71
+
72
+ def scope(self, node: ast.AST) -> ast.AST | None:
73
+ parent = self.context.parents.get(node)
74
+ while parent is not None and not isinstance(parent, _SCOPES):
75
+ parent = self.context.parents.get(parent)
76
+ return parent
77
+
78
+ def binding(self, name: str, at: ast.AST) -> ast.AST | None:
79
+ scope = self.scope(at)
80
+ while scope is not None:
81
+ nodes = self.bindings.get(scope, {}).get(name)
82
+ if nodes:
83
+ return nodes[0] if len(nodes) == 1 else None
84
+ scope = self.scope(scope)
85
+ return None
86
+
87
+ def imported(self, node: ast.expr, qualified: str) -> bool:
88
+ root = _root_name(node)
89
+ if (
90
+ self.wildcard
91
+ or self.dynamic_namespace
92
+ or root is None
93
+ or root in self.mutated
94
+ or not self._owner_namespace_untouched(qualified.partition(".")[0])
95
+ ):
96
+ return False
97
+ binding = self.binding(root, node)
98
+ for read in self.reads.get(root, ()):
99
+ parent = self.context.parents.get(read)
100
+ if isinstance(parent, ast.Attribute | ast.ExceptHandler):
101
+ continue
102
+ if not (isinstance(parent, ast.Call) and parent.func is read):
103
+ return False
104
+ return (
105
+ isinstance(binding, ast.alias)
106
+ and isinstance(self.scope(binding), ast.Module)
107
+ and self.context.imports.resolved_qualified_name(node) == qualified
108
+ )
109
+
110
+ def _owner_namespace_untouched(self, module: str) -> bool:
111
+ if module not in _RESOURCE_OWNERS:
112
+ return True
113
+ if module not in self.import_contract_cache:
114
+ self.import_contract_cache[module] = self._inspect_owner_namespace(module)
115
+ return self.import_contract_cache[module]
116
+
117
+ def _inspect_owner_namespace(self, module: str) -> bool:
118
+ origins: dict[str, set[str]] = defaultdict(set)
119
+ for node in self.context.nodes(ast.Import, ast.ImportFrom):
120
+ for name, target in _owner_import_targets(node, module).items():
121
+ origins[name].add(target)
122
+ if not self.mutated.isdisjoint(origins):
123
+ return False
124
+ owners = frozenset(f"{module}.{name}" for name in _RESOURCE_OWNERS[module])
125
+ return all(
126
+ self._safe_import_use(node)
127
+ for node in self.context.nodes(ast.Name, ast.Attribute)
128
+ if self._owner_reference(node, origins, owners)
129
+ )
130
+
131
+ def _owner_reference(
132
+ self, node: ast.Name | ast.Attribute, origins: dict[str, set[str]], owners: frozenset[str]
133
+ ) -> bool:
134
+ if not isinstance(node.ctx, ast.Load):
135
+ return False
136
+ if isinstance(node, ast.Name):
137
+ return node.id in origins
138
+ return any(
139
+ qualified == owner or qualified.startswith(owner + ".")
140
+ for qualified in _owner_attribute_targets(node, origins)
141
+ for owner in owners
142
+ )
143
+
144
+ def _safe_import_use(self, node: ast.Name | ast.Attribute) -> bool:
145
+ parent = self.context.parents.get(node)
146
+ return (
147
+ (isinstance(parent, ast.Attribute) and parent.value is node)
148
+ or (isinstance(parent, ast.Call) and parent.func is node)
149
+ or (isinstance(parent, ast.ExceptHandler) and parent.type is node)
150
+ )
151
+
152
+ def constructor(self, name: ast.Name, at: ast.AST) -> ast.Call | None:
153
+ binding = self.binding(name.id, at)
154
+ if not isinstance(binding, ast.Name):
155
+ return None
156
+ statement = self.context.parents.get(binding)
157
+ if isinstance(statement, ast.withitem):
158
+ owner = self.context.parents.get(statement)
159
+ if (
160
+ binding is statement.optional_vars
161
+ and self.contains(owner, at)
162
+ and isinstance(statement.context_expr, ast.Call)
163
+ ):
164
+ return statement.context_expr
165
+ return None
166
+ value = self.assignment_value(binding, at)
167
+ return value if isinstance(value, ast.Call) else None
168
+
169
+ def assignment_value(self, binding: ast.Name, at: ast.AST) -> ast.expr | None:
170
+ statement = self.context.parents.get(binding)
171
+ if not isinstance(statement, ast.Assign | ast.AnnAssign):
172
+ return None
173
+ if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
174
+ return None
175
+ if not isinstance(self.context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
176
+ return None
177
+ return statement.value if statement.lineno < getattr(at, "lineno", 0) else None
178
+
179
+ def async_function(self, value: ast.expr, at: ast.AST) -> ast.AsyncFunctionDef | None:
180
+ if self.dynamic_namespace:
181
+ return None
182
+ seen: set[str] = set()
183
+ while isinstance(value, ast.Name) and value.id not in seen:
184
+ seen.add(value.id)
185
+ if value.id in self.mutated:
186
+ return None
187
+ binding = self.binding(value.id, at)
188
+ if isinstance(binding, ast.AsyncFunctionDef):
189
+ return self._stable_async_definition(binding)
190
+ if not isinstance(binding, ast.Name):
191
+ return None
192
+ resolved = self.assignment_value(binding, at)
193
+ if resolved is None:
194
+ return None
195
+ value, at = resolved, binding
196
+ return None
197
+
198
+ @cached_property
199
+ def dynamic_namespace(self) -> bool:
200
+ return self.require_static_namespace and dynamic_namespace_access(
201
+ self.context.nodes(ast.AST), self.context.imports, self.context.parents
202
+ )
203
+
204
+ def _stable_async_definition(self, function: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef | None:
205
+ owner = self.context.parents.get(function)
206
+ if not isinstance(owner, ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
207
+ return None
208
+ if function.decorator_list or any(isinstance(node, ast.Yield | ast.YieldFrom) for node in ast.walk(function)):
209
+ return None
210
+ if function not in self.async_contract_cache:
211
+ self.async_contract_cache[function] = self._async_aliases_untouched(function)
212
+ return function if self.async_contract_cache[function] else None
213
+
214
+ def _async_aliases_untouched(self, function: ast.AsyncFunctionDef) -> bool:
215
+ pending: list[ast.AsyncFunctionDef | ast.Name] = [function]
216
+ seen: set[ast.AST] = set()
217
+ while pending:
218
+ binding = pending.pop()
219
+ if binding in seen:
220
+ continue
221
+ seen.add(binding)
222
+ name = binding.id if isinstance(binding, ast.Name) else binding.name
223
+ if name in self.mutated or name in self.external_bindings:
224
+ return False
225
+ for read in self._binding_reads(name, binding):
226
+ alias = self._function_alias(read)
227
+ if alias is not None:
228
+ pending.append(alias)
229
+ elif not self._safe_async_read(read):
230
+ return False
231
+ return True
232
+
233
+ def _function_alias(self, read: ast.Name) -> ast.Name | None:
234
+ parent = self.context.parents.get(read)
235
+ if not isinstance(parent, ast.Assign | ast.AnnAssign) or parent.value is not read:
236
+ return None
237
+ if not isinstance(self.context.parents.get(parent), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
238
+ return None
239
+ targets = parent.targets if isinstance(parent, ast.Assign) else [parent.target]
240
+ if len(targets) != 1 or not isinstance(targets[0], ast.Name):
241
+ return None
242
+ target = targets[0]
243
+ return target if self.binding(target.id, target) is target else None
244
+
245
+ def _safe_async_read(self, read: ast.Name) -> bool:
246
+ parent = self.context.parents.get(read)
247
+ if not isinstance(parent, ast.Call):
248
+ return False
249
+ if parent.func is read:
250
+ return True
251
+ if not parent.args or parent.args[0] is not read or not isinstance(parent.func, ast.Attribute):
252
+ return False
253
+ receiver = parent.func.value
254
+ if parent.func.attr not in {"callback", "push_async_callback"} or not isinstance(receiver, ast.Name):
255
+ return False
256
+ if not self.unescaped(receiver, parent, allow_context=True):
257
+ return False
258
+ constructor = self.constructor(receiver, parent)
259
+ return (
260
+ constructor is not None
261
+ and not constructor.args
262
+ and not constructor.keywords
263
+ and any(self.imported(constructor.func, f"contextlib.{kind}") for kind in ("ExitStack", "AsyncExitStack"))
264
+ )
265
+
266
+ def _binding_reads(self, name: str, binding: ast.AST) -> list[ast.Name]:
267
+ if name not in self._reads_by_binding:
268
+ grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
269
+ for read in self.reads.get(name, ()):
270
+ if (owner := self.binding(read.id, read)) is not None:
271
+ grouped[owner].append(read)
272
+ self._reads_by_binding[name] = grouped
273
+ return self._reads_by_binding[name].get(binding, [])
274
+
275
+ def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
276
+ binding = self.binding(name.id, at)
277
+ if self.dynamic_namespace or binding is None or name.id in self.mutated:
278
+ return False
279
+ key = (binding, allow_context)
280
+ if key not in self.escape_cache:
281
+ self.escape_cache[key] = all(
282
+ self._method_or_context_read(read, allow_context=allow_context)
283
+ for read in self._binding_reads(name.id, binding)
284
+ )
285
+ return self.escape_cache[key]
286
+
287
+ def _method_or_context_read(self, read: ast.Name, *, allow_context: bool) -> bool:
288
+ parent = self.context.parents.get(read)
289
+ if allow_context and isinstance(parent, ast.withitem) and parent.context_expr is read:
290
+ return True
291
+ if (
292
+ not isinstance(parent, ast.Attribute)
293
+ or parent.value is not read
294
+ or parent.attr in {"__setattr__", "__delattr__", "__getattribute__"}
295
+ ):
296
+ return False
297
+ call = self.context.parents.get(parent)
298
+ return isinstance(call, ast.Call) and call.func is parent
299
+
300
+ def contains(self, owner: ast.AST | None, node: ast.AST) -> bool:
301
+ while node in self.context.parents:
302
+ node = self.context.parents[node]
303
+ if node is owner:
304
+ return True
305
+ return False
306
+
307
+
308
+ def _owner_import_targets(node: ast.Import | ast.ImportFrom, module: str) -> dict[str, str]:
309
+ if isinstance(node, ast.Import):
310
+ return {alias.asname or alias.name: module for alias in node.names if alias.name == module}
311
+ if node.module == module:
312
+ return {
313
+ alias.asname or alias.name: f"{module}.{alias.name}"
314
+ for alias in node.names
315
+ if alias.name in _RESOURCE_OWNERS[module]
316
+ }
317
+ return {}
318
+
319
+
320
+ def _owner_attribute_targets(node: ast.Attribute, origins: dict[str, set[str]]) -> frozenset[str]:
321
+ parts: list[str] = []
322
+ current: ast.expr = node
323
+ while isinstance(current, ast.Attribute):
324
+ parts.append(current.attr)
325
+ current = current.value
326
+ if not isinstance(current, ast.Name):
327
+ return frozenset()
328
+ suffix = ".".join(reversed(parts))
329
+ return frozenset(f"{origin}.{suffix}" for origin in origins.get(current.id, ()))
330
+
331
+
332
+ def _root_name(node: ast.expr) -> str | None:
333
+ while isinstance(node, ast.Attribute):
334
+ node = node.value
335
+ return node.id if isinstance(node, ast.Name) else None
336
+
337
+
338
+ def _bound_names(node: ast.AST) -> tuple[str, ...]:
339
+ match node:
340
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
341
+ return (name,)
342
+ case ast.alias():
343
+ return (node.asname or node.name.partition(".")[0],)
344
+ case (
345
+ ast.FunctionDef(name=name)
346
+ | ast.AsyncFunctionDef(name=name)
347
+ | ast.ClassDef(name=name)
348
+ | ast.ExceptHandler(name=str() as name)
349
+ | ast.MatchAs(name=str() as name)
350
+ | ast.MatchStar(name=str() as name)
351
+ | ast.MatchMapping(rest=str() as name)
352
+ ):
353
+ return (name,)
354
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
355
+ return tuple(names)
356
+ case _:
357
+ return ()
@@ -30,7 +30,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
30
30
  id: str = "async-cleanup-registered-synchronously"
31
31
  code: str = "SARJ463"
32
32
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
33
- default_level=Severity.WARNING,
33
+ default_level=Severity.ERROR,
34
34
  summary="Do not register a proven async cleanup function with ExitStack.callback.",
35
35
  rationale="The synchronous callback API discards the returned coroutine without awaiting it, so the cleanup body never runs.",
36
36
  remediation="Use AsyncExitStack and register the coroutine function with push_async_callback.",
@@ -40,6 +40,8 @@ class AsyncCleanupRegisteredSynchronously(Rule):
40
40
  "Requires a proven contextlib ExitStack or AsyncExitStack constructor and an undecorated, visible async function, optionally through simple function aliases.",
41
41
  "Unknown methods/callables, decorated functions, async generators, conditional bindings, transferred stacks, wildcard imports, and generated files are excluded.",
42
42
  "Resource aliases and interprocedural mutation are not inferred. No autofix is offered because executing previously dropped cleanup can raise new exceptions.",
43
+ "Coroutine-function assignment aliases must remain unmutated and unescaped; attribute inspection, containers, returned functions and unknown helper arguments abstain. Direct calls and proven stack registrations remain supported.",
44
+ "Visible namespace APIs, frame/module introspection, code mutation and bound attribute setters invalidate the file's provenance proof.",
43
45
  ),
44
46
  examples=(
45
47
  RuleExample(
@@ -70,7 +72,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
70
72
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
71
73
  if "callback" not in context.symbol_source or context.generated or context.tree is None:
72
74
  return []
73
- provenance = ResourceProvenance(context)
75
+ provenance = ResourceProvenance(context, require_static_namespace=True)
74
76
  findings: list[Diagnostic] = []
75
77
  for call in context.nodes(ast.Call):
76
78
  if not _wrong_callback(call, provenance) or is_suppressed(context.source_lines, call.lineno, self.code):
@@ -81,7 +83,7 @@ class AsyncCleanupRegisteredSynchronously(Rule):
81
83
  line=call.lineno,
82
84
  col=call.col_offset + 1,
83
85
  code=self.code,
84
- severity=Severity.WARNING,
86
+ severity=Severity.ERROR,
85
87
  message="synchronous ExitStack.callback does not await this async cleanup function; use AsyncExitStack.push_async_callback",
86
88
  )
87
89
  )