sarj-python-lint 0.82.7__tar.gz → 0.83.1__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.
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/PKG-INFO +1 -1
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/pyproject.toml +1 -1
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +59 -5
- sarj_python_lint-0.83.1/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +941 -0
- sarj_python_lint-0.82.7/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -422
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/.gitignore +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/LICENSE +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/README.md +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.82.7 → sarj_python_lint-0.83.1}/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.
|
|
3
|
+
Version: 0.83.1
|
|
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/
|
|
@@ -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
|
|
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
|
|
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"`
|
|
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
|
|
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)):
|