sarj-python-lint 0.82.4__tar.gz → 0.82.6__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.4 → sarj_python_lint-0.82.6}/PKG-INFO +1 -1
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/pyproject.toml +1 -1
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_ratchet_cli.py +6 -1
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/ratchet.py +40 -8
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_registry.py +4 -0
- sarj_python_lint-0.82.6/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +367 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/.gitignore +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/LICENSE +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/README.md +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.
|
|
3
|
+
Version: 0.82.6
|
|
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/
|
|
@@ -63,6 +63,7 @@ def _run(args: _Options) -> int:
|
|
|
63
63
|
packages=baseline.packages,
|
|
64
64
|
per_file_ceiling=args.per_file_ceiling,
|
|
65
65
|
file_exceptions=baseline.file_exceptions,
|
|
66
|
+
file_selector_ceilings=baseline.file_selector_ceilings,
|
|
66
67
|
excluded_subtrees=baseline.excluded_subtrees,
|
|
67
68
|
)
|
|
68
69
|
|
|
@@ -85,6 +86,7 @@ def _run(args: _Options) -> int:
|
|
|
85
86
|
packages=baseline.packages,
|
|
86
87
|
per_file_ceiling=baseline.per_file_ceiling,
|
|
87
88
|
file_exceptions=baseline.file_exceptions,
|
|
89
|
+
file_selector_ceilings=baseline.file_selector_ceilings,
|
|
88
90
|
excluded_subtrees=excluded_subtrees,
|
|
89
91
|
)
|
|
90
92
|
try:
|
|
@@ -182,7 +184,10 @@ class _Result:
|
|
|
182
184
|
|
|
183
185
|
|
|
184
186
|
app = typer.Typer(
|
|
185
|
-
help=
|
|
187
|
+
help=(
|
|
188
|
+
"Ratchet on lint/type suppressions: per-selector, per-package, per-file, and per-file-selector ceilings that "
|
|
189
|
+
"may only shrink."
|
|
190
|
+
),
|
|
186
191
|
add_completion=False,
|
|
187
192
|
no_args_is_help=False,
|
|
188
193
|
pretty_exceptions_enable=False,
|
|
@@ -69,6 +69,7 @@ class Measurement:
|
|
|
69
69
|
codes: Counter[str]
|
|
70
70
|
packages: Counter[str]
|
|
71
71
|
files: dict[str, int]
|
|
72
|
+
file_selectors: dict[str, Counter[str]]
|
|
72
73
|
|
|
73
74
|
@property
|
|
74
75
|
def total(self) -> int:
|
|
@@ -82,6 +83,7 @@ class Baseline:
|
|
|
82
83
|
packages: dict[str, int] = field(default_factory=dict[str, int])
|
|
83
84
|
per_file_ceiling: int = DEFAULT_PER_FILE_CEILING
|
|
84
85
|
file_exceptions: dict[str, int] = field(default_factory=dict[str, int])
|
|
86
|
+
file_selector_ceilings: dict[str, dict[str, int]] | None = None
|
|
85
87
|
excluded_subtrees: tuple[str, ...] = ()
|
|
86
88
|
|
|
87
89
|
|
|
@@ -113,6 +115,10 @@ _REMEDIATION: Final[Mapping[str, str]] = MappingProxyType(
|
|
|
113
115
|
"burn one down here, or grandfather the file explicitly in the baseline's "
|
|
114
116
|
"`files.exceptions`."
|
|
115
117
|
),
|
|
118
|
+
"file-selector": (
|
|
119
|
+
"A suppression for this selector is new to this file. Fix the finding instead of moving suppression debt "
|
|
120
|
+
"between files or selectors, or get the exact ceiling raise reviewed explicitly (`--update --allow-increase`)."
|
|
121
|
+
),
|
|
116
122
|
}
|
|
117
123
|
)
|
|
118
124
|
|
|
@@ -128,6 +134,7 @@ def measure(
|
|
|
128
134
|
codes: Counter[str] = Counter()
|
|
129
135
|
package_counts: Counter[str] = Counter()
|
|
130
136
|
files: dict[str, int] = {}
|
|
137
|
+
file_selectors: dict[str, Counter[str]] = {}
|
|
131
138
|
excluded = tuple(_normalized_subtree(value) for value in excluded_subtrees)
|
|
132
139
|
for package in packages:
|
|
133
140
|
package_counts[package] = 0
|
|
@@ -142,7 +149,8 @@ def measure(
|
|
|
142
149
|
n = sum(found.values())
|
|
143
150
|
package_counts[package] += n
|
|
144
151
|
files[relative] = n
|
|
145
|
-
|
|
152
|
+
file_selectors[relative] = found
|
|
153
|
+
return Measurement(codes=codes, packages=package_counts, files=files, file_selectors=file_selectors)
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
def count_source(source: str, *, ruff_aliases: Mapping[str, str] | None = None) -> Counter[str]:
|
|
@@ -173,6 +181,13 @@ def gate(measurement: Measurement, baseline: Baseline) -> list[Failure]:
|
|
|
173
181
|
for key, n in sorted(measurement.files.items())
|
|
174
182
|
if n > (ceiling := baseline.file_exceptions.get(key, baseline.per_file_ceiling))
|
|
175
183
|
]
|
|
184
|
+
if baseline.file_selector_ceilings is not None:
|
|
185
|
+
failures += [
|
|
186
|
+
Failure(dimension="file-selector", key=f"{path} [{code}]", ceiling=ceiling, actual=actual)
|
|
187
|
+
for path, codes in sorted(measurement.file_selectors.items())
|
|
188
|
+
for code, actual in sorted(codes.items())
|
|
189
|
+
if actual > (ceiling := baseline.file_selector_ceilings.get(path, {}).get(code, 0))
|
|
190
|
+
]
|
|
176
191
|
return failures
|
|
177
192
|
|
|
178
193
|
|
|
@@ -193,6 +208,9 @@ def seed(measurement: Measurement, baseline: Baseline) -> Baseline:
|
|
|
193
208
|
packages=dict(sorted(measurement.packages.items())),
|
|
194
209
|
per_file_ceiling=baseline.per_file_ceiling,
|
|
195
210
|
file_exceptions=dict(sorted(exceptions.items())),
|
|
211
|
+
file_selector_ceilings={
|
|
212
|
+
path: dict(sorted(codes.items())) for path, codes in sorted(measurement.file_selectors.items())
|
|
213
|
+
},
|
|
196
214
|
excluded_subtrees=baseline.excluded_subtrees,
|
|
197
215
|
)
|
|
198
216
|
|
|
@@ -214,6 +232,7 @@ def load_baseline(path: Path) -> Baseline:
|
|
|
214
232
|
if isinstance(ceiling, int) and not isinstance(ceiling, bool)
|
|
215
233
|
else DEFAULT_PER_FILE_CEILING,
|
|
216
234
|
file_exceptions=_int_map(_get(files, "exceptions")),
|
|
235
|
+
file_selector_ceilings=_optional_nested_int_map(_get(files, "selectors")),
|
|
217
236
|
excluded_subtrees=_string_tuple(_get(raw, "excluded_subtrees")),
|
|
218
237
|
)
|
|
219
238
|
|
|
@@ -231,7 +250,8 @@ def dump_baseline(baseline: Baseline, packages: Iterable[str]) -> str:
|
|
|
231
250
|
"Suppression ceilings, written by `sarj-ratchet --update`. Counts may "
|
|
232
251
|
"only go DOWN: `codes` is per dialect+code, `packages` is per "
|
|
233
252
|
"top-level package, `files.per_file_ceiling` caps any single file "
|
|
234
|
-
"with `files.exceptions` grandfathering the pre-existing hot spots
|
|
253
|
+
"with `files.exceptions` grandfathering the pre-existing hot spots, and "
|
|
254
|
+
"`files.selectors` prevents debt from moving between files or lint rules. "
|
|
235
255
|
"Raising a ceiling requires `--update --allow-increase` and review."
|
|
236
256
|
),
|
|
237
257
|
"packages_scanned": sorted(packages),
|
|
@@ -241,6 +261,7 @@ def dump_baseline(baseline: Baseline, packages: Iterable[str]) -> str:
|
|
|
241
261
|
"files": {
|
|
242
262
|
"per_file_ceiling": baseline.per_file_ceiling,
|
|
243
263
|
"exceptions": baseline.file_exceptions,
|
|
264
|
+
"selectors": baseline.file_selector_ceilings or {},
|
|
244
265
|
},
|
|
245
266
|
}
|
|
246
267
|
return json.dumps(payload, indent=2) + "\n"
|
|
@@ -258,13 +279,17 @@ def discover_packages(root: Path, excluded_dir_names: frozenset[str] = DEFAULT_E
|
|
|
258
279
|
|
|
259
280
|
|
|
260
281
|
def _int_map(value: object) -> dict[str, int]:
|
|
261
|
-
if not
|
|
282
|
+
if not _is_string_object_mapping(value):
|
|
262
283
|
return {}
|
|
263
|
-
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
284
|
+
return {key: count for key, count in value.items() if isinstance(count, int) and not isinstance(count, bool)}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _optional_nested_int_map(value: object) -> dict[str, dict[str, int]] | None:
|
|
288
|
+
if value is None:
|
|
289
|
+
return None
|
|
290
|
+
if not _is_string_object_mapping(value):
|
|
291
|
+
return {}
|
|
292
|
+
return {path: counts for path, raw_counts in value.items() if (counts := _int_map(raw_counts))}
|
|
268
293
|
|
|
269
294
|
|
|
270
295
|
def _string_tuple(value: object) -> tuple[str, ...]:
|
|
@@ -282,6 +307,13 @@ def _is_object_list(value: object) -> TypeGuard[list[object]]:
|
|
|
282
307
|
return isinstance(value, list)
|
|
283
308
|
|
|
284
309
|
|
|
310
|
+
def _is_string_object_mapping(value: object) -> TypeGuard[dict[str, object]]:
|
|
311
|
+
return isinstance(value, dict) and all(
|
|
312
|
+
isinstance(key, str)
|
|
313
|
+
for key in value # pyright: ignore[reportUnknownVariableType] — runtime validation narrows untyped JSON keys.
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
285
317
|
def _normalized_subtree(value: str) -> str:
|
|
286
318
|
return value.strip("/")
|
|
287
319
|
|
|
@@ -103,6 +103,9 @@ from sarj_python_lint.rules.prefer_match_value_dispatch import PreferMatchValueD
|
|
|
103
103
|
from sarj_python_lint.rules.prefer_module_level_constant import (
|
|
104
104
|
PreferModuleLevelConstant,
|
|
105
105
|
)
|
|
106
|
+
from sarj_python_lint.rules.prefer_monkeypatch_for_process_state_in_test import (
|
|
107
|
+
PreferMonkeypatchForProcessStateInTest,
|
|
108
|
+
)
|
|
106
109
|
from sarj_python_lint.rules.prefer_namedtuple_over_tuple_return import (
|
|
107
110
|
PreferNamedtupleOverTupleReturn,
|
|
108
111
|
)
|
|
@@ -204,6 +207,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
204
207
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
205
208
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
206
209
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
210
|
+
PreferMonkeypatchForProcessStateInTest.id: PreferMonkeypatchForProcessStateInTest,
|
|
207
211
|
MockWithoutSpec.id: MockWithoutSpec,
|
|
208
212
|
OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
|
|
209
213
|
PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
|
sarj_python_lint-0.82.6/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
parse_or_none,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_OS = frozenset({"os"})
|
|
29
|
+
_SYS = frozenset({"sys"})
|
|
30
|
+
_SCOPE_NODES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Mutation(NamedTuple):
|
|
34
|
+
node: ast.expr
|
|
35
|
+
family: str
|
|
36
|
+
subject: str
|
|
37
|
+
remedy: str
|
|
38
|
+
identity: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
class PreferMonkeypatchForProcessStateInTest(Rule):
|
|
43
|
+
id = "prefer-monkeypatch-for-process-state-in-test"
|
|
44
|
+
code = "SARJ446"
|
|
45
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
46
|
+
summary="Test mutates process-wide state without a restoring test scope.",
|
|
47
|
+
rationale=(
|
|
48
|
+
"The working directory and Python import registries are shared by every test in the process. A direct "
|
|
49
|
+
"mutation can survive an assertion or setup failure, making later tests depend on execution order."
|
|
50
|
+
),
|
|
51
|
+
remediation=(
|
|
52
|
+
"Use pytest's monkeypatch.chdir, monkeypatch.syspath_prepend, monkeypatch.setitem, or "
|
|
53
|
+
"monkeypatch.delitem so teardown restores the previous state even when the test fails. A deliberate "
|
|
54
|
+
"manual restoration may remain when it is enclosed by a matching try/finally."
|
|
55
|
+
),
|
|
56
|
+
category=RuleCategory.TESTING,
|
|
57
|
+
autofix=AutofixPolicy.NONE,
|
|
58
|
+
limitations=(
|
|
59
|
+
"Only maintained test paths are analyzed; generated files, production code, and module/class bootstrap mutations are excluded.",
|
|
60
|
+
"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.",
|
|
61
|
+
"Environment variables remain owned by Ruff TID251/B003; argv, locale, timezone, warning filters, and process APIs without an equivalent helper are outside this rule.",
|
|
62
|
+
"Imports and import aliases are resolved conservatively. Indirect container aliases and dynamically selected modules are not inferred.",
|
|
63
|
+
),
|
|
64
|
+
examples=(
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="direct-module-registry-mutation",
|
|
67
|
+
title="Test installs a module without guaranteed restoration",
|
|
68
|
+
outcome=ExampleOutcome.MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"tests/test_plugin.py",
|
|
72
|
+
"import sys\n\ndef test_plugin(fake_plugin):\n sys.modules['optional_plugin'] = fake_plugin\n",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
focus_path=PurePosixPath("tests/test_plugin.py"),
|
|
76
|
+
expected_count=1,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
RuleExample(
|
|
80
|
+
example_id="scoped-module-registry-mutation",
|
|
81
|
+
title="Pytest restores the module registry after the test",
|
|
82
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
83
|
+
files=(
|
|
84
|
+
ExampleFile.python(
|
|
85
|
+
"tests/test_plugin.py",
|
|
86
|
+
"import sys\n\ndef test_plugin(monkeypatch, fake_plugin):\n"
|
|
87
|
+
" monkeypatch.setitem(sys.modules, 'optional_plugin', fake_plugin)\n",
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
focus_path=PurePosixPath("tests/test_plugin.py"),
|
|
91
|
+
expected_count=0,
|
|
92
|
+
public=True,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
description = documentation.summary
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
100
|
+
if not is_test_path(path) or is_generated(path, source):
|
|
101
|
+
return []
|
|
102
|
+
tree = parse_or_none(path, source)
|
|
103
|
+
if tree is None:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
module_imports = ImportIndex.from_tree(tree, module_scope_only=True)
|
|
107
|
+
mutations = [
|
|
108
|
+
mutation for function in _functions(tree) for mutation in _function_mutations(function, module_imports)
|
|
109
|
+
]
|
|
110
|
+
diagnostics = [
|
|
111
|
+
Diagnostic(
|
|
112
|
+
path=path,
|
|
113
|
+
line=mutation.node.lineno,
|
|
114
|
+
col=mutation.node.col_offset + 1,
|
|
115
|
+
code=self.code,
|
|
116
|
+
message=(
|
|
117
|
+
f"Direct {mutation.subject} mutation can leak into later tests when this scope exits early. "
|
|
118
|
+
f"Use {mutation.remedy} so pytest restores the previous process state automatically."
|
|
119
|
+
),
|
|
120
|
+
severity=Severity.WARNING,
|
|
121
|
+
)
|
|
122
|
+
for mutation in mutations
|
|
123
|
+
]
|
|
124
|
+
diagnostics.sort(key=lambda diagnostic: (diagnostic.line, diagnostic.col))
|
|
125
|
+
return diagnostics
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _functions(tree: ast.Module) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
129
|
+
for node in ast.walk(tree):
|
|
130
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
131
|
+
yield node
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _function_mutations(
|
|
135
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
136
|
+
module_imports: ImportIndex,
|
|
137
|
+
) -> list[_Mutation]:
|
|
138
|
+
local_tree = ast.Module(body=function.body, type_ignores=[])
|
|
139
|
+
local_imports = ImportIndex.from_tree(local_tree)
|
|
140
|
+
nodes = tuple(_lexical_nodes(function))
|
|
141
|
+
parents = {child: parent for parent in nodes for child in ast.iter_child_nodes(parent)}
|
|
142
|
+
mutations: list[_Mutation] = []
|
|
143
|
+
for node in nodes:
|
|
144
|
+
mutation = _mutation(node, parents, module_imports, local_imports)
|
|
145
|
+
if mutation is None or _import_root_rebound_before(function, mutation.node, module_imports, local_imports):
|
|
146
|
+
continue
|
|
147
|
+
mutations.append(mutation)
|
|
148
|
+
restored = _restored_mutations(nodes, mutations, module_imports, local_imports)
|
|
149
|
+
return [mutation for mutation in mutations if mutation.node not in restored]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _lexical_nodes(function: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[ast.AST]:
|
|
153
|
+
stack: list[ast.AST] = [node for node in reversed(function.body) if not isinstance(node, _SCOPE_NODES)]
|
|
154
|
+
while stack:
|
|
155
|
+
node = stack.pop()
|
|
156
|
+
yield node
|
|
157
|
+
for child in reversed(list(ast.iter_child_nodes(node))):
|
|
158
|
+
if isinstance(child, _SCOPE_NODES):
|
|
159
|
+
continue
|
|
160
|
+
stack.append(child)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _mutation(
|
|
164
|
+
node: ast.AST,
|
|
165
|
+
parents: dict[ast.AST, ast.AST],
|
|
166
|
+
module_imports: ImportIndex,
|
|
167
|
+
local_imports: ImportIndex,
|
|
168
|
+
) -> _Mutation | None:
|
|
169
|
+
if isinstance(node, ast.Call):
|
|
170
|
+
mutation = _call_mutation(node, parents, module_imports, local_imports)
|
|
171
|
+
if mutation is not None:
|
|
172
|
+
return mutation
|
|
173
|
+
target: ast.expr | None = None
|
|
174
|
+
if isinstance(node, ast.Assign):
|
|
175
|
+
target = next((item for item in node.targets if _is_modules_item(item, module_imports, local_imports)), None)
|
|
176
|
+
elif isinstance(node, (ast.AnnAssign, ast.AugAssign)):
|
|
177
|
+
target = node.target
|
|
178
|
+
if target is not None and _is_modules_item(target, module_imports, local_imports):
|
|
179
|
+
return _Mutation(
|
|
180
|
+
target,
|
|
181
|
+
"modules",
|
|
182
|
+
"sys.modules",
|
|
183
|
+
"monkeypatch.setitem(sys.modules, key, value)",
|
|
184
|
+
_item_key(target),
|
|
185
|
+
)
|
|
186
|
+
if isinstance(node, ast.Delete):
|
|
187
|
+
target = next((item for item in node.targets if _is_modules_item(item, module_imports, local_imports)), None)
|
|
188
|
+
if target is not None:
|
|
189
|
+
return _Mutation(
|
|
190
|
+
target,
|
|
191
|
+
"modules",
|
|
192
|
+
"sys.modules",
|
|
193
|
+
"monkeypatch.delitem(sys.modules, key)",
|
|
194
|
+
_item_key(target),
|
|
195
|
+
)
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _call_mutation(
|
|
200
|
+
call: ast.Call,
|
|
201
|
+
parents: dict[ast.AST, ast.AST],
|
|
202
|
+
module_imports: ImportIndex,
|
|
203
|
+
local_imports: ImportIndex,
|
|
204
|
+
) -> _Mutation | None:
|
|
205
|
+
if _resolves(call.func, module_imports, local_imports, sources=_OS, symbol="chdir"):
|
|
206
|
+
return _Mutation(call, "cwd", "working-directory", "monkeypatch.chdir(path)")
|
|
207
|
+
if (
|
|
208
|
+
isinstance(call.func, ast.Attribute)
|
|
209
|
+
and call.func.attr == "insert"
|
|
210
|
+
and _resolves(call.func.value, module_imports, local_imports, sources=_SYS, symbol="path")
|
|
211
|
+
and call.args
|
|
212
|
+
and _is_zero(call.args[0])
|
|
213
|
+
):
|
|
214
|
+
identity = ast.dump(call.args[1], include_attributes=False) if len(call.args) > 1 else None
|
|
215
|
+
return _Mutation(call, "path", "sys.path", "monkeypatch.syspath_prepend(path)", identity)
|
|
216
|
+
if (
|
|
217
|
+
isinstance(call.func, ast.Attribute)
|
|
218
|
+
and call.func.attr == "pop"
|
|
219
|
+
and _resolves(call.func.value, module_imports, local_imports, sources=_SYS, symbol="modules")
|
|
220
|
+
and isinstance(parents.get(call), ast.Expr)
|
|
221
|
+
):
|
|
222
|
+
identity = ast.dump(call.args[0], include_attributes=False) if call.args else None
|
|
223
|
+
return _Mutation(
|
|
224
|
+
call,
|
|
225
|
+
"modules",
|
|
226
|
+
"sys.modules",
|
|
227
|
+
"monkeypatch.delitem(sys.modules, key, raising=False)",
|
|
228
|
+
identity,
|
|
229
|
+
)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_modules_item(node: ast.expr, module_imports: ImportIndex, local_imports: ImportIndex) -> bool:
|
|
234
|
+
return isinstance(node, ast.Subscript) and _resolves(
|
|
235
|
+
node.value,
|
|
236
|
+
module_imports,
|
|
237
|
+
local_imports,
|
|
238
|
+
sources=_SYS,
|
|
239
|
+
symbol="modules",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _item_key(node: ast.expr) -> str | None:
|
|
244
|
+
return ast.dump(node.slice, include_attributes=False) if isinstance(node, ast.Subscript) else None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _is_zero(node: ast.expr) -> bool:
|
|
248
|
+
return isinstance(node, ast.Constant) and not isinstance(node.value, bool) and node.value == 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _resolves(
|
|
252
|
+
node: ast.expr,
|
|
253
|
+
module_imports: ImportIndex,
|
|
254
|
+
local_imports: ImportIndex,
|
|
255
|
+
*,
|
|
256
|
+
sources: frozenset[str],
|
|
257
|
+
symbol: str,
|
|
258
|
+
) -> bool:
|
|
259
|
+
return module_imports.resolves(node, sources=sources, symbol=symbol) or local_imports.resolves(
|
|
260
|
+
node,
|
|
261
|
+
sources=sources,
|
|
262
|
+
symbol=symbol,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _import_root_rebound_before(
|
|
267
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
268
|
+
use: ast.AST,
|
|
269
|
+
module_imports: ImportIndex,
|
|
270
|
+
local_imports: ImportIndex,
|
|
271
|
+
) -> bool:
|
|
272
|
+
roots = {
|
|
273
|
+
node.id
|
|
274
|
+
for node in ast.walk(use)
|
|
275
|
+
if isinstance(node, ast.Name) and (node.id in module_imports.bindings or node.id in local_imports.bindings)
|
|
276
|
+
}
|
|
277
|
+
parameters = {
|
|
278
|
+
argument.arg
|
|
279
|
+
for argument in (
|
|
280
|
+
*function.args.posonlyargs,
|
|
281
|
+
*function.args.args,
|
|
282
|
+
*function.args.kwonlyargs,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
if not roots.isdisjoint(parameters):
|
|
286
|
+
return True
|
|
287
|
+
use_position = _position(use)
|
|
288
|
+
return any(
|
|
289
|
+
isinstance(node, ast.Name)
|
|
290
|
+
and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
291
|
+
and node.id in roots
|
|
292
|
+
and _position(node) < use_position
|
|
293
|
+
for node in _lexical_nodes(function)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _position(node: ast.AST) -> tuple[int, int]:
|
|
298
|
+
return (getattr(node, "lineno", 0), getattr(node, "col_offset", 0))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _restored_mutations(
|
|
302
|
+
nodes: tuple[ast.AST, ...],
|
|
303
|
+
mutations: list[_Mutation],
|
|
304
|
+
module_imports: ImportIndex,
|
|
305
|
+
local_imports: ImportIndex,
|
|
306
|
+
) -> set[ast.AST]:
|
|
307
|
+
restored: set[ast.AST] = set()
|
|
308
|
+
for node in nodes:
|
|
309
|
+
if not isinstance(node, (ast.Try, ast.TryStar)) or not node.finalbody:
|
|
310
|
+
continue
|
|
311
|
+
restored.update(_restored_for_try(node, mutations, module_imports, local_imports))
|
|
312
|
+
return restored
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _restored_for_try(
|
|
316
|
+
node: ast.Try | ast.TryStar,
|
|
317
|
+
mutations: list[_Mutation],
|
|
318
|
+
module_imports: ImportIndex,
|
|
319
|
+
local_imports: ImportIndex,
|
|
320
|
+
) -> set[ast.AST]:
|
|
321
|
+
protected_nodes = set(_descendants(node.body))
|
|
322
|
+
final_nodes = set(_descendants(node.finalbody))
|
|
323
|
+
protected = [mutation for mutation in mutations if mutation.node in protected_nodes]
|
|
324
|
+
final = [mutation for mutation in mutations if mutation.node in final_nodes]
|
|
325
|
+
restored: set[ast.AST] = set()
|
|
326
|
+
for mutation in protected:
|
|
327
|
+
matching = next((item for item in final if _same_state(mutation, item)), None)
|
|
328
|
+
if matching is not None:
|
|
329
|
+
restored.update((mutation.node, matching.node))
|
|
330
|
+
continue
|
|
331
|
+
if mutation.family == "path" and _finalbody_removes_path(
|
|
332
|
+
node.finalbody,
|
|
333
|
+
mutation.identity,
|
|
334
|
+
module_imports,
|
|
335
|
+
local_imports,
|
|
336
|
+
):
|
|
337
|
+
restored.add(mutation.node)
|
|
338
|
+
return restored
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _descendants(statements: Iterable[ast.stmt]) -> Iterator[ast.AST]:
|
|
342
|
+
for statement in statements:
|
|
343
|
+
yield from ast.walk(statement)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _same_state(left: _Mutation, right: _Mutation) -> bool:
|
|
347
|
+
return left.family == right.family and (left.identity is None or left.identity == right.identity)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _finalbody_removes_path(
|
|
351
|
+
statements: list[ast.stmt],
|
|
352
|
+
identity: str | None,
|
|
353
|
+
module_imports: ImportIndex,
|
|
354
|
+
local_imports: ImportIndex,
|
|
355
|
+
) -> bool:
|
|
356
|
+
if identity is None:
|
|
357
|
+
return False
|
|
358
|
+
return any(
|
|
359
|
+
isinstance(node, ast.Call)
|
|
360
|
+
and isinstance(node.func, ast.Attribute)
|
|
361
|
+
and node.func.attr == "remove"
|
|
362
|
+
and _resolves(node.func.value, module_imports, local_imports, sources=_SYS, symbol="path")
|
|
363
|
+
and bool(node.args)
|
|
364
|
+
and ast.dump(node.args[0], include_attributes=False) == identity
|
|
365
|
+
for statement in statements
|
|
366
|
+
for node in ast.walk(statement)
|
|
367
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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
|
|
File without changes
|
{sarj_python_lint-0.82.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/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.4 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|