sarj-python-lint 1.31.0__tar.gz → 1.32.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.31.0 → sarj_python_lint-1.32.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_file_context.py +5 -0
  4. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_python_target.py +39 -8
  5. sarj_python_lint-1.32.0/src/sarj_python_lint/rules/_closed_helpers.py +185 -0
  6. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_copy_on_write.py +136 -19
  7. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_namespace_access.py +6 -2
  8. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_project_index.py +624 -5
  9. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +56 -12
  10. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_resource_provenance.py +63 -12
  11. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +2 -2
  12. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +5 -2
  13. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +5 -6
  14. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +9 -5
  15. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +71 -3
  16. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +6 -5
  17. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +64 -27
  18. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +25 -8
  19. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +20 -11
  20. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +83 -24
  21. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +63 -14
  22. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +2 -2
  23. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +58 -5
  24. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +4 -2
  25. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +3 -1
  26. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +3 -1
  27. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +2 -2
  28. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +130 -25
  29. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +17 -4
  30. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +17 -4
  31. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +67 -11
  32. sarj_python_lint-1.31.0/src/sarj_python_lint/rules/_closed_helpers.py +0 -100
  33. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/.gitignore +0 -0
  34. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/LICENSE +0 -0
  35. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/README.md +0 -0
  36. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/__init__.py +0 -0
  37. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/__main__.py +0 -0
  38. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  39. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_filesystem.py +0 -0
  40. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  41. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_secret_names.py +0 -0
  42. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_source.py +0 -0
  43. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_version.py +0 -0
  44. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/benchmark.py +0 -0
  45. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  46. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/json_boundary.py +0 -0
  47. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/py.typed +0 -0
  48. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/ratchet.py +0 -0
  49. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rule_base.py +0 -0
  50. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  51. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  52. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  53. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  54. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  55. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  56. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  57. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  58. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  59. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  60. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  61. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  62. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  63. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  64. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  65. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  66. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  67. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  68. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  69. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  70. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  71. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  72. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  73. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  74. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  75. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  76. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  77. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  78. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  79. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  80. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  81. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  82. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  83. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  84. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  85. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  86. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  87. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  88. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  89. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  90. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  91. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  92. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  93. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  94. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  95. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  96. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  97. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  98. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  99. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  100. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  101. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  102. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  103. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  104. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  105. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  106. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  107. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  108. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  109. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  110. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  111. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  112. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  113. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  114. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  115. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  116. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  117. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  118. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  119. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  121. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  122. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  123. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  124. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  125. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  126. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  127. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  128. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  129. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  130. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  131. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  132. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  133. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  134. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  135. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  136. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  137. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  138. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  139. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  140. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  141. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  142. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  143. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  144. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  145. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  146. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  147. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  148. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  149. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  150. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  151. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  152. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  153. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  154. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  155. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  156. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  157. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  158. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  159. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  160. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  161. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  162. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  163. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  164. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  165. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  166. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  167. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  168. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  169. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  170. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  171. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  172. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  173. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  174. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  175. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  176. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  177. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  178. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  179. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  180. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  181. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  182. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  183. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  184. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  185. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  186. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  187. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  188. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  189. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  190. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  191. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  192. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  193. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  194. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  195. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  196. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  197. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  198. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  199. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  200. {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.31.0
3
+ Version: 1.32.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.31.0"
3
+ version = "1.32.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" }]
@@ -9,6 +9,7 @@ from sarj_python_lint._source import symbol_prefilter_source
9
9
  from sarj_python_lint.rules._ast_index import NodeIndex
10
10
  from sarj_python_lint.rules._fastapi import FastapiIndex
11
11
  from sarj_python_lint.rules._imports import ImportIndex
12
+ from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
12
13
  from sarj_python_lint.rules._paths import is_generated
13
14
 
14
15
 
@@ -90,6 +91,10 @@ class PythonFileContext:
90
91
  def module_imports(self) -> ImportIndex:
91
92
  return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
92
93
 
94
+ @cached_property
95
+ def dynamic_namespace(self) -> bool:
96
+ return dynamic_namespace_access(self.nodes(ast.AST), self.imports, self.parents)
97
+
93
98
  @cached_property
94
99
  def fastapi(self) -> FastapiIndex:
95
100
  return FastapiIndex(self._valid_tree(), path=self.path)
@@ -23,25 +23,38 @@ class PythonTargetFacts:
23
23
  self._projects: dict[Path, str | None] = {}
24
24
  self._installed: dict[Path, dict[Path, str | None]] = {}
25
25
  self._witnesses: dict[tuple[str, tuple[int, int]], bool] = {}
26
+ self._minimums: dict[tuple[str, tuple[int, int]], bool] = {}
26
27
 
27
28
  def has_declared_support_before(self, path: Path, minimum: tuple[int, int]) -> bool:
29
+ declaration = self._declared_target(path)
30
+ if not declaration:
31
+ return False
32
+ key = declaration, minimum
33
+ if key not in self._witnesses:
34
+ self._witnesses[key] = _has_legacy_witness(declaration, minimum)
35
+ return self._witnesses[key]
36
+
37
+ def has_declared_minimum(self, path: Path, minimum: tuple[int, int]) -> bool:
38
+ declaration = self._declared_target(path)
39
+ if not declaration:
40
+ return False
41
+ key = declaration, minimum
42
+ if key not in self._minimums:
43
+ self._minimums[key] = _proves_minimum(declaration, minimum)
44
+ return self._minimums[key]
45
+
46
+ def _declared_target(self, path: Path) -> str | None:
28
47
  try:
29
48
  absolute = path.absolute()
30
49
  resolved = path.resolve()
31
50
  except OSError, RuntimeError:
32
- return False
51
+ return None
33
52
  if absolute not in self._declarations:
34
53
  try:
35
54
  self._declarations[absolute] = self._declaration(absolute, resolved)
36
55
  except OSError, RuntimeError:
37
56
  self._declarations[absolute] = None
38
- declaration = self._declarations[absolute]
39
- if not declaration:
40
- return False
41
- key = declaration, minimum
42
- if key not in self._witnesses:
43
- self._witnesses[key] = _has_legacy_witness(declaration, minimum)
44
- return self._witnesses[key]
57
+ return self._declarations[absolute]
45
58
 
46
59
  def _declaration(self, absolute: Path, resolved: Path) -> str | None:
47
60
  ancestors = [*list(absolute.parents)[:_MAX_ANCESTORS], *list(resolved.parents)[:_MAX_ANCESTORS]]
@@ -142,3 +155,21 @@ def _has_legacy_witness(declaration: str, minimum: tuple[int, int]) -> bool:
142
155
  and specifiers.contains(candidate, prereleases=False)
143
156
  for candidate in candidates
144
157
  )
158
+
159
+
160
+ def _proves_minimum(declaration: str, minimum: tuple[int, int]) -> bool:
161
+ try:
162
+ specifiers = SpecifierSet(declaration)
163
+ except InvalidSpecifier:
164
+ return False
165
+ threshold = Version(".".join(map(str, minimum)))
166
+ for specifier in specifiers:
167
+ if specifier.operator not in {">=", ">", "==", "~="}:
168
+ continue
169
+ try:
170
+ boundary = Version(specifier.version.removesuffix(".*"))
171
+ except InvalidVersion:
172
+ continue
173
+ if not boundary.epoch and boundary >= threshold:
174
+ return True
175
+ return False
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sarj_python_lint.rules._ast_index import nodes
7
+ from sarj_python_lint.rules._pydantic_field_contract import (
8
+ inert_annotation,
9
+ inert_class_value,
10
+ inert_signature,
11
+ literal_value,
12
+ )
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from sarj_python_lint._file_context import PythonFileContext
17
+ from sarj_python_lint.rules._ast_index import NodeIndex
18
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
19
+
20
+
21
+ _DYNAMIC_NAMES = frozenset(
22
+ {"globals", "locals", "vars", "eval", "exec", "getattr", "setattr", "delattr", "__import__", "__all__"}
23
+ )
24
+
25
+
26
+ def dynamic_namespace(context: PythonFileContext) -> bool:
27
+ return (
28
+ context.dynamic_namespace
29
+ or any(node.id in _DYNAMIC_NAMES for node in context.nodes(ast.Name))
30
+ or any(node.attr in _DYNAMIC_NAMES | {"__dict__"} for node in context.nodes(ast.Attribute))
31
+ or any(node.name == "*" for node in context.nodes(ast.alias))
32
+ )
33
+
34
+
35
+ def executable_body(function: ast.FunctionDef) -> list[ast.stmt]:
36
+ body = function.body
37
+ return (
38
+ body[1:]
39
+ if body
40
+ and isinstance(body[0], ast.Expr)
41
+ and isinstance(body[0].value, ast.Constant)
42
+ and isinstance(body[0].value.value, str)
43
+ else body
44
+ )
45
+
46
+
47
+ def closed_references(
48
+ context: PythonFileContext, function: ast.FunctionDef, provenance: ResourceProvenance
49
+ ) -> list[ast.Name] | None:
50
+ if (
51
+ context.parents.get(function) is not context.tree
52
+ or not function.name.startswith("_")
53
+ or function.name.startswith("__")
54
+ or function.decorator_list
55
+ or function.type_params
56
+ ):
57
+ return None
58
+ if has_member_import(context.tree, function.name, node_index=context.node_index):
59
+ return None
60
+ if (
61
+ function.args.vararg is not None
62
+ or function.args.kwarg is not None
63
+ or dynamic_namespace(context)
64
+ or any(node.attr == function.name for node in context.nodes(ast.Attribute))
65
+ or any(
66
+ isinstance(node.value, str) and function.name in node.value.split(".")
67
+ for node in context.nodes(ast.Constant)
68
+ )
69
+ ):
70
+ return None
71
+ references = [node for node in context.nodes(ast.Name) if node.id == function.name]
72
+ if not references or any(
73
+ not isinstance(node.ctx, ast.Load) or provenance.binding(node.id, node) is not function for node in references
74
+ ):
75
+ return None
76
+ return references
77
+
78
+
79
+ def has_member_import(tree: ast.Module | None, name: str, *, node_index: NodeIndex | None = None) -> bool:
80
+ return tree is None or any(
81
+ alias.name == name for node in nodes(tree, ast.ImportFrom, index=node_index) for alias in node.names
82
+ )
83
+
84
+
85
+ def direct_call(reference: ast.Name, context: PythonFileContext) -> ast.Call | None:
86
+ parent = context.parents.get(reference)
87
+ if not isinstance(parent, ast.Call) or parent.func is not reference:
88
+ return None
89
+ if any(isinstance(argument, ast.Starred) for argument in parent.args) or any(
90
+ keyword.arg is None for keyword in parent.keywords
91
+ ):
92
+ return None
93
+ return parent
94
+
95
+
96
+ def call_matches(function: ast.FunctionDef, call: ast.Call) -> bool:
97
+ positional = [*function.args.posonlyargs, *function.args.args]
98
+ keywords = {arg.arg for arg in (*function.args.args, *function.args.kwonlyargs)}
99
+ required = {arg.arg for arg in positional[: len(positional) - len(function.args.defaults)]}
100
+ required.update(
101
+ arg.arg
102
+ for arg, default in zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True)
103
+ if default is None
104
+ )
105
+ supplied = {arg.arg for arg in positional[: len(call.args)]}
106
+ if len(call.args) > len(positional):
107
+ return False
108
+ for keyword in call.keywords:
109
+ if keyword.arg not in keywords or keyword.arg in supplied:
110
+ return False
111
+ supplied.add(keyword.arg)
112
+ return required <= supplied
113
+
114
+
115
+ def stable_class(context: PythonFileContext, declaration: ast.ClassDef, provenance: ResourceProvenance) -> bool:
116
+ if declaration.decorator_list or declaration.keywords or context.parents.get(declaration) is not context.tree:
117
+ return False
118
+ return provenance.bindings.get(context.tree, {}).get(declaration.name) == [declaration]
119
+
120
+
121
+ def plain_class_members(
122
+ context: PythonFileContext,
123
+ declaration: ast.ClassDef,
124
+ provenance: ResourceProvenance,
125
+ *,
126
+ pydantic_fields: bool = False,
127
+ ) -> bool:
128
+ return all(
129
+ _plain_class_member(context, member, provenance, pydantic_fields=pydantic_fields) for member in declaration.body
130
+ )
131
+
132
+
133
+ def _plain_class_member(
134
+ context: PythonFileContext, member: ast.stmt, provenance: ResourceProvenance, *, pydantic_fields: bool
135
+ ) -> bool:
136
+ match member:
137
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
138
+ if member.name.startswith("__"):
139
+ return False
140
+ return inert_signature(member, context) and all(
141
+ _plain_decorator(context, decorator, provenance, pydantic_fields=pydantic_fields)
142
+ for decorator in member.decorator_list
143
+ )
144
+ case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
145
+ return inert_annotation(annotation, context) and (
146
+ value is None or inert_class_value(context, value, provenance, pydantic_fields=pydantic_fields)
147
+ )
148
+ case ast.Assign(targets=targets, value=value):
149
+ return all(isinstance(target, ast.Name) for target in targets) and inert_class_value(
150
+ context, value, provenance, pydantic_fields=pydantic_fields
151
+ )
152
+ case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
153
+ return True
154
+ case _:
155
+ return False
156
+
157
+
158
+ def _plain_decorator(
159
+ context: PythonFileContext, decorator: ast.expr, provenance: ResourceProvenance, *, pydantic_fields: bool
160
+ ) -> bool:
161
+ if isinstance(decorator, ast.Name) and decorator.id in {"classmethod", "staticmethod", "property"}:
162
+ return context.imports.builtin_is_unshadowed(decorator.id) and provenance.builtin_namespace_untouched
163
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
164
+ qualified = context.module_imports.resolved_qualified_name(target)
165
+ if qualified in {"typing.final", "typing_extensions.final"} and not isinstance(decorator, ast.Call):
166
+ return provenance.imported(target, qualified)
167
+ if (
168
+ not pydantic_fields
169
+ or not isinstance(decorator, ast.Call)
170
+ or qualified
171
+ not in {
172
+ "pydantic.model_validator",
173
+ "pydantic.field_validator",
174
+ "pydantic.functional_validators.model_validator",
175
+ "pydantic.functional_validators.field_validator",
176
+ }
177
+ or not provenance.imported(target, qualified)
178
+ ):
179
+ return False
180
+ return all(literal_value(argument) for argument in decorator.args) and all(
181
+ keyword.arg is not None
182
+ and literal_value(keyword.value)
183
+ and not (keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "wrap")
184
+ for keyword in decorator.keywords
185
+ )
@@ -2,23 +2,28 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from dataclasses import dataclass, replace
5
+ from functools import cached_property
5
6
  from types import MappingProxyType
6
7
  from typing import TYPE_CHECKING, final
7
8
 
9
+ from sarj_python_lint._analysis_session import AnalysisSession
10
+ from sarj_python_lint._file_context import PythonFileContext
8
11
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
12
+ from sarj_python_lint.rules._closed_helpers import plain_class_members
9
13
  from sarj_python_lint.rules._imports import ImportIndex
10
- from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
14
+ from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef, imports_outside
15
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
11
16
 
12
17
 
13
18
  if TYPE_CHECKING:
14
19
  from collections.abc import Mapping
15
-
16
- from sarj_python_lint._file_context import PythonFileContext
20
+ from pathlib import Path
17
21
 
18
22
 
19
23
  type _Function = ast.FunctionDef | ast.AsyncFunctionDef
20
24
  _MODEL_BASES = frozenset({"pydantic.BaseModel", "pydantic.main.BaseModel"})
21
25
  _PYDANTIC = frozenset({"pydantic", "pydantic.functional_validators"})
26
+ _MAX_BEFORE_VALIDATOR_DECORATORS = 2
22
27
  _TYPING = frozenset({"typing", "typing_extensions"})
23
28
  _COLLECTIONS = frozenset({"collections.abc", "typing", "typing_extensions"})
24
29
  _TWO_ARGUMENTS = 2
@@ -62,6 +67,12 @@ class Mutation:
62
67
  before_validator: bool
63
68
 
64
69
 
70
+ @dataclass(frozen=True, slots=True)
71
+ class _ValidationOwner:
72
+ context: PythonFileContext
73
+ provenance: ResourceProvenance
74
+
75
+
65
76
  @final
66
77
  class _Models:
67
78
  def __init__(self, context: PythonFileContext) -> None:
@@ -81,11 +92,28 @@ class _Models:
81
92
  },
82
93
  )
83
94
  )
95
+ if (
96
+ (unit is None or unit.module is None)
97
+ and self.unit.tree is not None
98
+ and any(isinstance(node, ast.ClassDef) for node in self.unit.tree.body)
99
+ ):
100
+ self.indexes = self.indexes.with_unit(self.unit)
101
+ self._validation_session = AnalysisSession()
102
+ self._validation_session.project = self.indexes
103
+ self._validation_owners: dict[Path, _ValidationOwner] = {}
84
104
  self._classes: dict[SymbolRef, tuple[SourceUnit, ast.ClassDef] | None] = {}
85
105
  self._ancestry: dict[SymbolRef, bool] = {}
86
106
  self._fields: dict[tuple[SymbolRef, str], _Shape | None] = {}
87
107
  self._annotations: dict[str | None, AnnotationSemantics] = {}
88
108
 
109
+ def validation_owner(self, unit: SourceUnit) -> _ValidationOwner:
110
+ if unit.path not in self._validation_owners:
111
+ context = PythonFileContext(unit.path, unit.source, self._validation_session)
112
+ self._validation_owners[unit.path] = _ValidationOwner(
113
+ context, ResourceProvenance(context, require_static_namespace=True)
114
+ )
115
+ return self._validation_owners[unit.path]
116
+
89
117
  def annotations(self, unit: SourceUnit) -> AnnotationSemantics:
90
118
  if unit.module not in self._annotations:
91
119
  tree = unit.tree if unit.tree is not None else ast.Module(body=[], type_ignores=[])
@@ -180,6 +208,39 @@ class _Models:
180
208
  for base in bases
181
209
  )
182
210
 
211
+ def plain_validation(self, symbol: SymbolRef, seen: frozenset[SymbolRef] = frozenset()) -> bool:
212
+ if (
213
+ symbol in seen
214
+ or self.indexes.nominal_membership_is_dynamic(symbol)
215
+ or (resolved := self.class_node(symbol)) is None
216
+ ):
217
+ return False
218
+ unit, node = resolved
219
+ if node.decorator_list or (
220
+ unit.module == "__copy_source__" and imports_outside(unit.tree, _PYDANTIC | _TYPING)
221
+ ):
222
+ return False
223
+ owner = self.validation_owner(unit)
224
+ if not plain_class_members(owner.context, node, owner.provenance, pydantic_fields=True):
225
+ return False
226
+ if any(
227
+ name
228
+ in {
229
+ "__init_subclass__",
230
+ "__pydantic_init_subclass__",
231
+ "__pydantic_on_complete__",
232
+ "__get_pydantic_core_schema__",
233
+ }
234
+ for name in scope_bound_names(node.body)
235
+ ):
236
+ return False
237
+ bases = [self.symbol(base, unit) for base in node.bases]
238
+ return bool(bases) and all(
239
+ base is not None
240
+ and (f"{base.module}.{base.name}" in _MODEL_BASES or self.plain_validation(base, seen | {symbol}))
241
+ for base in bases
242
+ )
243
+
183
244
  def shape(
184
245
  self,
185
246
  annotation: ast.expr | None,
@@ -314,6 +375,10 @@ class CopyOnWriteAnalysis:
314
375
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
315
376
  self._function(function)
316
377
 
378
+ @cached_property
379
+ def _validator_provider(self) -> ResourceProvenance:
380
+ return ResourceProvenance(self.context, require_static_namespace=True)
381
+
317
382
  def _function(self, function: _Function) -> None:
318
383
  parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
319
384
  names = {parameter.arg for parameter in parameters}
@@ -343,9 +408,11 @@ class CopyOnWriteAnalysis:
343
408
  receiver = self._receiver(function, parameters)
344
409
  for parameter in parameters:
345
410
  shape = self.models.shape(parameter.annotation, blocked=self._blocked)
346
- if receiver is not None and parameter is parameters[0]:
411
+ if parameter.arg == before_input:
412
+ bindings[parameter.arg] = _Value(shape, borrowed=True, shared_children=True)
413
+ elif receiver is not None and parameter is parameters[0]:
347
414
  bindings[parameter.arg] = receiver
348
- elif _contains_model(shape) or parameter.arg == before_input:
415
+ elif _contains_model(shape):
349
416
  bindings[parameter.arg] = _Value(shape, borrowed=True, shared_children=True)
350
417
  return bindings
351
418
 
@@ -362,26 +429,76 @@ class CopyOnWriteAnalysis:
362
429
 
363
430
  def _validator_input(self, function: _Function) -> str | None:
364
431
  parent = self.context.parents.get(function)
365
- class_bindings: set[str] = scope_bound_names(parent.body) if isinstance(parent, ast.ClassDef) else set()
366
- if not any(
367
- isinstance(decorator, ast.Call)
368
- and not any(isinstance(node, ast.Name) and node.id in class_bindings for node in ast.walk(decorator.func))
369
- and self.context.module_imports.resolved_symbol(decorator.func, sources=_PYDANTIC)
370
- in {"model_validator", "field_validator"}
371
- and any(
372
- keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "before"
373
- for keyword in decorator.keywords
374
- )
375
- for decorator in function.decorator_list
432
+ if (
433
+ not isinstance(parent, ast.ClassDef)
434
+ or self.context.parents.get(parent) is not self.context.tree
435
+ or not function.decorator_list
436
+ or self.context.dynamic_namespace
376
437
  ):
377
438
  return None
439
+ decorator = self._before_decorator(function)
440
+ if decorator is None:
441
+ return None
442
+ symbol = self.models.symbol(ast.Name(id=parent.name))
443
+ if symbol is None or not self.models.plain_validation(symbol):
444
+ return None
445
+ if function.name in scope_bound_names([member for member in parent.body if member is not function]):
446
+ return None
447
+ class_bindings = scope_bound_names(parent.body)
448
+ if any(isinstance(node, ast.Name) and node.id in class_bindings for node in ast.walk(decorator.func)):
449
+ return None
450
+ descriptor = self._validator_descriptor(function, class_bindings)
451
+ if descriptor is None:
452
+ return None
378
453
  parameters = [*function.args.posonlyargs, *function.args.args]
379
- is_classmethod = any(
380
- isinstance(decorator, ast.Name) and decorator.id == "classmethod" for decorator in function.decorator_list
454
+ qualified = self.context.module_imports.resolved_qualified_name(decorator.func)
455
+ if qualified is None or not self._validator_provider.imported(decorator.func, qualified):
456
+ return None
457
+ offset = int(
458
+ descriptor == "builtins.classmethod"
459
+ or (descriptor == "auto" and bool(parameters and parameters[0].arg == "cls"))
381
460
  )
382
- offset = int(is_classmethod or bool(parameters and parameters[0].arg == "cls"))
383
461
  return parameters[offset].arg if len(parameters) > offset else None
384
462
 
463
+ def _before_decorator(self, function: _Function) -> ast.Call | None:
464
+ decorator = function.decorator_list[0]
465
+ if not isinstance(decorator, ast.Call) or len(function.decorator_list) > _MAX_BEFORE_VALIDATOR_DECORATORS:
466
+ return None
467
+ if self.context.module_imports.resolved_symbol(decorator.func, sources=_PYDANTIC) not in {
468
+ "model_validator",
469
+ "field_validator",
470
+ }:
471
+ return None
472
+ return (
473
+ decorator
474
+ if any(
475
+ keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "before"
476
+ for keyword in decorator.keywords
477
+ )
478
+ else None
479
+ )
480
+
481
+ def _validator_descriptor(self, function: _Function, class_bindings: set[str]) -> str | None:
482
+ if not self._validator_provider.builtin_namespace_untouched:
483
+ return None
484
+ if len(function.decorator_list) == _MAX_BEFORE_VALIDATOR_DECORATORS:
485
+ inner = function.decorator_list[1]
486
+ qualified = self.context.module_imports.resolved_qualified_name(inner)
487
+ if (
488
+ isinstance(inner, ast.Name)
489
+ and inner.id in {"classmethod", "staticmethod"}
490
+ and (
491
+ inner.id not in class_bindings
492
+ and inner.id not in self.context.module_imports.shadowed_names
493
+ and inner.id not in self.context.module_imports.bindings
494
+ )
495
+ ):
496
+ qualified = f"builtins.{inner.id}"
497
+ if qualified not in {"builtins.classmethod", "builtins.staticmethod"}:
498
+ return None
499
+ return qualified
500
+ return "auto"
501
+
385
502
  def _value(self, node: ast.expr, bindings: Mapping[str, _Value]) -> _Value | None:
386
503
  match node:
387
504
  case ast.Name(id=name):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Final
5
5
 
6
6
 
7
7
  if TYPE_CHECKING:
@@ -27,11 +27,13 @@ _DYNAMIC_MEMBERS = frozenset(
27
27
  "f_builtins",
28
28
  }
29
29
  )
30
- _MODULE_APIS = {
30
+ _MODULE_APIS: Final[dict[str, frozenset[str]]] = {
31
31
  "sys": frozenset({"modules", "_getframe"}),
32
+ "inspect": frozenset({"currentframe"}),
32
33
  "importlib": frozenset({"import_module"}),
33
34
  "builtins": _NAMESPACE_APIS,
34
35
  }
36
+ _RESOLVED_MEMBERS = _NAMESPACE_APIS | {"modules", "import_module"}
35
37
 
36
38
 
37
39
  def dynamic_namespace_access(
@@ -72,6 +74,8 @@ def _namespace_reference(
72
74
  if isinstance(node, ast.Attribute):
73
75
  if node.attr in _DYNAMIC_MEMBERS:
74
76
  return True
77
+ if node.attr not in _RESOLVED_MEMBERS:
78
+ return False
75
79
  qualified = imports.resolved_qualified_name(node)
76
80
  return qualified in {"sys.modules", "importlib.import_module"} or (
77
81
  imports.resolved_symbol(node, sources=frozenset({"builtins"})) in _NAMESPACE_APIS