sarj-python-lint 0.82.6__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.
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +253 -51
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +59 -5
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/.gitignore +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/LICENSE +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/README.md +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.82.6 → 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.
|
|
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/
|
|
@@ -61,6 +61,15 @@ class _InitializedCollection:
|
|
|
61
61
|
class _Candidate:
|
|
62
62
|
kind: _CollectionKind
|
|
63
63
|
name: str
|
|
64
|
+
action: str = "populates"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class _LoopReplacement:
|
|
69
|
+
kind: _CollectionKind
|
|
70
|
+
expression: str
|
|
71
|
+
action: str = "populates"
|
|
72
|
+
allow_multiline: bool = False
|
|
64
73
|
|
|
65
74
|
|
|
66
75
|
@final
|
|
@@ -74,8 +83,9 @@ class PreferCollectionComprehension(Rule):
|
|
|
74
83
|
"spreads a declarative map/filter across mutable scaffolding."
|
|
75
84
|
),
|
|
76
85
|
remediation=(
|
|
77
|
-
"Build the fresh collection with one dict, list, or set comprehension.
|
|
78
|
-
"
|
|
86
|
+
"Build the fresh collection with one dict, list, or set comprehension. When a list candidate must be "
|
|
87
|
+
"computed once and reused in the element, bind it in the filter with :=. Keep the loop when mutation "
|
|
88
|
+
"is incremental, evaluation order is observable, or the imperative form carries additional behavior."
|
|
79
89
|
),
|
|
80
90
|
category=RuleCategory.STYLE,
|
|
81
91
|
autofix=AutofixPolicy.NONE,
|
|
@@ -83,17 +93,23 @@ class PreferCollectionComprehension(Rule):
|
|
|
83
93
|
"Only function-local, adjacent empty initializers and one synchronous single-purpose loop are checked.",
|
|
84
94
|
(
|
|
85
95
|
"The rule fills gaps left by Ruff PERF401, PERF403, and FURB142: derived dict projections, "
|
|
86
|
-
"destructured list projections, and filtered set
|
|
96
|
+
"destructured list projections, narrowly filtered computed list candidates, and filtered set "
|
|
97
|
+
"builders."
|
|
87
98
|
),
|
|
88
99
|
(
|
|
89
|
-
"Comments, loop-target leakage, try blocks, aliases, complex projections, and replacements
|
|
90
|
-
"
|
|
91
|
-
"differ."
|
|
100
|
+
"Comments, loop-target leakage, try blocks, aliases, complex projections, and replacements that "
|
|
101
|
+
"do not fit a 120-column line or a simple formatter-style multiline comprehension are excluded. "
|
|
102
|
+
"No autofix is offered because dict key/value evaluation order can differ."
|
|
92
103
|
),
|
|
93
104
|
(
|
|
94
105
|
"Attribute projections can invoke properties or descriptors, so reviewers should keep the loop "
|
|
95
106
|
"when the relative order of those reads is observable."
|
|
96
107
|
),
|
|
108
|
+
(
|
|
109
|
+
"Computed list candidates are limited to one local assignment, a truthiness or None filter, and "
|
|
110
|
+
"one bounded projection. Validation, logging, exceptions, multiple guards, and secondary mutation "
|
|
111
|
+
"remain imperative."
|
|
112
|
+
),
|
|
97
113
|
),
|
|
98
114
|
examples=(
|
|
99
115
|
RuleExample(
|
|
@@ -114,6 +130,47 @@ class PreferCollectionComprehension(Rule):
|
|
|
114
130
|
expected_count=1,
|
|
115
131
|
public=True,
|
|
116
132
|
),
|
|
133
|
+
RuleExample(
|
|
134
|
+
example_id="computed-filtered-list-loop",
|
|
135
|
+
title="Compute and filter a list candidate directly",
|
|
136
|
+
outcome=ExampleOutcome.MATCH,
|
|
137
|
+
files=(
|
|
138
|
+
ExampleFile.python(
|
|
139
|
+
"app/intervals.py",
|
|
140
|
+
"def clipped(items, bounds):\n"
|
|
141
|
+
" result: list[Interval] = []\n"
|
|
142
|
+
" for item in items:\n"
|
|
143
|
+
" value = item.clipped(bounds)\n"
|
|
144
|
+
" if value is not None:\n"
|
|
145
|
+
" result.append(value)\n"
|
|
146
|
+
" return result\n",
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
focus_path=PurePosixPath("app/intervals.py"),
|
|
150
|
+
expected_count=1,
|
|
151
|
+
public=True,
|
|
152
|
+
scenario="computed-filter",
|
|
153
|
+
),
|
|
154
|
+
RuleExample(
|
|
155
|
+
example_id="computed-filtered-list-comprehension",
|
|
156
|
+
title="Keep a computed filtered list declarative",
|
|
157
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
158
|
+
files=(
|
|
159
|
+
ExampleFile.python(
|
|
160
|
+
"app/intervals.py",
|
|
161
|
+
"def clipped(items, bounds):\n"
|
|
162
|
+
" return [\n"
|
|
163
|
+
" value\n"
|
|
164
|
+
" for item in items\n"
|
|
165
|
+
" if (value := item.clipped(bounds)) is not None\n"
|
|
166
|
+
" ]\n",
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
focus_path=PurePosixPath("app/intervals.py"),
|
|
170
|
+
expected_count=0,
|
|
171
|
+
public=True,
|
|
172
|
+
scenario="computed-filter",
|
|
173
|
+
),
|
|
117
174
|
RuleExample(
|
|
118
175
|
example_id="direct-dict-comprehension",
|
|
119
176
|
title="Keep the collection projection declarative",
|
|
@@ -163,10 +220,7 @@ class PreferCollectionComprehension(Rule):
|
|
|
163
220
|
line=loop.lineno,
|
|
164
221
|
col=loop.col_offset + 1,
|
|
165
222
|
code=self.code,
|
|
166
|
-
message=(
|
|
167
|
-
f"This loop only populates fresh {finding.kind} {finding.name!r} — prefer a "
|
|
168
|
-
f"{finding.kind} comprehension."
|
|
169
|
-
),
|
|
223
|
+
message=_message(finding),
|
|
170
224
|
)
|
|
171
225
|
)
|
|
172
226
|
return sorted(diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.col))
|
|
@@ -223,10 +277,12 @@ def _candidate(
|
|
|
223
277
|
):
|
|
224
278
|
return None
|
|
225
279
|
|
|
226
|
-
|
|
227
|
-
if
|
|
280
|
+
replacement = _loop_replacement(loop, name, initialized.kind, bound_names, owner)
|
|
281
|
+
if replacement is None or not _replacement_fits(
|
|
282
|
+
init, name, replacement.expression, allow_multiline=replacement.allow_multiline
|
|
283
|
+
):
|
|
228
284
|
return None
|
|
229
|
-
return _Candidate(kind=kind, name=name)
|
|
285
|
+
return _Candidate(kind=replacement.kind, name=name, action=replacement.action)
|
|
230
286
|
|
|
231
287
|
|
|
232
288
|
def _initialized_collection(statement: _Init) -> _InitializedCollection | None:
|
|
@@ -251,27 +307,179 @@ def _initialized_collection(statement: _Init) -> _InitializedCollection | None:
|
|
|
251
307
|
return None
|
|
252
308
|
|
|
253
309
|
|
|
254
|
-
def
|
|
255
|
-
loop: ast.For,
|
|
256
|
-
|
|
310
|
+
def _loop_replacement(
|
|
311
|
+
loop: ast.For,
|
|
312
|
+
name: str,
|
|
313
|
+
initialized_kind: _CollectionKind,
|
|
314
|
+
bound_names: frozenset[str],
|
|
315
|
+
owner: _Callable,
|
|
316
|
+
) -> _LoopReplacement | None:
|
|
257
317
|
if initialized_kind is _CollectionKind.DICT and len(loop.body) == 1:
|
|
258
318
|
match loop.body[0]:
|
|
259
319
|
case ast.Assign(targets=[ast.Subscript(value=ast.Name(id=target), slice=key)], value=value) if (
|
|
260
320
|
target == name
|
|
261
321
|
):
|
|
262
322
|
if _valid_dict_projection(key, value, name, bound_names):
|
|
263
|
-
return
|
|
323
|
+
return _LoopReplacement(
|
|
324
|
+
_CollectionKind.DICT,
|
|
325
|
+
f"{{{ast.unparse(key)}: {ast.unparse(value)} for {ast.unparse(loop.target)} "
|
|
326
|
+
f"in {ast.unparse(loop.iter)}}}",
|
|
327
|
+
)
|
|
264
328
|
case _:
|
|
265
329
|
pass
|
|
266
|
-
if initialized_kind is _CollectionKind.LIST
|
|
330
|
+
if initialized_kind is _CollectionKind.LIST:
|
|
331
|
+
replacement = _list_replacement(loop, name, bound_names, owner)
|
|
332
|
+
if replacement is not None:
|
|
333
|
+
return replacement
|
|
334
|
+
if initialized_kind is _CollectionKind.SET and len(loop.body) == 1:
|
|
335
|
+
return _set_replacement(loop, name, bound_names)
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _list_replacement(
|
|
340
|
+
loop: ast.For, name: str, bound_names: frozenset[str], owner: _Callable
|
|
341
|
+
) -> _LoopReplacement | None:
|
|
342
|
+
if len(bound_names) > 1 and len(loop.body) == 1:
|
|
267
343
|
expression = _single_method_argument(loop.body[0], name, "append")
|
|
268
344
|
if expression is not None and not _loads_name(expression, name) and _simple_projection(expression, bound_names):
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
345
|
+
return _LoopReplacement(
|
|
346
|
+
_CollectionKind.LIST,
|
|
347
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)}]",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
filtered = _filtered_list_parts(loop.body, name)
|
|
351
|
+
if filtered is not None and len(bound_names) > 1:
|
|
352
|
+
expression, test, inverted = filtered
|
|
353
|
+
if (
|
|
354
|
+
not _loads_name(expression, name)
|
|
355
|
+
and not _loads_name(test, name)
|
|
356
|
+
and _simple_projection(expression, bound_names)
|
|
357
|
+
and not _contains_prohibited_expression(test)
|
|
358
|
+
):
|
|
359
|
+
condition = _condition_source(test, inverted=inverted)
|
|
360
|
+
return _LoopReplacement(
|
|
361
|
+
_CollectionKind.LIST,
|
|
362
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)} "
|
|
363
|
+
f"if {condition}]",
|
|
364
|
+
"filters and appends to",
|
|
365
|
+
allow_multiline=True,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return _computed_list_replacement(loop, name, bound_names, owner)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _filtered_list_parts(body: list[ast.stmt], name: str) -> tuple[ast.expr, ast.expr, bool] | None:
|
|
372
|
+
match body:
|
|
373
|
+
case [ast.If(test=test, body=[statement], orelse=[])]:
|
|
374
|
+
expression = _single_method_argument(statement, name, "append")
|
|
375
|
+
if expression is not None:
|
|
376
|
+
return expression, test, False
|
|
377
|
+
case [ast.If(test=test, body=[ast.Continue()], orelse=[]), statement]:
|
|
378
|
+
expression = _single_method_argument(statement, name, "append")
|
|
379
|
+
if expression is not None:
|
|
380
|
+
return expression, test, True
|
|
381
|
+
case _:
|
|
382
|
+
pass
|
|
272
383
|
return None
|
|
273
384
|
|
|
274
385
|
|
|
386
|
+
def _computed_list_replacement(
|
|
387
|
+
loop: ast.For, name: str, bound_names: frozenset[str], owner: _Callable
|
|
388
|
+
) -> _LoopReplacement | None:
|
|
389
|
+
match loop.body:
|
|
390
|
+
case [ast.Assign(targets=[ast.Name(id=candidate)], value=value), *tail]:
|
|
391
|
+
pass
|
|
392
|
+
case _:
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
if candidate == name or candidate in bound_names:
|
|
396
|
+
return None
|
|
397
|
+
if _target_binding_is_observable(owner, frozenset({candidate}), loop):
|
|
398
|
+
return None
|
|
399
|
+
if _loads_name(value, name) or _contains_prohibited_expression(value):
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
filtered = _filtered_list_parts(tail, name)
|
|
403
|
+
if filtered is None:
|
|
404
|
+
return None
|
|
405
|
+
expression, test, inverted = filtered
|
|
406
|
+
condition_kind = _computed_condition_kind(test, candidate, inverted=inverted)
|
|
407
|
+
if condition_kind is None:
|
|
408
|
+
return None
|
|
409
|
+
if _loads_name(expression, name) or not _loads_name(expression, candidate):
|
|
410
|
+
return None
|
|
411
|
+
projection_names = bound_names | frozenset({candidate})
|
|
412
|
+
if not _bounded_list_projection(expression, projection_names):
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
assignment = f"({candidate} := {ast.unparse(value)})"
|
|
416
|
+
condition = assignment if condition_kind == "truthy" else f"{assignment} is not None"
|
|
417
|
+
return _LoopReplacement(
|
|
418
|
+
_CollectionKind.LIST,
|
|
419
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)} if {condition}]",
|
|
420
|
+
"computes, filters, and appends to",
|
|
421
|
+
allow_multiline=True,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _computed_condition_kind(test: ast.expr, candidate: str, *, inverted: bool) -> str | None:
|
|
426
|
+
if not inverted and isinstance(test, ast.Name) and test.id == candidate:
|
|
427
|
+
return "truthy"
|
|
428
|
+
if (
|
|
429
|
+
inverted
|
|
430
|
+
and isinstance(test, ast.UnaryOp)
|
|
431
|
+
and isinstance(test.op, ast.Not)
|
|
432
|
+
and isinstance(test.operand, ast.Name)
|
|
433
|
+
and test.operand.id == candidate
|
|
434
|
+
):
|
|
435
|
+
return "truthy"
|
|
436
|
+
match test:
|
|
437
|
+
case ast.Compare(left=ast.Name(id=left), ops=[operator], comparators=[ast.Constant(value=None)]) if (
|
|
438
|
+
left == candidate
|
|
439
|
+
and ((not inverted and isinstance(operator, ast.IsNot)) or (inverted and isinstance(operator, ast.Is)))
|
|
440
|
+
):
|
|
441
|
+
return "not-none"
|
|
442
|
+
case _:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _bounded_list_projection(node: ast.expr, names: frozenset[str]) -> bool:
|
|
447
|
+
if _simple_projection(node, names):
|
|
448
|
+
return True
|
|
449
|
+
match node:
|
|
450
|
+
case ast.Call(func=func, args=args, keywords=keywords):
|
|
451
|
+
if any(isinstance(argument, ast.Starred) for argument in args):
|
|
452
|
+
return False
|
|
453
|
+
if any(keyword.arg is None for keyword in keywords):
|
|
454
|
+
return False
|
|
455
|
+
if not _dotted_name(func):
|
|
456
|
+
return False
|
|
457
|
+
values = (*args, *(keyword.value for keyword in keywords))
|
|
458
|
+
return all(
|
|
459
|
+
not any(isinstance(item, ast.Call) for item in ast.walk(value))
|
|
460
|
+
and not _contains_prohibited_expression(value)
|
|
461
|
+
for value in values
|
|
462
|
+
)
|
|
463
|
+
case _:
|
|
464
|
+
return False
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _dotted_name(node: ast.expr) -> bool:
|
|
468
|
+
match node:
|
|
469
|
+
case ast.Name():
|
|
470
|
+
return True
|
|
471
|
+
case ast.Attribute(value=value):
|
|
472
|
+
return _dotted_name(value)
|
|
473
|
+
case _:
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _condition_source(test: ast.expr, *, inverted: bool) -> str:
|
|
478
|
+
if not inverted:
|
|
479
|
+
return ast.unparse(test)
|
|
480
|
+
return ast.unparse(ast.UnaryOp(op=ast.Not(), operand=test))
|
|
481
|
+
|
|
482
|
+
|
|
275
483
|
def _single_method_argument(statement: ast.stmt, name: str, method: str) -> ast.expr | None:
|
|
276
484
|
match statement:
|
|
277
485
|
case ast.Expr(
|
|
@@ -383,37 +591,19 @@ def _set_is_shadowed(tree: ast.Module) -> bool:
|
|
|
383
591
|
return False
|
|
384
592
|
|
|
385
593
|
|
|
386
|
-
def _replacement_fits(init: _Init,
|
|
387
|
-
expression = _replacement_expression(loop, name, kind)
|
|
388
|
-
if expression is None:
|
|
389
|
-
return False
|
|
594
|
+
def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multiline: bool = False) -> bool:
|
|
390
595
|
prefix = f"{name} = "
|
|
391
596
|
if isinstance(init, ast.AnnAssign):
|
|
392
597
|
prefix = f"{name}: {ast.unparse(init.annotation)} = "
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
case _CollectionKind.LIST, [statement]:
|
|
404
|
-
argument = _single_method_argument(statement, name, "append")
|
|
405
|
-
if argument is not None:
|
|
406
|
-
return f"[{ast.unparse(argument)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)}]"
|
|
407
|
-
case _CollectionKind.SET, [ast.If(test=test, body=[statement], orelse=[])]:
|
|
408
|
-
argument = _single_method_argument(statement, name, "add")
|
|
409
|
-
if argument is not None:
|
|
410
|
-
return (
|
|
411
|
-
f"{{{ast.unparse(argument)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)} "
|
|
412
|
-
f"if {ast.unparse(test)}}}"
|
|
413
|
-
)
|
|
414
|
-
case _:
|
|
415
|
-
pass
|
|
416
|
-
return None
|
|
598
|
+
if init.col_offset + len(prefix) + len(expression) <= _MAX_REPLACEMENT_WIDTH:
|
|
599
|
+
return True
|
|
600
|
+
if not allow_multiline or not (expression.startswith("[") and expression.endswith("]")):
|
|
601
|
+
return False
|
|
602
|
+
content = expression[1:-1]
|
|
603
|
+
return (
|
|
604
|
+
init.col_offset + len(prefix) + 1 <= _MAX_REPLACEMENT_WIDTH
|
|
605
|
+
and init.col_offset + 4 + len(content) <= _MAX_REPLACEMENT_WIDTH
|
|
606
|
+
)
|
|
417
607
|
|
|
418
608
|
|
|
419
609
|
def _comment_lines(source: str) -> frozenset[int]:
|
|
@@ -429,7 +619,7 @@ def _has_comment(init: _Init, loop: ast.For, comments: frozenset[int]) -> bool:
|
|
|
429
619
|
return any(init.lineno <= line <= end_line for line in comments)
|
|
430
620
|
|
|
431
621
|
|
|
432
|
-
def
|
|
622
|
+
def _set_replacement(loop: ast.For, name: str, bound_names: frozenset[str]) -> _LoopReplacement | None:
|
|
433
623
|
match loop.body[0]:
|
|
434
624
|
case ast.If(test=test, body=[body], orelse=[]):
|
|
435
625
|
expression = _single_method_argument(body, name, "add")
|
|
@@ -440,7 +630,19 @@ def _set_loop_kind(loop: ast.For, name: str, bound_names: frozenset[str]) -> _Co
|
|
|
440
630
|
and _simple_projection(expression, bound_names)
|
|
441
631
|
and not _contains_prohibited_expression(test)
|
|
442
632
|
):
|
|
443
|
-
return
|
|
633
|
+
return _LoopReplacement(
|
|
634
|
+
_CollectionKind.SET,
|
|
635
|
+
f"{{{ast.unparse(expression)} for {ast.unparse(loop.target)} in {ast.unparse(loop.iter)} "
|
|
636
|
+
f"if {ast.unparse(test)}}}",
|
|
637
|
+
)
|
|
444
638
|
case _:
|
|
445
639
|
pass
|
|
446
640
|
return None
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _message(finding: _Candidate) -> str:
|
|
644
|
+
qualifier = "filtered " if finding.action != "populates" else ""
|
|
645
|
+
return (
|
|
646
|
+
f"This loop only {finding.action} fresh {finding.kind} {finding.name!r} — prefer a "
|
|
647
|
+
f"{qualifier}{finding.kind} comprehension."
|
|
648
|
+
)
|
|
@@ -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)):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.6 → sarj_python_lint-0.83.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|