sarj-python-lint 0.30.0__tar.gz → 0.32.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.30.0 → sarj_python_lint-0.32.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/pyproject.toml +1 -1
- sarj_python_lint-0.32.0/src/sarj_python_lint/rules/_paths.py +173 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/duplicate_test_body.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/mock_without_spec.py +127 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_comment_cruft.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +2 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_restated_comment.py +2 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +2 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +14 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +2 -6
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/redundant_docstring.py +2 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/require_port_for_service.py +2 -2
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/single_public_export.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/stepdown.py +3 -3
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/trailing_value_narration.py +2 -2
- sarj_python_lint-0.30.0/src/sarj_python_lint/rules/_paths.py +0 -63
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/.gitignore +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/README.md +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_implicit_attribute_access.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.30.0 → sarj_python_lint-0.32.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.
|
|
3
|
+
Version: 0.32.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
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Shared file-scope predicates for the file-scope-gated rules.
|
|
2
|
+
|
|
3
|
+
A single definition of "is this a test file?" so the test-scoped rules
|
|
4
|
+
(SARJ031 no-sleep-in-test-body, SARJ036 no-raw-sql-in-tests) and the rules
|
|
5
|
+
that *exempt* tests (SARJ014 prefer-timedelta-for-durations, SARJ015
|
|
6
|
+
prefer-struct-over-namedtuple, SARJ026 prefer-namedtuple-over-tuple-return,
|
|
7
|
+
SARJ034 kwonly-same-type-params) never diverge on what counts as a test file.
|
|
8
|
+
|
|
9
|
+
Also the single definition of "is this a generated file?" (SARJ016/034):
|
|
10
|
+
generated code mirrors whatever its generator emits, so style rules cannot be
|
|
11
|
+
acted on there.
|
|
12
|
+
|
|
13
|
+
That definition is deliberately two-sided. `is_generated_source` reads the
|
|
14
|
+
header, which works for protoc/`@generated` tooling but is useless for the
|
|
15
|
+
generators that emit no banner at all. Measured on a checked-in
|
|
16
|
+
openapi-python-client SDK: 240 generated modules, *zero* carrying a marker in
|
|
17
|
+
their first five lines. `is_generated_path` covers those by looking at where
|
|
18
|
+
the file sits instead of what it says. `is_generated` is the predicate rules
|
|
19
|
+
should call — it is the union, and neither half alone is enough.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from functools import cache
|
|
25
|
+
import re
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_TEST_DIR_NAMES = frozenset({"tests", "test"})
|
|
34
|
+
|
|
35
|
+
# `auto-?generated` misses "automatically generated", which is the phrase
|
|
36
|
+
# @generated tooling, protoc and OpenAPI generators emit most often.
|
|
37
|
+
_GENERATED_RE = re.compile(
|
|
38
|
+
r"auto(?:-|matically )?generated|generated by|do not edit|@generated",
|
|
39
|
+
re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_GENERATED_HEADER_LINES = 5
|
|
43
|
+
|
|
44
|
+
_GENERATED_DIR_NAMES = frozenset({"generated", "vendor", "vendored"})
|
|
45
|
+
|
|
46
|
+
# A directory holding one of these is a code-generator root: the file is the
|
|
47
|
+
# generator's own config or ignore list, and the emitted package is checked in
|
|
48
|
+
# beneath it. Every entry is a filename published by a code generator —
|
|
49
|
+
# OpenAPI Generator, swagger-codegen, openapi-python-client, buf, and
|
|
50
|
+
# graphql-code-generator's `codegen.*` family. None is specific to any one
|
|
51
|
+
# repository's layout.
|
|
52
|
+
_CODEGEN_MARKER_NAMES = (
|
|
53
|
+
".openapi-generator",
|
|
54
|
+
".openapi-generator-ignore",
|
|
55
|
+
".swagger-codegen-ignore",
|
|
56
|
+
"openapi-python-client.yml",
|
|
57
|
+
"openapi-python-client.yaml",
|
|
58
|
+
"codegen.yml",
|
|
59
|
+
"codegen.yaml",
|
|
60
|
+
"codegen.config.yml",
|
|
61
|
+
"codegen.config.yaml",
|
|
62
|
+
"buf.gen.yaml",
|
|
63
|
+
"buf.gen.yml",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Ancestor walks stop at the repository root. Without this a file two levels
|
|
67
|
+
# deep would stat every directory up to `/` looking for a marker.
|
|
68
|
+
_REPO_ROOT_MARKERS = (".git",)
|
|
69
|
+
|
|
70
|
+
# Guards against a pathological path (or a symlink loop) turning the ancestor
|
|
71
|
+
# walk into an unbounded stat storm. No real source tree is this deep.
|
|
72
|
+
_MAX_ANCESTOR_DEPTH = 40
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_generated_source(source: str) -> bool:
|
|
76
|
+
"""Report whether `source` self-identifies as generated code.
|
|
77
|
+
|
|
78
|
+
Checks the first few lines for the conventional markers
|
|
79
|
+
("autogenerated", "generated by", "do not edit").
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True when the file header carries a generated-code marker.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
head = source.splitlines()[:_GENERATED_HEADER_LINES]
|
|
86
|
+
return any(_GENERATED_RE.search(line) for line in head)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cache
|
|
90
|
+
def _is_codegen_root(directory: Path) -> bool:
|
|
91
|
+
"""Report whether `directory` holds a code generator's config or ignore file.
|
|
92
|
+
|
|
93
|
+
Cached: a corpus run asks this of the same handful of directories once per
|
|
94
|
+
file, and the answer is a filesystem probe.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True when the directory carries a generator marker file.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@cache
|
|
104
|
+
def _is_repo_root(directory: Path) -> bool:
|
|
105
|
+
"""Report whether `directory` is a repository root.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True when the directory carries a repository marker (`.git`).
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_generated_path(path: Path) -> bool:
|
|
115
|
+
"""Report whether `path` sits in a tree of generated or vendored code.
|
|
116
|
+
|
|
117
|
+
Two signals, both about location rather than content:
|
|
118
|
+
|
|
119
|
+
* a `generated` / `vendor` / `vendored` directory anywhere in the path, and
|
|
120
|
+
* a code-generator root among the ancestors — a directory holding an
|
|
121
|
+
`.openapi-generator-ignore`, `codegen.config.yml` or similar.
|
|
122
|
+
|
|
123
|
+
The generator root itself stays linted: the config file's neighbours are
|
|
124
|
+
the hand-written driver script that invokes the generator, while the
|
|
125
|
+
emitted package is checked in one level down. Only the subtree below the
|
|
126
|
+
root is treated as generated.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True when the file is machine-written output rather than source.
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
if any(part.lower() in _GENERATED_DIR_NAMES for part in path.parts):
|
|
133
|
+
return True
|
|
134
|
+
for depth, ancestor in enumerate(path.parents):
|
|
135
|
+
if depth >= _MAX_ANCESTOR_DEPTH:
|
|
136
|
+
break
|
|
137
|
+
# `depth == 0` is the file's own directory: a generator config sitting
|
|
138
|
+
# next to the file makes the file the generator, not its output.
|
|
139
|
+
if depth and _is_codegen_root(ancestor):
|
|
140
|
+
return True
|
|
141
|
+
if _is_repo_root(ancestor):
|
|
142
|
+
break
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_generated(path: Path, source: str) -> bool:
|
|
147
|
+
"""Report whether the file is generated, by either header or location.
|
|
148
|
+
|
|
149
|
+
The predicate every rule that exempts generated code should call. Checking
|
|
150
|
+
only the header misses banner-less generators; checking only the path
|
|
151
|
+
misses a generated file dropped into a hand-written tree.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True when the file is machine-written output rather than source.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
return is_generated_source(source) or is_generated_path(path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def is_test_path(path: Path) -> bool:
|
|
161
|
+
"""Report whether `path` is a test file.
|
|
162
|
+
|
|
163
|
+
A test file is `conftest.py`, a `test_*.py` / `*_test.py` stem, or any file
|
|
164
|
+
under a `tests` / `test` directory segment.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True when `path` belongs to the test tree.
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
name = path.name
|
|
171
|
+
if name == "conftest.py" or name.startswith("test_") or name.endswith("_test.py"):
|
|
172
|
+
return True
|
|
173
|
+
return any(part in _TEST_DIR_NAMES for part in path.parts)
|
|
@@ -182,7 +182,7 @@ Deliberately NOT flagged:
|
|
|
182
182
|
* **short bodies.** A two-statement test is not evidence of copy-paste — call,
|
|
183
183
|
assert, done, and any two tests of the same helper look alike. The 3-statement
|
|
184
184
|
floor is what keeps the rule from firing on every well-factored unit suite,
|
|
185
|
-
* **generated test modules** (`
|
|
185
|
+
* **generated test modules** (`_paths.is_generated`) — a generator emitting N
|
|
186
186
|
near-identical cases is the generator's business, and the fix would be
|
|
187
187
|
overwritten on the next regeneration,
|
|
188
188
|
* the **first** member of a group. The diagnostic goes on the copies and points
|
|
@@ -200,7 +200,7 @@ from typing import TYPE_CHECKING, override
|
|
|
200
200
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
201
201
|
from sarj_python_lint.rules._ast_index import children
|
|
202
202
|
from sarj_python_lint.rules._comments import standalone_comments, trailing_comments
|
|
203
|
-
from sarj_python_lint.rules._paths import
|
|
203
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
204
204
|
|
|
205
205
|
|
|
206
206
|
if TYPE_CHECKING:
|
|
@@ -286,7 +286,7 @@ class DuplicateTestBody(Rule):
|
|
|
286
286
|
by position.
|
|
287
287
|
|
|
288
288
|
"""
|
|
289
|
-
if not is_test_path(path) or
|
|
289
|
+
if not is_test_path(path) or is_generated(path, source):
|
|
290
290
|
return []
|
|
291
291
|
tree = parse_or_none(path, source)
|
|
292
292
|
if tree is None:
|
|
@@ -34,7 +34,7 @@ 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.
|
|
37
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
38
38
|
generator's, and re-running the generator discards any edit, so a finding
|
|
39
39
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
40
40
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -49,7 +49,7 @@ from typing import TYPE_CHECKING, TypeGuard, override
|
|
|
49
49
|
|
|
50
50
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
51
51
|
from sarj_python_lint.rules._ast_index import children, walk
|
|
52
|
-
from sarj_python_lint.rules._paths import
|
|
52
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
if TYPE_CHECKING:
|
|
@@ -66,7 +66,7 @@ class InefficientStringConcatInLoop(Rule):
|
|
|
66
66
|
|
|
67
67
|
@override
|
|
68
68
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
69
|
-
if
|
|
69
|
+
if is_generated(path, source):
|
|
70
70
|
return []
|
|
71
71
|
if "+" not in source or ("for " not in source and "while " not in source):
|
|
72
72
|
return []
|
|
@@ -42,7 +42,7 @@ Never flags — these signatures cannot or should not change:
|
|
|
42
42
|
`black/scripts/diff_shades_gha_helper.py:167`),
|
|
43
43
|
* test files (`_paths.is_test_path`) — test fakes and helpers mirror the
|
|
44
44
|
signatures of the code under test and cannot unilaterally change them,
|
|
45
|
-
* generated files (`_paths.
|
|
45
|
+
* generated files (`_paths.is_generated`) — the signature mirrors
|
|
46
46
|
whatever the generator emits (found via trio's `_generated_io_kqueue.py`),
|
|
47
47
|
* functions whose name is referenced as a VALUE anywhere in the module
|
|
48
48
|
(passed to a registry, returned, stored) — the signature is a callback
|
|
@@ -101,7 +101,7 @@ from typing import TYPE_CHECKING, override
|
|
|
101
101
|
|
|
102
102
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
103
103
|
from sarj_python_lint.rules._ast_index import nodes, walk
|
|
104
|
-
from sarj_python_lint.rules._paths import
|
|
104
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
if TYPE_CHECKING:
|
|
@@ -196,7 +196,7 @@ class KwonlySameTypeParams(Rule):
|
|
|
196
196
|
|
|
197
197
|
@override
|
|
198
198
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
199
|
-
if is_test_path(path) or
|
|
199
|
+
if is_test_path(path) or is_generated(path, source):
|
|
200
200
|
return []
|
|
201
201
|
tree = parse_or_none(path, source)
|
|
202
202
|
if tree is None:
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
@@ -38,7 +38,9 @@ Deliberately NOT flagged:
|
|
|
38
38
|
* `create_autospec(...)`, `mock.ANY`, `mock.sentinel`, `mock.call` — specced by
|
|
39
39
|
construction or not doubles at all,
|
|
40
40
|
* bare `Mock` referenced without being called (annotations, `isinstance`
|
|
41
|
-
checks) — only a construction can carry a spec argument
|
|
41
|
+
checks) — only a construction can carry a spec argument,
|
|
42
|
+
* a canned callable stub bound to an attribute (`receiver.method =
|
|
43
|
+
AsyncMock(return_value=...)`) — see exemption 4 below.
|
|
42
44
|
|
|
43
45
|
The import-backed name check is the load-bearing false-positive guard. Test
|
|
44
46
|
suites routinely define their own `Mock`-suffixed fakes (`MockVisionBankClient`,
|
|
@@ -92,6 +94,45 @@ of this rule reported 137 hits, of which 38 were false positives:
|
|
|
92
94
|
constructions lexically inside an `ImportError`/`ModuleNotFoundError`
|
|
93
95
|
handler.
|
|
94
96
|
|
|
97
|
+
4. **A canned callable stub bound to an attribute (146 hits, 18.8%).** Measured
|
|
98
|
+
over the two first-party repos (bulbul + noura-be, 777 hits), the single
|
|
99
|
+
largest shape was `receiver.method = Mock(...)` — 283 hits, 36.4%. It is not
|
|
100
|
+
an unspecced *collaborator*; it replaces one callable on a receiver that
|
|
101
|
+
already exists, and the contract this rule protects belongs to that receiver.
|
|
102
|
+
Either the receiver carries `spec=` — in which case production's call through
|
|
103
|
+
a renamed attribute raises `AttributeError` off the specced parent and the
|
|
104
|
+
test fails loudly, which is exactly the rot this rule wants — or the receiver
|
|
105
|
+
is itself flagged here at its own construction, and reporting the leaf as
|
|
106
|
+
well says the same thing twice. Evidence:
|
|
107
|
+
`bulbul/python/agent/tests/conftest.py:167-170`, where
|
|
108
|
+
`backchanneler = mock.Mock(spec=Backchanneler)` is followed by
|
|
109
|
+
`backchanneler.start_audio = mock.AsyncMock()` / `.run = ...` / `.stop = ...`
|
|
110
|
+
— model spec discipline, and three findings; and
|
|
111
|
+
`bulbul/python/bulbul/tests/integrations/test_zoho_notifications_handler.py:86`
|
|
112
|
+
(`crm_service.get_record = mock.AsyncMock(return_value=record)`), where the
|
|
113
|
+
only thing the file reads off `crm_service.get_record` is
|
|
114
|
+
`assert_awaited_once_with`. `AsyncMock` dominates the shape (164 of 283)
|
|
115
|
+
because `Mock(spec=X)` children are not awaitable, so a specced double *must*
|
|
116
|
+
have its async methods stubbed this way to be usable at all.
|
|
117
|
+
|
|
118
|
+
The guard demands POSITIVE evidence of callability — a canned
|
|
119
|
+
`return_value=`/`side_effect=`, a mock-API read off the path
|
|
120
|
+
(`recv.method.assert_called_once_with(...)`), or an invocation — on top of
|
|
121
|
+
the absence of any domain-attribute read. Absence alone is not enough:
|
|
122
|
+
`agent/tests/test_for_call_settings.py:64` (`room.local_participant =
|
|
123
|
+
mock.Mock()`) is an object double this file never happens to walk, and it
|
|
124
|
+
stays flagged, as do the 60 namespace doubles the corpus does walk —
|
|
125
|
+
`test_for_call_settings.py:71,74,76` (`api.room` / `api.sip` / `api.egress`,
|
|
126
|
+
read back as `.delete_room`, `.create_sip_participant`,
|
|
127
|
+
`.start_room_composite_egress`) and `agent/tests/conftest.py:246-247`
|
|
128
|
+
(`job_context.api`, `job_context.api.sip`).
|
|
129
|
+
|
|
130
|
+
What this gives up is arity: `AsyncMock(spec=Store.get)` would reject a call
|
|
131
|
+
whose signature no longer matches, and the exempted stubs will not. That is
|
|
132
|
+
the narrower half of the defect, and it is the half `patch.object(mod,
|
|
133
|
+
"func")` still gets flagged for, since `autospec=True` is a one-word fix
|
|
134
|
+
there and there is no equivalent for a raw attribute assignment.
|
|
135
|
+
|
|
95
136
|
The 99 survivors are true positives: unspecced `MagicMock()` doubles for real
|
|
96
137
|
types (`black/tests/test_black.py:2933`, a `MagicMock()` standing in for `Path`
|
|
97
138
|
and answering `.relative_to`/`.resolve`/`.is_dir`/`.is_file`), and
|
|
@@ -175,6 +216,11 @@ _MOCK_API_ATTRS = frozenset(
|
|
|
175
216
|
# An import that failed leaves nothing importable to spec against.
|
|
176
217
|
_IMPORT_FAILURES = frozenset({"ImportError", "ModuleNotFoundError"})
|
|
177
218
|
|
|
219
|
+
# Keywords that canned-answer a *call*. You only say what a double returns, or
|
|
220
|
+
# raises, when something is going to invoke it — so their presence is positive
|
|
221
|
+
# evidence that the double stands in for one callable rather than an object.
|
|
222
|
+
_CANNED_RESULT_KEYWORDS = frozenset({"return_value", "side_effect"})
|
|
223
|
+
|
|
178
224
|
|
|
179
225
|
class MockWithoutSpec(Rule):
|
|
180
226
|
"""A `unittest.mock` double built with no `spec=` accepts any attribute."""
|
|
@@ -321,6 +367,9 @@ class _FileFacts:
|
|
|
321
367
|
self.reads: dict[str, set[str]] = {}
|
|
322
368
|
self.called: set[str] = set()
|
|
323
369
|
self.import_fallbacks: set[ast.Call] = set()
|
|
370
|
+
self.attribute_target: dict[ast.Call, str] = {}
|
|
371
|
+
self.path_reads: dict[str, set[str]] = {}
|
|
372
|
+
self.path_calls: set[str] = set()
|
|
324
373
|
|
|
325
374
|
@classmethod
|
|
326
375
|
def from_tree(cls, tree: ast.Module) -> _FileFacts:
|
|
@@ -339,16 +388,39 @@ class _FileFacts:
|
|
|
339
388
|
elif isinstance(node, ast.Attribute):
|
|
340
389
|
if isinstance(node.value, ast.Name):
|
|
341
390
|
found.reads.setdefault(node.value.id, set()).add(node.attr)
|
|
391
|
+
found._record_path_read(node)
|
|
342
392
|
elif isinstance(node, ast.Call):
|
|
343
393
|
if isinstance(node.func, ast.Name):
|
|
344
394
|
found.called.add(node.func.id)
|
|
395
|
+
found._record_path_call(node)
|
|
345
396
|
elif _catches_import_failure(node):
|
|
346
397
|
found.import_fallbacks.update(child for child in walk(node) if isinstance(child, ast.Call))
|
|
347
398
|
return found
|
|
348
399
|
|
|
349
400
|
def _bind(self, target: ast.expr | None, value: ast.expr | None) -> None:
|
|
350
|
-
if
|
|
401
|
+
if not isinstance(value, ast.Call):
|
|
402
|
+
return
|
|
403
|
+
if isinstance(target, ast.Name):
|
|
351
404
|
self.bound_name[value] = target.id
|
|
405
|
+
elif isinstance(target, ast.Attribute):
|
|
406
|
+
path = _dotted_path(target)
|
|
407
|
+
if path is not None:
|
|
408
|
+
self.attribute_target[value] = path
|
|
409
|
+
|
|
410
|
+
def _record_path_read(self, node: ast.Attribute) -> None:
|
|
411
|
+
# Load context only. `recv.method = Mock()` is a *store* through
|
|
412
|
+
# `recv.method`; counting it would make every stub look like a namespace
|
|
413
|
+
# double read back through its own name.
|
|
414
|
+
if not isinstance(node.ctx, ast.Load):
|
|
415
|
+
return
|
|
416
|
+
path = _dotted_path(node.value)
|
|
417
|
+
if path is not None:
|
|
418
|
+
self.path_reads.setdefault(path, set()).add(node.attr)
|
|
419
|
+
|
|
420
|
+
def _record_path_call(self, node: ast.Call) -> None:
|
|
421
|
+
path = _dotted_path(node.func)
|
|
422
|
+
if path is not None:
|
|
423
|
+
self.path_calls.add(path)
|
|
352
424
|
|
|
353
425
|
def is_call_recorder(self, node: ast.Call) -> bool:
|
|
354
426
|
"""Report whether the double bound by `node` is only ever called and introspected.
|
|
@@ -368,6 +440,58 @@ class _FileFacts:
|
|
|
368
440
|
return False
|
|
369
441
|
return self.reads.get(name, set()) <= _MOCK_API_ATTRS
|
|
370
442
|
|
|
443
|
+
def is_method_stub(self, node: ast.Call) -> bool:
|
|
444
|
+
"""Report whether `node` is a canned stub for one method of some receiver.
|
|
445
|
+
|
|
446
|
+
`receiver.method = AsyncMock(return_value=...)` replaces a single
|
|
447
|
+
callable, not an object. The contract this rule protects belongs to the
|
|
448
|
+
*receiver* — it is the receiver's construction that either carries
|
|
449
|
+
`spec=` or is itself flagged here — and a callable has no attribute
|
|
450
|
+
surface for `spec=` to fence. Two conditions must hold:
|
|
451
|
+
|
|
452
|
+
* nothing but the mock API is read back off the assigned path, so the
|
|
453
|
+
double is not standing in for an object the test walks, and
|
|
454
|
+
* something positively marks it as a callable — a canned
|
|
455
|
+
`return_value=`/`side_effect=`, an `assert_called*`-style read, or an
|
|
456
|
+
invocation of the path.
|
|
457
|
+
|
|
458
|
+
The second condition is what keeps `room.local_participant = Mock()`
|
|
459
|
+
flagged: absence of attribute reads is not evidence of callability, and
|
|
460
|
+
a namespace double that this file happens never to walk is exactly the
|
|
461
|
+
case `spec=` exists for.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
True when the double is a canned callable stub bound to an attribute.
|
|
465
|
+
|
|
466
|
+
"""
|
|
467
|
+
path = self.attribute_target.get(node)
|
|
468
|
+
if path is None:
|
|
469
|
+
return False
|
|
470
|
+
seen = self.path_reads.get(path, set())
|
|
471
|
+
if not seen <= _MOCK_API_ATTRS:
|
|
472
|
+
return False
|
|
473
|
+
canned = any(kw.arg in _CANNED_RESULT_KEYWORDS for kw in node.keywords)
|
|
474
|
+
return canned or bool(seen) or path in self.path_calls
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _dotted_path(expr: ast.expr) -> str | None:
|
|
478
|
+
"""Render a pure `name.attr.attr` chain as a dotted string.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
The dotted path, or None when the chain is rooted in anything other than
|
|
482
|
+
a bare name (a subscript, a call, a literal), where two occurrences of
|
|
483
|
+
the same text need not denote the same object.
|
|
484
|
+
|
|
485
|
+
"""
|
|
486
|
+
parts: list[str] = []
|
|
487
|
+
while isinstance(expr, ast.Attribute):
|
|
488
|
+
parts.append(expr.attr)
|
|
489
|
+
expr = expr.value
|
|
490
|
+
if not isinstance(expr, ast.Name):
|
|
491
|
+
return None
|
|
492
|
+
parts.append(expr.id)
|
|
493
|
+
return ".".join(reversed(parts))
|
|
494
|
+
|
|
371
495
|
|
|
372
496
|
def _catches_import_failure(handler: ast.ExceptHandler) -> bool:
|
|
373
497
|
caught = handler.type
|
|
@@ -386,7 +510,7 @@ def _unspecced_calls(tree: ast.Module, names: _MockNames, facts: _FileFacts) ->
|
|
|
386
510
|
label = _render_callee(node.func, symbol)
|
|
387
511
|
if _has_spec_argument(node) or _has_positional_replacement(node, label):
|
|
388
512
|
continue
|
|
389
|
-
if node in facts.import_fallbacks or facts.is_call_recorder(node):
|
|
513
|
+
if node in facts.import_fallbacks or facts.is_call_recorder(node) or facts.is_method_stub(node):
|
|
390
514
|
continue
|
|
391
515
|
hits.append((node, label))
|
|
392
516
|
return hits
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
@@ -66,7 +66,7 @@ comments — `# type:`, `# noqa`, `# sarj-noqa`,
|
|
|
66
66
|
coding declarations.
|
|
67
67
|
|
|
68
68
|
Also NOT flagged (famous-repo sweep hardening):
|
|
69
|
-
- generated files (`_paths.
|
|
69
|
+
- generated files (`_paths.is_generated`) — their banners are the
|
|
70
70
|
generator's warning header, not hand-written cruft;
|
|
71
71
|
- a punctuation-only "banner" directly beneath a texty comment line — that is
|
|
72
72
|
an RST-style heading underline or an ASCII-diagram row inside a prose comment
|
|
@@ -153,7 +153,7 @@ from sarj_python_lint.rules._comments import (
|
|
|
153
153
|
nested_comment_lines,
|
|
154
154
|
standalone_comments,
|
|
155
155
|
)
|
|
156
|
-
from sarj_python_lint.rules._paths import
|
|
156
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
if TYPE_CHECKING:
|
|
@@ -683,7 +683,7 @@ class NoCommentCruft(Rule):
|
|
|
683
683
|
|
|
684
684
|
@override
|
|
685
685
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
686
|
-
if
|
|
686
|
+
if is_generated(path, source):
|
|
687
687
|
return []
|
|
688
688
|
# A Sphinx `docs/**/conf.py` is quickstart-generated boilerplate whose
|
|
689
689
|
# `# -- Section ----` banners are the tool's own convention.
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py
RENAMED
|
@@ -69,7 +69,7 @@ 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.
|
|
72
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
73
73
|
generator's, and re-running the generator discards any edit, so a finding
|
|
74
74
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
75
75
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -86,7 +86,7 @@ from typing import TYPE_CHECKING, override
|
|
|
86
86
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
87
87
|
from sarj_python_lint.rules._ast_index import children, nodes
|
|
88
88
|
from sarj_python_lint.rules._logging import is_logger_expr
|
|
89
|
-
from sarj_python_lint.rules._paths import
|
|
89
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
if TYPE_CHECKING:
|
|
@@ -330,7 +330,7 @@ class NoFatTryBlocks(Rule):
|
|
|
330
330
|
|
|
331
331
|
@override
|
|
332
332
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
333
|
-
if
|
|
333
|
+
if is_generated(path, source):
|
|
334
334
|
return []
|
|
335
335
|
tree = parse_or_none(path, source)
|
|
336
336
|
if tree is None:
|
|
@@ -56,7 +56,7 @@ from typing import TYPE_CHECKING, final, override
|
|
|
56
56
|
|
|
57
57
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
58
58
|
from sarj_python_lint.rules._ast_index import nodes
|
|
59
|
-
from sarj_python_lint.rules._paths import
|
|
59
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
60
60
|
from sarj_python_lint.rules._sql import strip_sql_noise
|
|
61
61
|
|
|
62
62
|
|
|
@@ -111,7 +111,7 @@ class NoGenRandomUuidInSql(Rule):
|
|
|
111
111
|
The diagnostics, sorted by (line, col).
|
|
112
112
|
|
|
113
113
|
"""
|
|
114
|
-
if
|
|
114
|
+
if is_generated(path, source):
|
|
115
115
|
return []
|
|
116
116
|
tree = parse_or_none(path, source)
|
|
117
117
|
if tree is None:
|
|
@@ -84,7 +84,7 @@ duplicate can be suppressed per-line with `# sarj-noqa: SARJ024 — <reason>`.
|
|
|
84
84
|
|
|
85
85
|
Skipped entirely: `conftest.py`, test files (`test_*.py` or under a `tests/`
|
|
86
86
|
directory) — fixtures legitimately repeat literal payloads.
|
|
87
|
-
* **generated files** (`_paths.
|
|
87
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
88
88
|
generator's, and re-running the generator discards any edit, so a finding
|
|
89
89
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
90
90
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -100,7 +100,7 @@ from typing import TYPE_CHECKING, override
|
|
|
100
100
|
|
|
101
101
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
102
102
|
from sarj_python_lint.rules._ast_index import children, walk
|
|
103
|
-
from sarj_python_lint.rules._paths import
|
|
103
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
if TYPE_CHECKING:
|
|
@@ -130,7 +130,7 @@ class NoRepeatedStringLiteral(Rule):
|
|
|
130
130
|
|
|
131
131
|
@override
|
|
132
132
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
133
|
-
if
|
|
133
|
+
if is_generated(path, source):
|
|
134
134
|
return []
|
|
135
135
|
if _is_skipped_path(path):
|
|
136
136
|
return []
|
|
@@ -105,7 +105,7 @@ from sarj_python_lint.rules._comments import (
|
|
|
105
105
|
restates,
|
|
106
106
|
standalone_comments,
|
|
107
107
|
)
|
|
108
|
-
from sarj_python_lint.rules._paths import
|
|
108
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
if TYPE_CHECKING:
|
|
@@ -299,7 +299,7 @@ class NoRestatedComment(Rule):
|
|
|
299
299
|
|
|
300
300
|
@override
|
|
301
301
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
302
|
-
if
|
|
302
|
+
if is_generated(path, source):
|
|
303
303
|
return []
|
|
304
304
|
try:
|
|
305
305
|
standalone, _ = standalone_comments(source)
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_stdlib_logging.py
RENAMED
|
@@ -62,7 +62,7 @@ from typing import TYPE_CHECKING, final, override
|
|
|
62
62
|
|
|
63
63
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
64
64
|
from sarj_python_lint.rules._ast_index import nodes, walk
|
|
65
|
-
from sarj_python_lint.rules._paths import
|
|
65
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
if TYPE_CHECKING:
|
|
@@ -113,7 +113,7 @@ class NoStdlibLogging(Rule):
|
|
|
113
113
|
The diagnostics, sorted by (line, col).
|
|
114
114
|
|
|
115
115
|
"""
|
|
116
|
-
if is_test_path(path) or _EXEMPT_DIR_NAMES.intersection(path.parts) or
|
|
116
|
+
if is_test_path(path) or _EXEMPT_DIR_NAMES.intersection(path.parts) or is_generated(path, source):
|
|
117
117
|
return []
|
|
118
118
|
# Every diagnostic comes from an `import logging...` statement, which
|
|
119
119
|
# cannot exist unless the module name is spelled in the text. Checking
|
|
@@ -206,7 +206,7 @@ Deliberately NOT flagged:
|
|
|
206
206
|
f-string" would restyle an injection instead of fixing it, and this rule must
|
|
207
207
|
not become a competing SQL rule. The keyword match is case-SENSITIVE, so
|
|
208
208
|
prose like `"copied from " + src` is not mistaken for a query. 7 hits,
|
|
209
|
-
* **generated files** (`_paths.
|
|
209
|
+
* **generated files** (`_paths.is_generated`). Re-running the generator
|
|
210
210
|
discards any edit, so a style finding there can never be acted on — the same
|
|
211
211
|
exemption SARJ002 makes. 5 hits, all in bulbul's Speakeasy
|
|
212
212
|
`python/sdk/src/sarj_platform_sdk/`.
|
|
@@ -229,7 +229,7 @@ from typing import TYPE_CHECKING, override
|
|
|
229
229
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
230
230
|
from sarj_python_lint.rules._ast_index import nodes, walk
|
|
231
231
|
from sarj_python_lint.rules._logging import is_logger_expr
|
|
232
|
-
from sarj_python_lint.rules._paths import
|
|
232
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
233
233
|
|
|
234
234
|
|
|
235
235
|
if TYPE_CHECKING:
|
|
@@ -326,7 +326,7 @@ class PreferFstringOverConcat(Rule):
|
|
|
326
326
|
One diagnostic per outermost offending concatenation, sorted by position.
|
|
327
327
|
|
|
328
328
|
"""
|
|
329
|
-
if "+" not in source or
|
|
329
|
+
if "+" not in source or is_generated(path, source):
|
|
330
330
|
return []
|
|
331
331
|
tree = parse_or_none(path, source)
|
|
332
332
|
if tree is None:
|
|
@@ -27,6 +27,18 @@ Example refactoring:
|
|
|
27
27
|
case dict():
|
|
28
28
|
return parse_dict(data)
|
|
29
29
|
return cast(..., data)
|
|
30
|
+
|
|
31
|
+
## Not reported
|
|
32
|
+
|
|
33
|
+
* **generated files** (`_paths.is_generated`). This is the exemption that
|
|
34
|
+
matters most for this rule: the try/raise idiom in the module summary above
|
|
35
|
+
is *transcribed from* openapi-python-client's `_parse_*` template, and it
|
|
36
|
+
reproduces once per nullable field. Before the path half of `is_generated`
|
|
37
|
+
existed, 314 of this rule's 334 findings over two first-party corpora came
|
|
38
|
+
from a single checked-in SDK — a tree the consuming repo already excludes
|
|
39
|
+
from ruff, from its pre-commit hook and from its CI invocation. The
|
|
40
|
+
generator's output is not a refactor anyone can accept; re-running the
|
|
41
|
+
generator would undo it. The 20 findings that remain are hand-written.
|
|
30
42
|
"""
|
|
31
43
|
|
|
32
44
|
from __future__ import annotations
|
|
@@ -35,7 +47,7 @@ import ast
|
|
|
35
47
|
from typing import TYPE_CHECKING, final, override
|
|
36
48
|
|
|
37
49
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
38
|
-
from sarj_python_lint.rules._paths import
|
|
50
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
39
51
|
|
|
40
52
|
|
|
41
53
|
if TYPE_CHECKING:
|
|
@@ -263,7 +275,7 @@ class PreferMatchTypeDispatch(Rule):
|
|
|
263
275
|
|
|
264
276
|
@override
|
|
265
277
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
266
|
-
if
|
|
278
|
+
if is_generated(path, source):
|
|
267
279
|
return []
|
|
268
280
|
tree = parse_or_none(path, source)
|
|
269
281
|
if tree is None:
|
|
@@ -69,7 +69,7 @@ Deliberately NOT flagged:
|
|
|
69
69
|
so they are reads, while `x.sort()` mutates in place and bails.
|
|
70
70
|
* **Tiny displays** (`< _MIN_ELEMENTS` entries) read better next to their use.
|
|
71
71
|
* **Test files and generated files** (`_paths.is_test_path` /
|
|
72
|
-
`_paths.
|
|
72
|
+
`_paths.is_generated`): fixture tables belong next to the assertion
|
|
73
73
|
that explains them, and generated code mirrors its generator.
|
|
74
74
|
* **Bindings the function never reads** and **functions that call `locals()` /
|
|
75
75
|
`vars()`**. Both are the same hole in the escape analysis: a local can leave
|
|
@@ -104,7 +104,7 @@ from typing import TYPE_CHECKING, override
|
|
|
104
104
|
|
|
105
105
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
106
106
|
from sarj_python_lint.rules._ast_index import children, nodes
|
|
107
|
-
from sarj_python_lint.rules._paths import
|
|
107
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
if TYPE_CHECKING:
|
|
@@ -194,7 +194,7 @@ class PreferModuleLevelConstant(Rule):
|
|
|
194
194
|
|
|
195
195
|
@override
|
|
196
196
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
197
|
-
if is_test_path(path) or
|
|
197
|
+
if is_test_path(path) or is_generated(path, source):
|
|
198
198
|
return []
|
|
199
199
|
tree = parse_or_none(path, source)
|
|
200
200
|
if tree is None:
|
|
@@ -70,7 +70,7 @@ 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.
|
|
73
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
74
74
|
generator's, and re-running the generator discards any edit, so a finding
|
|
75
75
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
76
76
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -87,7 +87,7 @@ from typing import TYPE_CHECKING, override
|
|
|
87
87
|
|
|
88
88
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
89
89
|
from sarj_python_lint.rules._ast_index import children, nodes, walk
|
|
90
|
-
from sarj_python_lint.rules._paths import
|
|
90
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
if TYPE_CHECKING:
|
|
@@ -147,7 +147,7 @@ class PreferNamedtupleOverTupleReturn(Rule):
|
|
|
147
147
|
|
|
148
148
|
@override
|
|
149
149
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
150
|
-
if
|
|
150
|
+
if is_generated(path, source):
|
|
151
151
|
return []
|
|
152
152
|
if is_test_path(path):
|
|
153
153
|
return []
|
|
@@ -40,7 +40,7 @@ from typing import TYPE_CHECKING, override
|
|
|
40
40
|
|
|
41
41
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
42
42
|
from sarj_python_lint.rules._ast_index import walk
|
|
43
|
-
from sarj_python_lint.rules._paths import
|
|
43
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
if TYPE_CHECKING:
|
|
@@ -61,7 +61,7 @@ class PreferNonNullableCollection(Rule):
|
|
|
61
61
|
|
|
62
62
|
@override
|
|
63
63
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
64
|
-
if is_test_path(path) or
|
|
64
|
+
if is_test_path(path) or is_generated(path, source):
|
|
65
65
|
return []
|
|
66
66
|
tree = parse_or_none(path, source)
|
|
67
67
|
if tree is None:
|
|
@@ -94,10 +94,6 @@ class PreferNonNullableCollection(Rule):
|
|
|
94
94
|
return diags
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def _is_generated_or_vendored_path(path: Path) -> bool:
|
|
98
|
-
return any(part.lower() in {"generated", "vendor", "vendored"} for part in path.parts)
|
|
99
|
-
|
|
100
|
-
|
|
101
97
|
def _qualified_name(node: ast.expr) -> str:
|
|
102
98
|
if isinstance(node, ast.Name):
|
|
103
99
|
return node.id
|
|
@@ -71,7 +71,7 @@ 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.
|
|
74
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
75
75
|
generator's, and re-running the generator discards any edit, so a finding
|
|
76
76
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
77
77
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -87,7 +87,7 @@ from typing import TYPE_CHECKING, override
|
|
|
87
87
|
|
|
88
88
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
89
89
|
from sarj_python_lint.rules._ast_index import nodes
|
|
90
|
-
from sarj_python_lint.rules._paths import
|
|
90
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
if TYPE_CHECKING:
|
|
@@ -164,7 +164,7 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
164
164
|
|
|
165
165
|
@override
|
|
166
166
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
167
|
-
if
|
|
167
|
+
if is_generated(path, source):
|
|
168
168
|
return []
|
|
169
169
|
if is_test_path(path):
|
|
170
170
|
return []
|
|
@@ -55,7 +55,7 @@ from typing import TYPE_CHECKING, override
|
|
|
55
55
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
56
56
|
from sarj_python_lint.rules._ast_index import children
|
|
57
57
|
from sarj_python_lint.rules._comments import is_protected, split_identifier, stem
|
|
58
|
-
from sarj_python_lint.rules._paths import
|
|
58
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
if TYPE_CHECKING:
|
|
@@ -239,7 +239,7 @@ class RedundantDocstring(Rule):
|
|
|
239
239
|
|
|
240
240
|
@override
|
|
241
241
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
242
|
-
if
|
|
242
|
+
if is_generated(path, source):
|
|
243
243
|
return []
|
|
244
244
|
tree = parse_or_none(path, source)
|
|
245
245
|
if tree is None:
|
|
@@ -266,7 +266,7 @@ import re
|
|
|
266
266
|
from typing import TYPE_CHECKING, override
|
|
267
267
|
|
|
268
268
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
269
|
-
from sarj_python_lint.rules._paths import
|
|
269
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
if TYPE_CHECKING:
|
|
@@ -429,7 +429,7 @@ class RequirePortForService(Rule):
|
|
|
429
429
|
One diagnostic per unsubstitutable service class, sorted by position.
|
|
430
430
|
|
|
431
431
|
"""
|
|
432
|
-
if not _is_library_source(path) or
|
|
432
|
+
if not _is_library_source(path) or is_generated(path, source):
|
|
433
433
|
return []
|
|
434
434
|
tree = parse_or_none(path, source)
|
|
435
435
|
if tree is None:
|
|
@@ -39,7 +39,7 @@ 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.
|
|
42
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
43
43
|
generator's, and re-running the generator discards any edit, so a finding
|
|
44
44
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
45
45
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -53,7 +53,7 @@ import re
|
|
|
53
53
|
from typing import TYPE_CHECKING, override
|
|
54
54
|
|
|
55
55
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
56
|
-
from sarj_python_lint.rules._paths import
|
|
56
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
if TYPE_CHECKING:
|
|
@@ -134,7 +134,7 @@ class SinglePublicExport(Rule):
|
|
|
134
134
|
|
|
135
135
|
@override
|
|
136
136
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
137
|
-
if
|
|
137
|
+
if is_generated(path, source):
|
|
138
138
|
return []
|
|
139
139
|
if _is_skipped_path(path):
|
|
140
140
|
return []
|
|
@@ -58,7 +58,7 @@ 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.
|
|
61
|
+
* **generated files** (`_paths.is_generated`). Their layout is the
|
|
62
62
|
generator's, and re-running the generator discards any edit, so a finding
|
|
63
63
|
there can never be acted on in place. Measured on the 69 `DO NOT EDIT`
|
|
64
64
|
files git-tracked across bulbul and noura-be — Speakeasy's
|
|
@@ -73,7 +73,7 @@ from typing import TYPE_CHECKING, override
|
|
|
73
73
|
|
|
74
74
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
75
75
|
from sarj_python_lint.rules._ast_index import children
|
|
76
|
-
from sarj_python_lint.rules._paths import
|
|
76
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
if TYPE_CHECKING:
|
|
@@ -112,7 +112,7 @@ class Stepdown(Rule):
|
|
|
112
112
|
|
|
113
113
|
@override
|
|
114
114
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
115
|
-
if
|
|
115
|
+
if is_generated(path, source):
|
|
116
116
|
return []
|
|
117
117
|
if _is_test_path(path):
|
|
118
118
|
return []
|
|
@@ -69,7 +69,7 @@ from sarj_python_lint.rules._comments import (
|
|
|
69
69
|
stem,
|
|
70
70
|
trailing_comments,
|
|
71
71
|
)
|
|
72
|
-
from sarj_python_lint.rules._paths import
|
|
72
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
if TYPE_CHECKING:
|
|
@@ -191,7 +191,7 @@ class TrailingValueNarration(Rule):
|
|
|
191
191
|
|
|
192
192
|
@override
|
|
193
193
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
194
|
-
if
|
|
194
|
+
if is_generated(path, source):
|
|
195
195
|
return []
|
|
196
196
|
try:
|
|
197
197
|
trailing = trailing_comments(source)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""Shared file-scope predicates for the file-scope-gated rules.
|
|
2
|
-
|
|
3
|
-
A single definition of "is this a test file?" so the test-scoped rules
|
|
4
|
-
(SARJ031 no-sleep-in-test-body, SARJ036 no-raw-sql-in-tests) and the rules
|
|
5
|
-
that *exempt* tests (SARJ014 prefer-timedelta-for-durations, SARJ015
|
|
6
|
-
prefer-struct-over-namedtuple, SARJ026 prefer-namedtuple-over-tuple-return,
|
|
7
|
-
SARJ034 kwonly-same-type-params) never diverge on what counts as a test file.
|
|
8
|
-
|
|
9
|
-
Also the single definition of "is this a generated file?" (SARJ016/034):
|
|
10
|
-
generated code mirrors whatever its generator emits, so style rules cannot be
|
|
11
|
-
acted on there.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import re
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_TEST_DIR_NAMES = frozenset({"tests", "test"})
|
|
25
|
-
|
|
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
|
-
)
|
|
32
|
-
|
|
33
|
-
_GENERATED_HEADER_LINES = 5
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def is_generated_source(source: str) -> bool:
|
|
37
|
-
"""Report whether `source` self-identifies as generated code.
|
|
38
|
-
|
|
39
|
-
Checks the first few lines for the conventional markers
|
|
40
|
-
("autogenerated", "generated by", "do not edit").
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
True when the file header carries a generated-code marker.
|
|
44
|
-
|
|
45
|
-
"""
|
|
46
|
-
head = source.splitlines()[:_GENERATED_HEADER_LINES]
|
|
47
|
-
return any(_GENERATED_RE.search(line) for line in head)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def is_test_path(path: Path) -> bool:
|
|
51
|
-
"""Report whether `path` is a test file.
|
|
52
|
-
|
|
53
|
-
A test file is `conftest.py`, a `test_*.py` / `*_test.py` stem, or any file
|
|
54
|
-
under a `tests` / `test` directory segment.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
True when `path` belongs to the test tree.
|
|
58
|
-
|
|
59
|
-
"""
|
|
60
|
-
name = path.name
|
|
61
|
-
if name == "conftest.py" or name.startswith("test_") or name.endswith("_test.py"):
|
|
62
|
-
return True
|
|
63
|
-
return any(part in _TEST_DIR_NAMES for part in path.parts)
|
|
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.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/_first_party.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
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/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
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/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
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/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
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/prefer_str_enum.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
|
{sarj_python_lint-0.30.0 → sarj_python_lint-0.32.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|