sarj-python-lint 0.82.7__tar.gz → 0.83.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 (132) hide show
  1. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +59 -5
  4. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/.gitignore +0 -0
  5. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/LICENSE +0 -0
  6. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/README.md +0 -0
  7. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/__init__.py +0 -0
  8. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/__main__.py +0 -0
  9. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  10. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_filesystem.py +0 -0
  11. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_python_target.py +0 -0
  12. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  13. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_secret_names.py +0 -0
  14. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_version.py +0 -0
  15. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/py.typed +0 -0
  16. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/ratchet.py +0 -0
  17. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rule_base.py +0 -0
  18. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  19. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  20. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  21. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  22. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  23. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  24. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  25. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  26. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  27. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  28. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  29. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  30. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  31. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  32. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  33. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  34. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  35. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  36. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  37. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  38. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  39. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  40. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  41. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  42. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  43. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  44. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  45. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  46. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  47. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  48. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  49. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  50. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  51. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
  52. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  53. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  54. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  55. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  56. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  57. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  58. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  59. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  60. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  61. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  62. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  63. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  64. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  65. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  66. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  67. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  68. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  69. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  70. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  71. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  72. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  73. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  74. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  75. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  76. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  77. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  78. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  79. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  80. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  81. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  82. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  83. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  84. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  85. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  86. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  87. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  88. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  89. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  90. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  91. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  92. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  93. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  94. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  95. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  96. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  97. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  98. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  99. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  100. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  101. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  102. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  103. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  104. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  105. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  106. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  107. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  108. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  109. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  110. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  111. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  112. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  113. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  114. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  115. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  116. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  117. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  118. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  119. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  120. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  121. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  122. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  123. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  124. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  125. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  126. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  127. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  128. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  129. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  130. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  131. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  132. {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.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: 0.82.7
3
+ Version: 0.83.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 = "0.82.7"
3
+ version = "0.83.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" }]
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
28
28
  _ATTRIBUTE_MUTATIONS = frozenset({"delattr", "setattr"})
29
29
  _MONKEYPATCH = "monkeypatch"
30
30
  _PYTEST = frozenset({"pytest"})
31
+ _UNITTEST_MOCK = frozenset({"unittest.mock"})
31
32
  _SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
32
33
 
33
34
 
@@ -35,7 +36,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
35
36
  id: str = "prefer-injected-dependency-over-monkeypatch"
36
37
  code: str = "SARJ445"
37
38
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
- summary="Tests should inject dependencies instead of replacing attributes through pytest monkeypatch.",
39
+ summary="Tests should inject dependencies instead of replacing attributes through ambient patching.",
39
40
  rationale=(
40
41
  "Attribute patching hides collaborators and configuration behind ambient module or object state, coupling "
41
42
  "tests to lookup locations instead of an explicit boundary. Pytest monkeypatch remains appropriate for "
@@ -51,7 +52,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
51
52
  autofix=AutofixPolicy.NONE,
52
53
  limitations=(
53
54
  "Only maintained test paths are analyzed; generated files and production helpers are excluded.",
54
- "The rule recognizes pytest's conventional monkeypatch fixture name and parameters annotated as pytest.MonkeyPatch.",
55
+ "The rule recognizes pytest monkeypatch and statically resolved unittest.mock or pytest-mock patch APIs.",
55
56
  "Environment, mapping, import-path, and working-directory mutations are intentionally allowed.",
56
57
  "A nested function that captures a monkeypatch handle from an outer scope is not inferred.",
57
58
  ),
@@ -100,6 +101,12 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
100
101
  if tree is None:
101
102
  return []
102
103
  imports = ImportIndex.from_tree(tree, module_scope_only=True)
104
+ replacements = [
105
+ (call, f"monkeypatch.{_operation(call)}")
106
+ for function in _functions(tree)
107
+ for call in _attribute_mutations(function, imports)
108
+ ]
109
+ replacements.extend((call, label) for call, label in _patch_calls(tree, imports))
103
110
  diagnostics = [
104
111
  Diagnostic(
105
112
  path=path,
@@ -107,15 +114,14 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
107
114
  col=call.col_offset + 1,
108
115
  code=self.code,
109
116
  message=(
110
- f"`monkeypatch.{_operation(call)}` replaces an attribute through ambient state. Inject the "
117
+ f"`{label}` replaces an attribute through ambient state. Inject the "
111
118
  "dependency or configuration, use a framework override, or provide a purpose-built fake. "
112
119
  "If global lookup or interception is the behavior under test, suppress SARJ445 locally and explain "
113
120
  "why injection would invalidate the test."
114
121
  ),
115
122
  severity=Severity.WARNING,
116
123
  )
117
- for function in _functions(tree)
118
- for call in _attribute_mutations(function, imports)
124
+ for call, label in replacements
119
125
  ]
120
126
  diagnostics.sort(key=lambda diagnostic: (diagnostic.line, diagnostic.col))
121
127
  return diagnostics
@@ -129,6 +135,54 @@ def _operation(call: ast.Call) -> str:
129
135
  return func.attr
130
136
 
131
137
 
138
+ def _patch_calls(tree: ast.Module, imports: ImportIndex) -> list[tuple[ast.Call, str]]:
139
+ calls: list[tuple[ast.Call, str]] = []
140
+ for node in ast.walk(tree):
141
+ if not isinstance(node, ast.Call):
142
+ continue
143
+ if _resolves_patch(node.func, imports):
144
+ calls.append((node, "patch"))
145
+ continue
146
+ if (
147
+ isinstance(node.func, ast.Attribute)
148
+ and node.func.attr == "object"
149
+ and _resolves_patch(node.func.value, imports)
150
+ ):
151
+ calls.append((node, "patch.object"))
152
+ continue
153
+ label = _pytest_mock_patch_label(node)
154
+ if label is not None:
155
+ calls.append((node, label))
156
+ return calls
157
+
158
+
159
+ def _resolves_patch(node: ast.expr, imports: ImportIndex) -> bool:
160
+ if imports.resolves(node, sources=_UNITTEST_MOCK, symbol="patch"):
161
+ return True
162
+ return (
163
+ isinstance(node, ast.Attribute)
164
+ and node.attr == "patch"
165
+ and imports.resolved_symbol(node.value, sources=frozenset({"unittest"})) == "mock"
166
+ )
167
+
168
+
169
+ def _pytest_mock_patch_label(call: ast.Call) -> str | None:
170
+ func = call.func
171
+ if not isinstance(func, ast.Attribute):
172
+ return None
173
+ if isinstance(func.value, ast.Name) and func.value.id == "mocker" and func.attr == "patch":
174
+ return "mocker.patch"
175
+ if (
176
+ func.attr == "object"
177
+ and isinstance(func.value, ast.Attribute)
178
+ and func.value.attr == "patch"
179
+ and isinstance(func.value.value, ast.Name)
180
+ and func.value.value.id == "mocker"
181
+ ):
182
+ return "mocker.patch.object"
183
+ return None
184
+
185
+
132
186
  def _functions(tree: ast.Module) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
133
187
  for node in ast.walk(tree):
134
188
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):