sarj-python-lint 0.3.0__tar.gz → 0.4.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.3.0 → sarj_python_lint-0.4.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/prefer_str_enum.py +7 -15
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +33 -24
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/.gitignore +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/README.md +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/prefer_discriminated_union.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.4.0
|
|
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
|
{sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
@@ -40,30 +40,22 @@ from pathlib import Path
|
|
|
40
40
|
from sarj_python_lint.rule_base import Diagnostic, Rule
|
|
41
41
|
|
|
42
42
|
#: Field / variable name tokens that strongly suggest a closed enumeration.
|
|
43
|
+
#: Kept deliberately HIGH-PRECISION — only words that are almost always a fixed
|
|
44
|
+
#: set. Broader/free-form-prone tokens (type, provider, level, mode, category,
|
|
45
|
+
#: channel, method, strategy, format, source, language, environment, …) were
|
|
46
|
+
#: removed: they over-fired on free-form strings. Those cases are still caught
|
|
47
|
+
#: when corroborated — via a sibling `choices`/`states` attribute or a
|
|
48
|
+
#: comparison cluster against literal values.
|
|
43
49
|
CHOICE_NAME_TOKENS = frozenset(
|
|
44
50
|
{
|
|
45
51
|
"status",
|
|
46
52
|
"state",
|
|
47
|
-
"type",
|
|
48
53
|
"kind",
|
|
49
|
-
"provider",
|
|
50
|
-
"language",
|
|
51
|
-
"lang",
|
|
52
54
|
"role",
|
|
53
55
|
"priority",
|
|
54
|
-
"
|
|
55
|
-
"mode",
|
|
56
|
-
"category",
|
|
56
|
+
"severity",
|
|
57
57
|
"direction",
|
|
58
|
-
"environment",
|
|
59
|
-
"env",
|
|
60
58
|
"tier",
|
|
61
|
-
"severity",
|
|
62
|
-
"channel",
|
|
63
|
-
"method",
|
|
64
|
-
"strategy",
|
|
65
|
-
"format",
|
|
66
|
-
"source",
|
|
67
59
|
"stage",
|
|
68
60
|
}
|
|
69
61
|
)
|
|
@@ -27,15 +27,16 @@ Purely annotation-based (no type inference), checked on function definitions
|
|
|
27
27
|
(sync + async):
|
|
28
28
|
|
|
29
29
|
1. Return annotation that is ``dict[str, Any]`` / ``dict[str, object]`` /
|
|
30
|
-
bare ``dict`` / ``Dict``, ``list[dict[str, Any]]
|
|
31
|
-
with 2+ distinct element types.
|
|
30
|
+
bare ``dict`` / ``Dict``, or ``list[dict[str, Any]]``.
|
|
32
31
|
2. FastAPI route handlers (``@router.get(...)`` / ``@app.post(...)`` etc.)
|
|
33
32
|
with no return annotation and no ``response_model=`` in the decorator.
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
Deliberately NOT flagged (kept high-precision for real boundaries):
|
|
35
|
+
private / ``_``-prefixed functions (internal, not a public contract), pydantic
|
|
36
|
+
``@model_validator`` / ``@field_validator`` hooks (raw dict in/out is their
|
|
37
|
+
API), ``tuple[...]`` returns (multiple return values are idiomatic Python),
|
|
38
|
+
fully-concrete dict value types (``dict[str, str]``), ``@overload`` stubs, and
|
|
39
|
+
test files.
|
|
39
40
|
|
|
40
41
|
References:
|
|
41
42
|
- https://docs.pydantic.dev/latest/concepts/models/
|
|
@@ -53,7 +54,6 @@ from sarj_python_lint.rule_base import Diagnostic, Rule
|
|
|
53
54
|
_HTTP_METHODS = {"get", "post", "put", "patch", "delete"}
|
|
54
55
|
_DICT_NAMES = {"dict", "Dict"}
|
|
55
56
|
_LIST_NAMES = {"list", "List"}
|
|
56
|
-
_TUPLE_NAMES = {"tuple", "Tuple"}
|
|
57
57
|
_ANY_VALUE_NAMES = {"Any", "object"}
|
|
58
58
|
|
|
59
59
|
|
|
@@ -65,12 +65,12 @@ class _RouteInfo:
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class PydanticAtBoundaries(Rule):
|
|
68
|
-
"""Untyped dict
|
|
68
|
+
"""Untyped dict return at a public boundary — define a pydantic model."""
|
|
69
69
|
|
|
70
70
|
id = "pydantic-at-boundaries"
|
|
71
71
|
code = "SARJ008"
|
|
72
72
|
description = (
|
|
73
|
-
"
|
|
73
|
+
"Public function/route returns an untyped dict — "
|
|
74
74
|
"define a pydantic model (or frozen dataclass)."
|
|
75
75
|
)
|
|
76
76
|
|
|
@@ -87,6 +87,15 @@ class PydanticAtBoundaries(Rule):
|
|
|
87
87
|
continue
|
|
88
88
|
if _is_overload(node):
|
|
89
89
|
continue
|
|
90
|
+
# Private/internal functions are not public boundaries — their
|
|
91
|
+
# return shape is an implementation detail, not a data contract.
|
|
92
|
+
if node.name.startswith("_"):
|
|
93
|
+
continue
|
|
94
|
+
# Pydantic validator hooks (`@model_validator`/`@field_validator`)
|
|
95
|
+
# take and return raw dict/values by contract — that's the API, not
|
|
96
|
+
# a missing model.
|
|
97
|
+
if _is_validator(node):
|
|
98
|
+
continue
|
|
90
99
|
route = _route_info(node)
|
|
91
100
|
returns = _resolve_annotation(node.returns)
|
|
92
101
|
if returns is None:
|
|
@@ -108,10 +117,6 @@ class PydanticAtBoundaries(Rule):
|
|
|
108
117
|
kind = _classify_return(returns)
|
|
109
118
|
if kind is None:
|
|
110
119
|
continue
|
|
111
|
-
# Private functions returning heterogeneous tuples are common and
|
|
112
|
-
# fine-ish; untyped dicts are flagged everywhere.
|
|
113
|
-
if kind == "tuple" and route is None and node.name.startswith("_"):
|
|
114
|
-
continue
|
|
115
120
|
ann_text = ast.unparse(returns)
|
|
116
121
|
diags.append(
|
|
117
122
|
Diagnostic(
|
|
@@ -141,6 +146,19 @@ def _is_overload(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
|
141
146
|
return False
|
|
142
147
|
|
|
143
148
|
|
|
149
|
+
_VALIDATOR_DECORATORS = {"model_validator", "field_validator", "validator", "root_validator"}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_validator(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
153
|
+
"""A pydantic validator hook — dict/value in-and-out is its required contract."""
|
|
154
|
+
for dec in node.decorator_list:
|
|
155
|
+
target = dec.func if isinstance(dec, ast.Call) else dec
|
|
156
|
+
name = _flat_name(target) if isinstance(target, (ast.Name, ast.Attribute)) else ""
|
|
157
|
+
if name in _VALIDATOR_DECORATORS:
|
|
158
|
+
return True
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
144
162
|
def _route_info(node: ast.FunctionDef | ast.AsyncFunctionDef) -> _RouteInfo | None:
|
|
145
163
|
"""Detect a FastAPI route decorator: `@<router|app|*_router>.<method>(...)`."""
|
|
146
164
|
for dec in node.decorator_list:
|
|
@@ -199,8 +217,8 @@ def _classify_return(node: ast.expr) -> str | None:
|
|
|
199
217
|
return "dict" if inner == "dict" else None
|
|
200
218
|
if base in _DICT_NAMES:
|
|
201
219
|
return "dict" if _is_untyped_dict_args(node.slice) else None
|
|
202
|
-
|
|
203
|
-
|
|
220
|
+
# Heterogeneous tuple returns are NOT flagged — multiple return values are
|
|
221
|
+
# idiomatic Python, not a missing data contract.
|
|
204
222
|
return None
|
|
205
223
|
|
|
206
224
|
|
|
@@ -211,15 +229,6 @@ def _is_untyped_dict_args(slice_node: ast.expr) -> bool:
|
|
|
211
229
|
return _flat_name(slice_node.elts[1]) in _ANY_VALUE_NAMES
|
|
212
230
|
|
|
213
231
|
|
|
214
|
-
def _is_heterogeneous_tuple_args(slice_node: ast.expr) -> bool:
|
|
215
|
-
"""`tuple[...]` is flagged when it has 2+ distinct element types."""
|
|
216
|
-
if not isinstance(slice_node, ast.Tuple):
|
|
217
|
-
return False # single-element `tuple[X]`
|
|
218
|
-
# `tuple[X, ...]` is a homogeneous variadic tuple.
|
|
219
|
-
if any(isinstance(elt, ast.Constant) and elt.value is Ellipsis for elt in slice_node.elts):
|
|
220
|
-
return False
|
|
221
|
-
distinct = {ast.unparse(elt) for elt in slice_node.elts}
|
|
222
|
-
return len(distinct) >= 2
|
|
223
232
|
|
|
224
233
|
|
|
225
234
|
def _flat_name(node: ast.expr) -> str:
|
|
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.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.3.0 → sarj_python_lint-0.4.0}/src/sarj_python_lint/rules/no_sequential_await.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|