sarj-python-lint 1.8.0__tar.gz → 1.8.2__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 (164) hide show
  1. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +124 -74
  4. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +10 -62
  5. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/.gitignore +0 -0
  6. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/LICENSE +0 -0
  7. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/README.md +0 -0
  8. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_file_context.py +0 -0
  12. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_filesystem.py +0 -0
  13. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_python_target.py +0 -0
  14. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  15. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_secret_names.py +0 -0
  16. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/_version.py +0 -0
  17. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/benchmark.py +0 -0
  18. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/json_boundary.py +0 -0
  19. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/py.typed +0 -0
  20. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/ratchet.py +0 -0
  21. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rule_base.py +0 -0
  22. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  23. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  24. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  25. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  26. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  27. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  28. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  29. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  30. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  31. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  36. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  37. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  38. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  39. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  40. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  41. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  42. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  48. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  49. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  50. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  51. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  52. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  53. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  54. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  55. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  56. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  57. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  58. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  59. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  60. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  61. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  62. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  63. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  64. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  65. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  66. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  67. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  68. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  69. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  70. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  71. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  72. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  73. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  74. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  75. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  76. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  77. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  78. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  79. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  80. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  81. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  82. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  83. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  84. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  85. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  86. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  87. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  88. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  89. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  90. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  91. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  92. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  93. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  94. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  95. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  96. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  97. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  98. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  99. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  100. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  101. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  102. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  103. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  104. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  105. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  106. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  107. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  108. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  109. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  110. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  111. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  112. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  113. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  114. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  115. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  116. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  117. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  118. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  119. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  120. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  121. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  122. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  123. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  124. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  125. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  126. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  127. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  128. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  129. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  130. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  131. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  132. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  133. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  134. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  135. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  136. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  137. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  138. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  139. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  140. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  141. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  142. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  143. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  144. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  145. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  146. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  147. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  148. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  149. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  150. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  151. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  152. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  153. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  154. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  155. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  156. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  157. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  158. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  159. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  160. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  161. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  162. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  163. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  164. {sarj_python_lint-1.8.0 → sarj_python_lint-1.8.2}/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.8.0
3
+ Version: 1.8.2
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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.16.9
22
- Requires-Dist: sarj-rule-contracts==0.1.1
22
+ Requires-Dist: sarj-rule-contracts==0.2.0
23
23
  Requires-Dist: sqlglot==30.19.0
24
24
  Requires-Dist: typer==0.27.2
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.8.0"
3
+ version = "1.8.2"
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.1.1", "packaging==26.3", "ruff>=0.16.9", "sqlglot==30.19.0", "typer==0.27.2"]
18
+ dependencies = ["sarj-rule-contracts==0.2.0", "packaging==26.3", "ruff>=0.16.9", "sqlglot==30.19.0", "typer==0.27.2"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from enum import Enum, auto
4
5
  from pathlib import PurePosixPath
5
6
  from types import MappingProxyType
6
7
  from typing import TYPE_CHECKING, ClassVar, Final, final, override
@@ -65,12 +66,18 @@ type _TestFunction = ast.FunctionDef | ast.AsyncFunctionDef
65
66
  type _Reference = tuple[str, ...]
66
67
 
67
68
 
69
+ class _Evidence(Enum):
70
+ POSITIVE = auto()
71
+ INSUFFICIENT = auto()
72
+ UNRESOLVED = auto()
73
+
74
+
68
75
  @final
69
76
  class AsyncMockCallWithoutAwaitAssertion(Rule):
70
77
  id = "async-mock-call-without-await-assertion"
71
78
  code = "SARJ456"
72
79
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
73
- default_level=Severity.ERROR,
80
+ default_level=Severity.WARNING,
74
81
  summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
75
82
  rationale=(
76
83
  "Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
@@ -87,33 +94,50 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
87
94
  "Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
88
95
  "Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
89
96
  "Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
90
- "Positive call-count and called-state assertions are covered as well as call-assertion methods. Await-state assertions must prove at least one await; argument equality and exact call/await counts are not compared.",
91
- "Boolean conjunctions combine evidence; disjunctions require evidence in every branch. Dynamic counts and call lists are not inferred.",
97
+ "Positive call evidence includes direct assertion methods and literal integer or boolean call-state checks; unresolved call expectations are excluded.",
98
+ "Await evidence excludes provably vacuous checks, including zero or nonnegative counts and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
99
+ "Conjunctions need one accepted await branch; disjunctions need accepted evidence for the same mock in every branch. Argument equality and assertion strength are not compared.",
100
+ "Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.",
92
101
  "Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression.",
93
102
  ),
94
103
  examples=(
95
104
  RuleExample(
96
- example_id="numeric-call-with-vacuous-await-check",
97
- title="A nonnegative await count does not prove execution",
105
+ example_id="call-only-async-mock",
106
+ title="A call assertion does not prove awaiting",
98
107
  outcome=ExampleOutcome.MATCH,
99
108
  files=(
100
109
  ExampleFile.python(
101
110
  "tests/test_delivery.py",
102
- "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n assert send.call_count == 2\n assert send.await_count >= 0\n",
111
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n",
103
112
  ),
104
113
  ),
105
114
  focus_path=PurePosixPath("tests/test_delivery.py"),
106
115
  expected_count=1,
107
- public=False,
116
+ public=True,
108
117
  ),
109
118
  RuleExample(
110
- example_id="call-only-async-mock",
111
- title="A call assertion does not prove awaiting",
119
+ example_id="call-and-await-async-mock",
120
+ title="Call and await assertions protect different contracts",
121
+ outcome=ExampleOutcome.NO_MATCH,
122
+ files=(
123
+ ExampleFile.python(
124
+ "tests/test_delivery.py",
125
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n send.assert_awaited_once_with('item')\n",
126
+ ),
127
+ ),
128
+ focus_path=PurePosixPath("tests/test_delivery.py"),
129
+ expected_count=0,
130
+ public=True,
131
+ ),
132
+ RuleExample(
133
+ example_id="vacuous-await-count",
134
+ scenario="await-expectation-boundaries",
135
+ title="A nonnegative await count does not prove execution",
112
136
  outcome=ExampleOutcome.MATCH,
113
137
  files=(
114
138
  ExampleFile.python(
115
139
  "tests/test_delivery.py",
116
- "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n",
140
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n assert send.call_count == 1\n assert send.await_count >= 0\n",
117
141
  ),
118
142
  ),
119
143
  focus_path=PurePosixPath("tests/test_delivery.py"),
@@ -121,13 +145,14 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
121
145
  public=True,
122
146
  ),
123
147
  RuleExample(
124
- example_id="call-and-await-async-mock",
125
- title="Call and await assertions protect different contracts",
148
+ example_id="dynamic-expected-awaits",
149
+ scenario="await-expectation-boundaries",
150
+ title="Unresolved await expectations retain their acceptance",
126
151
  outcome=ExampleOutcome.NO_MATCH,
127
152
  files=(
128
153
  ExampleFile.python(
129
154
  "tests/test_delivery.py",
130
- "from unittest.mock import AsyncMock\n\nasync def test_delivery():\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once_with('item')\n send.assert_awaited_once_with('item')\n",
155
+ "from unittest.mock import AsyncMock\n\nasync def test_delivery(expected):\n send = AsyncMock()\n await deliver(send)\n send.assert_called_once()\n send.assert_has_awaits(expected)\n",
131
156
  ),
132
157
  ),
133
158
  focus_path=PurePosixPath("tests/test_delivery.py"),
@@ -166,7 +191,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
166
191
  line=call.lineno,
167
192
  col=call.col_offset + 1,
168
193
  code=self.code,
169
- severity=Severity.ERROR,
194
+ severity=Severity.WARNING,
170
195
  message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
171
196
  )
172
197
  for test in _collected_tests(tree.body)
@@ -231,7 +256,7 @@ def _call_oracles(statement: ast.stmt) -> Iterator[tuple[_Reference, ast.Call |
231
256
  ):
232
257
  yield _reference(receiver), call
233
258
  case ast.Assert(test=condition):
234
- for reference in sorted(_positive_states(condition, _CALL_STATE)):
259
+ for reference in sorted(_asserted_states(condition, _CALL_STATE)):
235
260
  oracle = next(
236
261
  node
237
262
  for node in walk_ast(condition)
@@ -304,49 +329,60 @@ def _await_oracles(statements: list[ast.stmt]) -> set[_Reference]:
304
329
  case ast.Expr(value=ast.Call(func=ast.Attribute(value=receiver, attr=method)) as call) if (
305
330
  method in _AWAIT_ASSERTIONS
306
331
  ):
307
- if method != "assert_has_awaits" or _has_nonempty_calls_argument(call):
332
+ if method != "assert_has_awaits" or _calls_evidence(call) is not _Evidence.INSUFFICIENT:
308
333
  covered.add(_reference(receiver))
309
334
  case ast.Assert(test=condition):
310
- covered.update(_positive_states(condition, _AWAIT_STATE))
335
+ covered.update(_asserted_states(condition, _AWAIT_STATE))
311
336
  case _:
312
337
  pass
313
338
  return covered
314
339
 
315
340
 
316
- def _has_nonempty_calls_argument(call: ast.Call) -> bool:
341
+ def _calls_evidence(call: ast.Call) -> _Evidence:
317
342
  if call.args:
318
- return _nonempty_sequence(call.args[0])
319
- return any(item.arg == "calls" and _nonempty_sequence(item.value) for item in call.keywords)
320
-
321
-
322
- def _positive_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
323
- if isinstance(condition, ast.UnaryOp) and isinstance(condition.op, ast.Not):
324
- operand = condition.operand
325
- if isinstance(operand, ast.Compare) and len(operand.ops) == 1:
326
- inverse = _negated_operator(operand.ops[0])
327
- if inverse is not None:
328
- return _comparison_states(operand.left, inverse, operand.comparators[0], states) | _comparison_states(
329
- operand.comparators[0], _reversed_operator(inverse), operand.left, states
330
- )
331
- return set()
332
- if isinstance(condition, ast.BoolOp):
333
- branches = [_positive_states(value, states) for value in condition.values]
334
- return (
335
- branches[0].union(*branches[1:])
336
- if isinstance(condition.op, ast.And)
337
- else branches[0].intersection(*branches[1:])
338
- )
339
- if isinstance(condition, ast.Attribute) and condition.attr in states:
340
- return {_reference(condition.value)}
341
- if isinstance(condition, ast.Compare):
342
- result: set[_Reference] = set()
343
- for left, operator, right in zip(
344
- (condition.left, *condition.comparators[:-1]), condition.ops, condition.comparators, strict=True
345
- ):
346
- result.update(_comparison_states(left, operator, right, states))
347
- result.update(_comparison_states(right, _reversed_operator(operator), left, states))
348
- return result
349
- return set()
343
+ return _sequence_evidence(call.args[0])
344
+ for keyword in call.keywords:
345
+ if keyword.arg == "calls":
346
+ return _sequence_evidence(keyword.value)
347
+ return _Evidence.UNRESOLVED
348
+
349
+
350
+ def _sequence_evidence(node: ast.expr) -> _Evidence:
351
+ if isinstance(node, (ast.List, ast.Tuple)):
352
+ if not node.elts:
353
+ return _Evidence.INSUFFICIENT
354
+ if any(not isinstance(item, ast.Starred) for item in node.elts):
355
+ return _Evidence.POSITIVE
356
+ return _Evidence.UNRESOLVED
357
+
358
+
359
+ def _asserted_states(condition: ast.expr, states: frozenset[str]) -> set[_Reference]:
360
+ match condition:
361
+ case ast.UnaryOp(op=ast.Not(), operand=ast.UnaryOp(op=ast.Not(), operand=operand)):
362
+ return _asserted_states(operand, states)
363
+ case ast.UnaryOp(op=ast.Not(), operand=ast.Compare(left=left, ops=[operator], comparators=[right])):
364
+ inverse = _negated_operator(operator)
365
+ return _comparison_states(left, inverse, right, states) if inverse is not None else set()
366
+ case ast.UnaryOp(op=ast.Not(), operand=ast.Attribute(attr=attribute)) if attribute in states:
367
+ return set()
368
+ case ast.BoolOp(op=operator, values=values):
369
+ branches = [_asserted_states(value, states) for value in values]
370
+ return (
371
+ branches[0].union(*branches[1:])
372
+ if isinstance(operator, ast.And)
373
+ else branches[0].intersection(*branches[1:])
374
+ )
375
+ case ast.Attribute(value=receiver, attr=attribute) if attribute in states:
376
+ return {_reference(receiver)}
377
+ case ast.Compare(left=left, ops=operators, comparators=rights):
378
+ result: set[_Reference] = set()
379
+ for first, operator, second in zip((left, *rights[:-1]), operators, rights, strict=True):
380
+ result.update(_comparison_states(first, operator, second, states))
381
+ return result
382
+ case _:
383
+ # Preserve acceptance of unresolved await-state assertions, such as
384
+ # len(mock.await_args_list) == expected, without value propagation.
385
+ return _unresolved_await_states(condition, states)
350
386
 
351
387
 
352
388
  def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
@@ -364,6 +400,18 @@ def _negated_operator(operator: ast.cmpop) -> ast.cmpop | None:
364
400
  return factory() if factory is not None else None
365
401
 
366
402
 
403
+ def _comparison_states(left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str]) -> set[_Reference]:
404
+ result: set[_Reference] = set()
405
+ for node, comparison, expected in ((left, operator, right), (right, _reversed_operator(operator), left)):
406
+ if isinstance(node, ast.Attribute) and node.attr in states:
407
+ evidence = _state_evidence(node.attr, comparison, expected)
408
+ if evidence is _Evidence.POSITIVE or (states == _AWAIT_STATE and evidence is _Evidence.UNRESOLVED):
409
+ result.add(_reference(node.value))
410
+ else:
411
+ result.update(_unresolved_await_states(node, states))
412
+ return result
413
+
414
+
367
415
  def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
368
416
  match operator:
369
417
  case ast.Lt():
@@ -378,32 +426,38 @@ def _reversed_operator(operator: ast.cmpop) -> ast.cmpop:
378
426
  return operator
379
427
 
380
428
 
381
- def _comparison_states(left: ast.expr, operator: ast.cmpop, right: ast.expr, states: frozenset[str]) -> set[_Reference]:
382
- if not isinstance(left, ast.Attribute):
429
+ def _unresolved_await_states(node: ast.expr, states: frozenset[str]) -> set[_Reference]:
430
+ if states != _AWAIT_STATE:
383
431
  return set()
432
+ return {
433
+ _reference(child.value) for child in walk_ast(node) if isinstance(child, ast.Attribute) and child.attr in states
434
+ }
435
+
436
+
437
+ def _state_evidence(attribute: str, operator: ast.cmpop, expected: ast.expr) -> _Evidence:
438
+ positive: bool
384
439
  if (
385
- states == _AWAIT_STATE
386
- and isinstance(left.value, ast.Attribute)
387
- and left.value.attr == "await_args"
388
- and left.attr in {"args", "kwargs"}
440
+ attribute in {"call_count", "await_count"}
441
+ and isinstance(expected, ast.Constant)
442
+ and isinstance(expected.value, int)
389
443
  ):
390
- return {_reference(left.value.value)}
391
- if left.attr not in states:
392
- return set()
393
- positive = False
394
- if left.attr in {"call_count", "await_count"} and isinstance(right, ast.Constant) and type(right.value) is int:
395
- positive = _proves_positive_count(operator, right.value)
396
- elif left.attr == "called" and isinstance(right, ast.Constant):
397
- positive = (isinstance(operator, (ast.Eq, ast.Is)) and right.value is True) or (
398
- isinstance(operator, (ast.NotEq, ast.IsNot)) and right.value is False
444
+ positive = _proves_positive_count(operator, expected.value)
445
+ elif attribute == "called" and isinstance(expected, ast.Constant) and isinstance(expected.value, bool):
446
+ positive = (isinstance(operator, (ast.Eq, ast.Is)) and expected.value) or (
447
+ isinstance(operator, (ast.NotEq, ast.IsNot)) and not expected.value
399
448
  )
400
- elif left.attr == "await_args" and isinstance(right, ast.Constant) and right.value is None:
449
+ elif attribute == "await_args" and isinstance(expected, ast.Constant) and expected.value is None:
401
450
  positive = isinstance(operator, (ast.NotEq, ast.IsNot))
402
- elif left.attr == "await_args_list":
403
- positive = (isinstance(operator, ast.Eq) and _nonempty_sequence(right)) or (
404
- isinstance(operator, ast.NotEq) and isinstance(right, ast.List) and not right.elts
451
+ elif attribute == "await_args_list" and isinstance(operator, (ast.Eq, ast.NotEq)):
452
+ sequence = _sequence_evidence(expected)
453
+ if sequence is _Evidence.UNRESOLVED:
454
+ return sequence
455
+ positive = (isinstance(operator, ast.Eq) and sequence is _Evidence.POSITIVE) or (
456
+ isinstance(operator, ast.NotEq) and isinstance(expected, ast.List) and sequence is _Evidence.INSUFFICIENT
405
457
  )
406
- return {_reference(left.value)} if positive else set()
458
+ else:
459
+ return _Evidence.UNRESOLVED
460
+ return _Evidence.POSITIVE if positive else _Evidence.INSUFFICIENT
407
461
 
408
462
 
409
463
  def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
@@ -418,10 +472,6 @@ def _proves_positive_count(operator: ast.cmpop, value: int) -> bool:
418
472
  return False
419
473
 
420
474
 
421
- def _nonempty_sequence(node: ast.expr) -> bool:
422
- return isinstance(node, (ast.List, ast.Tuple)) and any(not isinstance(item, ast.Starred) for item in node.elts)
423
-
424
-
425
475
  def _invalidates_reference(node: ast.AST, reference: _Reference, binding: ast.Name) -> bool:
426
476
  match node:
427
477
  case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
@@ -53,50 +53,23 @@ class PreferMonkeypatchForProcessStateInTest(Rule):
53
53
  "mutation can survive an assertion or setup failure, making later tests depend on execution order."
54
54
  ),
55
55
  remediation=(
56
- "Use pytest's monkeypatch.chdir, monkeypatch.syspath_prepend, monkeypatch.setitem, or "
57
- "monkeypatch.delitem so teardown restores the previous state even when the test fails. A deliberate "
58
- "manual restoration may remain when it is enclosed by a matching try/finally."
56
+ "Establish restoration in setup with pytest's monkeypatch.chdir, monkeypatch.syspath_prepend, "
57
+ "monkeypatch.setitem, or monkeypatch.delitem so the original state is recorded before the mutation. "
58
+ "Alternatively, explicitly restore the original state with a matching try/finally. Adding monkeypatch "
59
+ "operations only during teardown can undo intended cleanup: undoing a teardown deletion can reinstall "
60
+ "a temporary sys.modules entry. Intentional cleanup with established ownership may use a local, "
61
+ "reasoned SARJ446 suppression."
59
62
  ),
60
63
  category=RuleCategory.TESTING,
61
64
  autofix=AutofixPolicy.NONE,
62
65
  limitations=(
63
- "Teardown after a single direct yield in a module-level, import-proven pytest or pytest-asyncio fixture is excluded: registering undo operations during cleanup can restore the temporary state. Nested fixtures, conditional/delegated yields and out-of-line cleanup ownership are not inferred.",
66
+ "Teardown ownership and original-state restoration across yield are not inferred. Valid cleanup or restoration may still warn and require a reasoned local suppression.",
64
67
  "Only maintained test paths are analyzed; generated files, production code, and module/class bootstrap mutations are excluded.",
65
68
  "The rule covers os.chdir, sys.path.insert(0, ...), and direct sys.modules set, delete, or unused pop operations with an equivalent pytest restoring helper.",
66
69
  "Environment variables remain owned by Ruff TID251/B003; argv, locale, timezone, warning filters, and process APIs without an equivalent helper are outside this rule.",
67
70
  "Imports and import aliases are resolved conservatively. Indirect container aliases and dynamically selected modules are not inferred.",
68
71
  ),
69
72
  examples=(
70
- RuleExample(
71
- example_id="fixture-cleanup-unscoped",
72
- title="Restore setup mutations even when setup fails",
73
- outcome=ExampleOutcome.MATCH,
74
- files=(
75
- ExampleFile.python(
76
- "tests/test_plugins.py",
77
- "import sys\nimport pytest\n@pytest.fixture\ndef fresh_plugin():\n sys.modules.pop('temporary_plugin', None)\n yield\n",
78
- ),
79
- ),
80
- focus_path=PurePosixPath("tests/test_plugins.py"),
81
- expected_count=1,
82
- public=True,
83
- scenario="fixture-cleanup",
84
- ),
85
- RuleExample(
86
- example_id="yield-fixture-cleanup",
87
- title="Do not undo fixture teardown",
88
- outcome=ExampleOutcome.NO_MATCH,
89
- files=(
90
- ExampleFile.python(
91
- "tests/test_plugins.py",
92
- "import sys\nimport pytest\n@pytest.fixture\ndef fresh_plugin():\n yield\n sys.modules.pop('temporary_plugin', None)\n",
93
- ),
94
- ),
95
- focus_path=PurePosixPath("tests/test_plugins.py"),
96
- expected_count=0,
97
- public=True,
98
- scenario="fixture-cleanup",
99
- ),
100
73
  RuleExample(
101
74
  example_id="direct-module-registry-mutation",
102
75
  title="Test installs a module without guaranteed restoration",
@@ -143,7 +116,7 @@ class PreferMonkeypatchForProcessStateInTest(Rule):
143
116
  mutations = [
144
117
  mutation
145
118
  for function in _functions(tree, node_index=context.node_index)
146
- for mutation in _function_mutations(function, module_imports, module_level=function in tree.body)
119
+ for mutation in _function_mutations(function, module_imports)
147
120
  ]
148
121
  diagnostics = [
149
122
  Diagnostic(
@@ -153,7 +126,8 @@ class PreferMonkeypatchForProcessStateInTest(Rule):
153
126
  code=self.code,
154
127
  message=(
155
128
  f"Direct {mutation.subject} mutation can leak into later tests when this scope exits early. "
156
- f"Use {mutation.remedy} so pytest restores the previous process state automatically."
129
+ f"Establish restoration before the setup mutation using {mutation.remedy}, or explicitly restore "
130
+ "the original state. Teardown-only monkeypatch use can undo intended cleanup."
157
131
  ),
158
132
  severity=Severity.WARNING,
159
133
  )
@@ -174,8 +148,6 @@ def _functions(
174
148
  def _function_mutations(
175
149
  function: ast.FunctionDef | ast.AsyncFunctionDef,
176
150
  module_imports: ImportIndex,
177
- *,
178
- module_level: bool,
179
151
  ) -> list[_Mutation]:
180
152
  local_tree = ast.Module(body=function.body, type_ignores=[])
181
153
  local_imports = ImportIndex.from_tree(local_tree)
@@ -188,33 +160,9 @@ def _function_mutations(
188
160
  continue
189
161
  mutations.append(mutation)
190
162
  restored = _restored_mutations(nodes, mutations, module_imports, local_imports)
191
- if module_level:
192
- restored.update(_fixture_teardown_nodes(function, nodes, module_imports))
193
163
  return [mutation for mutation in mutations if mutation.node not in restored]
194
164
 
195
165
 
196
- def _fixture_teardown_nodes(
197
- function: ast.FunctionDef | ast.AsyncFunctionDef,
198
- nodes: tuple[ast.AST, ...],
199
- module_imports: ImportIndex,
200
- ) -> set[ast.AST]:
201
- if not any(
202
- module_imports.resolves(
203
- decorator.func if isinstance(decorator, ast.Call) else decorator,
204
- sources=frozenset({"pytest", "pytest_asyncio"}),
205
- symbol="fixture",
206
- )
207
- for decorator in function.decorator_list
208
- ):
209
- return set()
210
- if sum(isinstance(node, ast.Yield | ast.YieldFrom) for node in nodes) != 1:
211
- return set()
212
- for index, statement in enumerate(function.body):
213
- if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Yield):
214
- return set(_descendants(function.body[index + 1 :]))
215
- return set()
216
-
217
-
218
166
  def _lexical_nodes(function: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[ast.AST]:
219
167
  stack: list[ast.AST] = [node for node in reversed(function.body) if not isinstance(node, _SCOPE_NODES)]
220
168
  while stack: