sarj-python-lint 0.17.0__tar.gz → 0.18.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.
Files changed (54) hide show
  1. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/_paths.py +6 -1
  4. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +9 -0
  5. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +57 -42
  6. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +8 -0
  7. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +9 -1
  8. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +9 -1
  9. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/single_public_export.py +8 -0
  10. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/stepdown.py +8 -0
  11. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +3 -1
  12. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/.gitignore +0 -0
  13. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/README.md +0 -0
  14. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/__main__.py +0 -0
  16. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/_version.py +0 -0
  18. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/py.typed +0 -0
  19. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rule_base.py +0 -0
  20. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  21. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  22. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  23. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  24. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  25. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  26. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  27. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  28. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  29. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  30. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  31. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  32. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  33. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  34. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  35. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  36. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  37. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  38. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  39. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  40. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  41. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  42. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  43. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  44. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  45. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  46. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  47. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  48. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  49. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  50. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  51. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  52. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  53. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  54. {sarj_python_lint-0.17.0 → sarj_python_lint-0.18.0}/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.17.0
3
+ Version: 0.18.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.17.0"
3
+ version = "0.18.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -23,7 +23,12 @@ if TYPE_CHECKING:
23
23
 
24
24
  _TEST_DIR_NAMES = frozenset({"tests", "test"})
25
25
 
26
- _GENERATED_RE = re.compile(r"auto-?generated|generated by|do not edit", re.IGNORECASE)
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,8 @@ 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 []
61
70
  tree = parse_or_none(path, source)
62
71
  if tree is None:
63
72
  return []
@@ -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
- "inc",
129
- "dec",
130
- "observe",
131
- "set_to_current_time",
132
- "labels",
133
- "increment",
134
- "decrement",
135
- "gauge",
136
- "timing",
137
- "histogram",
138
- "record_exception",
139
- "add_event",
140
- "set_attribute",
141
- "set_attributes",
142
- "set_status",
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
- "monotonic",
151
- "monotonic_ns",
152
- "perf_counter",
153
- "perf_counter_ns",
154
- "time",
155
- "time_ns",
156
- "now",
157
- "utcnow",
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
- "abs",
168
- "bool",
169
- "float",
170
- "format",
171
- "id",
172
- "int",
173
- "len",
174
- "list",
175
- "repr",
176
- "round",
177
- "str",
178
- "tuple",
179
- "type",
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)
@@ -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)
@@ -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:
@@ -138,6 +144,8 @@ class PreferTimedeltaForDurations(Rule):
138
144
 
139
145
  @override
140
146
  def check(self, path: Path, source: str) -> list[Diagnostic]:
147
+ if is_generated_source(source):
148
+ return []
141
149
  if is_test_path(path):
142
150
  return []
143
151
  tree = parse_or_none(path, source)
@@ -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,6 +111,8 @@ 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 []
110
118
  tree = parse_or_none(path, source)
@@ -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(r"version|implementation|platform|machine|pypy|jython|win32|windows|linux|darwin|macos", re.IGNORECASE)
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: