sarj-python-lint 0.17.0__tar.gz → 0.18.1__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.17.0 → sarj_python_lint-0.18.1}/PKG-INFO +1 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/pyproject.toml +1 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rule_base.py +5 -5
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_paths.py +6 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +11 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/kwonly_same_type_params.py +34 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py +57 -42
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +8 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sequential_await.py +2 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +9 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_str_enum.py +18 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +30 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/single_public_export.py +8 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/stepdown.py +10 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/xfail_requires_strict.py +3 -1
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/.gitignore +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/README.md +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/zero_assertion_test.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.18.1
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
|
|
6
6
|
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
@@ -87,7 +87,7 @@ class Rule(ABC):
|
|
|
87
87
|
raise NotImplementedError
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
_last_parse: tuple[
|
|
90
|
+
_last_parse: tuple[str, str, ast.Module | None] | None = None
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def parse_or_none(path: Path, source: str) -> ast.Module | None:
|
|
@@ -98,12 +98,12 @@ def parse_or_none(path: Path, source: str) -> ast.Module | None:
|
|
|
98
98
|
|
|
99
99
|
"""
|
|
100
100
|
global _last_parse # ruff:ignore[global-statement] — single-slot memo; the CLI runs rules per file sequentially
|
|
101
|
-
|
|
102
|
-
if _last_parse is not None and _last_parse[0] ==
|
|
103
|
-
return _last_parse[
|
|
101
|
+
path_key = str(path)
|
|
102
|
+
if _last_parse is not None and _last_parse[0] == path_key and _last_parse[1] is source:
|
|
103
|
+
return _last_parse[2]
|
|
104
104
|
try:
|
|
105
105
|
tree = ast.parse(source, filename=str(path))
|
|
106
106
|
except SyntaxError:
|
|
107
107
|
tree = None
|
|
108
|
-
_last_parse = (
|
|
108
|
+
_last_parse = (path_key, source, tree)
|
|
109
109
|
return tree
|
|
@@ -23,7 +23,12 @@ if TYPE_CHECKING:
|
|
|
23
23
|
|
|
24
24
|
_TEST_DIR_NAMES = frozenset({"tests", "test"})
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# `auto-?generated` misses "automatically generated", which is the phrase
|
|
27
|
+
# @generated tooling, protoc and OpenAPI generators emit most often.
|
|
28
|
+
_GENERATED_RE = re.compile(
|
|
29
|
+
r"auto(?:-|matically )?generated|generated by|do not edit|@generated",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
_GENERATED_HEADER_LINES = 5
|
|
29
34
|
|
|
@@ -34,6 +34,12 @@ References:
|
|
|
34
34
|
- https://docs.python.org/3/library/stdtypes.html#str.join
|
|
35
35
|
- https://wiki.python.org/moin/PythonSpeed/PerformanceTips
|
|
36
36
|
|
|
37
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
38
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
39
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
40
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
41
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
42
|
+
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
from __future__ import annotations
|
|
@@ -42,6 +48,7 @@ import ast
|
|
|
42
48
|
from typing import TYPE_CHECKING, TypeGuard, override
|
|
43
49
|
|
|
44
50
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
51
|
+
from sarj_python_lint.rules._paths import is_generated_source
|
|
45
52
|
|
|
46
53
|
|
|
47
54
|
if TYPE_CHECKING:
|
|
@@ -58,6 +65,10 @@ class InefficientStringConcatInLoop(Rule):
|
|
|
58
65
|
|
|
59
66
|
@override
|
|
60
67
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
68
|
+
if is_generated_source(source):
|
|
69
|
+
return []
|
|
70
|
+
if "+" not in source or ("for " not in source and "while " not in source):
|
|
71
|
+
return []
|
|
61
72
|
tree = parse_or_none(path, source)
|
|
62
73
|
if tree is None:
|
|
63
74
|
return []
|
|
@@ -157,16 +157,30 @@ _DUCK_PROTOCOL_METHODS = frozenset(
|
|
|
157
157
|
#: drawn entirely from one of these reads unambiguously positionally.
|
|
158
158
|
_CONVENTIONAL_ORDER_GROUPS = (
|
|
159
159
|
frozenset({"x", "y", "z"}),
|
|
160
|
+
frozenset({"lat", "lon", "alt"}),
|
|
161
|
+
frozenset({"latitude", "longitude", "altitude"}),
|
|
160
162
|
frozenset({"width", "height", "depth"}),
|
|
161
163
|
frozenset({"red", "green", "blue", "alpha"}),
|
|
162
164
|
frozenset({"row", "column"}),
|
|
163
165
|
frozenset({"top", "right", "bottom", "left"}),
|
|
166
|
+
frozenset({"left", "right"}),
|
|
167
|
+
frozenset({"lo", "hi"}),
|
|
168
|
+
frozenset({"low", "high"}),
|
|
169
|
+
frozenset({"minimum", "maximum"}),
|
|
170
|
+
frozenset({"min_value", "max_value"}),
|
|
171
|
+
frozenset({"begin", "end"}),
|
|
172
|
+
frozenset({"source", "sink"}),
|
|
164
173
|
frozenset({"year", "month", "day"}),
|
|
165
174
|
frozenset({"hour", "minute", "second", "microsecond"}),
|
|
166
175
|
frozenset({"start", "stop", "step"}),
|
|
167
176
|
)
|
|
168
177
|
|
|
169
178
|
_EXEMPT_NAME_PREFIXES = ("visit_", "test_")
|
|
179
|
+
_RISKY_NAME_PART_RE = re.compile(
|
|
180
|
+
r"(?:^|_)(?:id|key|token|secret|password|signature|hash|email|url|uri|path|file|"
|
|
181
|
+
r"source|src|target|dst|dest|destination|parent|child|from|to|old|new|"
|
|
182
|
+
r"before|after|previous|next|expected|actual|left_id|right_id)(?:_|$)"
|
|
183
|
+
)
|
|
170
184
|
|
|
171
185
|
|
|
172
186
|
class KwonlySameTypeParams(Rule):
|
|
@@ -350,11 +364,30 @@ def _swap_prone_annotation(args: ast.arguments) -> str | None:
|
|
|
350
364
|
for name, arg_names in sorted(groups.items(), key=lambda kv: -len(kv[1])):
|
|
351
365
|
if len(arg_names) >= _MIN_SAME_TYPE and not (
|
|
352
366
|
_is_symmetric_numbering(arg_names) or _is_conventional_order(arg_names)
|
|
353
|
-
):
|
|
367
|
+
) and _is_high_value_group(name, arg_names):
|
|
354
368
|
return name
|
|
355
369
|
return None
|
|
356
370
|
|
|
357
371
|
|
|
372
|
+
def _is_high_value_group(annotation: str, arg_names: list[str]) -> bool:
|
|
373
|
+
"""Report whether a same-primitive group is worth enforcing globally.
|
|
374
|
+
|
|
375
|
+
Booleans are always high-risk because positional `True, False` carries no
|
|
376
|
+
call-site meaning. Other primitives fire only when the parameter names carry
|
|
377
|
+
production-domain identifiers or directed relationships (`source_id`,
|
|
378
|
+
`target_id`, `old_key`, `new_key`, `input_path`, `output_path`). This keeps
|
|
379
|
+
math / algorithm APIs such as `power(base, exponent)` and `f(a, b)` out of
|
|
380
|
+
the default rule while preserving the bug class the rule was written for.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
True when the group should be reported.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
if annotation == "bool":
|
|
387
|
+
return True
|
|
388
|
+
return sum(1 for name in arg_names if _RISKY_NAME_PART_RE.search(name)) >= _MIN_SAME_TYPE
|
|
389
|
+
|
|
390
|
+
|
|
358
391
|
def _is_dunder_prefixed(arg: str) -> bool:
|
|
359
392
|
"""Report whether `arg` uses the PEP 484 positional-only naming convention.
|
|
360
393
|
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
@@ -69,6 +69,12 @@ References:
|
|
|
69
69
|
- https://docs.python.org/3/tutorial/errors.html#handling-exceptions
|
|
70
70
|
- https://docs.python.org/3/library/ast.html#ast.Try
|
|
71
71
|
|
|
72
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
73
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
74
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
75
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
76
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
77
|
+
|
|
72
78
|
"""
|
|
73
79
|
|
|
74
80
|
from __future__ import annotations
|
|
@@ -79,6 +85,7 @@ from typing import TYPE_CHECKING, override
|
|
|
79
85
|
|
|
80
86
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
81
87
|
from sarj_python_lint.rules._logging import is_logger_expr
|
|
88
|
+
from sarj_python_lint.rules._paths import is_generated_source
|
|
82
89
|
|
|
83
90
|
|
|
84
91
|
if TYPE_CHECKING:
|
|
@@ -124,38 +131,42 @@ def _nested_scope_body_ids(node: ast.AST) -> frozenset[int]:
|
|
|
124
131
|
#: `set` and `record` are deliberately ABSENT — `cache.set(k, v)` /
|
|
125
132
|
#: `store.record(row)` collide with them and are real work whose failure a handler
|
|
126
133
|
#: is plausibly written for.
|
|
127
|
-
_OBSERVABILITY_METHODS = frozenset(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
_OBSERVABILITY_METHODS = frozenset(
|
|
135
|
+
{
|
|
136
|
+
"inc",
|
|
137
|
+
"dec",
|
|
138
|
+
"observe",
|
|
139
|
+
"set_to_current_time",
|
|
140
|
+
"labels",
|
|
141
|
+
"increment",
|
|
142
|
+
"decrement",
|
|
143
|
+
"gauge",
|
|
144
|
+
"timing",
|
|
145
|
+
"histogram",
|
|
146
|
+
"record_exception",
|
|
147
|
+
"add_event",
|
|
148
|
+
"set_attribute",
|
|
149
|
+
"set_attributes",
|
|
150
|
+
"set_status",
|
|
151
|
+
}
|
|
152
|
+
)
|
|
144
153
|
|
|
145
154
|
#: Clock reads. `time.monotonic()` / `perf_counter()` / `time()` and
|
|
146
155
|
#: `datetime.now()` / `utcnow()` cannot raise at all, but they are the calls that
|
|
147
156
|
#: turn an elapsed-time bookkeeping line into a "throwing" statement.
|
|
148
157
|
_CLOCK_ROOTS = frozenset({"time", "datetime", "date"})
|
|
149
|
-
_CLOCK_METHODS = frozenset(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
_CLOCK_METHODS = frozenset(
|
|
159
|
+
{
|
|
160
|
+
"monotonic",
|
|
161
|
+
"monotonic_ns",
|
|
162
|
+
"perf_counter",
|
|
163
|
+
"perf_counter_ns",
|
|
164
|
+
"time",
|
|
165
|
+
"time_ns",
|
|
166
|
+
"now",
|
|
167
|
+
"utcnow",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
159
170
|
|
|
160
171
|
#: Value-shaping builtins. These are what an instrumentation line calls on its
|
|
161
172
|
#: arguments — `logger.info(..., n=len(items), elapsed_s=round(t, 2))`,
|
|
@@ -163,21 +174,23 @@ _CLOCK_METHODS = frozenset({
|
|
|
163
174
|
#: line passes them, so they don't make the statement a candidate for the
|
|
164
175
|
#: handler. Builtins that genuinely do work and raise (`open`, `eval`, `next`,
|
|
165
176
|
#: `getattr`) are deliberately absent.
|
|
166
|
-
_INERT_BUILTINS = frozenset(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
_INERT_BUILTINS = frozenset(
|
|
178
|
+
{
|
|
179
|
+
"abs",
|
|
180
|
+
"bool",
|
|
181
|
+
"float",
|
|
182
|
+
"format",
|
|
183
|
+
"id",
|
|
184
|
+
"int",
|
|
185
|
+
"len",
|
|
186
|
+
"list",
|
|
187
|
+
"repr",
|
|
188
|
+
"round",
|
|
189
|
+
"str",
|
|
190
|
+
"tuple",
|
|
191
|
+
"type",
|
|
192
|
+
}
|
|
193
|
+
)
|
|
181
194
|
|
|
182
195
|
|
|
183
196
|
def _attr_root(expr: ast.expr) -> str | None:
|
|
@@ -316,6 +329,8 @@ class NoFatTryBlocks(Rule):
|
|
|
316
329
|
|
|
317
330
|
@override
|
|
318
331
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
332
|
+
if is_generated_source(source):
|
|
333
|
+
return []
|
|
319
334
|
tree = parse_or_none(path, source)
|
|
320
335
|
if tree is None:
|
|
321
336
|
return []
|
|
@@ -70,6 +70,11 @@ duplicate can be suppressed per-line with `# sarj-noqa: SARJ024 — <reason>`.
|
|
|
70
70
|
|
|
71
71
|
Skipped entirely: `conftest.py`, test files (`test_*.py` or under a `tests/`
|
|
72
72
|
directory) — fixtures legitimately repeat literal payloads.
|
|
73
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
74
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
75
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
76
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
77
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
73
78
|
"""
|
|
74
79
|
|
|
75
80
|
from __future__ import annotations
|
|
@@ -80,6 +85,7 @@ import re
|
|
|
80
85
|
from typing import TYPE_CHECKING, override
|
|
81
86
|
|
|
82
87
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
88
|
+
from sarj_python_lint.rules._paths import is_generated_source
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
if TYPE_CHECKING:
|
|
@@ -109,6 +115,8 @@ class NoRepeatedStringLiteral(Rule):
|
|
|
109
115
|
|
|
110
116
|
@override
|
|
111
117
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
118
|
+
if is_generated_source(source):
|
|
119
|
+
return []
|
|
112
120
|
if _is_skipped_path(path):
|
|
113
121
|
return []
|
|
114
122
|
tree = parse_or_none(path, source)
|
|
@@ -76,6 +76,8 @@ class NoSequentialAwait(Rule):
|
|
|
76
76
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
77
77
|
if _is_test_path(path):
|
|
78
78
|
return []
|
|
79
|
+
if "await" not in source or "for " not in source:
|
|
80
|
+
return []
|
|
79
81
|
tree = parse_or_none(path, source)
|
|
80
82
|
if tree is None:
|
|
81
83
|
return []
|
|
@@ -70,6 +70,12 @@ Suppress a deliberate positional return with `# sarj-noqa: SARJ026 — <reason>`
|
|
|
70
70
|
References:
|
|
71
71
|
- https://docs.python.org/3/library/typing.html#typing.NamedTuple
|
|
72
72
|
|
|
73
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
74
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
75
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
76
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
77
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
78
|
+
|
|
73
79
|
"""
|
|
74
80
|
|
|
75
81
|
from __future__ import annotations
|
|
@@ -80,7 +86,7 @@ from types import EllipsisType
|
|
|
80
86
|
from typing import TYPE_CHECKING, override
|
|
81
87
|
|
|
82
88
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
83
|
-
from sarj_python_lint.rules._paths import is_test_path
|
|
89
|
+
from sarj_python_lint.rules._paths import is_generated_source, is_test_path
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
if TYPE_CHECKING:
|
|
@@ -140,6 +146,8 @@ class PreferNamedtupleOverTupleReturn(Rule):
|
|
|
140
146
|
|
|
141
147
|
@override
|
|
142
148
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
149
|
+
if is_generated_source(source):
|
|
150
|
+
return []
|
|
143
151
|
if is_test_path(path):
|
|
144
152
|
return []
|
|
145
153
|
tree = parse_or_none(path, source)
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
@@ -235,6 +235,8 @@ class PreferStrEnum(Rule):
|
|
|
235
235
|
|
|
236
236
|
@override
|
|
237
237
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
238
|
+
if not _has_str_enum_signal(source):
|
|
239
|
+
return []
|
|
238
240
|
tree = parse_or_none(path, source)
|
|
239
241
|
if tree is None:
|
|
240
242
|
return []
|
|
@@ -332,6 +334,22 @@ class PreferStrEnum(Rule):
|
|
|
332
334
|
return diags
|
|
333
335
|
|
|
334
336
|
|
|
337
|
+
def _has_str_enum_signal(source: str) -> bool:
|
|
338
|
+
"""Cheap source gate for files that cannot contain this rule's triggers.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True when the source contains enough lexical signal to justify parsing.
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
has_string_literal = '"' in source or "'" in source
|
|
345
|
+
if "str" in source and any(name in source.lower() for name in CHOICES_ATTR_NAMES):
|
|
346
|
+
return True
|
|
347
|
+
return (
|
|
348
|
+
has_string_literal
|
|
349
|
+
and ("==" in source or "!=" in source or "case " in source or "match " in source)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
335
353
|
def _cluster_fires(key: str, entry: _ClusterEntry) -> bool:
|
|
336
354
|
_line, _col, literals, eq_literals, ne_literals, in_literals = entry
|
|
337
355
|
if not eq_literals and not ne_literals:
|
|
@@ -71,6 +71,12 @@ Suppress an intentional raw-numeric duration with `# sarj-noqa: SARJ014 — <rea
|
|
|
71
71
|
References:
|
|
72
72
|
- https://docs.python.org/3/library/datetime.html#timedelta-objects
|
|
73
73
|
|
|
74
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
75
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
76
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
77
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
78
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
79
|
+
|
|
74
80
|
"""
|
|
75
81
|
|
|
76
82
|
from __future__ import annotations
|
|
@@ -80,7 +86,7 @@ import re
|
|
|
80
86
|
from typing import TYPE_CHECKING, override
|
|
81
87
|
|
|
82
88
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
83
|
-
from sarj_python_lint.rules._paths import is_test_path
|
|
89
|
+
from sarj_python_lint.rules._paths import is_generated_source, is_test_path
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
if TYPE_CHECKING:
|
|
@@ -104,6 +110,25 @@ _EXCLUDE_RE = re.compile(
|
|
|
104
110
|
)
|
|
105
111
|
|
|
106
112
|
_NUMERIC_NAMES = frozenset({"int", "float"})
|
|
113
|
+
_BARE_UNIT_NAMES = frozenset(
|
|
114
|
+
{
|
|
115
|
+
"day",
|
|
116
|
+
"days",
|
|
117
|
+
"hour",
|
|
118
|
+
"hours",
|
|
119
|
+
"minute",
|
|
120
|
+
"minutes",
|
|
121
|
+
"min",
|
|
122
|
+
"mins",
|
|
123
|
+
"second",
|
|
124
|
+
"seconds",
|
|
125
|
+
"sec",
|
|
126
|
+
"secs",
|
|
127
|
+
"millisecond",
|
|
128
|
+
"milliseconds",
|
|
129
|
+
"ms",
|
|
130
|
+
}
|
|
131
|
+
)
|
|
107
132
|
|
|
108
133
|
#: Roots of the CLI frameworks whose decorators bind a parameter to an argv value.
|
|
109
134
|
_CLI_MODULES = frozenset({"click", "typer"})
|
|
@@ -138,6 +163,8 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
138
163
|
|
|
139
164
|
@override
|
|
140
165
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
166
|
+
if is_generated_source(source):
|
|
167
|
+
return []
|
|
141
168
|
if is_test_path(path):
|
|
142
169
|
return []
|
|
143
170
|
tree = parse_or_none(path, source)
|
|
@@ -172,6 +199,8 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
172
199
|
) -> None:
|
|
173
200
|
if annotation is None:
|
|
174
201
|
return
|
|
202
|
+
if name.lower() in _BARE_UNIT_NAMES or name.lower().endswith(("_worked", "_elapsed")):
|
|
203
|
+
return
|
|
175
204
|
if not _UNIT_RE.search(name) or _EXCLUDE_RE.search(name):
|
|
176
205
|
return
|
|
177
206
|
numeric = _numeric_annotation(annotation)
|
|
@@ -39,6 +39,11 @@ a `tests/` directory), and framework-convention filenames whose stem is fixed by
|
|
|
39
39
|
a framework/tool and cannot be renamed (`models.py`, `views.py`, `base.py`, ...).
|
|
40
40
|
Modules whose single export already snake-cases to the stem are not flagged
|
|
41
41
|
(there is nothing to improve).
|
|
42
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
43
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
44
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
45
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
46
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
42
47
|
"""
|
|
43
48
|
|
|
44
49
|
from __future__ import annotations
|
|
@@ -48,6 +53,7 @@ import re
|
|
|
48
53
|
from typing import TYPE_CHECKING, override
|
|
49
54
|
|
|
50
55
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
56
|
+
from sarj_python_lint.rules._paths import is_generated_source
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
if TYPE_CHECKING:
|
|
@@ -128,6 +134,8 @@ class SinglePublicExport(Rule):
|
|
|
128
134
|
|
|
129
135
|
@override
|
|
130
136
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
137
|
+
if is_generated_source(source):
|
|
138
|
+
return []
|
|
131
139
|
if _is_skipped_path(path):
|
|
132
140
|
return []
|
|
133
141
|
if path.stem.lower() not in _JUNK_DRAWER_STEMS:
|
|
@@ -58,6 +58,11 @@ Never fires on:
|
|
|
58
58
|
`_code_str`). Siblings are excluded — an identically-named sibling method is a
|
|
59
59
|
different method. Callers in classes outside the module remain invisible to
|
|
60
60
|
syntactic analysis.
|
|
61
|
+
* **generated files** (`_paths.is_generated_source`). Their layout is the
|
|
62
|
+
generator's, and re-running the generator discards any edit, so a finding
|
|
63
|
+
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
64
|
+
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
65
|
+
`python/sdk/src/sarj_platform_sdk/` accounts for all of them.
|
|
61
66
|
"""
|
|
62
67
|
|
|
63
68
|
from __future__ import annotations
|
|
@@ -67,6 +72,7 @@ from collections import Counter
|
|
|
67
72
|
from typing import TYPE_CHECKING, override
|
|
68
73
|
|
|
69
74
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
75
|
+
from sarj_python_lint.rules._paths import is_generated_source
|
|
70
76
|
|
|
71
77
|
|
|
72
78
|
if TYPE_CHECKING:
|
|
@@ -105,8 +111,12 @@ class Stepdown(Rule):
|
|
|
105
111
|
|
|
106
112
|
@override
|
|
107
113
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
114
|
+
if is_generated_source(source):
|
|
115
|
+
return []
|
|
108
116
|
if _is_test_path(path):
|
|
109
117
|
return []
|
|
118
|
+
if "def _" not in source and "async def _" not in source:
|
|
119
|
+
return []
|
|
110
120
|
tree = parse_or_none(path, source)
|
|
111
121
|
if tree is None:
|
|
112
122
|
return []
|
|
@@ -189,7 +189,9 @@ def _is_rotting_xfail(dec: ast.expr) -> bool:
|
|
|
189
189
|
_ENV_PROBE_MODULES = frozenset({"sys", "os", "platform", "sysconfig"})
|
|
190
190
|
|
|
191
191
|
# Attribute / name words that identify an interpreter- or OS-version probe.
|
|
192
|
-
_ENV_PROBE_RE = re.compile(
|
|
192
|
+
_ENV_PROBE_RE = re.compile(
|
|
193
|
+
r"version|implementation|platform|machine|pypy|jython|win32|windows|linux|darwin|macos", re.IGNORECASE
|
|
194
|
+
)
|
|
193
195
|
|
|
194
196
|
|
|
195
197
|
def _is_environment_gated(dec: ast.Call) -> bool:
|
|
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.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fstring_in_log.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/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
|
{sarj_python_lint-0.17.0 → sarj_python_lint-0.18.1}/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
|