sarj-python-lint 1.32.0__tar.gz → 1.34.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.32.0 → sarj_python_lint-1.34.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_python_target.py +1 -1
  4. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_ratchet_cli.py +2 -3
  5. sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_inert_model_annotation.py +160 -0
  6. sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_resolved_operation_signature.py +102 -0
  7. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_resource_provenance.py +7 -2
  8. sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_static_json_alias.py +142 -0
  9. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +45 -12
  10. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +23 -5
  11. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +2 -2
  12. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +2 -2
  13. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +2 -2
  14. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +60 -7
  15. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +3 -2
  16. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +5 -2
  17. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +26 -2
  18. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +2 -2
  19. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +9 -3
  20. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +2 -2
  21. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +6 -3
  22. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +71 -9
  23. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +2 -2
  24. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +4 -4
  25. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_docstring.py +15 -3
  26. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +9 -97
  27. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +3 -3
  28. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +2 -2
  29. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +74 -3
  30. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +2 -2
  31. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +16 -3
  32. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +67 -7
  33. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/.gitignore +0 -0
  34. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/LICENSE +0 -0
  35. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/README.md +0 -0
  36. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/__init__.py +0 -0
  37. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/__main__.py +0 -0
  38. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  39. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_file_context.py +0 -0
  40. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_filesystem.py +0 -0
  41. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_secret_names.py +0 -0
  42. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_source.py +0 -0
  43. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_version.py +0 -0
  44. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/benchmark.py +0 -0
  45. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  46. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/json_boundary.py +0 -0
  47. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/py.typed +0 -0
  48. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/ratchet.py +0 -0
  49. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rule_base.py +0 -0
  50. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  51. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  52. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  53. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  54. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  55. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  56. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  57. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  58. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  59. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  60. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  61. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  62. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  63. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  64. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  65. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  66. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  67. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  68. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  69. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  70. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  71. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  72. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  73. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  74. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  75. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  76. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  77. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  78. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  79. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  80. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  81. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  82. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  83. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  84. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  85. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  86. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  87. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  88. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  89. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  90. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  91. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  92. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  93. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  94. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  95. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  96. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  97. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  98. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  99. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  100. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  101. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  102. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  103. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  104. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  105. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  106. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  107. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  108. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  109. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  110. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  111. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  112. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  113. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  114. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  115. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  116. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  117. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  118. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  119. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  120. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  121. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  122. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  123. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  124. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  125. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  126. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  127. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  128. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  129. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  130. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  131. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  132. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  133. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  134. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  135. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  136. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  137. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  138. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  139. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  140. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  141. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  142. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  143. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  144. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  145. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  146. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  147. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  148. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  149. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  150. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  151. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  152. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  153. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  154. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  155. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  156. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  157. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  158. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  159. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  160. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  161. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  162. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  163. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  164. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  165. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  166. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  167. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  168. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  169. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  170. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  171. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  172. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  173. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  174. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  175. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  176. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  177. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  178. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  179. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  180. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  181. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  182. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  183. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  184. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  185. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  186. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  187. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  188. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  189. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  190. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  191. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  192. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  193. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  194. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  195. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  196. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  197. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  198. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  199. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  200. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  201. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  202. {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.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.32.0
3
+ Version: 1.34.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.32.0"
3
+ version = "1.34.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" }]
@@ -99,7 +99,7 @@ def _project_declaration(path: Path) -> str | None:
99
99
  return declaration if isinstance(declaration, str) else None
100
100
 
101
101
 
102
- def _is_toml_table(value: object) -> TypeGuard[dict[str, object]]:
102
+ def _is_toml_table(value: object) -> TypeGuard[dict[object, object]]:
103
103
  return isinstance(value, dict)
104
104
 
105
105
 
@@ -148,7 +148,7 @@ def _ruff_selector_aliases() -> dict[str, str]:
148
148
  raise TypeError(msg)
149
149
  aliases: dict[str, str] = {}
150
150
  for raw_item in raw:
151
- if not _is_string_object_mapping(raw_item):
151
+ if not _is_object_mapping(raw_item):
152
152
  msg = "Ruff rule catalog contains a non-object entry"
153
153
  raise TypeError(msg)
154
154
  code = raw_item.get("code")
@@ -173,8 +173,7 @@ def _is_object_list(value: object) -> TypeGuard[list[object]]:
173
173
  return isinstance(value, list)
174
174
 
175
175
 
176
- def _is_string_object_mapping(value: object) -> TypeGuard[dict[str, object]]:
177
- # JSON object keys are strings by definition; json.loads supplied `value`.
176
+ def _is_object_mapping(value: object) -> TypeGuard[dict[object, object]]:
178
177
  return isinstance(value, dict)
179
178
 
180
179
 
@@ -0,0 +1,160 @@
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._imports import ImportIndex
9
+
10
+
11
+ _BUILTINS = frozenset({"bool", "bytes", "dict", "float", "frozenset", "int", "list", "object", "set", "str", "tuple"})
12
+ _QUALIFIED_BUILTINS = frozenset(f"builtins.{name}" for name in _BUILTINS)
13
+ _MAX_ANNOTATION_LENGTH = 4096
14
+ _CANONICAL_TYPES = frozenset(
15
+ {
16
+ "typing.Annotated",
17
+ "typing.Any",
18
+ "typing.ClassVar",
19
+ "typing.Literal",
20
+ "typing.Optional",
21
+ "typing.Union",
22
+ "typing_extensions.Annotated",
23
+ "typing_extensions.ClassVar",
24
+ "typing_extensions.Literal",
25
+ "pydantic.PositiveInt",
26
+ "pydantic.NonNegativeInt",
27
+ "pydantic.types.PositiveInt",
28
+ "pydantic.types.NonNegativeInt",
29
+ }
30
+ )
31
+ _LITERAL_CONSTRAINTS = frozenset(
32
+ {
33
+ "pydantic.Field",
34
+ "pydantic.fields.Field",
35
+ "annotated_types.Ge",
36
+ "annotated_types.Gt",
37
+ "annotated_types.Le",
38
+ "annotated_types.Lt",
39
+ "annotated_types.MultipleOf",
40
+ }
41
+ )
42
+
43
+
44
+ def inert_model_annotation(node: ast.expr, imports: ImportIndex, *, remaining: int = 64) -> bool:
45
+ if remaining <= 0:
46
+ return False
47
+ match node:
48
+ case ast.Constant(value=str() as source):
49
+ if len(source) > _MAX_ANNOTATION_LENGTH:
50
+ return False
51
+ try:
52
+ parsed = ast.parse(source, mode="eval").body
53
+ except SyntaxError, ValueError, RecursionError:
54
+ return False
55
+ return inert_model_annotation(parsed, imports, remaining=remaining - 1)
56
+ case ast.Constant(value=None):
57
+ return True
58
+ case ast.Name(id=name):
59
+ return (name in _BUILTINS and imports.builtin_is_unshadowed(name)) or imports.resolved_qualified_name(
60
+ node
61
+ ) in _CANONICAL_TYPES
62
+ case ast.Attribute():
63
+ qualified = imports.resolved_qualified_name(node)
64
+ return qualified in _CANONICAL_TYPES or qualified in _QUALIFIED_BUILTINS
65
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
66
+ return inert_model_annotation(left, imports, remaining=remaining - 1) and inert_model_annotation(
67
+ right, imports, remaining=remaining - 1
68
+ )
69
+ case ast.Subscript():
70
+ return _subscript_annotation(node, imports, remaining=remaining)
71
+ case _:
72
+ return False
73
+
74
+
75
+ def _subscript_annotation(node: ast.Subscript, imports: ImportIndex, *, remaining: int) -> bool:
76
+ if not inert_model_annotation(node.value, imports, remaining=remaining - 1):
77
+ return False
78
+ arguments = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
79
+ qualified = imports.resolved_qualified_name(node.value)
80
+ if qualified in {"typing.Literal", "typing_extensions.Literal"}:
81
+ return all(_literal(item) for item in arguments)
82
+ if qualified in {"typing.Annotated", "typing_extensions.Annotated"}:
83
+ return (
84
+ bool(arguments)
85
+ and inert_model_annotation(arguments[0], imports, remaining=remaining - 1)
86
+ and all(_constraint(item, imports) for item in arguments[1:])
87
+ )
88
+ return all(inert_model_annotation(item, imports, remaining=remaining - 1) for item in arguments)
89
+
90
+
91
+ def _constraint(node: ast.expr, imports: ImportIndex) -> bool:
92
+ if _literal(node):
93
+ return True
94
+ return (
95
+ isinstance(node, ast.Call)
96
+ and imports.resolved_qualified_name(node.func) in _LITERAL_CONSTRAINTS
97
+ and all(_literal(argument) for argument in node.args)
98
+ and all(keyword.arg is not None and _literal(keyword.value) for keyword in node.keywords)
99
+ )
100
+
101
+
102
+ def _literal(node: ast.expr) -> bool:
103
+ return isinstance(node, ast.Constant) or (
104
+ isinstance(node, ast.UnaryOp)
105
+ and isinstance(node.op, ast.UAdd | ast.USub)
106
+ and isinstance(node.operand, ast.Constant)
107
+ and isinstance(node.operand.value, int | float)
108
+ and not isinstance(node.operand.value, bool)
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
@@ -0,0 +1,102 @@
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._imports import ImportIndex
9
+ from sarj_python_lint.rules._project_index import SourceUnit
10
+
11
+
12
+ _BUILTINS = frozenset({"str", "bytes", "int", "float", "bool", "object", "list", "dict", "tuple", "set", "frozenset"})
13
+
14
+
15
+ def stable_local_class(unit: SourceUnit, name: str) -> bool:
16
+ if unit.tree is None:
17
+ return False
18
+ return sum(
19
+ isinstance(node, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef) and node.name == name
20
+ for node in ast.walk(unit.tree)
21
+ ) == 1 and not any(
22
+ (isinstance(node, ast.Name) and node.id == name and not isinstance(node.ctx, ast.Load))
23
+ or (isinstance(node, ast.arg) and node.arg == name)
24
+ for node in ast.walk(unit.tree)
25
+ )
26
+
27
+
28
+ def resolved_operation_signature(
29
+ method: ast.FunctionDef | ast.AsyncFunctionDef, unit: SourceUnit, imports: ImportIndex
30
+ ) -> str | None:
31
+ arguments = method.args
32
+ if method.type_params or arguments.vararg is not None or arguments.kwarg is not None or method.returns is None:
33
+ return None
34
+ positional = [*arguments.posonlyargs, *arguments.args]
35
+ if not positional or positional[0].arg != "self" or positional[0].annotation is not None:
36
+ return None
37
+ keys = [_type_key(argument.annotation, unit, imports) for argument in (*positional[1:], *arguments.kwonlyargs)]
38
+ result = _type_key(method.returns, unit, imports)
39
+ defaults = [*arguments.defaults, *(default for default in arguments.kw_defaults if default is not None)]
40
+ if (
41
+ result is None
42
+ or any(key is None for key in keys)
43
+ or any(not isinstance(default, ast.Constant) for default in defaults)
44
+ ):
45
+ return None
46
+ return repr(
47
+ (
48
+ type(method).__name__,
49
+ len(arguments.posonlyargs),
50
+ tuple(argument.arg for argument in (*positional, *arguments.kwonlyargs)),
51
+ keys,
52
+ result,
53
+ tuple(ast.dump(default) for default in arguments.defaults),
54
+ tuple(None if default is None else ast.dump(default) for default in arguments.kw_defaults),
55
+ )
56
+ )
57
+
58
+
59
+ def _type_key(node: ast.expr | None, unit: SourceUnit, imports: ImportIndex) -> str | None:
60
+ if node is None or unit.tree is None:
61
+ return None
62
+ match node:
63
+ case ast.Name():
64
+ return _name_type_key(node, unit, imports)
65
+ case ast.Attribute():
66
+ return imports.resolved_qualified_name(node)
67
+ case ast.Constant(value=str()):
68
+ return None
69
+ case ast.Constant():
70
+ return ast.dump(node)
71
+ case _:
72
+ return _compound_type_key(node, unit, imports)
73
+
74
+
75
+ def _name_type_key(node: ast.Name, unit: SourceUnit, imports: ImportIndex) -> str | None:
76
+ if unit.tree is None:
77
+ return None
78
+ name = node.id
79
+ if name in _BUILTINS and imports.builtin_is_unshadowed(name):
80
+ return f"builtins.{name}"
81
+ imported = imports.resolved_qualified_name(node)
82
+ if imported is not None:
83
+ return imported
84
+ if any(isinstance(child, ast.ClassDef) and child.name == name for child in unit.tree.body) and stable_local_class(
85
+ unit, name
86
+ ):
87
+ return f"{unit.module}.{name}"
88
+ return None
89
+
90
+
91
+ def _compound_type_key(node: ast.expr, unit: SourceUnit, imports: ImportIndex) -> str | None:
92
+ match node:
93
+ case ast.Subscript(value=value, slice=argument):
94
+ kind, parts = "subscript", (value, argument)
95
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
96
+ kind, parts = "union", (left, right)
97
+ case ast.Tuple(elts=elements):
98
+ kind, parts = "tuple", tuple(elements)
99
+ case _:
100
+ return None
101
+ keys = tuple(_type_key(part, unit, imports) for part in parts)
102
+ return repr((kind, keys)) if all(key is not None for key in keys) else 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)
@@ -45,13 +45,13 @@ class NoBroadKeywordCapture(Rule):
45
45
  id = "no-broad-keyword-capture"
46
46
  code = "SARJ477"
47
47
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
48
- default_level=Severity.WARNING,
48
+ default_level=Severity.ERROR,
49
49
  summary="Prefer precise keyword interfaces over missing, Any- or object-valued captures.",
50
50
  rationale=(
51
51
  "Replacing a fixed typed interface with broad **kwargs can erase required keyword names and per-key "
52
52
  "value types, making test doubles accept calls the real implementation rejects. object remains a safe "
53
- "typed top type, and deliberately open data interfaces can be valid; this warning requests interface "
54
- "review rather than proving every broad capture is defective."
53
+ "typed top type, and deliberately open data interfaces can be valid; this strict interface policy "
54
+ "requires a precise contract or a documented open-boundary exception."
55
55
  ),
56
56
  remediation=(
57
57
  "Keep explicit typed parameters at fixed interfaces. Preserve required keyword names with named unused "
@@ -61,13 +61,14 @@ class NoBroadKeywordCapture(Rule):
61
61
  category=RuleCategory.MAINTAINABILITY,
62
62
  autofix=AutofixPolicy.NONE,
63
63
  limitations=(
64
- "This is an advisory interface policy, not proof of incorrect behavior or a universal ban on the object type.",
64
+ "This strict interface policy is not proof of incorrect behavior or a universal ban on the object type; genuinely open boundaries use a reasoned exact-code exception.",
65
65
  "Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
66
66
  "Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.",
67
67
  "Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.",
68
68
  "Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.",
69
69
  "Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.",
70
70
  "No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
71
+ "Runtime special methods and canonical override declarations retain WARNING because framework and inherited signatures can require an open keyword contract; ordinary declared interfaces use ERROR.",
71
72
  ),
72
73
  examples=(
73
74
  RuleExample(
@@ -196,7 +197,7 @@ class NoBroadKeywordCapture(Rule):
196
197
  line=argument.lineno,
197
198
  col=argument.col_offset + 1,
198
199
  code=self.code,
199
- severity=Severity.WARNING,
200
+ severity=_interface_severity(function, context, semantics, mutated_members),
200
201
  message=(
201
202
  f"keyword capture `{argument.arg}` has no declared names or narrow value type; "
202
203
  "prefer an explicit typed interface or document a deliberately open contract."
@@ -206,6 +207,23 @@ class NoBroadKeywordCapture(Rule):
206
207
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
207
208
 
208
209
 
210
+ def _interface_severity(
211
+ function: ast.FunctionDef | ast.AsyncFunctionDef,
212
+ context: PythonFileContext,
213
+ semantics: AnnotationSemantics,
214
+ mutated_members: frozenset[str],
215
+ ) -> Severity:
216
+ if function.name.startswith("__") and function.name.endswith("__"):
217
+ return Severity.WARNING
218
+ if any(
219
+ _known_decorator(decorator, function, context, semantics, mutated_members)
220
+ in {"typing.override", "typing_extensions.override"}
221
+ for decorator in function.decorator_list
222
+ ):
223
+ return Severity.WARNING
224
+ return Severity.ERROR
225
+
226
+
209
227
  def _stable_annotation_semantics(context: PythonFileContext, tree: ast.Module) -> AnnotationSemantics:
210
228
  semantics = AnnotationSemantics.from_tree(tree)
211
229
  unstable = _conflicting_annotation_imports(context, semantics) | _annotation_binding_writes(context)
@@ -45,7 +45,7 @@ class NoFixedSleepInLoop(Rule):
45
45
  id = "no-fixed-sleep-in-loop"
46
46
  code = "SARJ485"
47
47
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
48
- default_level=Severity.WARNING,
48
+ default_level=Severity.ERROR,
49
49
  summary="Do not sleep for a fixed literal interval inside a production loop.",
50
50
  rationale=(
51
51
  "A polling or cleanup loop that sleeps for a literal interval gives tests no seam: every test that "
@@ -161,7 +161,7 @@ class NoFixedSleepInLoop(Rule):
161
161
  col=reported.col_offset + 1,
162
162
  code=self.code,
163
163
  message=_MESSAGE,
164
- severity=Severity.WARNING,
164
+ severity=Severity.ERROR,
165
165
  )
166
166
  for reported, duration in sleeps
167
167
  if (seconds := _folded_seconds(duration, constants)) is not None