sarj-python-lint 1.33.0__tar.gz → 1.35.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.33.0 → sarj_python_lint-1.35.0}/PKG-INFO +5 -5
  2. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/pyproject.toml +4 -4
  3. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +51 -0
  4. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_resource_provenance.py +7 -2
  5. sarj_python_lint-1.35.0/src/sarj_python_lint/rules/_static_json_alias.py +142 -0
  6. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +45 -12
  7. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +60 -7
  8. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +3 -2
  9. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +26 -2
  10. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +2 -2
  11. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +2 -2
  12. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +71 -9
  13. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_docstring.py +15 -3
  14. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +2 -30
  15. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/.gitignore +0 -0
  16. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/LICENSE +0 -0
  17. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/README.md +0 -0
  18. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/__init__.py +0 -0
  19. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/__main__.py +0 -0
  20. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  21. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_file_context.py +0 -0
  22. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_filesystem.py +0 -0
  23. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_python_target.py +0 -0
  24. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  25. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_secret_names.py +0 -0
  26. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_source.py +0 -0
  27. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_version.py +0 -0
  28. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/benchmark.py +0 -0
  29. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  30. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/json_boundary.py +0 -0
  31. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/py.typed +0 -0
  32. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/ratchet.py +0 -0
  33. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rule_base.py +0 -0
  34. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  35. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  36. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  37. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  38. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  39. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  40. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  41. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  42. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  43. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  44. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  45. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  46. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  47. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  48. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  49. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  50. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  51. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  52. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  53. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  54. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  55. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  56. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  57. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  58. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  59. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  60. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  61. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  62. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  63. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  64. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  65. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  66. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  67. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  85. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  86. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  87. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  88. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  89. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  90. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  91. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  92. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  93. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  94. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  95. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  96. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  97. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  98. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  99. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  100. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  101. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  102. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  103. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  104. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  105. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  106. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  107. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  108. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  109. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  110. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  111. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  112. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  113. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  114. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  115. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  116. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  117. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  118. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  119. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  120. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  121. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  122. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  123. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  124. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  125. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  126. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  127. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  128. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  129. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  130. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  131. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  132. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  133. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  134. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  135. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  136. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  137. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  138. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  139. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  140. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  141. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  142. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  143. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  144. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  145. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  146. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  147. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  148. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  149. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  150. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  151. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  152. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  153. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  154. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  155. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  156. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  157. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  158. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  159. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  160. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  161. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  162. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  163. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  164. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  165. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  166. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  167. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  168. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  169. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  170. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  171. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  172. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  173. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  174. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  175. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  176. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  177. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  178. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  179. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  180. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  181. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  182. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  183. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  184. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  185. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  186. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  187. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  188. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  189. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  190. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  191. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  192. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  193. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  194. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  195. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  196. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  197. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  198. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  199. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  200. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  201. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  202. {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.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.33.0
3
+ Version: 1.35.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.33.0"
3
+ version = "1.35.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"
@@ -31,8 +31,8 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
31
31
  [dependency-groups]
32
32
  dev = [
33
33
  "pytest>=9.1.1",
34
- "ruff>=0.16.10",
35
- "basedpyright>=1.40.1",
34
+ "ruff>=0.17.0",
35
+ "basedpyright>=1.40.2",
36
36
  ]
37
37
 
38
38
  [build-system]
@@ -107,3 +107,54 @@ def _literal(node: ast.expr) -> bool:
107
107
  and isinstance(node.operand.value, int | float)
108
108
  and not isinstance(node.operand.value, bool)
109
109
  )
110
+
111
+
112
+ _MODEL_FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields", "pydantic.v1", "pydantic.v1.fields"})
113
+
114
+
115
+ def inert_direct_model_body(cls: ast.ClassDef, imports: ImportIndex) -> bool:
116
+ if cls.decorator_list or cls.keywords or cls.type_params or len(cls.bases) != 1:
117
+ return False
118
+ for statement in cls.body:
119
+ match statement:
120
+ case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
121
+ continue
122
+ case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
123
+ if not inert_model_annotation(annotation, imports):
124
+ return False
125
+ if value is None or isinstance(value, ast.Constant):
126
+ continue
127
+ if (
128
+ isinstance(value, ast.Call)
129
+ and imports.resolves(value.func, sources=_MODEL_FIELD_SOURCES, symbol="Field")
130
+ and not value.args
131
+ and all(
132
+ keyword.arg is not None and _model_literal_value(keyword.value) is not None
133
+ for keyword in value.keywords
134
+ )
135
+ ):
136
+ continue
137
+ return False
138
+ case _:
139
+ return False
140
+ return True
141
+
142
+
143
+ def _model_literal_value(node: ast.expr | None) -> object:
144
+ if isinstance(node, ast.Constant):
145
+ return node.value
146
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant):
147
+ numeric = _model_numeric_value(node.operand.value)
148
+ if numeric is not None:
149
+ return -numeric
150
+ return None
151
+
152
+
153
+ def _model_numeric_value(value: object) -> float | None:
154
+ match value:
155
+ case bool():
156
+ return None
157
+ case int() | float():
158
+ return float(value)
159
+ case _:
160
+ return None
@@ -26,18 +26,23 @@ _SCOPES = (
26
26
  )
27
27
  _RESOURCE_OWNERS = MappingProxyType(
28
28
  {
29
+ "asyncio": frozenset({"sleep", "CancelledError", "exceptions.CancelledError"}),
29
30
  "contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
30
31
  "subprocess": frozenset({"Popen", "TimeoutExpired"}),
31
32
  "re": frozenset({"compile", "Match", "Pattern"}),
32
33
  "unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
33
- "typing": frozenset({"final"}),
34
- "typing_extensions": frozenset({"final"}),
34
+ "typing": frozenset({"final", "Literal", "Annotated"}),
35
+ "typing_extensions": frozenset({"final", "Literal", "Annotated"}),
35
36
  "pydantic": frozenset(
36
37
  {
37
38
  "BaseModel",
38
39
  "main.BaseModel",
40
+ "v1.BaseModel",
41
+ "v1.main.BaseModel",
39
42
  "Field",
40
43
  "fields.Field",
44
+ "v1.Field",
45
+ "v1.fields.Field",
41
46
  "ConfigDict",
42
47
  "config.ConfigDict",
43
48
  "model_validator",
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
9
+
10
+
11
+ _CONTAINERS = frozenset({"list", "dict", "tuple"})
12
+ _TYPING_CONTAINERS = frozenset({"List", "Dict", "Tuple", "Optional", "Union"})
13
+ _SCALARS = frozenset({"str", "int", "float", "bool", "None"})
14
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
15
+ _RUNTIME_ATTRIBUTES = frozenset({"__annotations__", "__value__", "get_type_hints", "TypeAdapter", "validate_call"})
16
+
17
+
18
+ def private_static_json_alias(tree: ast.Module, name: str, semantics: AnnotationSemantics) -> bool:
19
+ if not name.startswith("_") or name.startswith("__"):
20
+ return False
21
+ nodes = tuple(ast.walk(tree))
22
+ parents = {id(child): parent for parent in nodes for child in ast.iter_child_nodes(parent)}
23
+ if any(_runtime_annotation_consumer(node, semantics) for node in nodes):
24
+ return False
25
+ safe: set[int] = set()
26
+ for statement in tree.body:
27
+ if (value := _alias_value(statement, name)) is not None:
28
+ safe.update(id(node) for node in ast.walk(value))
29
+ annotations = _private_annotations(tree, name, semantics, nodes, parents)
30
+ if not annotations:
31
+ return False
32
+ safe.update(id(node) for annotation in annotations for node in ast.walk(annotation))
33
+ return all(id(node) in safe for node in nodes if _references_name(node, name))
34
+
35
+
36
+ def _private_annotations(
37
+ tree: ast.Module, name: str, semantics: AnnotationSemantics, nodes: tuple[ast.AST, ...], parents: dict[int, ast.AST]
38
+ ) -> list[ast.expr]:
39
+ annotations: list[ast.expr] = []
40
+ for function in tree.body:
41
+ if not isinstance(function, ast.FunctionDef | ast.AsyncFunctionDef):
42
+ continue
43
+ if not _function_stays_local(function, nodes, parents):
44
+ continue
45
+ annotations.extend(
46
+ annotation
47
+ for annotation in _function_annotations(function)
48
+ if any(_references_name(node, name) for node in ast.walk(annotation))
49
+ and _static_annotation(annotation, name, semantics)
50
+ )
51
+ return annotations
52
+
53
+
54
+ def _alias_value(statement: ast.stmt, name: str) -> ast.expr | None:
55
+ match statement:
56
+ case ast.TypeAlias(name=ast.Name(id=identifier), value=value) if identifier == name:
57
+ return value
58
+ case ast.AnnAssign(target=ast.Name(id=identifier), value=value) if identifier == name:
59
+ return value
60
+ case _:
61
+ return None
62
+
63
+
64
+ def _references_name(node: ast.AST, name: str) -> bool:
65
+ if isinstance(node, ast.Name):
66
+ return isinstance(node.ctx, ast.Load) and node.id == name
67
+ return isinstance(node, ast.Constant) and isinstance(node.value, str) and name in node.value
68
+
69
+
70
+ def _function_stays_local(
71
+ function: ast.FunctionDef | ast.AsyncFunctionDef, nodes: tuple[ast.AST, ...], parents: dict[int, ast.AST]
72
+ ) -> bool:
73
+ if not function.name.startswith("_") or function.name.startswith("__") or function.decorator_list:
74
+ return False
75
+ for node in nodes:
76
+ if not _references_name(node, function.name):
77
+ continue
78
+ parent = parents.get(id(node))
79
+ if not isinstance(node, ast.Name) or not isinstance(parent, ast.Call) or parent.func is not node:
80
+ return False
81
+ return True
82
+
83
+
84
+ def _function_annotations(function: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.expr]:
85
+ arguments = function.args
86
+ annotations = [
87
+ argument.annotation
88
+ for argument in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs)
89
+ if argument.annotation is not None
90
+ ]
91
+ if function.returns is not None:
92
+ annotations.append(function.returns)
93
+ return annotations
94
+
95
+
96
+ def _static_annotation(node: ast.expr, name: str, semantics: AnnotationSemantics, *, remaining: int = 64) -> bool:
97
+ if remaining <= 0 or (parsed := semantics.parse(node)) is None:
98
+ return False
99
+ match parsed:
100
+ case ast.Name(id=identifier):
101
+ return identifier == name or (
102
+ identifier in _SCALARS | _CONTAINERS and semantics.imports.builtin_is_unshadowed(identifier)
103
+ )
104
+ case ast.Constant(value=None):
105
+ return True
106
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
107
+ return _static_annotation(left, name, semantics, remaining=remaining - 1) and _static_annotation(
108
+ right, name, semantics, remaining=remaining - 1
109
+ )
110
+ case ast.Subscript(value=value, slice=arguments):
111
+ builtin = (
112
+ isinstance(value, ast.Name)
113
+ and value.id in _CONTAINERS
114
+ and semantics.imports.builtin_is_unshadowed(value.id)
115
+ )
116
+ canonical = any(
117
+ semantics.imports.resolves(value, sources=_TYPING_SOURCES, symbol=symbol)
118
+ for symbol in _TYPING_CONTAINERS
119
+ )
120
+ return (builtin or canonical) and all(
121
+ _static_annotation(argument, name, semantics, remaining=remaining - 1)
122
+ for argument in (arguments.elts if isinstance(arguments, ast.Tuple) else [arguments])
123
+ )
124
+ case _:
125
+ return False
126
+
127
+
128
+ def _runtime_annotation_consumer(node: ast.AST, semantics: AnnotationSemantics) -> bool:
129
+ if isinstance(node, ast.Attribute) and node.attr in _RUNTIME_ATTRIBUTES:
130
+ return True
131
+ if isinstance(node, ast.Constant) and isinstance(node.value, str) and node.value in _RUNTIME_ATTRIBUTES:
132
+ return True
133
+ if not isinstance(node, ast.Call):
134
+ return False
135
+ qualified = semantics.imports.resolved_qualified_name(node.func)
136
+ return qualified in {
137
+ "pydantic.TypeAdapter",
138
+ "pydantic.type_adapter.TypeAdapter",
139
+ "pydantic.validate_call",
140
+ "typing.get_type_hints",
141
+ "typing_extensions.get_type_hints",
142
+ }
@@ -21,6 +21,7 @@ from sarj_python_lint.rule_base import (
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
23
  from sarj_python_lint.rules._pytest import collected_tests
24
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
24
25
 
25
26
 
26
27
  if TYPE_CHECKING:
@@ -78,7 +79,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
78
79
  id = "async-mock-call-without-await-assertion"
79
80
  code = "SARJ456"
80
81
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
81
- default_level=Severity.WARNING,
82
+ default_level=Severity.ERROR,
82
83
  summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
83
84
  rationale=(
84
85
  "Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
@@ -94,6 +95,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
94
95
  limitations=(
95
96
  "Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
96
97
  "Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
98
+ "Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.",
97
99
  "Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
98
100
  "Positive call evidence includes direct assertion methods and literal integer or boolean call-state checks; unresolved call expectations are excluded.",
99
101
  "Await evidence excludes provably vacuous checks, including zero or nonnegative counts and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
@@ -215,23 +217,54 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
215
217
  for node in context.nodes(ast.AST)
216
218
  if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
217
219
  }
218
- lines = context.source_lines
219
220
  return [
220
- Diagnostic(
221
- path=path,
222
- line=call.lineno,
223
- col=call.col_offset + 1,
224
- code=self.code,
225
- severity=Severity.WARNING,
226
- message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
227
- )
221
+ finding
228
222
  for test in collected_tests(tree.body)
229
223
  if not _is_pytest_fixture(test, imports)
230
- for call in _uncovered_calls(_test_imports(imports, test), test, writes)
231
- if not is_suppressed(lines, call.lineno, self.code)
224
+ for finding in _test_diagnostics(test, context, imports, writes, self.code)
232
225
  ]
233
226
 
234
227
 
228
+ def _test_diagnostics(
229
+ test: _TestFunction, context: PythonFileContext, imports: ImportIndex, writes: set[_Reference], code: str
230
+ ) -> list[Diagnostic]:
231
+ calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes))
232
+ if not calls:
233
+ return []
234
+ severity = Severity.ERROR if _mock_provider_proven(test, context) else Severity.WARNING
235
+ message = "AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion"
236
+ if severity is Severity.WARNING:
237
+ message = (
238
+ "Verify the mock provider is asynchronous before adding an await assertion; "
239
+ "keep the call assertion and preserve intentional scheduling or synchronous replacement contracts."
240
+ )
241
+ return [
242
+ Diagnostic(
243
+ path=context.path,
244
+ line=call.lineno,
245
+ col=call.col_offset + 1,
246
+ code=code,
247
+ severity=severity,
248
+ message=message,
249
+ )
250
+ for call in calls
251
+ if not is_suppressed(context.source_lines, call.lineno, code)
252
+ ]
253
+
254
+
255
+ def _mock_provider_proven(test: _TestFunction, context: PythonFileContext) -> bool:
256
+ provenance = ResourceProvenance(context, require_static_namespace=True)
257
+ constructors = [
258
+ node
259
+ for node in ast.walk(test)
260
+ if isinstance(node, ast.Call)
261
+ and context.imports.resolved_qualified_name(node.func) == "unittest.mock.AsyncMock"
262
+ ]
263
+ return bool(constructors) and all(
264
+ provenance.imported(node.func, "unittest.mock.AsyncMock") for node in constructors
265
+ )
266
+
267
+
235
268
  def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
236
269
  return any(
237
270
  imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)
@@ -18,7 +18,9 @@ from sarj_python_lint.rule_base import (
18
18
  )
19
19
  from sarj_python_lint.rules._ast_index import walk as walk_ast
20
20
  from sarj_python_lint.rules._imports import ImportIndex
21
+ from sarj_python_lint.rules._inert_model_annotation import inert_direct_model_body
21
22
  from sarj_python_lint.rules._paths import is_test_path
23
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
22
24
 
23
25
 
24
26
  if TYPE_CHECKING:
@@ -44,7 +46,7 @@ class NoRedundantLiteralDescription(Rule):
44
46
  id = "no-restated-closed-domain-description"
45
47
  code = "SARJ423"
46
48
  documentation = RuleDocumentation(
47
- default_level=Severity.WARNING,
49
+ default_level=Severity.ERROR,
48
50
  summary="Do not restate a string Literal or local string Enum domain in its Pydantic description.",
49
51
  rationale=(
50
52
  "Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can "
@@ -61,6 +63,7 @@ class NoRedundantLiteralDescription(Rule):
61
63
  "Only direct fields on classes with a direct BaseModel base are inspected.",
62
64
  "Domains are resolved only from direct string Literals and static Literal aliases or string-valued Enums in the same module.",
63
65
  "Descriptions are reported only when a narrow clause repeats the complete domain without mapping or rationale.",
66
+ "ERROR requires an inert direct model with no json_schema_extra options; custom construction, schema metadata, local aliases and Enum annotations retain WARNING with schema-ownership review guidance.",
64
67
  "Test and generated files are excluded.",
65
68
  ),
66
69
  examples=(
@@ -116,7 +119,9 @@ class NoRedundantLiteralDescription(Rule):
116
119
  local_domains = _local_closed_domains(tree, imports)
117
120
  diagnostics: list[Diagnostic] = []
118
121
 
119
- def collect_field_descriptions(statement: ast.AnnAssign, field_name: str, domain: frozenset[str]) -> None:
122
+ def collect_field_descriptions(
123
+ statement: ast.AnnAssign, field_name: str, domain: frozenset[str], *, severity: Severity
124
+ ) -> None:
120
125
  for field_call in _field_calls(statement, imports):
121
126
  description = next(
122
127
  (keyword.value for keyword in field_call.keywords if keyword.arg == "description"), None
@@ -134,26 +139,74 @@ class NoRedundantLiteralDescription(Rule):
134
139
  col=description.col_offset + 1,
135
140
  code=self.code,
136
141
  message=(
137
- f"`{field_name}` repeats values already emitted by its Literal or Enum schema; "
138
- "remove the repeated domain while preserving behavioral guidance."
142
+ f"`{field_name}` repeats its declared Literal or Enum domain; "
143
+ "remove the repeated list only after preserving required schema metadata and behavioral guidance."
139
144
  ),
140
- severity=Severity.WARNING,
145
+ severity=severity,
141
146
  )
142
147
  )
143
148
 
144
- for model in (node for node in context.nodes(ast.AST) if isinstance(node, ast.ClassDef)):
149
+ for model in context.nodes(ast.ClassDef):
145
150
  if not _is_direct_model(model, imports) or _has_custom_schema_hook(model):
146
151
  continue
152
+ severity = _description_level(model, context)
147
153
  for statement in model.body:
148
154
  if not isinstance(statement, ast.AnnAssign) or not isinstance(statement.target, ast.Name):
149
155
  continue
150
156
  domain = _closed_domain(statement.annotation, imports, local_domains)
151
157
  if domain is None or _has_schema_override(statement.annotation, imports):
152
158
  continue
153
- collect_field_descriptions(statement, statement.target.id, domain)
159
+ collect_field_descriptions(
160
+ statement,
161
+ statement.target.id,
162
+ domain,
163
+ severity=severity,
164
+ )
154
165
  return diagnostics
155
166
 
156
167
 
168
+ def _description_level(model: ast.ClassDef, context: PythonFileContext) -> Severity:
169
+ return Severity.ERROR if _inert_description_model(model, context) else Severity.WARNING
170
+
171
+
172
+ def _inert_description_model(model: ast.ClassDef, context: PythonFileContext) -> bool:
173
+ if context.dynamic_namespace or not inert_direct_model_body(model, context.imports):
174
+ return False
175
+ provenance = ResourceProvenance(context, require_static_namespace=True)
176
+ for node in walk_ast(model):
177
+ if isinstance(node, ast.keyword) and node.arg == "json_schema_extra":
178
+ return False
179
+ if not _annotation_provider_proven(node, context, provenance):
180
+ return False
181
+ if isinstance(node, ast.Call) and not provenance.imported(
182
+ node.func, context.imports.resolved_qualified_name(node.func) or "pydantic.Field"
183
+ ):
184
+ return False
185
+ return any(
186
+ provenance.imported(base, context.imports.resolved_qualified_name(base) or "pydantic.BaseModel")
187
+ for base in model.bases
188
+ )
189
+
190
+
191
+ def _annotation_provider_proven(node: ast.AST, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
192
+ if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Constant):
193
+ return False
194
+ if isinstance(node, ast.Subscript) and context.imports.resolved_qualified_name(node.value) in {
195
+ "typing.Annotated",
196
+ "typing_extensions.Annotated",
197
+ }:
198
+ argument = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) and node.slice.elts else node.slice
199
+ return not isinstance(argument, ast.Constant)
200
+ if isinstance(node, ast.Name | ast.Attribute) and (qualified := context.imports.resolved_qualified_name(node)) in {
201
+ "typing.Literal",
202
+ "typing.Annotated",
203
+ "typing_extensions.Literal",
204
+ "typing_extensions.Annotated",
205
+ }:
206
+ return provenance.imported(node, qualified)
207
+ return True
208
+
209
+
157
210
  def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
158
211
  return any(imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel") for base in node.bases)
159
212
 
@@ -30,7 +30,7 @@ class NoRedundantModuleAliasExports(Rule):
30
30
  id: str = "no-redundant-module-alias-exports"
31
31
  code: str = "SARJ440"
32
32
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
33
- default_level=Severity.WARNING,
33
+ default_level=Severity.ERROR,
34
34
  summary="Do not manufacture public APIs from private names or replace the current module.",
35
35
  rationale=(
36
36
  "Binding a public name to a private implementation creates two names for one API and disguises the "
@@ -41,7 +41,8 @@ class NoRedundantModuleAliasExports(Rule):
41
41
  remediation=(
42
42
  "Define maintained APIs under their public names and update local callers directly. Import the canonical "
43
43
  "path inside maintained code. If a compatibility module is still required, expose its supported names with "
44
- "explicit same-name imports grouped by source module."
44
+ "explicit same-name imports grouped by source module. Preserve a required compatibility or module-identity "
45
+ "contract with a reasoned exact suppression rather than renaming automatically."
45
46
  ),
46
47
  category=RuleCategory.MAINTAINABILITY,
47
48
  autofix=AutofixPolicy.NONE,
@@ -19,6 +19,7 @@ from sarj_python_lint.rule_base import (
19
19
  )
20
20
  from sarj_python_lint.rules._ast_index import nodes
21
21
  from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
22
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
22
23
 
23
24
 
24
25
  if TYPE_CHECKING:
@@ -41,7 +42,7 @@ class NoSwallowedAsyncioCancellation(Rule):
41
42
  id = "no-swallowed-asyncio-cancellation"
42
43
  code = "SARJ458"
43
44
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
44
- default_level=Severity.WARNING,
45
+ default_level=Severity.ERROR,
45
46
  summary="Preserve cancellation when an async operation is cancelled.",
46
47
  rationale=(
47
48
  "Returning or falling through after catching CancelledError can make cancelled work look successful "
@@ -60,6 +61,7 @@ class NoSwallowedAsyncioCancellation(Rule):
60
61
  "Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.",
61
62
  "A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.",
62
63
  "Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.",
64
+ "Only a try body consisting of one awaited, unmodified canonical asyncio.sleep call with literal arguments is blocking. Unresolved operations and child cancellation remain advisory.",
63
65
  ),
64
66
  examples=(
65
67
  RuleExample(
@@ -129,12 +131,34 @@ def _function_diagnostics(function: ast.AsyncFunctionDef, context: PythonFileCon
129
131
  col=handler.col_offset + 1,
130
132
  code=code,
131
133
  message=_MESSAGE,
132
- severity=Severity.WARNING,
134
+ severity=Severity.ERROR if _caller_sleep(node, context) else Severity.WARNING,
133
135
  )
134
136
  )
135
137
  return diagnostics
136
138
 
137
139
 
140
+ def _caller_sleep(node: ast.Try, context: PythonFileContext) -> bool:
141
+ if context.dynamic_namespace or len(node.body) != 1 or len(node.handlers) != 1:
142
+ return False
143
+ provenance = ResourceProvenance(context, require_static_namespace=True)
144
+ caught = node.handlers[0].type
145
+ if caught is None or not provenance.imported(
146
+ caught, context.imports.resolved_qualified_name(caught) or "asyncio.CancelledError"
147
+ ):
148
+ return False
149
+ match node.body[0]:
150
+ case ast.Expr(value=ast.Await(value=ast.Call() as call)):
151
+ if not context.imports.resolves(call.func, sources=frozenset({"asyncio"}), symbol="sleep"):
152
+ return False
153
+ if not provenance.imported(call.func, "asyncio.sleep"):
154
+ return False
155
+ if not all(isinstance(value, ast.Constant) for value in call.args):
156
+ return False
157
+ return not call.keywords
158
+ case _:
159
+ return False
160
+
161
+
138
162
  def _local_nodes(function: ast.AsyncFunctionDef) -> Iterator[ast.AST]:
139
163
  stack: list[ast.AST] = list(reversed(function.body))
140
164
  yield function
@@ -28,7 +28,7 @@ class NoTestMethodGrafting(ProjectRule):
28
28
  id = "no-test-method-grafting"
29
29
  code = "SARJ478"
30
30
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
31
- default_level=Severity.WARNING,
31
+ default_level=Severity.ERROR,
32
32
  summary="Avoid assigning replacement implementations directly onto resolved methods in tests.",
33
33
  rationale=(
34
34
  "Grafting a function onto a collaborator or mock hides the boundary's callable contract and bypasses "
@@ -135,7 +135,7 @@ class NoTestMethodGrafting(ProjectRule):
135
135
  line=position.line,
136
136
  col=position.column + 1,
137
137
  code=self.code,
138
- severity=Severity.WARNING,
138
+ severity=Severity.ERROR,
139
139
  message=(
140
140
  f"Test assigns an implementation directly to resolved method `{method}`; inject a fake, "
141
141
  "or configure the existing autospecced mock method's side_effect"
@@ -26,7 +26,7 @@ class NoUnusedCopyResult(ProjectRule):
26
26
  id = "no-unused-copy-result"
27
27
  code = "SARJ483"
28
28
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
29
- default_level=Severity.WARNING,
29
+ default_level=Severity.ERROR,
30
30
  summary="Use the replacement returned by a resolved copy API.",
31
31
  rationale="Copy-on-write replacement APIs return another value rather than updating the original. Discarding their result can silently leave subsequent operations using unchanged data.",
32
32
  remediation="Assign, return, or otherwise consume the replacement. A copy hook intentionally exercised for its effects or exceptions can use a narrow suppression; do not automatically delete the call.",
@@ -113,7 +113,7 @@ class NoUnusedCopyResult(ProjectRule):
113
113
  col=node.col_offset + 1,
114
114
  code=self.code,
115
115
  message="This copy API returns a replacement that is discarded; assign or return it, or document intentional copy-hook effects.",
116
- severity=Severity.WARNING,
116
+ severity=Severity.ERROR,
117
117
  )
118
118
  for node in (context.copy_on_write.discarded)
119
119
  if not is_suppressed(context.source_lines, node.lineno, self.code)