anti-slop-python 0.1.0__tar.gz → 0.2.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.
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/.github/workflows/publish.yml +10 -3
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/PKG-INFO +247 -6
- anti_slop_python-0.2.0/README.md +565 -0
- anti_slop_python-0.2.0/examples/basic_project/README.md +94 -0
- anti_slop_python-0.2.0/examples/basic_project/REFACTOR_TRIAL.md +89 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/annotation_violations.py +40 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/explicit_exports.py +5 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report.py +554 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/demo.py +53 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/exports.py +83 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/inventory.py +23 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/models.py +73 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/order_report.py +155 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/parsing.py +129 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/presentation.py +71 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/pricing.py +70 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/order_report_refactored/reporting.py +65 -0
- anti_slop_python-0.2.0/examples/basic_project/src/example_project/wildcard_violations.py +5 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/pyproject.toml +5 -1
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/checker.py +20 -6
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/cli.py +14 -2
- anti_slop_python-0.2.0/src/anti_slop_python/configuration.py +115 -0
- anti_slop_python-0.2.0/src/anti_slop_python/diagnostics.py +94 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/ruff_integration.py +32 -11
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/ruff_policy.py +17 -1
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/rules/__init__.py +2 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/rules/base.py +3 -0
- anti_slop_python-0.2.0/src/anti_slop_python/rules/too_many_module_lines.py +42 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/tests/test_cli.py +48 -6
- anti_slop_python-0.2.0/tests/test_diagnostics.py +41 -0
- anti_slop_python-0.2.0/tests/test_examples.py +78 -0
- anti_slop_python-0.2.0/tests/test_module_size_configuration.py +176 -0
- anti_slop_python-0.2.0/tests/test_order_report_refactoring.py +235 -0
- anti_slop_python-0.2.0/tests/test_release_version.py +70 -0
- anti_slop_python-0.2.0/tests/test_ruff_annotations.py +86 -0
- anti_slop_python-0.2.0/tests/test_ruff_exports.py +74 -0
- anti_slop_python-0.2.0/tests/test_ruff_project_root.py +53 -0
- anti_slop_python-0.2.0/tests/test_too_many_module_lines.py +80 -0
- anti_slop_python-0.1.0/README.md +0 -328
- anti_slop_python-0.1.0/examples/basic_project/README.md +0 -34
- anti_slop_python-0.1.0/src/anti_slop_python/diagnostics.py +0 -18
- anti_slop_python-0.1.0/tests/test_examples.py +0 -31
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/.github/workflows/checks.yml +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/.gitignore +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/.pre-commit-config.yaml +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/.pre-commit-hooks.yaml +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/AGENTS.md +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/LICENSE +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/examples/basic_project/pyproject.toml +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/examples/basic_project/src/example_project/__init__.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/examples/basic_project/src/example_project/preferred.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/examples/basic_project/src/example_project/violations.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/skills/install-anti-slop-python/SKILL.md +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/skills/install-anti-slop-python/agents/openai.yaml +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/__init__.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/__main__.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/rules/no_any_containers.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/src/anti_slop_python/rules/no_dynamic_attribute_access.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/tests/test_checker.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/tests/test_no_any_containers.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/tests/test_no_dynamic_attribute_access.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/tests/test_ruff_integration.py +0 -0
- {anti_slop_python-0.1.0 → anti_slop_python-0.2.0}/uv.lock +0 -0
|
@@ -4,6 +4,12 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
6
|
- main
|
|
7
|
+
paths-ignore:
|
|
8
|
+
- "**.md"
|
|
9
|
+
- "docs/**"
|
|
10
|
+
- "examples/**"
|
|
11
|
+
- ".gitignore"
|
|
12
|
+
- "LICENSE"
|
|
7
13
|
|
|
8
14
|
concurrency:
|
|
9
15
|
group: publish-main
|
|
@@ -73,15 +79,16 @@ jobs:
|
|
|
73
79
|
tags = [t.strip() for t in tags_proc.stdout.splitlines() if t.strip()]
|
|
74
80
|
|
|
75
81
|
if not tags:
|
|
76
|
-
next_tag = "v0.
|
|
82
|
+
next_tag = "v0.2.0"
|
|
77
83
|
else:
|
|
78
84
|
latest = tags[0]
|
|
79
85
|
match = re.match(r"^v(\d+)\.(\d+)\.(\d+)$", latest)
|
|
80
86
|
if match:
|
|
81
87
|
major, minor, patch = map(int, match.groups())
|
|
82
|
-
|
|
88
|
+
next_version = max((major, minor, patch + 1), (0, 2, 0))
|
|
89
|
+
next_tag = "v" + ".".join(map(str, next_version))
|
|
83
90
|
else:
|
|
84
|
-
next_tag = "v0.
|
|
91
|
+
next_tag = "v0.2.0"
|
|
85
92
|
|
|
86
93
|
print(f"tag={next_tag}")
|
|
87
94
|
print(f"version={next_tag.lstrip('v')}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: anti-slop-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: An opinionated architectural linter for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/ruarfff/anti-slop-python
|
|
6
6
|
Project-URL: Repository, https://github.com/ruarfff/anti-slop-python
|
|
@@ -14,6 +14,10 @@ Classifier: Environment :: Console
|
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
21
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
22
|
Requires-Python: >=3.11
|
|
19
23
|
Requires-Dist: ruff<0.17,>=0.16.5
|
|
@@ -21,7 +25,10 @@ Description-Content-Type: text/markdown
|
|
|
21
25
|
|
|
22
26
|
# anti-slop-python
|
|
23
27
|
|
|
28
|
+
[](https://pypi.org/project/anti-slop-python/)
|
|
29
|
+
[](https://pypi.org/project/anti-slop-python/)
|
|
24
30
|
[](https://skills.sh/ruarfff/anti-slop-python)
|
|
31
|
+
[](LICENSE)
|
|
25
32
|
|
|
26
33
|
`anti-slop-python` is a small, opinionated architectural
|
|
27
34
|
linter for Python inspired by and largely copied from
|
|
@@ -70,7 +77,7 @@ The repository includes hook metadata. Add this entry to
|
|
|
70
77
|
```yaml
|
|
71
78
|
repos:
|
|
72
79
|
- repo: https://github.com/ruarfff/anti-slop-python
|
|
73
|
-
rev: v0.
|
|
80
|
+
rev: v0.2.0
|
|
74
81
|
hooks:
|
|
75
82
|
- id: anti-slop-python
|
|
76
83
|
```
|
|
@@ -89,7 +96,7 @@ parts of a project:
|
|
|
89
96
|
```yaml
|
|
90
97
|
repos:
|
|
91
98
|
- repo: https://github.com/ruarfff/anti-slop-python
|
|
92
|
-
rev: v0.
|
|
99
|
+
rev: v0.2.0
|
|
93
100
|
hooks:
|
|
94
101
|
- id: anti-slop-python
|
|
95
102
|
files: ^(?:src/a-specific-module/|tests/tests-for-that-module/)
|
|
@@ -100,6 +107,40 @@ Useful if you want to gradually introduce `anti-slop-python` to a project.
|
|
|
100
107
|
Expand the `files` expression as more directories adopt the policy.
|
|
101
108
|
This does not override the project's Ruff configuration for files outside the selected directories.
|
|
102
109
|
|
|
110
|
+
#### Exclude specific files during adoption
|
|
111
|
+
|
|
112
|
+
To adopt the module-size limit incrementally, add a hook-level `exclude` in
|
|
113
|
+
`.pre-commit-config.yaml` for existing large files:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
repos:
|
|
117
|
+
- repo: https://github.com/ruarfff/anti-slop-python
|
|
118
|
+
rev: v0.2.0 # Replace with the release to use.
|
|
119
|
+
hooks:
|
|
120
|
+
- id: anti-slop-python
|
|
121
|
+
exclude: ^(?:src/legacy/reporting\.py|tests/test_legacy_reporting\.py)$
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The [pre-commit filter](https://pre-commit.com/#regular-expressions) is a regular
|
|
125
|
+
expression over repository-relative paths. The anchors and escaped dots make
|
|
126
|
+
this example match only the two named files. Remove each path as its file is
|
|
127
|
+
refactored to meet the limit. This filter also applies to
|
|
128
|
+
`pre-commit run anti-slop-python --all-files`.
|
|
129
|
+
|
|
130
|
+
This skips **all checks in this hook** for those files, including other native
|
|
131
|
+
rules and Ruff-backed checks. It does not disable only `SPY003`. A separate Ruff
|
|
132
|
+
hook or command can still check them.
|
|
133
|
+
|
|
134
|
+
Native rules do not currently support per-file rule ignores. Ruff's
|
|
135
|
+
`per-file-ignores`, Ruff exclusions, and `# noqa: SPY003` do not disable
|
|
136
|
+
`SPY003`. The pre-commit filter also does not apply to direct CLI runs such as
|
|
137
|
+
`anti-slop-python .`. For direct runs, pass only the files or directories ready
|
|
138
|
+
for enforcement, for example:
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
uvx anti-slop-python src/new_package/ tests/test_new_package.py
|
|
142
|
+
```
|
|
143
|
+
|
|
103
144
|
## Rules
|
|
104
145
|
|
|
105
146
|
Native `anti-slop-python` rules cover some checks that Ruff does not provide:
|
|
@@ -108,6 +149,7 @@ Native `anti-slop-python` rules cover some checks that Ruff does not provide:
|
|
|
108
149
|
| --- | --- | --- |
|
|
109
150
|
| SPY001 | `no-any-containers` | `dict`, `list`, `set`, or `tuple` parameterized with `Any` (including their `typing` aliases) |
|
|
110
151
|
| SPY002 | `no-dynamic-attribute-access` | Calls to `getattr()`, `setattr()`, or `delattr()` |
|
|
152
|
+
| SPY003 | `too-many-module-lines` | Modules over 500 physical lines, or test modules over 1,500; both limits are configurable |
|
|
111
153
|
|
|
112
154
|
### Ruff-backed policy
|
|
113
155
|
|
|
@@ -116,6 +158,10 @@ policy by default:
|
|
|
116
158
|
|
|
117
159
|
| Rule | Default policy |
|
|
118
160
|
| --- | --- |
|
|
161
|
+
| [`ANN001`, `ANN002`, `ANN003`](https://docs.astral.sh/ruff/rules/#flake8-annotations-ann) | Require annotations on function parameters, including `*args` and `**kwargs` |
|
|
162
|
+
| [`ANN201`, `ANN202`, `ANN204`, `ANN205`, `ANN206`](https://docs.astral.sh/ruff/rules/#flake8-annotations-ann) | Require return annotations on public/private functions and special, static, and class methods |
|
|
163
|
+
| [`ANN401`](https://docs.astral.sh/ruff/rules/any-type/) | Reject `Any` on function arguments |
|
|
164
|
+
| [`F403`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/) | Require named imports instead of wildcard imports |
|
|
119
165
|
| [`C901`](https://docs.astral.sh/ruff/rules/complex-structure/) | Cyclomatic complexity of at most 10 |
|
|
120
166
|
| [`PLR0915`](https://docs.astral.sh/ruff/rules/too-many-statements/) | At most 40 statements per function or method |
|
|
121
167
|
| [`TID251`](https://docs.astral.sh/ruff/rules/banned-api/) | Ban `unittest.mock.patch` and `mock.patch` |
|
|
@@ -127,7 +173,7 @@ equivalent to:
|
|
|
127
173
|
|
|
128
174
|
```toml
|
|
129
175
|
[tool.ruff.lint]
|
|
130
|
-
extend-select = ["BLE001", "C901", "E722", "PLR0915", "TID251"]
|
|
176
|
+
extend-select = ["ANN", "BLE001", "C901", "E722", "F403", "PLR0915", "TID251"]
|
|
131
177
|
|
|
132
178
|
[tool.ruff.lint.mccabe]
|
|
133
179
|
max-complexity = 10
|
|
@@ -154,6 +200,10 @@ max-statements = 50
|
|
|
154
200
|
`ignore` and `extend-ignore` disable default rules. Explicit thresholds,
|
|
155
201
|
exclusions, per-file ignores, and `noqa` comments also apply.
|
|
156
202
|
|
|
203
|
+
Relative Ruff paths, including source roots and exclusions, resolve from the
|
|
204
|
+
directory containing that project's Ruff configuration. Checking a project from
|
|
205
|
+
its parent directory uses the same settings as checking from the project itself.
|
|
206
|
+
|
|
157
207
|
If the project defines the `TID251` banned-API table, it replaces the default
|
|
158
208
|
provided by `anti-slop-python`.
|
|
159
209
|
|
|
@@ -163,7 +213,7 @@ source location. Ruff exclusions apply to Ruff-backed checks. Native SPY rules
|
|
|
163
213
|
still check every Python file in the paths passed to `anti-slop-python`; use the
|
|
164
214
|
command paths or pre-commit `files`/`exclude` settings to control that scope.
|
|
165
215
|
|
|
166
|
-
When
|
|
216
|
+
When a Ruff override is weaker than the default policy, `anti-slop-python` prints a
|
|
167
217
|
non-failing policy notice. Stricter settings do not produce a notice. The
|
|
168
218
|
defaults apply only to Ruff runs started by `anti-slop-python`; add the equivalent
|
|
169
219
|
configuration above if a separate `ruff check` command must enforce the same
|
|
@@ -228,6 +278,144 @@ attribute name is a constant string. They therefore allow calls such as
|
|
|
228
278
|
depend on runtime strings. `SPY002` rejects
|
|
229
279
|
the built-ins regardless of whether the name is constant.
|
|
230
280
|
|
|
281
|
+
### SPY003 — Limit module size
|
|
282
|
+
|
|
283
|
+
`SPY003` allows 500 physical lines per production module and 1,500 per test
|
|
284
|
+
module by default. It counts code, comments, blank lines, and docstrings. A
|
|
285
|
+
final newline terminates the last line; it does not add another line. The
|
|
286
|
+
diagnostic appears at line 1 and includes the actual count and active limit.
|
|
287
|
+
Files with syntax errors report the syntax error instead of native diagnostics.
|
|
288
|
+
Native rules do not support `noqa` suppression.
|
|
289
|
+
|
|
290
|
+
For incremental adoption, see [Exclude specific files during adoption](#exclude-specific-files-during-adoption).
|
|
291
|
+
|
|
292
|
+
A module can contain many small, simple functions and still become hard to
|
|
293
|
+
maintain. This check complements Ruff's function size and complexity limits by
|
|
294
|
+
catching growth across the whole file. The 500-line limit is an opinionated
|
|
295
|
+
review threshold, not proof that a module has poor design.
|
|
296
|
+
|
|
297
|
+
When it fails, look for distinct responsibilities and move cohesive code into
|
|
298
|
+
focused modules. Do not remove useful documentation, compress code, or split
|
|
299
|
+
files at arbitrary line boundaries just to pass the check.
|
|
300
|
+
|
|
301
|
+
The diagnostic includes refactoring guidance. Each extracted module should have
|
|
302
|
+
a clear purpose and interface. Keep closely related code together, minimize
|
|
303
|
+
shared state and cross-module calls, and avoid circular imports. Moving unrelated
|
|
304
|
+
code into a generic helpers module does not improve the design. Preserve public
|
|
305
|
+
APIs and verify behavior after changing the boundaries.
|
|
306
|
+
Check package imports, standalone imports where supported, and every entry point.
|
|
307
|
+
Each supported import mode must retain the full public API.
|
|
308
|
+
Preserve validation order, side effects, and type information when moving code.
|
|
309
|
+
|
|
310
|
+
The [agent comparison](examples/basic_project/REFACTOR_TRIAL.md) records a strong
|
|
311
|
+
prompt baseline, repeated linter-guided runs, withheld behavior checks and the
|
|
312
|
+
failures that led to improvements. A clean lint result does not establish API
|
|
313
|
+
compatibility or correct behavior.
|
|
314
|
+
|
|
315
|
+
#### Test modules and configuration
|
|
316
|
+
|
|
317
|
+
Test modules can contain many independent scenarios, fixtures, and deliberate
|
|
318
|
+
repetition. Their larger budget helps keep related cases together. Files named
|
|
319
|
+
`test_*.py`, `*_test.py`, or `conftest.py` use the test limit automatically,
|
|
320
|
+
at any directory depth. Names are case-sensitive. Importing `pytest` or using
|
|
321
|
+
assertions does not change a file's classification. A helper such as
|
|
322
|
+
`tests/helpers.py` uses the production limit unless a configured pattern matches.
|
|
323
|
+
|
|
324
|
+
Set the limits and add test-helper paths in `pyproject.toml`:
|
|
325
|
+
|
|
326
|
+
```toml
|
|
327
|
+
[tool.anti-slop-python]
|
|
328
|
+
max-module-lines = 500
|
|
329
|
+
max-test-module-lines = 1500
|
|
330
|
+
test-file-patterns = ["tests/**", "specs/**"]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Limits must be positive integers. Patterns extend the automatic filename
|
|
334
|
+
conventions; an empty list keeps only those conventions. Patterns are matched
|
|
335
|
+
against the complete, case-sensitive path relative to this `pyproject.toml`,
|
|
336
|
+
using `/` separators. They use shell-style matching: `*` can span directories,
|
|
337
|
+
so `tests/**` includes both immediate and nested files. Absolute paths and `..`
|
|
338
|
+
segments are rejected. Patterns cannot classify files outside the configuration
|
|
339
|
+
directory as tests.
|
|
340
|
+
|
|
341
|
+
For each source file, the checker searches its directory and parents for the
|
|
342
|
+
nearest `[tool.anti-slop-python]` table. A nested `pyproject.toml` without this
|
|
343
|
+
table inherits the parent settings. A nearer table replaces the parent table;
|
|
344
|
+
omitted options use the defaults above. This works for direct file arguments,
|
|
345
|
+
directory scans, and pre-commit, regardless of the current working directory.
|
|
346
|
+
Invalid options stop the CLI with exit code 2.
|
|
347
|
+
|
|
348
|
+
Only the module-size budget changes for tests. Other native and Ruff-backed
|
|
349
|
+
rules still apply. Oversized tests receive guidance specific to test design:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
tests/test_orders.py:1:1 SPY003 Too many lines in test module (1620 > 1500)
|
|
353
|
+
Group tests by the behavior or component they verify.
|
|
354
|
+
Keep each scenario readable and preserve assertions and edge cases.
|
|
355
|
+
Do not remove coverage, compress cases, or hide setup in shared fixtures
|
|
356
|
+
merely to satisfy this limit.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
For Python callers, `check_file()` discovers configuration automatically.
|
|
360
|
+
`check_source()` uses defaults without reading configuration files; pass a
|
|
361
|
+
`ModuleSizeSettings` instance from `anti_slop_python.configuration` with its
|
|
362
|
+
`settings=` argument to supply custom values. The `root` field sets the base
|
|
363
|
+
directory for additional test patterns. Invalid file configuration raises
|
|
364
|
+
`ConfigurationError` when calling `check_file()` directly.
|
|
365
|
+
|
|
366
|
+
### ANN — Keep function contracts explicit
|
|
367
|
+
|
|
368
|
+
The annotation rules require parameter and return types on functions and methods,
|
|
369
|
+
including private helpers, special methods, and variadic arguments. `ANN401`
|
|
370
|
+
also rejects `Any` on function arguments. Local variables can use type inference;
|
|
371
|
+
this policy does not require annotations on every assignment.
|
|
372
|
+
|
|
373
|
+
A refactoring agent can preserve runtime output while removing type information
|
|
374
|
+
that callers and type checkers need. These rules catch missing function
|
|
375
|
+
annotations even when the function moves to a different file. They check the
|
|
376
|
+
current source, not Git history, and do not establish that the annotations are
|
|
377
|
+
correct. Use a type checker to check their consistency with the implementation.
|
|
378
|
+
|
|
379
|
+
Keep the actual types when moving code. Do not replace them with `Any`, broad
|
|
380
|
+
`object` types, or casts merely to pass. Projects can adopt these Ruff-backed
|
|
381
|
+
rules incrementally without disabling other checks on a legacy file:
|
|
382
|
+
|
|
383
|
+
```toml
|
|
384
|
+
[tool.ruff.lint.per-file-ignores]
|
|
385
|
+
"src/legacy.py" = ["ANN"]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The native rules still run on that file, and ignored annotation rules produce
|
|
389
|
+
policy notices. This policy does not require explicit annotations on `self`
|
|
390
|
+
or `cls`. Ruff's annotation-specific settings remain project-controlled.
|
|
391
|
+
|
|
392
|
+
### F403 — Keep imports and public exports explicit
|
|
393
|
+
|
|
394
|
+
Wildcard imports hide which module provides a name. They can also hide unused
|
|
395
|
+
import findings during a refactor. Use named imports and preserve the intended
|
|
396
|
+
public API. For a facade that re-exports a name, declare it in `__all__`:
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
from .pricing import calculate_tax
|
|
400
|
+
|
|
401
|
+
__all__ = ["calculate_tax"]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
When Ruff reports `F401`, the output also explains how to preserve public
|
|
405
|
+
re-exports. Do not delete a public name or replace named imports with `import *`
|
|
406
|
+
merely to clear that finding. `F403` checks wildcard syntax; it does not verify
|
|
407
|
+
that imports resolve at runtime. Test imports and behavior separately.
|
|
408
|
+
|
|
409
|
+
For incremental adoption, retain a specific legacy facade while checking other
|
|
410
|
+
files and rules:
|
|
411
|
+
|
|
412
|
+
```toml
|
|
413
|
+
[tool.ruff.lint.per-file-ignores]
|
|
414
|
+
"src/legacy/__init__.py" = ["F403"]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
This produces a policy notice. Native rules continue to run on the file.
|
|
418
|
+
|
|
231
419
|
### [`C901`](https://docs.astral.sh/ruff/rules/complex-structure/) — Limit decision complexity
|
|
232
420
|
|
|
233
421
|
`C901` measures the number of paths through a function. Anti-slop-python uses a
|
|
@@ -300,10 +488,48 @@ command exit with status 1:
|
|
|
300
488
|
|
|
301
489
|
```text
|
|
302
490
|
src/api/parser.py:41:12 SPY001 Avoid containers parameterized with Any
|
|
491
|
+
Describe the actual data with concrete types, TypedDict, or a dataclass.
|
|
492
|
+
Validate untrusted data at the boundary; narrow unknown values before use.
|
|
493
|
+
Do not remove annotations or hide Any behind aliases, casts, or bare containers.
|
|
303
494
|
src/api/parser.py:45:8 SPY002 Avoid dynamic attribute access
|
|
495
|
+
Use direct attribute access for a known interface.
|
|
496
|
+
For runtime choices, use an explicit mapping of supported operations.
|
|
497
|
+
Preserve missing-value behavior explicitly.
|
|
498
|
+
Do not replace this call with __dict__, vars(), or a reflection wrapper.
|
|
499
|
+
src/api/large_module.py:1:1 SPY003 Too many lines in module (642 > 500)
|
|
500
|
+
Separate distinct responsibilities into cohesive modules with clear interfaces.
|
|
501
|
+
Keep closely related code together and preserve public APIs and behavior.
|
|
502
|
+
Preserve validation order, side effects, and type information when moving code.
|
|
503
|
+
Verify package and standalone imports where supported, plus each entry point.
|
|
504
|
+
Check that every supported import mode exposes the full public API.
|
|
505
|
+
Do not compress code, remove useful comments, split at arbitrary line counts,
|
|
506
|
+
or move unrelated code into a generic helpers module to satisfy this limit.
|
|
304
507
|
src/orders/service.py:18:5 C901 `create_order` is too complex (14 > 10)
|
|
508
|
+
Simplify the decision model; extract cohesive operations with explicit inputs.
|
|
509
|
+
Use a lookup table only when the branches represent a data mapping.
|
|
510
|
+
Preserve edge cases; do not hide branches in lambdas or raise the limit.
|
|
305
511
|
```
|
|
306
512
|
|
|
513
|
+
Each native rule and recommended Ruff rule includes indented guidance after the
|
|
514
|
+
source line. It describes the intended design change and common shortcuts to
|
|
515
|
+
avoid. Other Ruff diagnostics retain their original output. Ruff's message,
|
|
516
|
+
including any project-defined banned-API explanation, remains on the first line.
|
|
517
|
+
When reading diagnostics through the Python API, `message` contains the summary;
|
|
518
|
+
`str(diagnostic)` includes the source location and guidance.
|
|
519
|
+
|
|
520
|
+
| Rule | Guidance |
|
|
521
|
+
| --- | --- |
|
|
522
|
+
| `ANN` | Declare real parameter and return types; preserve existing type information |
|
|
523
|
+
| `SPY001` | Describe and validate the actual data instead of hiding `Any` |
|
|
524
|
+
| `SPY002` | Use explicit interfaces and preserve missing-value behavior |
|
|
525
|
+
| `SPY003` | Separate production responsibilities or group tests by behavior; preserve related code and test coverage |
|
|
526
|
+
| `F401`, `F403` | Keep public exports through named imports and `__all__`; check every supported import mode |
|
|
527
|
+
| `C901` | Simplify decisions while preserving edge cases |
|
|
528
|
+
| `PLR0915` | Extract meaningful steps while preserving ordering and side effects |
|
|
529
|
+
| `TID251` | Follow the project's API policy; pass dependencies when test isolation is needed |
|
|
530
|
+
| `E722` | Catch specific recoverable errors and allow interrupts to propagate |
|
|
531
|
+
| `BLE001` | Handle known failures; do not add logging or defaults just to silence the rule |
|
|
532
|
+
|
|
307
533
|
Policy notices are written to standard error and do not change the exit code:
|
|
308
534
|
|
|
309
535
|
```text
|
|
@@ -337,13 +563,28 @@ Run the example explicitly to see its diagnostics:
|
|
|
337
563
|
uv run anti-slop-python examples/basic_project
|
|
338
564
|
```
|
|
339
565
|
|
|
566
|
+
The [agent comparison](examples/basic_project/REFACTOR_TRIAL.md) records nine
|
|
567
|
+
fresh refactoring runs, including a strong-prompt control and the failures
|
|
568
|
+
observed. It shows agents correcting specific lint findings, but does not
|
|
569
|
+
establish an overall refactor-correctness advantage.
|
|
570
|
+
|
|
340
571
|
## Scope and limitations
|
|
341
572
|
|
|
342
573
|
Native checks use Python's built-in `ast` and a pragmatic import alias map.
|
|
343
574
|
They do not perform scope-aware name resolution, type checking, cross-file
|
|
344
|
-
analysis,
|
|
575
|
+
analysis, suppressions, or autofixes. Native configuration currently covers only
|
|
576
|
+
module-size limits and additional test-file patterns. Ruff-backed checks use
|
|
345
577
|
the project's effective Ruff configuration and suppression behavior.
|
|
346
578
|
|
|
579
|
+
## Releases
|
|
580
|
+
|
|
581
|
+
Version `0.2.0` adds module-size enforcement, function annotation checks,
|
|
582
|
+
explicit-import checks, and refactoring guidance. Versions come from Git tags
|
|
583
|
+
through `hatch-vcs`.
|
|
584
|
+
The publishing workflow selects at least `v0.2.0` for the next untagged `main`
|
|
585
|
+
commit, then continues with patch increments. Opening a PR does not publish a
|
|
586
|
+
release; publishing runs after eligible changes reach `main`.
|
|
587
|
+
|
|
347
588
|
## License
|
|
348
589
|
|
|
349
590
|
[MIT](LICENSE)
|