sarj-python-lint 1.28.5__tar.gz → 1.29.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 (197) hide show
  1. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__main__.py +55 -21
  4. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_file_context.py +11 -0
  5. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_namespace_access.py +84 -0
  6. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_pydantic_field_contract.py +285 -0
  7. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_registry.py +6 -0
  8. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -13
  9. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +2 -15
  10. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +49 -19
  11. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +148 -0
  12. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +244 -0
  13. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_offset_pagination.py +2 -14
  14. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +163 -0
  15. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/.gitignore +0 -0
  16. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/LICENSE +0 -0
  17. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/README.md +0 -0
  18. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__init__.py +0 -0
  19. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  20. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_filesystem.py +0 -0
  21. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_python_target.py +0 -0
  22. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  23. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_secret_names.py +0 -0
  24. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_source.py +0 -0
  25. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_version.py +0 -0
  26. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/benchmark.py +0 -0
  27. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  28. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/json_boundary.py +0 -0
  29. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/py.typed +0 -0
  30. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/ratchet.py +0 -0
  31. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rule_base.py +0 -0
  32. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  33. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  34. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  35. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  36. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  37. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  38. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  39. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  40. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  41. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  42. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  43. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  44. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  45. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  46. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  47. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  48. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  49. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  50. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  51. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  52. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  53. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  54. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  55. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  56. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  57. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  58. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  59. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  60. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  61. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  62. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  63. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  64. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  65. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  66. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  67. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  68. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  69. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  70. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  71. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  72. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  73. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  74. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  75. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  76. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  77. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  78. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  79. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  80. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  81. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  82. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  83. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  84. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  85. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  86. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  87. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  88. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  89. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  90. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  91. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  92. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  93. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  94. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  95. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  96. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  97. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  98. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  99. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  100. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  101. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  102. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  103. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  104. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  105. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  106. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  107. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  108. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  109. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  110. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  111. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  112. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  113. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  114. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  115. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  116. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  117. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  118. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  119. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  120. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  121. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  122. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  123. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  124. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  125. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  126. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  127. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  128. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  129. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  130. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  131. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  132. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  133. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  134. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  135. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  136. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  137. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  138. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  139. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  140. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  141. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  142. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  143. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  144. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  145. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  146. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  147. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  148. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  149. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  150. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  151. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  152. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  153. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  154. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  155. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  156. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  157. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  158. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  159. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  160. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  161. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  162. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  163. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  164. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  165. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  166. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  167. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  168. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  169. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  170. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  171. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  172. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  173. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  174. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  175. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  176. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  177. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  178. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  179. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  180. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  181. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  182. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  183. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  184. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  185. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  186. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  187. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  188. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  189. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  190. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  191. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  192. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  193. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  194. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  195. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  196. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  197. {sarj_python_lint-1.28.5 → sarj_python_lint-1.29.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.28.5
3
+ Version: 1.29.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.28.5"
3
+ version = "1.29.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" }]
@@ -177,6 +177,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
177
177
  "SARJ447": frozenset({"SARJ008"}),
178
178
  "SARJ457": frozenset({"SARJ045"}),
179
179
  "SARJ465": frozenset({"SARJ071"}),
180
+ "SARJ491": frozenset({"SARJ400"}),
180
181
  }
181
182
  )
182
183
 
@@ -190,6 +191,33 @@ class _OwnerLocation(NamedTuple):
190
191
  column: int
191
192
 
192
193
 
194
+ @dataclass(frozen=True, slots=True)
195
+ class _DiagnosticOwners:
196
+ docstrings: dict[int, _OwnerLocation]
197
+ signatures: dict[int, _OwnerLocation]
198
+ fields: tuple[ast.AnnAssign, ...]
199
+
200
+ def location(self, diagnostic: Diagnostic) -> _OwnerLocation:
201
+ if diagnostic.code in {"SARJ400", "SARJ491"}:
202
+ return _field_owner_location(diagnostic, self.fields)
203
+ owners = (
204
+ self.signatures
205
+ if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ094", "SARJ447"}
206
+ else self.docstrings
207
+ )
208
+ return _OwnerLocation(*owners.get(diagnostic.line, (diagnostic.line, diagnostic.col)))
209
+
210
+ def presence(self, diags: list[Diagnostic]) -> dict[tuple[Path, int, int], dict[str, set[Severity]]]:
211
+ present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
212
+ for diagnostic in diags:
213
+ if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
214
+ continue
215
+ location = self.location(diagnostic)
216
+ by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
217
+ by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
218
+ return present
219
+
220
+
193
221
  def deduplicate_diagnostics(
194
222
  diags: list[Diagnostic], *, source: str | None = None, context: PythonFileContext | None = None
195
223
  ) -> list[Diagnostic]:
@@ -203,30 +231,20 @@ def deduplicate_diagnostics(
203
231
  or ("SARJ094" in codes and not codes.isdisjoint(_DIAGNOSTIC_PRECEDENCE["SARJ094"]))
204
232
  )
205
233
  needs_composition_owners = {"SARJ066", "SARJ457"} <= codes
234
+ needs_field_owners = {"SARJ400", "SARJ491"} <= codes
206
235
  tree = (
207
236
  _diagnostic_owner_tree(source, context)
208
- if needs_docstring_owners or needs_signature_owners or needs_composition_owners
237
+ if needs_docstring_owners or needs_signature_owners or needs_composition_owners or needs_field_owners
209
238
  else None
210
239
  )
211
240
  diags = _deduplicate_test_composition(diags, tree)
212
- docstring_owners = _docstring_owner_locations(tree) if needs_docstring_owners else {}
213
- signature_owners = _function_signature_owner_locations(tree) if needs_signature_owners else {}
214
-
215
- def owner_location(diagnostic: Diagnostic) -> _OwnerLocation:
216
- if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ094", "SARJ447"}:
217
- line, column = signature_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
218
- else:
219
- line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
220
- return _OwnerLocation(line, column)
221
-
222
- present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
223
- for diagnostic in diags:
224
- if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
225
- continue
226
- location = owner_location(diagnostic)
227
- by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
228
- by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
229
- suppressed = _suppressed_diagnostics(present)
241
+ owners = _DiagnosticOwners(
242
+ docstrings=_docstring_owner_locations(tree) if needs_docstring_owners else {},
243
+ signatures=_function_signature_owner_locations(tree) if needs_signature_owners else {},
244
+ fields=_field_owners(tree) if needs_field_owners else (),
245
+ )
246
+
247
+ suppressed = _suppressed_diagnostics(owners.presence(diags))
230
248
  return [
231
249
  diagnostic
232
250
  for diagnostic in diags
@@ -234,8 +252,8 @@ def deduplicate_diagnostics(
234
252
  (
235
253
  (
236
254
  diagnostic.path,
237
- owner_location(diagnostic).line,
238
- owner_location(diagnostic).column,
255
+ owners.location(diagnostic).line,
256
+ owners.location(diagnostic).column,
239
257
  ),
240
258
  diagnostic.code,
241
259
  diagnostic.severity,
@@ -255,6 +273,22 @@ def _diagnostic_owner_tree(source: str | None, context: PythonFileContext | None
255
273
  return None
256
274
 
257
275
 
276
+ def _field_owners(tree: ast.Module | None) -> tuple[ast.AnnAssign, ...]:
277
+ if tree is None:
278
+ return ()
279
+ return tuple(node for node in ast.walk(tree) if isinstance(node, ast.AnnAssign))
280
+
281
+
282
+ def _field_owner_location(diagnostic: Diagnostic, fields: tuple[ast.AnnAssign, ...]) -> _OwnerLocation:
283
+ location = (diagnostic.line, diagnostic.col - 1)
284
+ for field in fields:
285
+ start = (field.lineno, field.col_offset)
286
+ end = (field.end_lineno or field.lineno, field.end_col_offset or field.col_offset)
287
+ if start <= location < end:
288
+ return _OwnerLocation(field.lineno, field.col_offset + 1)
289
+ return _OwnerLocation(diagnostic.line, diagnostic.col)
290
+
291
+
258
292
  def _deduplicate_test_composition(diags: list[Diagnostic], tree: ast.Module | None) -> list[Diagnostic]:
259
293
  codes = {diagnostic.code for diagnostic in diags}
260
294
  if tree is None or not {"SARJ066", "SARJ457"} <= codes:
@@ -67,6 +67,17 @@ class PythonFileContext:
67
67
 
68
68
  return consumed_docstring_names(self._valid_tree(), node_index=self.node_index)
69
69
 
70
+ @cached_property
71
+ def docstring_node_ids(self) -> frozenset[int]:
72
+ result: set[int] = set()
73
+ for owner in self.nodes(ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef):
74
+ if not owner.body or not isinstance(owner.body[0], ast.Expr):
75
+ continue
76
+ value = owner.body[0].value
77
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
78
+ result.add(id(value))
79
+ return frozenset(result)
80
+
70
81
  @property
71
82
  def parents(self) -> Mapping[ast.AST, ast.AST]:
72
83
  return self.node_index.parents
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterable, Mapping
9
+
10
+ from sarj_python_lint.rules._imports import ImportIndex
11
+
12
+
13
+ _NAMESPACE_APIS = frozenset({"globals", "locals", "vars", "exec", "eval", "__import__"})
14
+ _DYNAMIC_MEMBERS = frozenset(
15
+ {
16
+ "__dict__",
17
+ "__globals__",
18
+ "__code__",
19
+ "__setattr__",
20
+ "__delattr__",
21
+ "__getattribute__",
22
+ "f_globals",
23
+ "f_locals",
24
+ "f_builtins",
25
+ }
26
+ )
27
+ _MODULE_APIS = {
28
+ "sys": frozenset({"modules", "_getframe"}),
29
+ "importlib": frozenset({"import_module"}),
30
+ "builtins": _NAMESPACE_APIS,
31
+ }
32
+
33
+
34
+ def dynamic_namespace_access(
35
+ nodes: Iterable[ast.AST], imports: ImportIndex, parents: Mapping[ast.AST, ast.AST]
36
+ ) -> bool:
37
+ indexed = tuple(nodes)
38
+ aliases = _namespace_api_aliases(indexed)
39
+ modules = _namespace_module_bindings(indexed)
40
+ return any(_namespace_reference(node, aliases, modules, imports, parents) for node in indexed)
41
+
42
+
43
+ def _namespace_api_aliases(nodes: Iterable[ast.AST]) -> set[str]:
44
+ aliases = set(_NAMESPACE_APIS) | {"__builtins__"}
45
+ for node in nodes:
46
+ if not isinstance(node, ast.ImportFrom) or node.module not in _MODULE_APIS:
47
+ continue
48
+ members = _MODULE_APIS[node.module]
49
+ aliases.update(alias.asname or alias.name for alias in node.names if alias.name in members)
50
+ return aliases
51
+
52
+
53
+ def _namespace_module_bindings(nodes: Iterable[ast.AST]) -> dict[str, str]:
54
+ modules: dict[str, str] = {}
55
+ for node in nodes:
56
+ if not isinstance(node, ast.Import):
57
+ continue
58
+ modules.update({alias.asname or alias.name: alias.name for alias in node.names if alias.name in _MODULE_APIS})
59
+ return modules
60
+
61
+
62
+ def _namespace_reference(
63
+ node: ast.AST,
64
+ aliases: set[str],
65
+ modules: dict[str, str],
66
+ imports: ImportIndex,
67
+ parents: Mapping[ast.AST, ast.AST],
68
+ ) -> bool:
69
+ if isinstance(node, ast.Attribute):
70
+ if node.attr in _DYNAMIC_MEMBERS:
71
+ return True
72
+ qualified = imports.resolved_qualified_name(node)
73
+ return qualified in {"sys.modules", "importlib.import_module"} or (
74
+ imports.resolved_symbol(node, sources=frozenset({"builtins"})) in _NAMESPACE_APIS
75
+ )
76
+ if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load):
77
+ return False
78
+ if node.id in aliases:
79
+ return True
80
+ module = modules.get(node.id)
81
+ if module is None:
82
+ return False
83
+ parent = parents.get(node)
84
+ return not isinstance(parent, ast.Attribute) or parent.value is not node or parent.attr in _MODULE_APIS[module]
@@ -0,0 +1,285 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import Counter
5
+ from functools import lru_cache
6
+ import tomllib
7
+ from typing import TYPE_CHECKING, TypeIs
8
+
9
+ from packaging.requirements import InvalidRequirement, Requirement
10
+ from packaging.version import InvalidVersion, Version
11
+
12
+ from sarj_python_lint.json_boundary import is_object_mapping
13
+ from sarj_python_lint.rules._first_party import distribution_root
14
+ from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from sarj_python_lint._file_context import PythonFileContext
19
+
20
+
21
+ MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
22
+ FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
23
+ DECORATOR_SOURCES = frozenset({"pydantic", "pydantic.functional_validators", "pydantic.functional_serializers"})
24
+ TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
25
+ _ALIAS_VERSION = Version("2.11")
26
+ _BUILTIN_BINDINGS = frozenset({"int", "float", "str", "list", "classmethod", "staticmethod"})
27
+ _SENSITIVE_MODULES = frozenset({"pydantic", "typing", "typing_extensions", "builtins"})
28
+
29
+
30
+ def direct_models(context: PythonFileContext, *, methods: bool = False) -> tuple[ast.ClassDef, ...]:
31
+ tree = context.tree
32
+ if tree is None or context.generated or context.has_wildcard_import:
33
+ return ()
34
+ if (
35
+ _mutated_imports(context)
36
+ or _ambiguous_imports(context)
37
+ or _escaped_imports(context)
38
+ or dynamic_namespace_access(context.nodes(ast.AST), context.imports, context.parents)
39
+ ):
40
+ return ()
41
+ return tuple(
42
+ node
43
+ for node in tree.body
44
+ if isinstance(node, ast.ClassDef)
45
+ and len(node.bases) == 1
46
+ and context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel")
47
+ and not node.decorator_list
48
+ and not node.keywords
49
+ and not node.type_params
50
+ and all(_plain_member(member, context, methods=methods) for member in node.body)
51
+ )
52
+
53
+
54
+ def _plain_member(node: ast.stmt, context: PythonFileContext, *, methods: bool) -> bool:
55
+ match node:
56
+ case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation, value=value):
57
+ return (
58
+ not name.startswith("__")
59
+ and _inert_annotation(annotation, context)
60
+ and (value is None or _inert_expr(value, context))
61
+ )
62
+ case ast.Assign(targets=targets, value=value):
63
+ return all(
64
+ isinstance(target, ast.Name) and not target.id.startswith("__") for target in targets
65
+ ) and _inert_expr(value, context)
66
+ case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name):
67
+ return (
68
+ methods and not name.startswith("__") and name != "model_post_init" and _inert_signature(node, context)
69
+ )
70
+ case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
71
+ return True
72
+ case _:
73
+ return False
74
+
75
+
76
+ def _inert_annotation(node: ast.expr, context: PythonFileContext) -> bool:
77
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
78
+ try:
79
+ node = ast.parse(node.value, mode="eval").body
80
+ except SyntaxError:
81
+ return False
82
+ return _inert_expr(node, context)
83
+
84
+
85
+ def _inert_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
86
+ if node.type_params:
87
+ return False
88
+ arguments = (
89
+ *node.args.posonlyargs,
90
+ *node.args.args,
91
+ *node.args.kwonlyargs,
92
+ *((node.args.vararg,) if node.args.vararg else ()),
93
+ *((node.args.kwarg,) if node.args.kwarg else ()),
94
+ )
95
+ annotations = [argument.annotation for argument in arguments if argument.annotation is not None]
96
+ if node.returns is not None:
97
+ annotations.append(node.returns)
98
+ defaults = (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None))
99
+ return all(_inert_annotation(annotation, context) for annotation in annotations) and all(
100
+ _inert_expr(value, context) for value in defaults
101
+ )
102
+
103
+
104
+ def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
105
+ if isinstance(node, ast.Lambda):
106
+ return not node.args.defaults and not node.args.kw_defaults
107
+ if isinstance(node, ast.Call):
108
+ target = context.imports.resolved_qualified_name(node.func)
109
+ if target not in {
110
+ "pydantic.Field",
111
+ "pydantic.fields.Field",
112
+ "pydantic.ConfigDict",
113
+ "pydantic.config.ConfigDict",
114
+ }:
115
+ return False
116
+ if any(keyword.arg is None for keyword in node.keywords) or any(
117
+ isinstance(argument, ast.Starred) for argument in node.args
118
+ ):
119
+ return False
120
+ if isinstance(
121
+ node,
122
+ (ast.NamedExpr, ast.Await, ast.Yield, ast.YieldFrom, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp),
123
+ ):
124
+ return False
125
+ return all(_inert_expr(child, context) for child in ast.iter_child_nodes(node))
126
+
127
+
128
+ def _mutated_imports(context: PythonFileContext) -> bool:
129
+ for node in context.nodes(ast.Attribute, ast.Subscript, ast.Call):
130
+ if isinstance(node, (ast.Attribute, ast.Subscript)) and isinstance(node.ctx, (ast.Store, ast.Del)):
131
+ if _sensitive_import_root(node, context):
132
+ return True
133
+ elif (
134
+ isinstance(node, ast.Call)
135
+ and node.args
136
+ and _is_attribute_mutator(node.func, context)
137
+ and _sensitive_import_root(node.args[0], context)
138
+ ):
139
+ return True
140
+ return False
141
+
142
+
143
+ def _sensitive_import_root(node: ast.expr, context: PythonFileContext) -> bool:
144
+ while isinstance(node, (ast.Attribute, ast.Subscript)):
145
+ node = node.value
146
+ binding = context.module_imports.bindings.get(node.id) if isinstance(node, ast.Name) else None
147
+ return binding is not None and binding.module.partition(".")[0] in _SENSITIVE_MODULES
148
+
149
+
150
+ def _is_attribute_mutator(node: ast.expr, context: PythonFileContext) -> bool:
151
+ names = {"setattr", "delattr"}
152
+ return (
153
+ isinstance(node, ast.Name) and node.id in names and context.imports.builtin_is_unshadowed(node.id)
154
+ ) or context.imports.resolved_symbol(node, sources=frozenset({"builtins"})) in names
155
+
156
+
157
+ def _escaped_imports(context: PythonFileContext) -> bool:
158
+ for node in context.nodes(ast.Name, ast.Attribute):
159
+ if not isinstance(node.ctx, ast.Load) or not _sensitive_import_root(node, context):
160
+ continue
161
+ parent = context.parents.get(node)
162
+ if isinstance(parent, ast.Attribute) and parent.value is node:
163
+ continue
164
+ if (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or _type_reference(node, parent):
165
+ continue
166
+ return True
167
+ return False
168
+
169
+
170
+ def _type_reference(node: ast.expr, parent: ast.AST | None) -> bool:
171
+ match parent:
172
+ case ast.Call(func=func):
173
+ return func is node
174
+ case ast.ClassDef(bases=bases):
175
+ return node in bases
176
+ case ast.Subscript(value=value):
177
+ return value is node
178
+ case ast.arg(annotation=annotation) | ast.AnnAssign(annotation=annotation):
179
+ return annotation is node
180
+ case ast.FunctionDef(returns=annotation) | ast.AsyncFunctionDef(returns=annotation):
181
+ return annotation is node
182
+ case _:
183
+ return False
184
+
185
+
186
+ def _ambiguous_imports(context: PythonFileContext) -> bool:
187
+ return any(
188
+ _nested_namespace_import(node, context) or _ambiguous_import(node, context)
189
+ for node in context.nodes(ast.Import, ast.ImportFrom)
190
+ )
191
+
192
+
193
+ def _nested_namespace_import(node: ast.Import | ast.ImportFrom, context: PythonFileContext) -> bool:
194
+ if context.parents.get(node) is context.tree:
195
+ return False
196
+ if isinstance(node, ast.ImportFrom):
197
+ return (node.module or "").partition(".")[0] in _SENSITIVE_MODULES
198
+ return any(alias.name.partition(".")[0] in _SENSITIVE_MODULES for alias in node.names)
199
+
200
+
201
+ def _ambiguous_import(node: ast.Import | ast.ImportFrom, context: PythonFileContext) -> bool:
202
+ for alias in node.names:
203
+ name = alias.asname or alias.name.partition(".")[0]
204
+ if name in _BUILTIN_BINDINGS:
205
+ return True
206
+ binding = context.module_imports.bindings.get(name)
207
+ if binding is None or binding.module.partition(".")[0] not in {"pydantic", "typing", "typing_extensions"}:
208
+ continue
209
+ if context.parents.get(node) is not context.tree:
210
+ return True
211
+ if isinstance(node, ast.ImportFrom) and (
212
+ node.level != 0 or node.module != binding.module or alias.name != binding.symbol
213
+ ):
214
+ return True
215
+ return False
216
+
217
+
218
+ def unique_members(model: ast.ClassDef) -> frozenset[str]:
219
+ names: list[str] = []
220
+ for member in model.body:
221
+ match member:
222
+ case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.AnnAssign(target=ast.Name(id=name)):
223
+ names.append(name)
224
+ case ast.Assign(targets=targets):
225
+ names.extend(target.id for target in targets if isinstance(target, ast.Name))
226
+ case _:
227
+ pass
228
+ return frozenset(name for name, count in Counter(names).items() if count == 1)
229
+
230
+
231
+ def has_alias_validation_options(context: PythonFileContext) -> bool:
232
+ root = distribution_root(context.path, facts=context.session.first_party)
233
+ if root is None:
234
+ return False
235
+ try:
236
+ source = (root / "pyproject.toml").read_text(encoding="utf-8")
237
+ except OSError:
238
+ return False
239
+ return _declares_alias_validation_options(source)
240
+
241
+
242
+ @lru_cache(maxsize=256)
243
+ def _declares_alias_validation_options(source: str) -> bool:
244
+ try:
245
+ document = _parse_toml(source)
246
+ except tomllib.TOMLDecodeError:
247
+ return False
248
+ if not is_object_mapping(document):
249
+ return False
250
+ project = document.get("project")
251
+ if not is_object_mapping(project):
252
+ return False
253
+ dependencies = project.get("dependencies")
254
+ if not _is_object_list(dependencies):
255
+ return False
256
+ return any(_alias_dependency(raw) for raw in dependencies)
257
+
258
+
259
+ def _alias_dependency(raw: object) -> bool:
260
+ if not isinstance(raw, str):
261
+ return False
262
+ try:
263
+ requirement = Requirement(raw)
264
+ except InvalidRequirement:
265
+ return False
266
+ if requirement.name.casefold() != "pydantic" or requirement.marker is not None or requirement.url is not None:
267
+ return False
268
+ for specifier in requirement.specifier:
269
+ if specifier.operator not in {">", ">=", "==", "===", "~="}:
270
+ continue
271
+ try:
272
+ version = Version(specifier.version.removesuffix(".*"))
273
+ except InvalidVersion:
274
+ continue
275
+ if version >= _ALIAS_VERSION and version.major == _ALIAS_VERSION.major:
276
+ return True
277
+ return False
278
+
279
+
280
+ def _parse_toml(source: str) -> object:
281
+ return tomllib.loads(source)
282
+
283
+
284
+ def _is_object_list(value: object) -> TypeIs[list[object]]:
285
+ return isinstance(value, list)
@@ -209,12 +209,18 @@ if TYPE_CHECKING:
209
209
  from sarj_python_lint.rule_base import Rule
210
210
  from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
211
211
  from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
212
+ from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
213
+ from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
214
+ from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
212
215
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
213
216
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
214
217
 
215
218
 
216
219
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
217
220
  {
221
+ NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
222
+ NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
223
+ NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
218
224
  RequireInjectableRetrySleep.id: RequireInjectableRetrySleep,
219
225
  NoFixedSleepInLoop.id: NoFixedSleepInLoop,
220
226
  NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
@@ -71,18 +71,6 @@ class _ConstructorImport(NamedTuple):
71
71
  name: str
72
72
 
73
73
 
74
- def _docstring_node_ids(tree: ast.AST, *, node_index: NodeIndex | None = None) -> set[int]:
75
- result: set[int] = set()
76
- for owner in nodes(tree, ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, index=node_index):
77
- body = owner.body
78
- if not body or not isinstance(body[0], ast.Expr):
79
- continue
80
- value = body[0].value
81
- if isinstance(value, ast.Constant) and isinstance(value.value, str):
82
- result.add(id(value))
83
- return result
84
-
85
-
86
74
  def _is_query_context(node: ast.expr, parents: Mapping[ast.AST, ast.AST], roots: frozenset[int]) -> bool:
87
75
  current: ast.AST = node
88
76
  while True:
@@ -368,7 +356,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
368
356
  if not _is_postgres_module(tree, node_index=context.node_index):
369
357
  return []
370
358
 
371
- docstrings = _docstring_node_ids(tree, node_index=context.node_index)
359
+ docstrings = context.docstring_node_ids
372
360
  parents = context.parents
373
361
  query_roots = _query_roots(tree, parents, node_index=context.node_index)
374
362
  diagnostics: list[Diagnostic] = []
@@ -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 nodes, walk
19
+ from sarj_python_lint.rules._ast_index import walk
20
20
  from sarj_python_lint.rules._sql import is_store_module, sql_string_value, strip_sql_noise
21
21
 
22
22
 
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
24
24
  from sqlglot import exp
25
25
 
26
26
  from sarj_python_lint._file_context import PythonFileContext
27
- from sarj_python_lint.rules._ast_index import NodeIndex
28
27
 
29
28
 
30
29
  _QUERY_SHAPE = re.compile(r"\bSELECT\b[\s\S]*?\bFROM\b", re.IGNORECASE)
@@ -65,18 +64,6 @@ _STRONG_ANALYTICAL: tuple[tuple[str, re.Pattern[str]], ...] = (
65
64
  )
66
65
 
67
66
 
68
- def _docstring_node_ids(tree: ast.AST, *, node_index: NodeIndex | None = None) -> set[int]:
69
- result: set[int] = set()
70
- for owner in nodes(tree, ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, index=node_index):
71
- body = owner.body
72
- if not body or not isinstance(body[0], ast.Expr):
73
- continue
74
- value = body[0].value
75
- if isinstance(value, ast.Constant) and isinstance(value.value, str):
76
- result.add(id(value))
77
- return result
78
-
79
-
80
67
  def analytical_signal(sql: str) -> str | None:
81
68
  if _LOCKING_READ.search(sql) is not None:
82
69
  return None
@@ -217,7 +204,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
217
204
  if tree is None:
218
205
  return []
219
206
 
220
- docstrings = _docstring_node_ids(tree, node_index=context.node_index)
207
+ docstrings = context.docstring_node_ids
221
208
  diagnostics: list[Diagnostic] = []
222
209
  consumed: set[int] = set()
223
210
  for node in context.nodes(ast.Constant, ast.BinOp):