sarj-python-lint 0.18.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.18.0 → sarj_python_lint-0.18.1}/PKG-INFO +1 -1
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/pyproject.toml +1 -1
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rule_base.py +5 -5
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +2 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/kwonly_same_type_params.py +34 -1
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sequential_await.py +2 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_str_enum.py +18 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +21 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/stepdown.py +2 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/.gitignore +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/README.md +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.18.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.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.18.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.18.
|
|
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
|
|
@@ -67,6 +67,8 @@ class InefficientStringConcatInLoop(Rule):
|
|
|
67
67
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
68
68
|
if is_generated_source(source):
|
|
69
69
|
return []
|
|
70
|
+
if "+" not in source or ("for " not in source and "while " not in source):
|
|
71
|
+
return []
|
|
70
72
|
tree = parse_or_none(path, source)
|
|
71
73
|
if tree is None:
|
|
72
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
|
|
|
@@ -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 []
|
{sarj_python_lint-0.18.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:
|
|
@@ -110,6 +110,25 @@ _EXCLUDE_RE = re.compile(
|
|
|
110
110
|
)
|
|
111
111
|
|
|
112
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
|
+
)
|
|
113
132
|
|
|
114
133
|
#: Roots of the CLI frameworks whose decorators bind a parameter to an argv value.
|
|
115
134
|
_CLI_MODULES = frozenset({"click", "typer"})
|
|
@@ -180,6 +199,8 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
180
199
|
) -> None:
|
|
181
200
|
if annotation is None:
|
|
182
201
|
return
|
|
202
|
+
if name.lower() in _BARE_UNIT_NAMES or name.lower().endswith(("_worked", "_elapsed")):
|
|
203
|
+
return
|
|
183
204
|
if not _UNIT_RE.search(name) or _EXCLUDE_RE.search(name):
|
|
184
205
|
return
|
|
185
206
|
numeric = _numeric_annotation(annotation)
|
|
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.18.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.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.18.0 → sarj_python_lint-0.18.1}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.18.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
|
|
File without changes
|
{sarj_python_lint-0.18.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.18.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.18.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|