sarj-python-lint 0.70.1__tar.gz → 0.72.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/__main__.py +27 -10
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rule_base.py +12 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_comments.py +14 -8
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_fastapi.py +42 -25
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_first_party.py +5 -0
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/_project_index.py +366 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_registry.py +12 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +10 -6
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +22 -11
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +24 -12
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +11 -4
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +53 -39
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/over_mocked_test.py +18 -8
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +21 -9
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +25 -12
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +11 -6
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +190 -24
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_str_enum.py +28 -10
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +39 -6
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +16 -7
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +143 -0
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/preserve_enum_types.py +136 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +15 -5
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/redundant_docstring.py +5 -2
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +31 -18
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/require_port_for_service.py +13 -5
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +164 -17
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +171 -0
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/require_validated_row_factory.py +159 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/source_coupled_test.py +6 -0
- sarj_python_lint-0.72.0/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +207 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +11 -5
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +15 -7
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/.gitignore +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/LICENSE +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/README.md +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.72.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -9,12 +9,14 @@ import json
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import sys
|
|
11
11
|
from types import MappingProxyType
|
|
12
|
+
from typing import NamedTuple
|
|
12
13
|
|
|
13
14
|
from sarj_python_lint import __version__
|
|
14
15
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
15
|
-
from sarj_python_lint.rule_base import Diagnostic, Severity, is_suppressed
|
|
16
|
+
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Severity, is_suppressed
|
|
16
17
|
from sarj_python_lint.rules import REGISTRY
|
|
17
18
|
from sarj_python_lint.rules._paths import clear_path_caches
|
|
19
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
SKIP_DIR_NAMES = frozenset(
|
|
@@ -79,12 +81,19 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
79
81
|
rules = [REGISTRY[rid]() for rid in rule_ids]
|
|
80
82
|
clear_path_caches()
|
|
81
83
|
expanded = _expand_paths(paths)
|
|
82
|
-
|
|
83
|
-
for
|
|
84
|
+
loaded: dict[Path, str] = {}
|
|
85
|
+
for path in expanded:
|
|
84
86
|
try:
|
|
85
|
-
|
|
87
|
+
loaded[path] = path.read_text(encoding="utf-8", errors="replace")
|
|
86
88
|
except OSError:
|
|
87
89
|
continue
|
|
90
|
+
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
91
|
+
if project_rules:
|
|
92
|
+
indexes = ProjectIndexSet.build(expanded, loaded)
|
|
93
|
+
for rule in project_rules:
|
|
94
|
+
rule.prepare(indexes)
|
|
95
|
+
diags: list[Diagnostic] = []
|
|
96
|
+
for p, source in loaded.items():
|
|
88
97
|
source_lines = source.splitlines()
|
|
89
98
|
raw = [diagnostic for rule in rules for diagnostic in rule.check(p, source)]
|
|
90
99
|
diags.extend(
|
|
@@ -120,6 +129,11 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
120
129
|
)
|
|
121
130
|
|
|
122
131
|
|
|
132
|
+
class _OwnerLocation(NamedTuple):
|
|
133
|
+
line: int
|
|
134
|
+
column: int
|
|
135
|
+
|
|
136
|
+
|
|
123
137
|
def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = None) -> list[Diagnostic]:
|
|
124
138
|
"""Keep the most specific remediation at a source location."""
|
|
125
139
|
codes = frozenset(diagnostic.code for diagnostic in diags)
|
|
@@ -130,15 +144,17 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
|
|
|
130
144
|
_function_signature_owner_locations(source) if source is not None and needs_signature_owners else {}
|
|
131
145
|
)
|
|
132
146
|
|
|
133
|
-
def owner_location(diagnostic: Diagnostic) ->
|
|
147
|
+
def owner_location(diagnostic: Diagnostic) -> _OwnerLocation:
|
|
134
148
|
if diagnostic.code in {"SARJ034", "SARJ093"}:
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
line, column = signature_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
|
|
150
|
+
else:
|
|
151
|
+
line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
|
|
152
|
+
return _OwnerLocation(line, column)
|
|
137
153
|
|
|
138
154
|
present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
|
|
139
155
|
for diagnostic in diags:
|
|
140
|
-
|
|
141
|
-
by_code = present.setdefault((diagnostic.path, line,
|
|
156
|
+
location = owner_location(diagnostic)
|
|
157
|
+
by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
|
|
142
158
|
by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
|
|
143
159
|
suppressed = {
|
|
144
160
|
(location, generic, generic_severity)
|
|
@@ -156,7 +172,8 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
|
|
|
156
172
|
(
|
|
157
173
|
(
|
|
158
174
|
diagnostic.path,
|
|
159
|
-
|
|
175
|
+
owner_location(diagnostic).line,
|
|
176
|
+
owner_location(diagnostic).column,
|
|
160
177
|
),
|
|
161
178
|
diagnostic.code,
|
|
162
179
|
diagnostic.severity,
|
|
@@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, ClassVar, Final, Self
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from collections.abc import Sequence
|
|
16
16
|
|
|
17
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
# Each rule points directly to its executable examples.
|
|
19
21
|
REPO_BLOB: Final = "https://github.com/sarj-ai/standards/blob/main"
|
|
@@ -309,6 +311,16 @@ class Rule(ABC):
|
|
|
309
311
|
return () if spec is None else spec.public_examples
|
|
310
312
|
|
|
311
313
|
|
|
314
|
+
class ProjectRule(Rule):
|
|
315
|
+
"""A rule that may resolve first-party symbols prepared once per CLI run."""
|
|
316
|
+
|
|
317
|
+
_project_indexes: ProjectIndexSet | None = None
|
|
318
|
+
|
|
319
|
+
def prepare(self, indexes: ProjectIndexSet) -> None:
|
|
320
|
+
"""Attach immutable project symbols before checking the selected files."""
|
|
321
|
+
self._project_indexes = indexes
|
|
322
|
+
|
|
323
|
+
|
|
312
324
|
_last_parse: tuple[str, str, ast.Module | None] | None = None
|
|
313
325
|
|
|
314
326
|
|
|
@@ -314,15 +314,21 @@ class _CommentScan[T](NamedTuple):
|
|
|
314
314
|
first_code_line: int
|
|
315
315
|
|
|
316
316
|
|
|
317
|
-
_Scan
|
|
317
|
+
class _Scan(NamedTuple):
|
|
318
|
+
standalone: list[tuple[int, int, str]]
|
|
319
|
+
trailing: list[tuple[int, int, str]]
|
|
320
|
+
nested: set[int]
|
|
321
|
+
first_code_line: int
|
|
322
|
+
ordered: _Ordered
|
|
323
|
+
|
|
318
324
|
|
|
319
325
|
_last_scan: tuple[str, _Scan] | None = None
|
|
320
326
|
|
|
321
327
|
|
|
322
328
|
def all_comments(source: str) -> _CommentScan[_Ordered]:
|
|
323
329
|
"""Return every comment as `(line, col0, body, standalone)`, plus the first code line."""
|
|
324
|
-
|
|
325
|
-
return _CommentScan(ordered, first_code_line)
|
|
330
|
+
scan = _scan_memo(source)
|
|
331
|
+
return _CommentScan(scan.ordered, scan.first_code_line)
|
|
326
332
|
|
|
327
333
|
|
|
328
334
|
def _scan_memo(source: str) -> _Scan:
|
|
@@ -361,23 +367,23 @@ def _scan(source: str) -> _Scan:
|
|
|
361
367
|
prev_end_row = tok.end[0]
|
|
362
368
|
if tok.type not in _NON_CODE_TOKENS:
|
|
363
369
|
first_code_line = min(first_code_line, tok.start[0])
|
|
364
|
-
return standalone, trailing, nested, first_code_line, ordered
|
|
370
|
+
return _Scan(standalone, trailing, nested, first_code_line, ordered)
|
|
365
371
|
|
|
366
372
|
|
|
367
373
|
def trailing_comments(source: str) -> list[tuple[int, int, str]]:
|
|
368
374
|
"""Return every comment that shares its line with code, as `(line, col, body)`."""
|
|
369
|
-
return _scan_memo(source)
|
|
375
|
+
return _scan_memo(source).trailing
|
|
370
376
|
|
|
371
377
|
|
|
372
378
|
def nested_comment_lines(source: str) -> set[int]:
|
|
373
379
|
"""Return the lines of comments sitting INSIDE a bracketed expression."""
|
|
374
|
-
return _scan_memo(source)
|
|
380
|
+
return _scan_memo(source).nested
|
|
375
381
|
|
|
376
382
|
|
|
377
383
|
def standalone_comments(source: str) -> _CommentScan[list[tuple[int, int, str]]]:
|
|
378
384
|
"""Return every own-line comment as `(line, col, body)`, plus the first code line."""
|
|
379
|
-
|
|
380
|
-
return _CommentScan(standalone, first_code_line)
|
|
385
|
+
scan = _scan_memo(source)
|
|
386
|
+
return _CommentScan(scan.standalone, scan.first_code_line)
|
|
381
387
|
|
|
382
388
|
|
|
383
389
|
def comment_runs(standalone: Sequence[tuple[int, int, str]]) -> list[list[tuple[int, int, str]]]:
|
|
@@ -74,6 +74,16 @@ class ParameterMarker(NamedTuple):
|
|
|
74
74
|
call: ast.Call
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
class _ReceiverKey(NamedTuple):
|
|
78
|
+
scope: int
|
|
79
|
+
name: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _DecoratorBinding(NamedTuple):
|
|
83
|
+
receiver: str
|
|
84
|
+
method: str
|
|
85
|
+
|
|
86
|
+
|
|
77
87
|
@dataclass(frozen=True, slots=True)
|
|
78
88
|
class _ImportedReference:
|
|
79
89
|
module: str
|
|
@@ -113,12 +123,12 @@ class FastapiIndex:
|
|
|
113
123
|
self.annotated: set[str] = set()
|
|
114
124
|
self.symbols: dict[str, str] = {}
|
|
115
125
|
self.type_aliases: dict[str, ast.expr] = {}
|
|
116
|
-
self.receivers: set[
|
|
117
|
-
self.receiver_origins: dict[
|
|
118
|
-
self.receiver_kinds: dict[
|
|
119
|
-
self.hidden_receivers: set[
|
|
120
|
-
self.decorators: dict[
|
|
121
|
-
self.bound_names: set[
|
|
126
|
+
self.receivers: set[_ReceiverKey] = set()
|
|
127
|
+
self.receiver_origins: dict[_ReceiverKey, _ReceiverKey] = {}
|
|
128
|
+
self.receiver_kinds: dict[_ReceiverKey, Literal["FastAPI", "APIRouter"]] = {}
|
|
129
|
+
self.hidden_receivers: set[_ReceiverKey] = set()
|
|
130
|
+
self.decorators: dict[_ReceiverKey, _DecoratorBinding] = {}
|
|
131
|
+
self.bound_names: set[_ReceiverKey] = set()
|
|
122
132
|
self._node_scopes: dict[int, int] = {}
|
|
123
133
|
self._node_classes: dict[int, int | None] = {}
|
|
124
134
|
self._route_scopes: dict[int, int] = {}
|
|
@@ -182,31 +192,37 @@ class FastapiIndex:
|
|
|
182
192
|
assignments: list[tuple[int, str, ast.expr]] = []
|
|
183
193
|
for node in ast.walk(tree):
|
|
184
194
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
185
|
-
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
195
|
+
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
186
196
|
arguments = [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]
|
|
187
197
|
if node.args.vararg is not None:
|
|
188
198
|
arguments.append(node.args.vararg)
|
|
189
199
|
if node.args.kwarg is not None:
|
|
190
200
|
arguments.append(node.args.kwarg)
|
|
191
|
-
self.bound_names.update((node.lineno, argument.arg) for argument in arguments)
|
|
201
|
+
self.bound_names.update(_ReceiverKey(node.lineno, argument.arg) for argument in arguments)
|
|
192
202
|
elif isinstance(node, ast.ClassDef):
|
|
193
|
-
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
203
|
+
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
194
204
|
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
195
205
|
self.bound_names.update(
|
|
196
|
-
(self._node_scopes[id(node)], alias.asname or alias.name.split(".")[0])
|
|
206
|
+
_ReceiverKey(self._node_scopes[id(node)], alias.asname or alias.name.split(".")[0])
|
|
207
|
+
for alias in node.names
|
|
197
208
|
)
|
|
198
209
|
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
199
|
-
self.bound_names.update(
|
|
210
|
+
self.bound_names.update(
|
|
211
|
+
_ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(node.target)
|
|
212
|
+
)
|
|
200
213
|
elif isinstance(node, ast.With):
|
|
201
214
|
for item in node.items:
|
|
202
215
|
if item.optional_vars is not None:
|
|
203
216
|
self.bound_names.update(
|
|
204
|
-
(self._node_scopes[id(node)], name)
|
|
217
|
+
_ReceiverKey(self._node_scopes[id(node)], name)
|
|
218
|
+
for name in self._target_names(item.optional_vars)
|
|
205
219
|
)
|
|
206
220
|
elif isinstance(node, ast.ExceptHandler) and node.name is not None:
|
|
207
|
-
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
221
|
+
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
208
222
|
elif isinstance(node, ast.NamedExpr):
|
|
209
|
-
self.bound_names.update(
|
|
223
|
+
self.bound_names.update(
|
|
224
|
+
_ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(node.target)
|
|
225
|
+
)
|
|
210
226
|
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
211
227
|
name = _binding_name(node.targets[0])
|
|
212
228
|
if name:
|
|
@@ -219,12 +235,12 @@ class FastapiIndex:
|
|
|
219
235
|
assignments.append((self._node_scopes[id(node)], node.name.id, node.value))
|
|
220
236
|
|
|
221
237
|
changed = True
|
|
222
|
-
counts = Counter((scope, name) for scope, name, _value in assignments)
|
|
238
|
+
counts = Counter(_ReceiverKey(scope, name) for scope, name, _value in assignments)
|
|
223
239
|
self.bound_names.update(counts)
|
|
224
240
|
while changed:
|
|
225
241
|
changed = False
|
|
226
242
|
for scope, name, value in assignments:
|
|
227
|
-
key = (scope, name)
|
|
243
|
+
key = _ReceiverKey(scope, name)
|
|
228
244
|
if counts[key] != 1:
|
|
229
245
|
continue
|
|
230
246
|
if key in self.receivers or key in self.decorators:
|
|
@@ -250,7 +266,7 @@ class FastapiIndex:
|
|
|
250
266
|
if isinstance(value, ast.Attribute) and value.attr in HTTP_METHODS:
|
|
251
267
|
receiver = _binding_name(value.value)
|
|
252
268
|
if self._receiver_key(scope, receiver) is not None:
|
|
253
|
-
self.decorators[key] = (receiver, value.attr)
|
|
269
|
+
self.decorators[key] = _DecoratorBinding(receiver, value.attr)
|
|
254
270
|
changed = True
|
|
255
271
|
continue
|
|
256
272
|
if self._is_annotated(value) or (isinstance(value, ast.Name) and value.id in self.type_aliases):
|
|
@@ -268,12 +284,12 @@ class FastapiIndex:
|
|
|
268
284
|
return self._node_classes[id(node)] or self._node_scopes[id(node)]
|
|
269
285
|
return self._node_scopes[id(node)]
|
|
270
286
|
|
|
271
|
-
def _receiver_key(self, scope: int, name: str) ->
|
|
287
|
+
def _receiver_key(self, scope: int, name: str) -> _ReceiverKey | None:
|
|
272
288
|
if not name:
|
|
273
289
|
return None
|
|
274
290
|
current: int | None = scope
|
|
275
291
|
while current is not None:
|
|
276
|
-
binding = (current, name)
|
|
292
|
+
binding = _ReceiverKey(current, name)
|
|
277
293
|
if binding in self.receivers:
|
|
278
294
|
return binding
|
|
279
295
|
if binding in self.bound_names:
|
|
@@ -325,7 +341,8 @@ class FastapiIndex:
|
|
|
325
341
|
if receiver_key is not None:
|
|
326
342
|
method = decorator.func.attr
|
|
327
343
|
elif isinstance(decorator.func, ast.Name) and (resolved := self._decorator(scope, decorator.func.id)):
|
|
328
|
-
receiver
|
|
344
|
+
receiver = resolved.receiver
|
|
345
|
+
method = resolved.method
|
|
329
346
|
receiver_key = self._receiver_key(scope, receiver)
|
|
330
347
|
if method not in HTTP_METHODS or receiver_key is None:
|
|
331
348
|
continue
|
|
@@ -336,11 +353,11 @@ class FastapiIndex:
|
|
|
336
353
|
)
|
|
337
354
|
path = path_node.value if isinstance(path_node, ast.Constant) and isinstance(path_node.value, str) else None
|
|
338
355
|
route_methods = self._route_methods(decorator) if method == "api_route" else (method,)
|
|
339
|
-
|
|
356
|
+
origin = self.receiver_origins[receiver_key]
|
|
340
357
|
routes.extend(
|
|
341
358
|
Route(
|
|
342
359
|
decorator=decorator,
|
|
343
|
-
receiver=f"{
|
|
360
|
+
receiver=f"{origin.scope}:{origin.name}",
|
|
344
361
|
method=route_method,
|
|
345
362
|
path=path,
|
|
346
363
|
receiver_kind=self.receiver_kinds[receiver_key],
|
|
@@ -350,10 +367,10 @@ class FastapiIndex:
|
|
|
350
367
|
)
|
|
351
368
|
return tuple(routes)
|
|
352
369
|
|
|
353
|
-
def _decorator(self, scope: int, name: str) ->
|
|
370
|
+
def _decorator(self, scope: int, name: str) -> _DecoratorBinding | None:
|
|
354
371
|
current: int | None = scope
|
|
355
372
|
while current is not None:
|
|
356
|
-
binding = (current, name)
|
|
373
|
+
binding = _ReceiverKey(current, name)
|
|
357
374
|
if binding in self.decorators:
|
|
358
375
|
return self.decorators[binding]
|
|
359
376
|
if binding in self.bound_names:
|
|
@@ -473,7 +490,7 @@ class FastapiIndex:
|
|
|
473
490
|
# it, so begin with that function's parent scope.
|
|
474
491
|
scope: int | None = self._scope_parents.get(node_scope)
|
|
475
492
|
while scope is not None and scope != 0:
|
|
476
|
-
if (scope, root) in self.bound_names:
|
|
493
|
+
if _ReceiverKey(scope, root) in self.bound_names:
|
|
477
494
|
return True
|
|
478
495
|
scope = self._scope_parents.get(scope)
|
|
479
496
|
return False
|
{sarj_python_lint-0.70.1 → sarj_python_lint-0.72.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
@@ -126,6 +126,11 @@ def has_first_party_source(module: str, path: Path) -> bool:
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def project_root(path: Path) -> Path | None:
|
|
130
|
+
"""Return the conservative repository/workspace root used by project-aware rules."""
|
|
131
|
+
return _project_root(path)
|
|
132
|
+
|
|
133
|
+
|
|
129
134
|
def _declares_module(package_dir: Path, segments: list[str]) -> bool:
|
|
130
135
|
target = package_dir.joinpath(*segments)
|
|
131
136
|
try:
|