sarj-python-lint 0.80.15__tar.gz → 0.80.17__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.80.15 → sarj_python_lint-0.80.17}/PKG-INFO +2 -1
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/pyproject.toml +2 -2
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/__main__.py +78 -71
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/_ratchet_cli.py +67 -55
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_comment_cruft.py +24 -0
- sarj_python_lint-0.80.17/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +230 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_str_enum.py +7 -6
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/.gitignore +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/LICENSE +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/README.md +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.80.
|
|
3
|
+
Version: 0.80.17
|
|
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/
|
|
@@ -19,6 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: ruff>=0.16.5
|
|
21
21
|
Requires-Dist: sqlglot==30.18.0
|
|
22
|
+
Requires-Dist: typer==0.27.2
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.80.
|
|
3
|
+
version = "0.80.17"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["ruff>=0.16.5", "sqlglot==30.18.0"]
|
|
18
|
+
dependencies = ["ruff>=0.16.5", "sqlglot==30.18.0", "typer==0.27.2"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import argparse
|
|
4
3
|
import ast
|
|
5
4
|
from collections import Counter
|
|
5
|
+
from dataclasses import dataclass
|
|
6
6
|
import json
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import sys
|
|
9
9
|
from types import MappingProxyType
|
|
10
|
-
from typing import NamedTuple
|
|
10
|
+
from typing import Annotated, NamedTuple
|
|
11
|
+
|
|
12
|
+
import typer
|
|
11
13
|
|
|
12
14
|
from sarj_python_lint import __version__
|
|
13
15
|
from sarj_python_lint._analysis_session import AnalysisSession
|
|
@@ -236,24 +238,9 @@ def _docstring_owner_locations(source: str) -> dict[int, _OwnerLocation]:
|
|
|
236
238
|
return owners
|
|
237
239
|
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# `explain` takes exactly one rule, so it cannot share `--rule`'s list slot
|
|
243
|
-
# without widening the type and losing the check on every `check` call site.
|
|
244
|
-
which: str
|
|
245
|
-
files: list[Path]
|
|
246
|
-
baseline: Path | None
|
|
247
|
-
update_baseline: Path | None
|
|
248
|
-
|
|
249
|
-
def __init__(self) -> None:
|
|
250
|
-
super().__init__()
|
|
251
|
-
self.cmd = None
|
|
252
|
-
self.rule = []
|
|
253
|
-
self.which = ""
|
|
254
|
-
self.files = []
|
|
255
|
-
self.baseline = None
|
|
256
|
-
self.update_baseline = None
|
|
241
|
+
@dataclass
|
|
242
|
+
class _Result:
|
|
243
|
+
code: int = 0
|
|
257
244
|
|
|
258
245
|
|
|
259
246
|
def _explain(wanted: str) -> int:
|
|
@@ -328,78 +315,98 @@ def _apply_baseline(
|
|
|
328
315
|
return out
|
|
329
316
|
|
|
330
317
|
|
|
331
|
-
def
|
|
332
|
-
parser = argparse.ArgumentParser(
|
|
333
|
-
prog="sarj-python-lint",
|
|
334
|
-
description="Custom Python lint rules.",
|
|
335
|
-
)
|
|
336
|
-
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
337
|
-
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
338
|
-
|
|
339
|
-
check_p = sub.add_parser("check", help="Run rules over files.")
|
|
340
|
-
check_p.add_argument(
|
|
341
|
-
"--rule",
|
|
342
|
-
action="append",
|
|
343
|
-
required=True,
|
|
344
|
-
help="Rule ID (repeat for multiple).",
|
|
345
|
-
)
|
|
346
|
-
check_p.add_argument(
|
|
347
|
-
"--baseline",
|
|
348
|
-
type=Path,
|
|
349
|
-
help="Per-file shrink-only baseline JSON: {path: {CODE: count}}. Diags up to the baselined count are suppressed.",
|
|
350
|
-
)
|
|
351
|
-
check_p.add_argument(
|
|
352
|
-
"--update-baseline",
|
|
353
|
-
type=Path,
|
|
354
|
-
help="Write the current per-file diagnostic counts to this JSON and exit 0.",
|
|
355
|
-
)
|
|
356
|
-
check_p.add_argument("files", nargs="+", type=Path)
|
|
357
|
-
|
|
358
|
-
sub.add_parser("list-rules", help="List available rule IDs.")
|
|
359
|
-
|
|
360
|
-
explain_p = sub.add_parser("explain", help="Print one rule's summary and the links to its examples and evidence.")
|
|
361
|
-
explain_p.add_argument("which", metavar="rule", help="Rule ID or SARJ code.")
|
|
362
|
-
|
|
363
|
-
args = parser.parse_args(argv, namespace=_Args())
|
|
364
|
-
|
|
365
|
-
if args.cmd == "list-rules":
|
|
366
|
-
for rid, cls in sorted(REGISTRY.items()):
|
|
367
|
-
inst = cls()
|
|
368
|
-
sys.stdout.write(f"{inst.code:8} {rid:40} {inst.description}\n")
|
|
369
|
-
return 0
|
|
370
|
-
|
|
371
|
-
if args.cmd == "explain":
|
|
372
|
-
return _explain(args.which)
|
|
373
|
-
|
|
318
|
+
def _run_check(rule: list[str], files: list[Path], baseline: Path | None, update_baseline: Path | None) -> int:
|
|
374
319
|
try:
|
|
375
|
-
diags = analyze(
|
|
320
|
+
diags = analyze(rule, files)
|
|
376
321
|
except (OSError, ValueError) as exc:
|
|
377
322
|
sys.stderr.write(f"error: {exc}\n")
|
|
378
323
|
return 2
|
|
379
|
-
if
|
|
324
|
+
if update_baseline is not None:
|
|
380
325
|
counts = _baseline_counts(diags)
|
|
381
326
|
blocking = sum(d.severity is Severity.ERROR for d in diags)
|
|
382
327
|
warnings = len(diags) - blocking
|
|
383
328
|
try:
|
|
384
|
-
atomic_write_text(
|
|
329
|
+
atomic_write_text(update_baseline, json.dumps(counts, indent=2, sort_keys=True) + "\n")
|
|
385
330
|
except OSError as exc:
|
|
386
|
-
sys.stderr.write(f"error: cannot write baseline {
|
|
331
|
+
sys.stderr.write(f"error: cannot write baseline {update_baseline}: {exc}\n")
|
|
387
332
|
return 2
|
|
388
333
|
sys.stdout.write(
|
|
389
|
-
f"baseline written: {
|
|
334
|
+
f"baseline written: {update_baseline} "
|
|
390
335
|
f"({blocking} blocking diagnostics over {len(counts)} files; {warnings} warnings excluded)\n"
|
|
391
336
|
)
|
|
392
337
|
return 0
|
|
393
|
-
if
|
|
338
|
+
if baseline is not None:
|
|
394
339
|
try:
|
|
395
|
-
diags = _apply_baseline(diags, _read_baseline(
|
|
340
|
+
diags = _apply_baseline(diags, _read_baseline(baseline))
|
|
396
341
|
except (OSError, ValueError) as exc:
|
|
397
|
-
sys.stderr.write(f"error: invalid baseline {
|
|
342
|
+
sys.stderr.write(f"error: invalid baseline {baseline}: {exc}\n")
|
|
398
343
|
return 2
|
|
399
344
|
for d in diags:
|
|
400
345
|
sys.stdout.write(d.format() + "\n")
|
|
401
346
|
return 1 if any(d.severity is Severity.ERROR for d in diags) else 0
|
|
402
347
|
|
|
403
348
|
|
|
349
|
+
app = typer.Typer(
|
|
350
|
+
help="Custom Python lint rules.",
|
|
351
|
+
add_completion=False,
|
|
352
|
+
no_args_is_help=False,
|
|
353
|
+
pretty_exceptions_enable=False,
|
|
354
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _show_version(*, value: bool) -> None:
|
|
359
|
+
if value:
|
|
360
|
+
sys.stdout.write(f"sarj-python-lint {__version__}\n")
|
|
361
|
+
raise typer.Exit
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@app.callback()
|
|
365
|
+
def _root(
|
|
366
|
+
*,
|
|
367
|
+
_version: Annotated[bool, typer.Option("--version", callback=_show_version, is_eager=True)] = False,
|
|
368
|
+
) -> None:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.command("list-rules", help="List available rule IDs.")
|
|
373
|
+
def _list_rules() -> None:
|
|
374
|
+
for rid, cls in sorted(REGISTRY.items()):
|
|
375
|
+
inst = cls()
|
|
376
|
+
sys.stdout.write(f"{inst.code:8} {rid:40} {inst.description}\n")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@app.command("explain", help="Print one rule's summary and the links to its examples and evidence.")
|
|
380
|
+
def _explain_command(
|
|
381
|
+
context: typer.Context, which: Annotated[str, typer.Argument(metavar="rule", help="Rule ID or SARJ code.")]
|
|
382
|
+
) -> None:
|
|
383
|
+
context.ensure_object(_Result).code = _explain(which)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command("check", help="Run rules over files.")
|
|
387
|
+
def _check_command(
|
|
388
|
+
context: typer.Context,
|
|
389
|
+
files: Annotated[list[Path], typer.Argument()],
|
|
390
|
+
rule: Annotated[list[str], typer.Option("--rule", help="Rule ID (repeat for multiple).")],
|
|
391
|
+
baseline: Annotated[
|
|
392
|
+
Path | None, typer.Option(help="Per-file shrink-only baseline JSON: {path: {CODE: count}}.")
|
|
393
|
+
] = None,
|
|
394
|
+
update_baseline: Annotated[
|
|
395
|
+
Path | None, typer.Option(help="Write current per-file diagnostic counts to JSON and exit 0.")
|
|
396
|
+
] = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
context.ensure_object(_Result).code = _run_check(rule, files, baseline, update_baseline)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def main(argv: list[str] | None = None) -> int:
|
|
402
|
+
result = _Result()
|
|
403
|
+
try:
|
|
404
|
+
app(args=argv, prog_name="sarj-python-lint", obj=result)
|
|
405
|
+
except SystemExit as exc:
|
|
406
|
+
if exc.code != 0:
|
|
407
|
+
raise
|
|
408
|
+
return result.code
|
|
409
|
+
|
|
410
|
+
|
|
404
411
|
if __name__ == "__main__":
|
|
405
412
|
sys.exit(main())
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from dataclasses import dataclass
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import subprocess # ruff: ignore[suspicious-subprocess-import] -- fixed local Ruff introspection only
|
|
7
7
|
import sys
|
|
8
|
-
from typing import TYPE_CHECKING, TypeGuard
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated, TypeGuard
|
|
9
|
+
|
|
10
|
+
import typer
|
|
9
11
|
|
|
10
12
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
11
13
|
from sarj_python_lint._version import __version__
|
|
@@ -29,7 +31,8 @@ if TYPE_CHECKING:
|
|
|
29
31
|
_DEFAULT_BASELINE_NAME = "suppression-baseline.json"
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class _Options:
|
|
33
36
|
root: Path
|
|
34
37
|
baseline: Path | None
|
|
35
38
|
package: list[str]
|
|
@@ -38,19 +41,8 @@ class _Args(argparse.Namespace):
|
|
|
38
41
|
update: bool
|
|
39
42
|
allow_increase: bool
|
|
40
43
|
|
|
41
|
-
def __init__(self) -> None:
|
|
42
|
-
super().__init__()
|
|
43
|
-
self.root = Path()
|
|
44
|
-
self.baseline = None
|
|
45
|
-
self.package = []
|
|
46
|
-
self.exclude_subtree = []
|
|
47
|
-
self.per_file_ceiling = None
|
|
48
|
-
self.update = False
|
|
49
|
-
self.allow_increase = False
|
|
50
|
-
|
|
51
44
|
|
|
52
|
-
def
|
|
53
|
-
args = _build_parser().parse_args(argv, namespace=_Args())
|
|
45
|
+
def _run(args: _Options) -> int:
|
|
54
46
|
root = args.root.resolve()
|
|
55
47
|
baseline_path = args.baseline if args.baseline is not None else root / _DEFAULT_BASELINE_NAME
|
|
56
48
|
|
|
@@ -180,48 +172,68 @@ def _is_string_object_mapping(value: object) -> TypeGuard[dict[str, object]]:
|
|
|
180
172
|
return isinstance(value, dict)
|
|
181
173
|
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
@dataclass
|
|
176
|
+
class _Result:
|
|
177
|
+
code: int = 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
app = typer.Typer(
|
|
181
|
+
help="Ratchet on lint/type suppressions: per-code, per-package and per-file ceilings that may only shrink.",
|
|
182
|
+
add_completion=False,
|
|
183
|
+
no_args_is_help=False,
|
|
184
|
+
pretty_exceptions_enable=False,
|
|
185
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _show_version(*, value: bool) -> None:
|
|
190
|
+
if value:
|
|
191
|
+
sys.stdout.write(f"sarj-ratchet {__version__}\n")
|
|
192
|
+
raise typer.Exit
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def _command(
|
|
197
|
+
context: typer.Context,
|
|
198
|
+
*,
|
|
199
|
+
root: Annotated[Path, typer.Argument(help="tree to scan (default: cwd)")] = Path(),
|
|
200
|
+
baseline: Annotated[
|
|
201
|
+
Path | None, typer.Option(help=f"baseline JSON (default: <root>/{_DEFAULT_BASELINE_NAME})")
|
|
202
|
+
] = None,
|
|
203
|
+
package: Annotated[
|
|
204
|
+
list[str] | None,
|
|
205
|
+
typer.Option(
|
|
206
|
+
"--package", help="package directory relative to root (repeatable; default: baseline or auto-discovered)"
|
|
188
207
|
),
|
|
208
|
+
] = None,
|
|
209
|
+
exclude_subtree: Annotated[
|
|
210
|
+
list[str] | None, typer.Option("--exclude-subtree", help="root-relative path prefix to skip (repeatable)")
|
|
211
|
+
] = None,
|
|
212
|
+
per_file_ceiling: Annotated[
|
|
213
|
+
int | None,
|
|
214
|
+
typer.Option(help=f"max suppressions in any one file (default: baseline or {DEFAULT_PER_FILE_CEILING})"),
|
|
215
|
+
] = None,
|
|
216
|
+
update: Annotated[
|
|
217
|
+
bool, typer.Option("--update", help="rewrite baseline from current counts (refuses to raise a ceiling)")
|
|
218
|
+
] = False,
|
|
219
|
+
allow_increase: Annotated[
|
|
220
|
+
bool, typer.Option("--allow-increase", help="with --update: permit ceilings to rise (requires explicit review)")
|
|
221
|
+
] = False,
|
|
222
|
+
_version: Annotated[bool, typer.Option("--version", callback=_show_version, is_eager=True)] = False,
|
|
223
|
+
) -> None:
|
|
224
|
+
context.ensure_object(_Result).code = _run(
|
|
225
|
+
_Options(root, baseline, package or [], exclude_subtree or [], per_file_ceiling, update, allow_increase)
|
|
189
226
|
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
default=[],
|
|
201
|
-
help="package directory relative to root (repeatable; default: the baseline's, else auto-discovered)",
|
|
202
|
-
)
|
|
203
|
-
parser.add_argument(
|
|
204
|
-
"--exclude-subtree",
|
|
205
|
-
action="append",
|
|
206
|
-
default=[],
|
|
207
|
-
help="root-relative path prefix to skip, e.g. generated client output (repeatable)",
|
|
208
|
-
)
|
|
209
|
-
parser.add_argument(
|
|
210
|
-
"--per-file-ceiling",
|
|
211
|
-
type=int,
|
|
212
|
-
help=f"max suppressions in any one file (default: the baseline's, else {DEFAULT_PER_FILE_CEILING})",
|
|
213
|
-
)
|
|
214
|
-
parser.add_argument(
|
|
215
|
-
"--update",
|
|
216
|
-
action="store_true",
|
|
217
|
-
help="rewrite the baseline from current counts (refuses to raise a ceiling)",
|
|
218
|
-
)
|
|
219
|
-
parser.add_argument(
|
|
220
|
-
"--allow-increase",
|
|
221
|
-
action="store_true",
|
|
222
|
-
help="with --update: permit ceilings to rise (requires explicit review)",
|
|
223
|
-
)
|
|
224
|
-
return parser
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str] | None = None) -> int:
|
|
230
|
+
result = _Result()
|
|
231
|
+
try:
|
|
232
|
+
app(args=argv, prog_name="sarj-ratchet", obj=result)
|
|
233
|
+
except SystemExit as exc:
|
|
234
|
+
if exc.code != 0:
|
|
235
|
+
raise
|
|
236
|
+
return result.code
|
|
225
237
|
|
|
226
238
|
|
|
227
239
|
def _update(
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -92,6 +92,7 @@ from sarj_python_lint.rules.prefer_immutable_module_constant import (
|
|
|
92
92
|
from sarj_python_lint.rules.prefer_library_fake import PreferLibraryFake
|
|
93
93
|
from sarj_python_lint.rules.prefer_match_assert_never import PreferMatchAssertNever
|
|
94
94
|
from sarj_python_lint.rules.prefer_match_type_dispatch import PreferMatchTypeDispatch
|
|
95
|
+
from sarj_python_lint.rules.prefer_match_value_dispatch import PreferMatchValueDispatch
|
|
95
96
|
from sarj_python_lint.rules.prefer_module_level_constant import (
|
|
96
97
|
PreferModuleLevelConstant,
|
|
97
98
|
)
|
|
@@ -218,6 +219,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
218
219
|
RequirePortForService.id: RequirePortForService,
|
|
219
220
|
PreferNonNullableCollection.id: PreferNonNullableCollection,
|
|
220
221
|
PreferMatchTypeDispatch.id: PreferMatchTypeDispatch,
|
|
222
|
+
PreferMatchValueDispatch.id: PreferMatchValueDispatch,
|
|
221
223
|
PreferWalrusRegexMatch.id: PreferWalrusRegexMatch,
|
|
222
224
|
PreferWalrusComprehensionFilter.id: PreferWalrusComprehensionFilter,
|
|
223
225
|
PreferWalrusStreamLoop.id: PreferWalrusStreamLoop,
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
5
|
import re
|
|
6
6
|
import tokenize
|
|
7
|
+
import tomllib
|
|
7
8
|
from typing import TYPE_CHECKING, ClassVar, override
|
|
8
9
|
|
|
9
10
|
from sarj_python_lint.rule_base import (
|
|
@@ -227,6 +228,27 @@ _CODING_COOKIE_RE = re.compile(r"coding[:=]\s*[-_.a-zA-Z0-9]+")
|
|
|
227
228
|
# Only `>>>` arms a doctest block.
|
|
228
229
|
_DOCTEST_PROMPT = ">>>"
|
|
229
230
|
|
|
231
|
+
_SCRIPT_METADATA_RE = re.compile(r"^# /// script\n(?P<content>(?:#(?: .*)?\n)+)# ///(?=\n|$)", re.MULTILINE)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _script_metadata_lines(source: str, standalone: Sequence[PositionedComment]) -> frozenset[int]:
|
|
235
|
+
normalized = "\n".join(source.splitlines())
|
|
236
|
+
matches = list(_SCRIPT_METADATA_RE.finditer(normalized))
|
|
237
|
+
if len(matches) != 1:
|
|
238
|
+
return frozenset()
|
|
239
|
+
match = matches[0]
|
|
240
|
+
first = normalized.count("\n", 0, match.start()) + 1
|
|
241
|
+
lines = frozenset(range(first, first + match.group().count("\n") + 1))
|
|
242
|
+
top_level = {line for line, col, _ in standalone if col == 0}
|
|
243
|
+
if not lines <= top_level:
|
|
244
|
+
return frozenset()
|
|
245
|
+
content = "\n".join(line[2:] if line.startswith("# ") else "" for line in match.group("content").splitlines())
|
|
246
|
+
try:
|
|
247
|
+
tomllib.loads(content)
|
|
248
|
+
except tomllib.TOMLDecodeError:
|
|
249
|
+
return frozenset()
|
|
250
|
+
return lines
|
|
251
|
+
|
|
230
252
|
|
|
231
253
|
def _is_coding_cookie(body: str) -> bool:
|
|
232
254
|
return bool(_CODING_COOKIE_RE.search(body))
|
|
@@ -499,6 +521,8 @@ class NoCommentCruft(Rule):
|
|
|
499
521
|
standalone, first_code_line = standalone_comments(source)
|
|
500
522
|
except tokenize.TokenError, IndentationError, SyntaxError:
|
|
501
523
|
return []
|
|
524
|
+
skip = _script_metadata_lines(source, standalone)
|
|
525
|
+
standalone = [comment for comment in standalone if comment.line not in skip]
|
|
502
526
|
diags: dict[int, Diagnostic] = {}
|
|
503
527
|
by_line = {line: (col, body) for line, col, body in standalone}
|
|
504
528
|
skip = _doctest_block_lines(standalone) | _illustration_block_lines(standalone)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
parse_or_none,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
type _Literal = str | bytes | int | float
|
|
28
|
+
_MIN_TESTS = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@final
|
|
32
|
+
class PreferMatchValueDispatch(Rule):
|
|
33
|
+
id: str = "prefer-match-value-dispatch"
|
|
34
|
+
code: str = "SARJ439"
|
|
35
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
36
|
+
summary="Prefer match/case for repeated equality dispatch with a fallback.",
|
|
37
|
+
rationale="A single dispatch subject makes distinct value cases and their fallback easier to review.",
|
|
38
|
+
remediation=(
|
|
39
|
+
"Consider match/case while preserving fallback and constant semantics; use literal or dotted value "
|
|
40
|
+
"patterns, never bare constant names that capture instead of compare."
|
|
41
|
+
),
|
|
42
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
43
|
+
autofix=AutofixPolicy.NONE,
|
|
44
|
+
limitations=(
|
|
45
|
+
"Only two or more equality tests on the same left-hand name or attribute chain, followed by else, are checked.",
|
|
46
|
+
"Cases must be distinct string, bytes, or numeric literals, or singly bound module constants containing them; shadowed constants and singleton values are excluded.",
|
|
47
|
+
"This is a readability suggestion, not proof of exhaustiveness or a safe automatic rewrite: attributes can have side effects and match evaluates its subject once.",
|
|
48
|
+
),
|
|
49
|
+
examples=(
|
|
50
|
+
RuleExample(
|
|
51
|
+
example_id="named-file-dispatch",
|
|
52
|
+
title="Make repeated filename dispatch explicit",
|
|
53
|
+
outcome=ExampleOutcome.MATCH,
|
|
54
|
+
files=(
|
|
55
|
+
ExampleFile.python(
|
|
56
|
+
"app/migrate.py",
|
|
57
|
+
"_ESLINT_SUPPRESSIONS = 'eslint.json'\n"
|
|
58
|
+
"_RATCHET_SUPPRESSIONS = 'ratchet.json'\n"
|
|
59
|
+
"def migrate(path):\n"
|
|
60
|
+
" if path.name == _ESLINT_SUPPRESSIONS:\n"
|
|
61
|
+
" migrated = rewrite_eslint(path)\n"
|
|
62
|
+
" elif path.name == _RATCHET_SUPPRESSIONS:\n"
|
|
63
|
+
" migrated = rewrite_ratchet(path)\n"
|
|
64
|
+
" else:\n"
|
|
65
|
+
" migrated = rewrite(path)\n"
|
|
66
|
+
" return migrated\n",
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
focus_path=PurePosixPath("app/migrate.py"),
|
|
70
|
+
expected_count=1,
|
|
71
|
+
public=True,
|
|
72
|
+
),
|
|
73
|
+
RuleExample(
|
|
74
|
+
example_id="literal-match-dispatch",
|
|
75
|
+
title="Use value cases and retain the fallback",
|
|
76
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
77
|
+
files=(
|
|
78
|
+
ExampleFile.python(
|
|
79
|
+
"app/migrate.py",
|
|
80
|
+
"def migrate(path):\n"
|
|
81
|
+
" match path.name:\n"
|
|
82
|
+
" case 'eslint.json':\n"
|
|
83
|
+
" return rewrite_eslint(path)\n"
|
|
84
|
+
" case 'ratchet.json':\n"
|
|
85
|
+
" return rewrite_ratchet(path)\n"
|
|
86
|
+
" case _:\n"
|
|
87
|
+
" return rewrite(path)\n",
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
focus_path=PurePosixPath("app/migrate.py"),
|
|
91
|
+
expected_count=0,
|
|
92
|
+
public=True,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
description: str = documentation.summary
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
100
|
+
if is_generated(path, source):
|
|
101
|
+
return []
|
|
102
|
+
tree = parse_or_none(path, source)
|
|
103
|
+
if tree is None:
|
|
104
|
+
return []
|
|
105
|
+
constants = _module_constants(tree)
|
|
106
|
+
lines = source.splitlines()
|
|
107
|
+
continuations: set[int] = set()
|
|
108
|
+
findings: list[Diagnostic] = []
|
|
109
|
+
for node in ast.walk(tree):
|
|
110
|
+
if not isinstance(node, ast.If) or id(node) in continuations:
|
|
111
|
+
continue
|
|
112
|
+
branches = [node]
|
|
113
|
+
current = node
|
|
114
|
+
while len(current.orelse) == 1:
|
|
115
|
+
child = current.orelse[0]
|
|
116
|
+
if not isinstance(child, ast.If) or not lines[child.lineno - 1].lstrip().startswith("elif"):
|
|
117
|
+
break
|
|
118
|
+
continuations.add(id(child))
|
|
119
|
+
branches.append(child)
|
|
120
|
+
current = child
|
|
121
|
+
if len(branches) < _MIN_TESTS or not current.orelse:
|
|
122
|
+
continue
|
|
123
|
+
subject = _dispatch_subject(branches, constants)
|
|
124
|
+
if subject is not None:
|
|
125
|
+
findings.append(
|
|
126
|
+
Diagnostic(
|
|
127
|
+
path=path,
|
|
128
|
+
line=node.lineno,
|
|
129
|
+
col=node.col_offset + 1,
|
|
130
|
+
code=self.code,
|
|
131
|
+
severity=Severity.WARNING,
|
|
132
|
+
message=(
|
|
133
|
+
f"Repeated equality dispatch on '{subject}' with a fallback — consider match/case; "
|
|
134
|
+
"preserve fallback and constant semantics, and review repeated attribute evaluation."
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return findings
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _module_constants(tree: ast.Module) -> dict[str, _Literal]:
|
|
142
|
+
bindings: Counter[str] = Counter()
|
|
143
|
+
for node in ast.walk(tree):
|
|
144
|
+
match node:
|
|
145
|
+
case (
|
|
146
|
+
ast.Name(id=name, ctx=ast.Store() | ast.Del())
|
|
147
|
+
| ast.arg(arg=name)
|
|
148
|
+
| ast.FunctionDef(name=name)
|
|
149
|
+
| ast.AsyncFunctionDef(name=name)
|
|
150
|
+
| ast.ClassDef(name=name)
|
|
151
|
+
| ast.TypeVar(name=name)
|
|
152
|
+
| ast.ParamSpec(name=name)
|
|
153
|
+
| ast.TypeVarTuple(name=name)
|
|
154
|
+
):
|
|
155
|
+
bindings[name] += 1
|
|
156
|
+
case ast.alias(name=name, asname=alias):
|
|
157
|
+
if name == "*":
|
|
158
|
+
return {}
|
|
159
|
+
bindings[alias or name.partition(".")[0]] += 1
|
|
160
|
+
case (
|
|
161
|
+
ast.ExceptHandler(name=name)
|
|
162
|
+
| ast.MatchAs(name=name)
|
|
163
|
+
| ast.MatchStar(name=name)
|
|
164
|
+
| ast.MatchMapping(rest=name)
|
|
165
|
+
):
|
|
166
|
+
if name is not None:
|
|
167
|
+
bindings[name] += 1
|
|
168
|
+
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
169
|
+
bindings.update(names)
|
|
170
|
+
case _:
|
|
171
|
+
pass
|
|
172
|
+
constants: dict[str, _Literal] = {}
|
|
173
|
+
for statement in tree.body:
|
|
174
|
+
match statement:
|
|
175
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=value):
|
|
176
|
+
literal = _literal(value)
|
|
177
|
+
case ast.AnnAssign(target=ast.Name(id=name), value=value) if value is not None:
|
|
178
|
+
literal = _literal(value)
|
|
179
|
+
case _:
|
|
180
|
+
continue
|
|
181
|
+
if literal is not None and bindings[name] == 1:
|
|
182
|
+
constants[name] = literal
|
|
183
|
+
return constants
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _literal(expression: ast.expr) -> _Literal | None:
|
|
187
|
+
match expression:
|
|
188
|
+
case ast.Constant(value=value) if isinstance(value, (str, bytes, int, float)) and not isinstance(value, bool):
|
|
189
|
+
return value
|
|
190
|
+
case ast.UnaryOp(op=ast.USub() | ast.UAdd(), operand=ast.Constant(value=value)) if isinstance(
|
|
191
|
+
value, (int, float)
|
|
192
|
+
) and not isinstance(value, bool):
|
|
193
|
+
return -value if isinstance(expression.op, ast.USub) else value
|
|
194
|
+
case _:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _dispatch_subject(branches: list[ast.If], constants: dict[str, _Literal]) -> str | None:
|
|
199
|
+
subject: str | None = None
|
|
200
|
+
values: set[_Literal] = set()
|
|
201
|
+
bodies: set[tuple[str, ...]] = set()
|
|
202
|
+
for branch in branches:
|
|
203
|
+
body = tuple(ast.dump(statement) for statement in branch.body)
|
|
204
|
+
if body in bodies:
|
|
205
|
+
return None
|
|
206
|
+
bodies.add(body)
|
|
207
|
+
match branch.test:
|
|
208
|
+
case ast.Compare(left=left, ops=[ast.Eq()], comparators=[right]):
|
|
209
|
+
candidate = _subject(left)
|
|
210
|
+
value = constants.get(right.id) if isinstance(right, ast.Name) else _literal(right)
|
|
211
|
+
case _:
|
|
212
|
+
return None
|
|
213
|
+
if candidate is None or candidate.partition(".")[0] in constants or value is None or value in values:
|
|
214
|
+
return None
|
|
215
|
+
if subject is not None and candidate != subject:
|
|
216
|
+
return None
|
|
217
|
+
subject = candidate
|
|
218
|
+
values.add(value)
|
|
219
|
+
return subject
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _subject(expression: ast.expr) -> str | None:
|
|
223
|
+
match expression:
|
|
224
|
+
case ast.Name(id=name):
|
|
225
|
+
return name
|
|
226
|
+
case ast.Attribute(value=value, attr=attribute):
|
|
227
|
+
parent = _subject(value)
|
|
228
|
+
return f"{parent}.{attribute}" if parent is not None else None
|
|
229
|
+
case _:
|
|
230
|
+
return None
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
@@ -1492,12 +1492,13 @@ def _merge_cluster(
|
|
|
1492
1492
|
entry = clusters.get(key, (*pos, set[str](), set[str](), set[str](), set[str](), False))
|
|
1493
1493
|
line, col, seen, eq_seen, ne_seen, in_seen, was_closed = entry
|
|
1494
1494
|
line, col = min((line, col), pos)
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1495
|
+
match operator:
|
|
1496
|
+
case "==":
|
|
1497
|
+
eq_seen |= set(literals)
|
|
1498
|
+
case "!=":
|
|
1499
|
+
ne_seen |= set(literals)
|
|
1500
|
+
case _:
|
|
1501
|
+
in_seen |= set(literals)
|
|
1501
1502
|
clusters[key] = (line, col, seen | set(literals), eq_seen, ne_seen, in_seen, was_closed or closed)
|
|
1502
1503
|
|
|
1503
1504
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/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
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/__init__.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_comments.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_fastapi.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_imports.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/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.80.15 → sarj_python_lint-0.80.17}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/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
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/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
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.80.15 → sarj_python_lint-0.80.17}/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
|
|
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.80.15 → sarj_python_lint-0.80.17}/src/sarj_python_lint/rules/stepdown.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
|