sarj-python-lint 1.28.6__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.6 → sarj_python_lint-1.29.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__main__.py +55 -21
  4. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_namespace_access.py +84 -0
  5. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_pydantic_field_contract.py +285 -0
  6. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_registry.py +6 -0
  7. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +49 -19
  8. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +148 -0
  9. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +244 -0
  10. sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +163 -0
  11. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/.gitignore +0 -0
  12. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/LICENSE +0 -0
  13. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/README.md +0 -0
  14. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_file_context.py +0 -0
  17. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_filesystem.py +0 -0
  18. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_python_target.py +0 -0
  19. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  20. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_secret_names.py +0 -0
  21. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_source.py +0 -0
  22. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_version.py +0 -0
  23. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/benchmark.py +0 -0
  24. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  25. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/json_boundary.py +0 -0
  26. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/py.typed +0 -0
  27. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/ratchet.py +0 -0
  28. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rule_base.py +0 -0
  29. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  30. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  31. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  32. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  34. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  35. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  36. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  37. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  38. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  39. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  40. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  41. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  42. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  43. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  44. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  45. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  46. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  47. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  48. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  49. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  50. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  51. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  52. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  53. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  54. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  55. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  56. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  57. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  58. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  59. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  60. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  61. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  62. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  63. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  64. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  65. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  66. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  67. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  68. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  69. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  70. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  71. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  72. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  73. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  74. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  75. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  76. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  77. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  78. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  79. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  80. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  81. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  82. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  83. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  84. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  85. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  86. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  87. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  88. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  89. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  90. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  91. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  92. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  93. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  94. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  95. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  96. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  97. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  98. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  99. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  100. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  101. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  102. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → 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.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  108. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  110. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  111. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  112. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  113. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  114. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  118. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  119. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  120. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  121. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  123. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  124. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  126. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  127. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  130. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  131. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  133. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  134. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  136. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  137. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  139. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  140. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  141. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  142. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  143. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  144. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  148. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  149. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  150. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  151. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  152. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  153. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  154. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  155. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  156. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  157. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  158. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  159. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  160. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  162. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  163. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  164. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  165. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  167. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  168. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  169. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  170. {sarj_python_lint-1.28.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  172. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  173. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  174. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  175. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  176. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  177. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  178. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  181. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  182. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  183. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  187. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  188. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  189. {sarj_python_lint-1.28.6 → 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.6 → 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.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  192. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  193. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  194. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  195. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  196. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  197. {sarj_python_lint-1.28.6 → 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.6
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.6"
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:
@@ -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,
@@ -33,6 +33,7 @@ _PYDANTIC_SOURCES = frozenset({"pydantic"})
33
33
  _MAPPING_SOURCES = frozenset({"collections.abc", "typing"})
34
34
  _MAPPING_NAMES = frozenset({"dict", "Dict", "Mapping", "MutableMapping"})
35
35
  _MAPPING_ARGUMENT_COUNT = 2
36
+ _BUILTIN_TYPE_CONTAINERS = frozenset({"dict", "list", "set", "frozenset", "tuple", "type"})
36
37
 
37
38
 
38
39
  @final
@@ -65,6 +66,7 @@ class NoAnyMappingTypes(Rule):
65
66
  "used in a module-level annotation; imported, conditional, rebound, and ambiguous aliases are excluded."
66
67
  ),
67
68
  "Generated and vendored sources are excluded; exact local suppressions remain auditable.",
69
+ "Annotated metadata, Literal values and unresolved generic parameter positions are excluded; only type arguments are inspected.",
68
70
  ),
69
71
  examples=(
70
72
  RuleExample(
@@ -117,7 +119,7 @@ class NoAnyMappingTypes(Rule):
117
119
  seen: set[tuple[int, int]] = set()
118
120
  expressions = [
119
121
  *_type_expressions(tree, imports, parents, scope_cache, node_index=context.node_index),
120
- *_implicit_mapping_aliases(tree),
122
+ *_implicit_mapping_aliases(tree, semantics),
121
123
  ]
122
124
  for expression, owner in expressions:
123
125
  for mapping, location in _unshadowed_any_mappings(expression, owner, semantics, parents, scope_cache):
@@ -140,7 +142,7 @@ class NoAnyMappingTypes(Rule):
140
142
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
141
143
 
142
144
 
143
- def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assign]]:
145
+ def _implicit_mapping_aliases(tree: ast.Module, semantics: AnnotationSemantics) -> list[tuple[ast.expr, ast.Assign]]:
144
146
  bindings = [scope_bound_names([statement]) for statement in tree.body]
145
147
  aliases: list[tuple[ast.expr, ast.Assign]] = []
146
148
  for statement in tree.body:
@@ -151,14 +153,14 @@ def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assi
151
153
  continue
152
154
  if sum(target.id in names for names in bindings) != 1:
153
155
  continue
154
- if _used_in_module_annotation(tree, target.id):
156
+ if _used_in_module_annotation(tree, target.id, semantics):
155
157
  aliases.append((statement.value, statement))
156
158
  return aliases
157
159
 
158
160
 
159
- def _used_in_module_annotation(tree: ast.Module, name: str) -> bool:
161
+ def _used_in_module_annotation(tree: ast.Module, name: str, semantics: AnnotationSemantics) -> bool:
160
162
  return any(
161
- _annotation_contains_name(annotation, name)
163
+ _annotation_contains_name(annotation, name, semantics)
162
164
  for statement in tree.body
163
165
  for annotation in _module_statement_annotations(statement, name)
164
166
  )
@@ -179,14 +181,17 @@ def _module_statement_annotations(statement: ast.stmt, alias_name: str) -> list[
179
181
  return annotations
180
182
 
181
183
 
182
- def _annotation_contains_name(annotation: ast.expr, name: str) -> bool:
183
- parsed = annotation
184
- if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
185
- try:
186
- parsed = ast.parse(annotation.value, mode="eval").body
187
- except SyntaxError:
188
- return False
189
- return any(isinstance(node, ast.Name) and node.id == name for node in walk_ast(parsed))
184
+ def _annotation_contains_name(annotation: ast.expr, name: str, semantics: AnnotationSemantics) -> bool:
185
+ parsed = semantics.parse(annotation)
186
+ if parsed is None:
187
+ return False
188
+ pending = [parsed]
189
+ while pending:
190
+ node = pending.pop()
191
+ if isinstance(node, ast.Name) and node.id == name:
192
+ return True
193
+ pending.extend(_type_children(node, semantics))
194
+ return False
190
195
 
191
196
 
192
197
  def _unshadowed_any_mappings(
@@ -320,7 +325,14 @@ def _any_mappings(annotation: ast.expr, semantics: AnnotationSemantics) -> list[
320
325
  resolved = semantics.parse(annotation)
321
326
  if resolved is None:
322
327
  return []
323
- return [node for node in walk_ast(resolved) if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics)]
328
+ pending = [resolved]
329
+ mappings: list[ast.Subscript] = []
330
+ while pending:
331
+ node = pending.pop()
332
+ if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics):
333
+ mappings.append(node)
334
+ pending.extend(_type_children(node, semantics))
335
+ return mappings
324
336
 
325
337
 
326
338
  def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics) -> bool:
@@ -355,11 +367,29 @@ def _contains_any(
355
367
  return True
356
368
  if isinstance(parsed, ast.Name) and parsed.id in semantics.aliases and parsed.id not in seen:
357
369
  return _contains_any(semantics.aliases[parsed.id], semantics, seen=seen | {parsed.id})
358
- return any(
359
- _contains_any(child, semantics, seen=seen)
360
- for child in ast.iter_child_nodes(parsed)
361
- if isinstance(child, ast.expr)
362
- )
370
+ return any(_contains_any(child, semantics, seen=seen) for child in _type_children(parsed, semantics))
371
+
372
+
373
+ def _type_children(node: ast.expr, semantics: AnnotationSemantics) -> tuple[ast.expr, ...]:
374
+ if isinstance(node, ast.Subscript):
375
+ imports = semantics.imports
376
+ symbol = imports.resolved_symbol(node.value, sources=_TYPING_SOURCES | _MAPPING_SOURCES)
377
+ if symbol == "Literal":
378
+ return ()
379
+ members = tuple(node.slice.elts) if isinstance(node.slice, ast.Tuple) else (node.slice,)
380
+ if symbol == "Annotated":
381
+ return members[:1]
382
+ builtin = (
383
+ isinstance(node.value, ast.Name)
384
+ and node.value.id in _BUILTIN_TYPE_CONTAINERS
385
+ and imports.builtin_is_unshadowed(node.value.id)
386
+ )
387
+ return members if builtin or symbol is not None else ()
388
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
389
+ return node.left, node.right
390
+ if isinstance(node, (ast.Tuple, ast.List)):
391
+ return tuple(node.elts)
392
+ return ()
363
393
 
364
394
 
365
395
  def _is_vendor_path(path: Path) -> bool: