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