sarj-python-lint 1.34.0__tar.gz → 1.36.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 (202) hide show
  1. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/PKG-INFO +5 -5
  2. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/pyproject.toml +5 -4
  3. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_ast_index.py +4 -0
  4. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_copy_on_write.py +3 -5
  5. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_imports.py +4 -8
  6. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +36 -1
  7. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +10 -3
  8. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_dunder_all.py +3 -5
  9. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +2 -3
  10. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +5 -4
  11. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +65 -6
  12. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +2 -2
  13. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +2 -2
  14. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +3 -0
  15. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/stepdown.py +3 -8
  16. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +2 -4
  17. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/.gitignore +0 -0
  18. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/LICENSE +0 -0
  19. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/README.md +0 -0
  20. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/__init__.py +0 -0
  21. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/__main__.py +0 -0
  22. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  23. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_file_context.py +0 -0
  24. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_filesystem.py +0 -0
  25. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_python_target.py +0 -0
  26. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  27. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_secret_names.py +0 -0
  28. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_source.py +0 -0
  29. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/_version.py +0 -0
  30. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/benchmark.py +0 -0
  31. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  32. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/json_boundary.py +0 -0
  33. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/py.typed +0 -0
  34. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/ratchet.py +0 -0
  35. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rule_base.py +0 -0
  36. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  37. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  38. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  39. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  40. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  41. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  42. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  43. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  44. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  45. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  46. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  47. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  48. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  49. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  50. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  51. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  52. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  53. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  54. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  55. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  56. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  57. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  58. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  59. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  60. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  61. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  62. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  63. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  64. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  65. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  66. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  67. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  68. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  69. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  70. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  71. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  72. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  73. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  74. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  75. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  76. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  77. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  78. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  79. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  80. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  81. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  82. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  83. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  84. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  85. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  91. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  92. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  93. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  94. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  95. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  96. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  97. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  98. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  99. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  100. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  101. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  102. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  103. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  104. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  105. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  106. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  107. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  108. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  109. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  110. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  111. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  112. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  113. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  114. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  115. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  116. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  117. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  118. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  119. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  120. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  121. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  122. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  123. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  124. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  125. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  126. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  127. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  128. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  129. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  130. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  131. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  132. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  133. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  134. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  135. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  136. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  137. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  138. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  139. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  140. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  141. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  142. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  143. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  144. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  145. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  146. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  147. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  148. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  149. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  150. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  151. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  152. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  153. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  154. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  155. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  156. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  157. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  158. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  159. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  160. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  161. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  162. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  163. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  164. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  165. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  166. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  167. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  168. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  169. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  170. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  171. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  172. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  173. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  174. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  175. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  176. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  177. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  178. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  179. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  180. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  181. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  182. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  183. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  184. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  185. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  186. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  187. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  188. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  189. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  190. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  191. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  192. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  193. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  194. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  195. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  196. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  197. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  198. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  199. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  200. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  201. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  202. {sarj_python_lint-1.34.0 → sarj_python_lint-1.36.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.34.0
3
+ Version: 1.36.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/
@@ -18,10 +18,10 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
- Requires-Dist: ruff>=0.16.10
22
- Requires-Dist: sarj-rule-contracts==0.2.3
23
- Requires-Dist: sqlglot==30.19.0
24
- Requires-Dist: typer==0.27.2
21
+ Requires-Dist: ruff>=0.17.0
22
+ Requires-Dist: sarj-rule-contracts==0.2.4
23
+ Requires-Dist: sqlglot==30.22.0
24
+ Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  <!-- Generated by `code-standards maintain docs sync`; do not edit. -->
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.34.0"
3
+ version = "1.36.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.2.3", "packaging==26.3", "ruff>=0.16.10", "sqlglot==30.19.0", "typer==0.27.2"]
18
+ dependencies = ["sarj-rule-contracts==0.2.4", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -30,9 +30,10 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
30
30
 
31
31
  [dependency-groups]
32
32
  dev = [
33
+ "pydantic==2.14.1",
33
34
  "pytest>=9.1.1",
34
- "ruff>=0.16.10",
35
- "basedpyright>=1.40.1",
35
+ "ruff>=0.17.0",
36
+ "basedpyright>=1.40.2",
36
37
  ]
37
38
 
38
39
  [build-system]
@@ -29,6 +29,10 @@ def children(node: ast.AST) -> list[ast.AST]:
29
29
  return out
30
30
 
31
31
 
32
+ def comprehension_results(node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp) -> list[ast.expr]:
33
+ return [child for child in children(node) if isinstance(child, ast.expr)]
34
+
35
+
32
36
  def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
33
37
  if index is not None and index.tree is node:
34
38
  yield from index.query((ast.AST,))
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, final
9
9
  from sarj_python_lint._analysis_session import AnalysisSession
10
10
  from sarj_python_lint._file_context import PythonFileContext
11
11
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
12
+ from sarj_python_lint.rules._ast_index import comprehension_results
12
13
  from sarj_python_lint.rules._closed_helpers import plain_class_members
13
14
  from sarj_python_lint.rules._imports import ImportIndex
14
15
  from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef, imports_outside
@@ -695,11 +696,8 @@ class CopyOnWriteAnalysis:
695
696
  self._bind(generator.target, self._iter_element(generator.iter, local), local)
696
697
  for condition in generator.ifs:
697
698
  self._expression(condition, local)
698
- if isinstance(node, ast.DictComp):
699
- self._expression(node.key, local)
700
- self._expression(node.value, local)
701
- else:
702
- self._expression(node.elt, local)
699
+ for child in comprehension_results(node):
700
+ self._expression(child, local)
703
701
 
704
702
  def _iter_element(self, expression: ast.expr, bindings: Mapping[str, _Value]) -> _Value | None:
705
703
  owner = self._value(expression, bindings)
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  from types import MappingProxyType
7
7
  from typing import Self
8
8
 
9
- from sarj_python_lint.rules._ast_index import walk as walk_ast
9
+ from sarj_python_lint.rules._ast_index import comprehension_results, walk as walk_ast
10
10
 
11
11
 
12
12
  ABC_SOURCES = frozenset({"abc"})
@@ -138,13 +138,9 @@ def _collect_module_bindings(node: ast.AST, names: set[str]) -> None:
138
138
  case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
139
139
  names.add(node.name)
140
140
  return
141
- case ast.ListComp() | ast.SetComp() | ast.GeneratorExp():
142
- _collect_module_bindings(node.elt, names)
143
- _collect_comprehension_bindings(node.generators, names)
144
- return
145
- case ast.DictComp():
146
- _collect_module_bindings(node.key, names)
147
- _collect_module_bindings(node.value, names)
141
+ case ast.ListComp() | ast.SetComp() | ast.GeneratorExp() | ast.DictComp():
142
+ for result in comprehension_results(node):
143
+ _collect_module_bindings(result, names)
148
144
  _collect_comprehension_bindings(node.generators, names)
149
145
  return
150
146
  case ast.Lambda():
@@ -25,6 +25,8 @@ DECORATOR_SOURCES = frozenset({"pydantic", "pydantic.functional_validators", "py
25
25
  TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
26
26
  _ALIAS_VERSION = Version("2.11")
27
27
  _BUILTIN_BINDINGS = frozenset({"int", "float", "str", "list", "classmethod", "staticmethod"})
28
+ _GENERIC_BUILTINS = frozenset({"dict", "frozenset", "list", "set", "tuple", "type"})
29
+ _BUILTIN_SOURCES = frozenset({"builtins"})
28
30
  _SENSITIVE_MODULES = frozenset({"pydantic", "typing", "typing_extensions", "builtins"})
29
31
 
30
32
 
@@ -76,6 +78,8 @@ def _plain_member(node: ast.stmt, context: PythonFileContext, provenance: Resour
76
78
 
77
79
 
78
80
  def inert_annotation(node: ast.expr, context: PythonFileContext) -> bool:
81
+ if isinstance(node, (ast.Tuple, ast.List)):
82
+ return all(inert_annotation(element, context) for element in node.elts)
79
83
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
80
84
  try:
81
85
  node = ast.parse(node.value, mode="eval").body
@@ -104,6 +108,8 @@ def inert_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, context: Pytho
104
108
 
105
109
 
106
110
  def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
111
+ if isinstance(node, ast.Subscript) and not _inert_subscription(node, context):
112
+ return False
107
113
  if isinstance(node, ast.Lambda):
108
114
  return not node.args.defaults and not node.args.kw_defaults
109
115
  if isinstance(node, ast.Call):
@@ -121,12 +127,41 @@ def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
121
127
  return False
122
128
  if isinstance(
123
129
  node,
124
- (ast.NamedExpr, ast.Await, ast.Yield, ast.YieldFrom, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp),
130
+ (
131
+ ast.NamedExpr,
132
+ ast.Starred,
133
+ ast.Await,
134
+ ast.Yield,
135
+ ast.YieldFrom,
136
+ ast.ListComp,
137
+ ast.SetComp,
138
+ ast.DictComp,
139
+ ast.GeneratorExp,
140
+ ),
125
141
  ):
126
142
  return False
127
143
  return all(_inert_expr(child, context) for child in ast.iter_child_nodes(node))
128
144
 
129
145
 
146
+ def _inert_subscription(node: ast.Subscript, context: PythonFileContext) -> bool:
147
+ symbol = context.imports.resolved_symbol(node.value, sources=TYPING_SOURCES)
148
+ if symbol is None and not (
149
+ (
150
+ isinstance(node.value, ast.Name)
151
+ and node.value.id in _GENERIC_BUILTINS
152
+ and context.imports.builtin_is_unshadowed(node.value.id)
153
+ )
154
+ or context.imports.resolved_symbol(node.value, sources=_BUILTIN_SOURCES) in _GENERIC_BUILTINS
155
+ ):
156
+ return False
157
+ arguments = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
158
+ if symbol == "Literal":
159
+ return True
160
+ if symbol == "Annotated":
161
+ return bool(arguments) and inert_annotation(arguments[0], context)
162
+ return all(inert_annotation(argument, context) for argument in arguments)
163
+
164
+
130
165
  def literal_value(value: ast.expr) -> bool:
131
166
  if any(isinstance(node, ast.Call) for node in ast.walk(value)):
132
167
  return False
@@ -4,6 +4,7 @@ import ast
4
4
  from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
6
  from pathlib import PurePosixPath
7
+ import re
7
8
  from typing import TYPE_CHECKING, ClassVar, final, override
8
9
 
9
10
  from sarj_python_lint.rule_base import (
@@ -38,6 +39,7 @@ _MAPPING_NAMES = frozenset({"dict", "Dict", "Mapping", "MutableMapping"})
38
39
  _MAPPING_ARGUMENT_COUNT = 2
39
40
  _BUILTIN_TYPE_CONTAINERS = frozenset({"dict", "list", "set", "frozenset", "tuple", "type"})
40
41
  _TYPE_MODULES = frozenset({"typing", "typing_extensions", "pydantic", "collections", "builtins"})
42
+ _TYPE_COMMENT_MARKER = re.compile(r"#\s*type:")
41
43
 
42
44
 
43
45
  @dataclass(frozen=True, slots=True)
@@ -125,9 +127,14 @@ class NoAnyMappingTypes(Rule):
125
127
  source = context.source
126
128
  if context.generated or _is_vendor_path(path):
127
129
  return []
128
- try:
129
- tree = ast.parse(source, filename=str(path), type_comments=True)
130
- except SyntaxError:
130
+ if _TYPE_COMMENT_MARKER.search(source) is None:
131
+ tree = context.tree
132
+ else:
133
+ try:
134
+ tree = ast.parse(source, filename=str(path), type_comments=True)
135
+ except SyntaxError:
136
+ return []
137
+ if tree is None:
131
138
  return []
132
139
  semantics = AnnotationSemantics.from_tree(tree)
133
140
  imports = semantics.imports
@@ -14,6 +14,7 @@ from sarj_python_lint.rule_base import (
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
16
  )
17
+ from sarj_python_lint.rules._ast_index import comprehension_results
17
18
 
18
19
 
19
20
  if TYPE_CHECKING:
@@ -344,11 +345,8 @@ class _DunderAllVisitor(_ScopePruningVisitor):
344
345
  self.generic_visit(node)
345
346
 
346
347
  def _visit_comprehension(self, node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp) -> None:
347
- if isinstance(node, ast.DictComp):
348
- self.visit(node.key)
349
- self.visit(node.value)
350
- else:
351
- self.visit(node.elt)
348
+ for result in comprehension_results(node):
349
+ self.visit(result)
352
350
  for generator in node.generators:
353
351
  self.visit(generator.iter)
354
352
  for condition in generator.ifs:
@@ -15,7 +15,7 @@ from sarj_python_lint.rule_base import (
15
15
  RuleExample,
16
16
  Severity,
17
17
  )
18
- from sarj_python_lint.rules._ast_index import children
18
+ from sarj_python_lint.rules._ast_index import children, comprehension_results
19
19
 
20
20
 
21
21
  if TYPE_CHECKING:
@@ -154,8 +154,7 @@ class _Complexity:
154
154
  self.add(condition, nesting, "comprehension filter")
155
155
  self.visit(condition, nesting)
156
156
  nesting += 1
157
- values = (node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,)
158
- self.visit_all(values, nesting)
157
+ self.visit_all(comprehension_results(node), nesting)
159
158
 
160
159
 
161
160
  @final
@@ -18,6 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  RuleExample,
19
19
  Severity,
20
20
  )
21
+ from sarj_python_lint.rules._ast_index import comprehension_results
21
22
  from sarj_python_lint.rules._first_party import FirstPartyFacts, distribution_root
22
23
  from sarj_python_lint.rules._paths import is_generated, is_test_path
23
24
 
@@ -870,8 +871,8 @@ def _calls_with_shadowing(
870
871
  case ast.ListComp() | ast.SetComp() | ast.GeneratorExp():
871
872
  yield from _comprehension_calls(node.generators, (node.elt,), shadowed, nested_scope_base)
872
873
  return
873
- case ast.DictComp(generators=generators, key=key, value=value):
874
- yield from _comprehension_calls(generators, (key, value), shadowed, nested_scope_base)
874
+ case ast.DictComp(generators=generators):
875
+ yield from _comprehension_calls(generators, tuple(comprehension_results(node)), shadowed, nested_scope_base)
875
876
  return
876
877
  case ast.Lambda(args=args, body=body):
877
878
  outer_nodes = [*args.defaults, *(default for default in args.kw_defaults if default is not None)]
@@ -1008,8 +1009,8 @@ class LocalBindingCollector(ast.NodeVisitor):
1008
1009
 
1009
1010
  @override
1010
1011
  def visit_DictComp(self, node: ast.DictComp) -> None:
1011
- self.visit(node.key)
1012
- self.visit(node.value)
1012
+ for result in comprehension_results(node):
1013
+ self.visit(result)
1013
1014
  self._visit_generators(node.generators)
1014
1015
 
1015
1016
  @override
@@ -28,6 +28,8 @@ if TYPE_CHECKING:
28
28
  _NUMERIC_BOUNDS = frozenset({"gt", "ge", "lt", "le"})
29
29
  _LENGTH_BOUNDS = frozenset({"min_length", "max_length"})
30
30
  _KINDS = frozenset({"int", "float", "str", "list"})
31
+ _PASSIVE_CONFIG_BOOLEANS = frozenset({"strict", "frozen", "validate_default"})
32
+ _EXTRA_POLICIES = frozenset({"allow", "forbid", "ignore"})
31
33
  _PASSIVE_OPTIONS = frozenset(
32
34
  {
33
35
  "default",
@@ -59,13 +61,15 @@ class NoImpossiblePydanticFieldBounds(Rule):
59
61
  documentation = RuleDocumentation(
60
62
  default_level=Severity.ERROR,
61
63
  summary="Pydantic field bounds must admit at least one value.",
62
- rationale="Contradictory literal constraints reject every field value, including integer intervals with no integer member.",
64
+ rationale="Contradictory literal constraints reject every field value, including integer multiples and float intervals with no representable member.",
63
65
  remediation="Correct the conflicting bounds so the declared field domain admits the intended values.",
64
66
  category=RuleCategory.CORRECTNESS,
65
67
  autofix=AutofixPolicy.NONE,
66
68
  limitations=(
67
69
  "Only plain direct module-level BaseModel subclasses and built-in int, float, str and list fields are inspected.",
68
70
  "Bounds must be finite literals; int and length bounds must be integers, and float bounds must convert exactly.",
71
+ "Positive integer multiple_of constraints use exact integer arithmetic; floating-point steps remain outside the rule.",
72
+ "Literal strict, frozen, validate_default and extra model settings preserve coverage; other or dynamic configuration is excluded.",
69
73
  "Unknown metadata, nullable types, repeated bound names, methods, dynamic namespaces and generated files are excluded.",
70
74
  "Visible namespace APIs, dynamic module access, ambiguous imports and escaped or mutated imported objects exclude the file.",
71
75
  "No autofix chooses a domain or changes validation behavior.",
@@ -130,7 +134,7 @@ class NoImpossiblePydanticFieldBounds(Rule):
130
134
  def _candidate_fields(context: PythonFileContext) -> list[ast.AnnAssign]:
131
135
  fields: list[ast.AnnAssign] = []
132
136
  for model in direct_models(context):
133
- if any(isinstance(member, (ast.Assign, ast.AnnAssign)) and _names_config(member) for member in model.body):
137
+ if not _passive_model_configuration(model, context):
134
138
  continue
135
139
  unique = unique_members(model)
136
140
  fields.extend(
@@ -144,11 +148,50 @@ def _candidate_fields(context: PythonFileContext) -> list[ast.AnnAssign]:
144
148
  return fields
145
149
 
146
150
 
151
+ def _passive_model_configuration(model: ast.ClassDef, context: PythonFileContext) -> bool:
152
+ configurations = [
153
+ member for member in model.body if isinstance(member, (ast.Assign, ast.AnnAssign)) and _names_config(member)
154
+ ]
155
+ if not configurations:
156
+ return True
157
+ if len(configurations) != 1:
158
+ return False
159
+ options = _configuration_options(configurations[0].value, context)
160
+ return options is not None and all(
161
+ isinstance(item, ast.Constant)
162
+ and (
163
+ (name in _PASSIVE_CONFIG_BOOLEANS and type(item.value) is bool)
164
+ or (name == "extra" and isinstance(item.value, str) and item.value in _EXTRA_POLICIES)
165
+ )
166
+ for name, item in options
167
+ )
168
+
169
+
147
170
  def _names_config(node: ast.Assign | ast.AnnAssign) -> bool:
148
171
  targets = node.targets if isinstance(node, ast.Assign) else [node.target]
149
172
  return any(isinstance(target, ast.Name) and target.id == "model_config" for target in targets)
150
173
 
151
174
 
175
+ def _configuration_options(value: ast.expr | None, context: PythonFileContext) -> list[tuple[str, ast.expr]] | None:
176
+ if isinstance(value, ast.Dict):
177
+ options: list[tuple[str, ast.expr]] = []
178
+ for key, item in zip(value.keys, value.values, strict=True):
179
+ if not isinstance(key, ast.Constant) or not isinstance(key.value, str):
180
+ return None
181
+ options.append((key.value, item))
182
+ return options
183
+ if (
184
+ not isinstance(value, ast.Call)
185
+ or value.args
186
+ or not context.module_imports.resolves(
187
+ value.func, sources=frozenset({"pydantic", "pydantic.config"}), symbol="ConfigDict"
188
+ )
189
+ or any(keyword.arg is None for keyword in value.keywords)
190
+ ):
191
+ return None
192
+ return [(keyword.arg, keyword.value) for keyword in value.keywords if keyword.arg is not None]
193
+
194
+
152
195
  def _field_contract(field: ast.AnnAssign, context: PythonFileContext) -> tuple[str, dict[str, int | float]] | None:
153
196
  annotation = field.annotation
154
197
  calls: list[ast.Call] = []
@@ -186,7 +229,9 @@ def _literal_bounds(calls: list[ast.Call], kind: str) -> dict[str, int | float]
186
229
  if any(call.args for call in calls):
187
230
  return None
188
231
  bounds: dict[str, int | float] = {}
189
- allowed = _NUMERIC_BOUNDS if kind in {"int", "float"} else _LENGTH_BOUNDS
232
+ allowed = (
233
+ _NUMERIC_BOUNDS | {"multiple_of"} if kind == "int" else _NUMERIC_BOUNDS if kind == "float" else _LENGTH_BOUNDS
234
+ )
190
235
  for keyword in (keyword for call in calls for keyword in call.keywords):
191
236
  name = keyword.arg
192
237
  if name in _PASSIVE_OPTIONS:
@@ -194,7 +239,7 @@ def _literal_bounds(calls: list[ast.Call], kind: str) -> dict[str, int | float]
194
239
  if name is None or name not in allowed or name in bounds:
195
240
  return None
196
241
  value = _bound_value(keyword.value, kind)
197
- if value is None:
242
+ if value is None or (name == "multiple_of" and value <= 0):
198
243
  return None
199
244
  bounds[name] = value
200
245
  return bounds
@@ -240,5 +285,19 @@ def _empty_interval(kind: str, bounds: dict[str, int | float]) -> bool:
240
285
  lower = max(lowers, key=lambda bound: (bound.value, bound.exclusive))
241
286
  upper = min(uppers, key=lambda bound: (bound.value, not bound.exclusive))
242
287
  if kind != "float":
243
- return lower.value + int(lower.exclusive) > upper.value - int(upper.exclusive)
244
- return lower.value > upper.value or (lower.value == upper.value and (lower.exclusive or upper.exclusive))
288
+ minimum = lower.value + int(lower.exclusive)
289
+ maximum = upper.value - int(upper.exclusive)
290
+ if minimum > maximum:
291
+ return True
292
+ multiple = bounds.get("multiple_of")
293
+ return (
294
+ kind == "int"
295
+ and isinstance(minimum, int)
296
+ and isinstance(maximum, int)
297
+ and isinstance(multiple, int)
298
+ and multiple > 0
299
+ and -(-minimum // multiple) * multiple > maximum
300
+ )
301
+ minimum_float = math.nextafter(lower.value, math.inf) if lower.exclusive else float(lower.value)
302
+ maximum_float = math.nextafter(upper.value, -math.inf) if upper.exclusive else float(upper.value)
303
+ return minimum_float > maximum_float
@@ -18,7 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  Severity,
19
19
  is_suppressed,
20
20
  )
21
- from sarj_python_lint.rules._ast_index import walk as walk_ast
21
+ from sarj_python_lint.rules._ast_index import comprehension_results, walk as walk_ast
22
22
  from sarj_python_lint.rules._imports import ImportIndex
23
23
 
24
24
 
@@ -611,7 +611,7 @@ class _FunctionAnalyzer:
611
611
 
612
612
  @override
613
613
  def visit_DictComp(self, node: ast.DictComp) -> None:
614
- self._comprehension(node.generators, [node.key, node.value])
614
+ self._comprehension(node.generators, comprehension_results(node))
615
615
 
616
616
  def _comprehension(self, generators: list[ast.comprehension], results: list[ast.expr]) -> None:
617
617
  saved_shadowed = set(analyzer._shadowed)
@@ -19,7 +19,7 @@ from sarj_python_lint.rule_base import (
19
19
  Severity,
20
20
  is_suppressed,
21
21
  )
22
- from sarj_python_lint.rules._ast_index import walk as walk_ast
22
+ from sarj_python_lint.rules._ast_index import comprehension_results, walk as walk_ast
23
23
  from sarj_python_lint.rules._imports import ImportIndex
24
24
  from sarj_python_lint.rules._paths import is_test_path
25
25
 
@@ -1060,7 +1060,7 @@ def _comprehension_oracle(
1060
1060
  flow = _bind_comprehension_target(generator, flow)
1061
1061
  if any(_raw_text_oracle(condition, flow) for condition in generator.ifs):
1062
1062
  return True
1063
- values = [node.key, node.value] if isinstance(node, ast.DictComp) else [node.elt]
1063
+ values = comprehension_results(node)
1064
1064
  return any(
1065
1065
  _raw_text_oracle(value, flow) or (consumes_values and _raw_text_expression(value, flow)) for value in values
1066
1066
  )
@@ -69,6 +69,9 @@ def _reference_tokens(value: ast.expr) -> tuple[str, ...]:
69
69
  return tuple(identifier_tokens(name))
70
70
  case ast.Attribute(value=receiver, attr=attribute):
71
71
  return (*_reference_tokens(receiver), *identifier_tokens(attribute))
72
+ case ast.Subscript(value=receiver, slice=ast.Constant(value=str() as key)):
73
+ metadata = tuple(word for word in identifier_tokens(key) if word in _SAFE_METADATA or word == "not")
74
+ return (*_reference_tokens(receiver), *metadata)
72
75
  case ast.Subscript(value=receiver):
73
76
  return _reference_tokens(receiver)
74
77
  case ast.Await(value=awaited):
@@ -16,7 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleDocumentation,
17
17
  RuleExample,
18
18
  )
19
- from sarj_python_lint.rules._ast_index import children
19
+ from sarj_python_lint.rules._ast_index import children, comprehension_results
20
20
  from sarj_python_lint.rules._paths import is_test_path
21
21
 
22
22
 
@@ -571,11 +571,7 @@ def _comprehension_expressions(
571
571
  for generator in node.generators:
572
572
  yield generator.iter
573
573
  yield from generator.ifs
574
- if isinstance(node, ast.DictComp):
575
- yield node.key
576
- yield node.value
577
- else:
578
- yield node.elt
574
+ yield from comprehension_results(node)
579
575
 
580
576
 
581
577
  def _module_pinned_names(tree: ast.Module, definition_names: frozenset[str]) -> set[str]:
@@ -712,8 +708,7 @@ def _resolved_comprehension_loads(
712
708
  yield from _resolved_loads((generator.iter,), candidates, comp_blocked)
713
709
  comp_blocked.update(_target_names(generator.target) & candidates)
714
710
  yield from _resolved_loads(generator.ifs, candidates, comp_blocked)
715
- values = (node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,)
716
- yield from _resolved_loads(values, candidates, comp_blocked)
711
+ yield from _resolved_loads(comprehension_results(node), candidates, comp_blocked)
717
712
 
718
713
 
719
714
  def _direct_scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  RuleExample,
17
17
  Severity,
18
18
  )
19
+ from sarj_python_lint.rules._ast_index import comprehension_results
19
20
  from sarj_python_lint.rules._paths import is_test_path
20
21
 
21
22
 
@@ -444,10 +445,7 @@ def _repeated_comprehension_regions(
444
445
  regions.extend(generator.ifs)
445
446
  prior_generator_repeats = prior_generator_repeats or this_generator_repeats
446
447
  if prior_generator_repeats:
447
- if isinstance(node, ast.DictComp):
448
- regions.extend((node.key, node.value))
449
- else:
450
- regions.append(node.elt)
448
+ regions.extend(comprehension_results(node))
451
449
  return tuple(regions)
452
450
 
453
451