sarj-python-lint 1.28.6__tar.gz → 1.29.1__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 (198) hide show
  1. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/__main__.py +55 -21
  4. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_exports.py +27 -0
  5. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_namespace_access.py +84 -0
  6. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_pydantic_field_contract.py +285 -0
  7. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_pytest.py +16 -0
  8. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_registry.py +6 -0
  9. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +2 -12
  10. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +49 -19
  11. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +148 -0
  12. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +2 -24
  13. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +2 -12
  14. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +2 -23
  15. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +244 -0
  16. sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +163 -0
  17. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/.gitignore +0 -0
  18. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/LICENSE +0 -0
  19. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/README.md +0 -0
  20. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/__init__.py +0 -0
  21. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  22. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_file_context.py +0 -0
  23. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_filesystem.py +0 -0
  24. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_python_target.py +0 -0
  25. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  26. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_secret_names.py +0 -0
  27. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_source.py +0 -0
  28. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_version.py +0 -0
  29. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/benchmark.py +0 -0
  30. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  31. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/json_boundary.py +0 -0
  32. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/py.typed +0 -0
  33. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/ratchet.py +0 -0
  34. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rule_base.py +0 -0
  35. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  36. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  37. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  38. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  39. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  40. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  41. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  42. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  43. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  44. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  45. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  46. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  47. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  48. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  49. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  50. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  51. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  52. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  53. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  54. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  55. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  56. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  59. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  60. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  61. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  62. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  63. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  64. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  65. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  66. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  67. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  68. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  69. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  70. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  71. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  72. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  73. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  74. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  75. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  76. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  77. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  78. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  79. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  80. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  81. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  82. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  83. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  84. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  85. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  86. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  87. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  88. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  89. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  90. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  91. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  92. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  93. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  94. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  95. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  96. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  97. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  98. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  99. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  100. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  101. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  102. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  103. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  104. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  105. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  106. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  107. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  108. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  109. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  110. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  111. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  112. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  113. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  114. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  115. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  116. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  117. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  118. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  119. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  120. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  121. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  122. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  123. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  124. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  125. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  126. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  127. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  128. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  129. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  130. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  131. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  132. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  133. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  134. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  135. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  136. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  137. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  138. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  139. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  140. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  141. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  142. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  143. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  144. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  145. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  146. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  147. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  148. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  149. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  150. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  151. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  152. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  153. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  154. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  155. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  156. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  157. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  158. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  159. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  160. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  161. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  162. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  163. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  164. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  165. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  166. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  167. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  168. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  169. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  170. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  171. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  172. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  173. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  174. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  175. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  176. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  177. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  178. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  179. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  180. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  181. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  182. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  183. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  184. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  185. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  186. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  187. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  188. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  189. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  190. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  191. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  192. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  193. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  194. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  195. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  196. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  197. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  198. {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1
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.1"
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,27 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+
5
+ from sarj_python_lint.rules._ast_index import walk
6
+
7
+
8
+ def mentions_dunder_all(node: ast.AST) -> bool:
9
+ for child in walk(node):
10
+ match child:
11
+ case ast.Name(id="__all__"):
12
+ return True
13
+ case ast.alias() if (child.asname or child.name).split(".")[0] == "__all__":
14
+ return True
15
+ case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() if child.name == "__all__":
16
+ return True
17
+ case ast.arg(arg="__all__") | ast.ExceptHandler(name="__all__"):
18
+ return True
19
+ case ast.MatchAs() | ast.MatchStar() if child.name == "__all__":
20
+ return True
21
+ case ast.MatchMapping(rest="__all__"):
22
+ return True
23
+ case ast.Global() | ast.Nonlocal() if "__all__" in child.names:
24
+ return True
25
+ case _:
26
+ pass
27
+ return False
@@ -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)
@@ -1,10 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from sarj_python_lint.rules._ast_index import walk
6
7
 
7
8
 
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Iterator
11
+
12
+
13
+ def collected_tests(statements: list[ast.stmt]) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
14
+ for statement in statements:
15
+ match statement:
16
+ case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
17
+ yield statement
18
+ case ast.ClassDef() if statement.name.startswith("Test"):
19
+ yield from collected_tests(statement.body)
20
+ case _:
21
+ pass
22
+
23
+
8
24
  # The pytest-benchmark fixture: the test measures time, it does not verify.
9
25
  _BENCHMARK = "benchmark"
10
26
 
@@ -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,
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  )
21
21
  from sarj_python_lint.rules._ast_index import walk as walk_ast
22
22
  from sarj_python_lint.rules._imports import ImportIndex
23
+ from sarj_python_lint.rules._pytest import collected_tests
23
24
 
24
25
 
25
26
  if TYPE_CHECKING:
@@ -224,24 +225,13 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
224
225
  severity=Severity.WARNING,
225
226
  message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
226
227
  )
227
- for test in _collected_tests(tree.body)
228
+ for test in collected_tests(tree.body)
228
229
  if not _is_pytest_fixture(test, imports)
229
230
  for call in _uncovered_calls(_test_imports(imports, test), test, writes)
230
231
  if not is_suppressed(lines, call.lineno, self.code)
231
232
  ]
232
233
 
233
234
 
234
- def _collected_tests(statements: list[ast.stmt]) -> Iterator[_TestFunction]:
235
- for statement in statements:
236
- match statement:
237
- case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
238
- yield statement
239
- case ast.ClassDef() if statement.name.startswith("Test"):
240
- yield from _collected_tests(statement.body)
241
- case _:
242
- pass
243
-
244
-
245
235
  def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
246
236
  return any(
247
237
  imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)