sarj-python-lint 0.4.1__tar.gz → 0.5.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.4.1 → sarj_python_lint-0.5.0}/PKG-INFO +2 -1
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/README.md +1 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/__init__.py +2 -0
- sarj_python_lint-0.5.0/src/sarj_python_lint/rules/prefer_class_row.py +85 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/.gitignore +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/prefer_discriminated_union.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.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.5.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
|
|
@@ -36,6 +36,7 @@ uv tool install sarj-python-lint
|
|
|
36
36
|
- id: sarj-prefer-str-enum
|
|
37
37
|
- id: sarj-no-fat-try-blocks
|
|
38
38
|
- id: sarj-pydantic-at-boundaries
|
|
39
|
+
- id: sarj-prefer-class-row
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
## CLI
|
|
@@ -12,6 +12,7 @@ from sarj_python_lint.rules.no_sequential_await import NoSequentialAwait
|
|
|
12
12
|
from sarj_python_lint.rules.no_unreachable_after_terminal import (
|
|
13
13
|
NoUnreachableAfterTerminal,
|
|
14
14
|
)
|
|
15
|
+
from sarj_python_lint.rules.prefer_class_row import PreferClassRow
|
|
15
16
|
from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
|
|
16
17
|
PreferConstantTimeSecretCompare,
|
|
17
18
|
)
|
|
@@ -24,6 +25,7 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
24
25
|
NoSequentialAwait.id: NoSequentialAwait,
|
|
25
26
|
InefficientStringConcatInLoop.id: InefficientStringConcatInLoop,
|
|
26
27
|
PreferDiscriminatedUnion.id: PreferDiscriminatedUnion,
|
|
28
|
+
PreferClassRow.id: PreferClassRow,
|
|
27
29
|
PreferStrEnum.id: PreferStrEnum,
|
|
28
30
|
NoFatTryBlocks.id: NoFatTryBlocks,
|
|
29
31
|
NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""SARJ013: psycopg `row_factory=dict_row` where a validated model row is intended.
|
|
2
|
+
|
|
3
|
+
The repo standard is to map each DB row straight into a pydantic model with
|
|
4
|
+
`class_row(Model)`, so every row is validated at the database boundary and the
|
|
5
|
+
cursor is typed `Cursor[Model]`. `dict_row` instead hands back an unvalidated
|
|
6
|
+
`dict[str, Any]` that callers then feed to `Model.model_validate(...)` by hand —
|
|
7
|
+
an extra step that is easy to forget and leaves the value untyped in between.
|
|
8
|
+
|
|
9
|
+
Flags any `row_factory=dict_row` keyword argument (typically on
|
|
10
|
+
`conn.cursor(...)`). If you genuinely need a plain mapping — an ad-hoc
|
|
11
|
+
aggregate, a `COUNT(*)`, or a dynamic projection with no model — suppress with
|
|
12
|
+
`# sarj-noqa: prefer-class-row — <reason>`.
|
|
13
|
+
|
|
14
|
+
Replace:
|
|
15
|
+
async with conn.cursor(row_factory=dict_row) as cur:
|
|
16
|
+
await cur.execute(..., RETURNING id, status)
|
|
17
|
+
row = await cur.fetchone()
|
|
18
|
+
return Task.model_validate(row)
|
|
19
|
+
|
|
20
|
+
with:
|
|
21
|
+
async with conn.cursor(row_factory=class_row(Task)) as cur:
|
|
22
|
+
await cur.execute(..., RETURNING id, status)
|
|
23
|
+
return one(await cur.fetchone())
|
|
24
|
+
|
|
25
|
+
References:
|
|
26
|
+
- https://www.psycopg.org/psycopg3/docs/api/rows.html#psycopg.rows.class_row
|
|
27
|
+
- https://docs.pydantic.dev/latest/concepts/models/#validating-data
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import ast
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from sarj_python_lint.rule_base import Diagnostic, Rule
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_ROW_FACTORY_KW = "row_factory"
|
|
39
|
+
_BANNED_FACTORY = "dict_row"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PreferClassRow(Rule):
|
|
43
|
+
"""`row_factory=dict_row` returns unvalidated dicts — prefer `class_row(Model)`."""
|
|
44
|
+
|
|
45
|
+
id = "prefer-class-row"
|
|
46
|
+
code = "SARJ013"
|
|
47
|
+
description = "psycopg row_factory=dict_row returns unvalidated dicts — prefer class_row(Model)."
|
|
48
|
+
|
|
49
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
50
|
+
try:
|
|
51
|
+
tree = ast.parse(source, filename=str(path))
|
|
52
|
+
except SyntaxError:
|
|
53
|
+
return []
|
|
54
|
+
diags: list[Diagnostic] = []
|
|
55
|
+
for node in ast.walk(tree):
|
|
56
|
+
if not isinstance(node, ast.keyword):
|
|
57
|
+
continue
|
|
58
|
+
if node.arg != _ROW_FACTORY_KW:
|
|
59
|
+
continue
|
|
60
|
+
if _factory_name(node.value) != _BANNED_FACTORY:
|
|
61
|
+
continue
|
|
62
|
+
diags.append(
|
|
63
|
+
Diagnostic(
|
|
64
|
+
path=path,
|
|
65
|
+
line=node.value.lineno,
|
|
66
|
+
col=node.value.col_offset + 1,
|
|
67
|
+
code=self.code,
|
|
68
|
+
message=(
|
|
69
|
+
"`row_factory=dict_row` yields unvalidated dict rows — "
|
|
70
|
+
"prefer `class_row(YourModel)` to validate at the DB boundary "
|
|
71
|
+
"(suppress with `# sarj-noqa: prefer-class-row` for genuine ad-hoc shapes)"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
diags.sort(key=lambda d: (d.line, d.col))
|
|
76
|
+
return diags
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _factory_name(node: ast.expr) -> str | None:
|
|
80
|
+
"""Resolve a `row_factory=` value to its callable name (`dict_row`, …)."""
|
|
81
|
+
if isinstance(node, ast.Name):
|
|
82
|
+
return node.id
|
|
83
|
+
if isinstance(node, ast.Attribute):
|
|
84
|
+
return node.attr
|
|
85
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/no_sequential_await.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.4.1 → sarj_python_lint-0.5.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|