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