pytest-tidy 0.1.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.
- pytest_tidy-0.1.0/LICENSE +21 -0
- pytest_tidy-0.1.0/PKG-INFO +316 -0
- pytest_tidy-0.1.0/README.md +258 -0
- pytest_tidy-0.1.0/pyproject.toml +81 -0
- pytest_tidy-0.1.0/setup.cfg +4 -0
- pytest_tidy-0.1.0/src/pytest_tidy/__init__.py +44 -0
- pytest_tidy-0.1.0/src/pytest_tidy/__main__.py +4 -0
- pytest_tidy-0.1.0/src/pytest_tidy/astutils.py +259 -0
- pytest_tidy-0.1.0/src/pytest_tidy/autofix.py +100 -0
- pytest_tidy-0.1.0/src/pytest_tidy/cli.py +383 -0
- pytest_tidy-0.1.0/src/pytest_tidy/config.py +186 -0
- pytest_tidy-0.1.0/src/pytest_tidy/context.py +97 -0
- pytest_tidy-0.1.0/src/pytest_tidy/discovery.py +102 -0
- pytest_tidy-0.1.0/src/pytest_tidy/engine.py +135 -0
- pytest_tidy-0.1.0/src/pytest_tidy/models.py +79 -0
- pytest_tidy-0.1.0/src/pytest_tidy/plugin.py +137 -0
- pytest_tidy-0.1.0/src/pytest_tidy/py.typed +0 -0
- pytest_tidy-0.1.0/src/pytest_tidy/reporting.py +178 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/__init__.py +45 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/assertions.py +548 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/base.py +92 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/exceptions.py +178 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/fixtures.py +275 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/markers.py +168 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/mocking.py +104 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/structure.py +163 -0
- pytest_tidy-0.1.0/src/pytest_tidy/rules/timing.py +170 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/PKG-INFO +316 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/SOURCES.txt +45 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/dependency_links.txt +1 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/entry_points.txt +5 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/requires.txt +11 -0
- pytest_tidy-0.1.0/src/pytest_tidy.egg-info/top_level.txt +1 -0
- pytest_tidy-0.1.0/tests/test_autofix.py +78 -0
- pytest_tidy-0.1.0/tests/test_cli.py +167 -0
- pytest_tidy-0.1.0/tests/test_config.py +130 -0
- pytest_tidy-0.1.0/tests/test_discovery.py +62 -0
- pytest_tidy-0.1.0/tests/test_engine.py +66 -0
- pytest_tidy-0.1.0/tests/test_plugin.py +56 -0
- pytest_tidy-0.1.0/tests/test_rules_assertions.py +215 -0
- pytest_tidy-0.1.0/tests/test_rules_catalog.py +68 -0
- pytest_tidy-0.1.0/tests/test_rules_exceptions.py +165 -0
- pytest_tidy-0.1.0/tests/test_rules_fixtures.py +153 -0
- pytest_tidy-0.1.0/tests/test_rules_markers.py +101 -0
- pytest_tidy-0.1.0/tests/test_rules_mocking.py +45 -0
- pytest_tidy-0.1.0/tests/test_rules_structure.py +95 -0
- pytest_tidy-0.1.0/tests/test_rules_timing.py +66 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ariz Zubair
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-tidy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A static, AST-based test-smell linter for pytest suites.
|
|
5
|
+
Author: Ariz Zubair
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Ariz Zubair
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/arizzubair/pytest-tidy
|
|
29
|
+
Project-URL: Documentation, https://github.com/arizzubair/pytest-tidy/blob/main/docs/rules.md
|
|
30
|
+
Project-URL: Source, https://github.com/arizzubair/pytest-tidy
|
|
31
|
+
Project-URL: Changelog, https://github.com/arizzubair/pytest-tidy/blob/main/CHANGELOG.md
|
|
32
|
+
Keywords: pytest,lint,linter,ast,testing,test-smells,static-analysis,pre-commit
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: Framework :: Pytest
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Operating System :: OS Independent
|
|
39
|
+
Classifier: Programming Language :: Python
|
|
40
|
+
Classifier: Programming Language :: Python :: 3
|
|
41
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
47
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
48
|
+
Classifier: Topic :: Software Development :: Testing
|
|
49
|
+
Requires-Python: >=3.9
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
License-File: LICENSE
|
|
52
|
+
Provides-Extra: toml
|
|
53
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "toml"
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
56
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "dev"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# pytest-tidy
|
|
60
|
+
|
|
61
|
+
[](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml)
|
|
62
|
+
[](https://pypi.org/project/pytest-tidy/)
|
|
63
|
+
[](https://pypi.org/project/pytest-tidy/)
|
|
64
|
+
[](LICENSE)
|
|
65
|
+
|
|
66
|
+
**A static, AST-based test-smell linter for pytest suites.** Cross-platform, zero
|
|
67
|
+
runtime execution — it reads your tests, it never runs them.
|
|
68
|
+
|
|
69
|
+
`pytest-tidy` walks the abstract syntax tree of your test files and flags the
|
|
70
|
+
*semantic* smells that let test suites rot: tests that assert nothing,
|
|
71
|
+
`assert True`, `except: pass` swallowing failures, `pytest.raises(Exception)`
|
|
72
|
+
catching everything, `time.sleep(5)` making CI slow and flaky, fixtures with the
|
|
73
|
+
wrong scope, and more. `flake8-pytest-style` catches formatting; `pytest-tidy`
|
|
74
|
+
catches meaning.
|
|
75
|
+
|
|
76
|
+
It ships three ways: a standalone **CLI**, a **pre-commit hook**, and a **pytest
|
|
77
|
+
plugin** that warns during collection.
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ pytest-tidy tests/
|
|
81
|
+
tests/test_orders.py:14:12: PTD002 assertion on a 2-tuple is always true; did you mean `assert <cond>, <message>`? [*]
|
|
82
|
+
tests/test_orders.py:28:5: PTD101 `except Exception:` with no handling body swallows failures
|
|
83
|
+
tests/test_orders.py:41:5: PTD003 test 'test_refund' contains no assertions
|
|
84
|
+
tests/test_billing.py:9:23: PTD203 scope="function" is the default and can be removed [*]
|
|
85
|
+
|
|
86
|
+
Found 4 problems 2 fixable with --fix
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Why
|
|
90
|
+
|
|
91
|
+
A test that silently passes is worse than no test: it gives false confidence and
|
|
92
|
+
hides regressions forever. These bugs are invisible in review because the code
|
|
93
|
+
*looks* like it tests something. `pytest-tidy` finds them statically:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# All of these pass no matter what your code does:
|
|
97
|
+
assert (order.total == 99, "wrong total") # PTD002 - a 2-tuple is always truthy
|
|
98
|
+
mock.assert_called_once # PTD501 - never called; missing ()
|
|
99
|
+
try:
|
|
100
|
+
charge(order)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass # PTD101 - swallows AssertionError too
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Highlights
|
|
106
|
+
|
|
107
|
+
- **32 rules** across 7 categories (assertions, exceptions, fixtures, timing,
|
|
108
|
+
structure, mocking, markers) - see the [full catalog](docs/rules.md).
|
|
109
|
+
- **Zero required dependencies.** The core is pure-stdlib `ast`. Works on
|
|
110
|
+
**Python 3.9+**, every OS.
|
|
111
|
+
- **Autofix.** `--fix` rewrites the unambiguous ones (byte-precise range edits
|
|
112
|
+
that preserve your formatting and comments); `--diff` previews them.
|
|
113
|
+
- **Near-zero false positives** by design. Noisier rules are opt-in, not on by
|
|
114
|
+
default.
|
|
115
|
+
- **Three delivery modes:** CLI, `pre-commit`, and a pytest plugin.
|
|
116
|
+
- **CI-friendly** output: `--format github` emits GitHub Actions annotations;
|
|
117
|
+
`--format json` is machine-readable.
|
|
118
|
+
- **Configurable** via `pyproject.toml` or `setup.cfg`, with `# noqa` support.
|
|
119
|
+
|
|
120
|
+
## Install
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
pip install pytest-tidy
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
On Python 3.11+ TOML config works out of the box. On 3.9 / 3.10, add the `toml`
|
|
127
|
+
extra if you want to configure via `pyproject.toml` (or just use `setup.cfg`,
|
|
128
|
+
which needs nothing extra):
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
pip install "pytest-tidy[toml]"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
pytest-tidy # lint test files under the current directory
|
|
138
|
+
pytest-tidy tests/ pkg/tests # lint specific paths
|
|
139
|
+
pytest-tidy tests/ --fix # apply autofixes in place
|
|
140
|
+
pytest-tidy tests/ --diff # show what --fix would change, write nothing
|
|
141
|
+
pytest-tidy tests/ --statistics # per-rule counts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When given a **directory**, `pytest-tidy` lints only files matching your
|
|
145
|
+
test-file patterns (`test_*.py`, `*_test.py`, `conftest.py`). When given an
|
|
146
|
+
**explicit file**, it always lints it.
|
|
147
|
+
|
|
148
|
+
### Useful flags
|
|
149
|
+
|
|
150
|
+
| Flag | Purpose |
|
|
151
|
+
| ---- | ------- |
|
|
152
|
+
| `--select CODES` | Enable only these rules/prefixes (replaces defaults). `--select ALL` turns on everything. |
|
|
153
|
+
| `--extend-select CODES` | Enable extra rules **on top of** the defaults (handy for opt-in rules). |
|
|
154
|
+
| `--ignore CODES` | Disable rules/prefixes. |
|
|
155
|
+
| `--fix` / `--diff` | Apply / preview autofixes. |
|
|
156
|
+
| `--format {text,json,github}` | Output format. |
|
|
157
|
+
| `--show-source` | Print the offending line with a caret. |
|
|
158
|
+
| `--statistics` | Per-rule problem counts. |
|
|
159
|
+
| `--list-rules` | Print the whole catalog (respects your config). |
|
|
160
|
+
| `--explain PTD002` | Show a single rule's rationale. |
|
|
161
|
+
| `--no-config` | Ignore any discovered config file. |
|
|
162
|
+
|
|
163
|
+
Codes are matched by prefix, so `--select PTD2` selects the whole fixtures
|
|
164
|
+
category and `--ignore PTD3` silences all timing rules.
|
|
165
|
+
|
|
166
|
+
Exit codes: `0` clean, `1` problems found, `2` usage/internal error.
|
|
167
|
+
|
|
168
|
+
## pre-commit
|
|
169
|
+
|
|
170
|
+
Add to `.pre-commit-config.yaml`:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
repos:
|
|
174
|
+
- repo: https://github.com/arizzubair/pytest-tidy
|
|
175
|
+
rev: v0.1.0
|
|
176
|
+
hooks:
|
|
177
|
+
- id: pytest-tidy
|
|
178
|
+
# ...or auto-fix on commit instead:
|
|
179
|
+
# - id: pytest-tidy-fix
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Both hooks only run on `test_*.py`, `*_test.py`, and `conftest.py` files.
|
|
183
|
+
|
|
184
|
+
## pytest plugin
|
|
185
|
+
|
|
186
|
+
The plugin is **opt-in** so it never surprises an existing suite. Enable it per
|
|
187
|
+
run:
|
|
188
|
+
|
|
189
|
+
```console
|
|
190
|
+
pytest --tidy
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
...or persistently in `pyproject.toml`:
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
[tool.pytest.ini_options]
|
|
197
|
+
tidy = true
|
|
198
|
+
# optional, mirror the CLI selection flags:
|
|
199
|
+
tidy_extend_select = ["PTD404"]
|
|
200
|
+
tidy_ignore = ["PTD003"]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Each finding surfaces as a `PytestTidyWarning` in the warnings summary during
|
|
204
|
+
collection - visible, but it never fails the run.
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
207
|
+
|
|
208
|
+
`pytest-tidy` reads `[tool.pytest-tidy]` from the nearest `pyproject.toml`, or
|
|
209
|
+
`[pytest-tidy]` from `setup.cfg` / `tox.ini` (closest file wins):
|
|
210
|
+
|
|
211
|
+
```toml
|
|
212
|
+
[tool.pytest-tidy]
|
|
213
|
+
# Turn opt-in rules on alongside the defaults:
|
|
214
|
+
extend-select = ["PTD008", "PTD302"]
|
|
215
|
+
|
|
216
|
+
# Silence a rule or a whole category:
|
|
217
|
+
ignore = ["PTD404"]
|
|
218
|
+
|
|
219
|
+
# Or lock to an explicit set (replaces the defaults):
|
|
220
|
+
# select = ["PTD001", "PTD002", "PTD1"]
|
|
221
|
+
|
|
222
|
+
# Don't lint these paths:
|
|
223
|
+
exclude = ["tests/data", "tests/legacy"]
|
|
224
|
+
|
|
225
|
+
# Extra helper calls that count as "an assertion" for PTD003:
|
|
226
|
+
assert-calls = ["assert_matches", "verify"]
|
|
227
|
+
|
|
228
|
+
# Treat functions with this prefix as tests (default: "test"):
|
|
229
|
+
test-function-prefix = "test"
|
|
230
|
+
|
|
231
|
+
# Which files count as test files when a directory is scanned:
|
|
232
|
+
test-file-patterns = ["test_*.py", "*_test.py", "conftest.py"]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The equivalent `setup.cfg` (no extra dependency needed):
|
|
236
|
+
|
|
237
|
+
```ini
|
|
238
|
+
[pytest-tidy]
|
|
239
|
+
extend-select = PTD008, PTD302
|
|
240
|
+
ignore = PTD404
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
CLI flags always override file config.
|
|
244
|
+
|
|
245
|
+
### Inline suppression
|
|
246
|
+
|
|
247
|
+
Standard `# noqa` comments are honored:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
assert True # noqa - suppress every rule on this line
|
|
251
|
+
assert value == None # noqa: PTD004 - suppress just PTD004 (comma-separate more)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Rules
|
|
255
|
+
|
|
256
|
+
32 rules, grouped by category. Full rationale, examples, and autofix notes live
|
|
257
|
+
in **[docs/rules.md](docs/rules.md)**; run `pytest-tidy --list-rules` to see the
|
|
258
|
+
set enabled by your config.
|
|
259
|
+
|
|
260
|
+
| Category | Codes | Examples |
|
|
261
|
+
| -------- | ----- | -------- |
|
|
262
|
+
| Assertions | PTD001-PTD009 | assert-on-constant, assert-on-tuple, test-without-assertion, assert-eq-none |
|
|
263
|
+
| Exceptions | PTD101-PTD104 | except-pass, raises-broad-exception, raises-multiple-statements |
|
|
264
|
+
| Fixtures | PTD201-PTD206 | fixture-scope-mismatch, redundant-fixture-scope, yield-fixture-deprecated |
|
|
265
|
+
| Timing & determinism | PTD301-PTD304 | sleep-in-test, datetime-now, random-without-seed, network-call |
|
|
266
|
+
| Structure & collection | PTD401-PTD404 | test-class-init, duplicate-test-name, return-in-test |
|
|
267
|
+
| Mocking | PTD501-PTD502 | mock-uncalled-assert, mock-nonexistent-attr |
|
|
268
|
+
| Markers & parametrize | PTD601-PTD603 | skip-without-reason, unconditional-skip, parametrize-values-mismatch |
|
|
269
|
+
|
|
270
|
+
Opt-in (disabled by default) rules: PTD008, PTD009, PTD103, PTD206, PTD302,
|
|
271
|
+
PTD303, PTD304, PTD404, PTD602. Enable them with `--extend-select` or config.
|
|
272
|
+
|
|
273
|
+
## Philosophy
|
|
274
|
+
|
|
275
|
+
- **Read, never run.** Everything is a pure `ast` walk. No imports of your code,
|
|
276
|
+
no side effects, fully deterministic and cross-platform.
|
|
277
|
+
- **Near-zero false positives.** A linter you have to argue with gets turned off.
|
|
278
|
+
Rules that can't be sure stay opt-in, and default rules bias hard toward "only
|
|
279
|
+
flag it if it's almost certainly a bug."
|
|
280
|
+
- **Fix what's safe.** Autofixes only fire on unambiguous rewrites, and re-lint
|
|
281
|
+
to convergence so chained fixes settle.
|
|
282
|
+
|
|
283
|
+
## Contributing / development
|
|
284
|
+
|
|
285
|
+
```console
|
|
286
|
+
python -m venv .venv
|
|
287
|
+
.venv/Scripts/pip install -e ".[dev]" # POSIX: .venv/bin/pip
|
|
288
|
+
python -m pytest # run the suite
|
|
289
|
+
python -m pytest_tidy tests # dogfood the linter on itself
|
|
290
|
+
python scripts/gen_rules_doc.py # regenerate docs/rules.md
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Each rule is a small class in `src/pytest_tidy/rules/` declaring the AST node
|
|
294
|
+
types it cares about and a `check()` method; the engine walks each file once and
|
|
295
|
+
dispatches nodes to interested rules.
|
|
296
|
+
|
|
297
|
+
### Releasing
|
|
298
|
+
|
|
299
|
+
Continuous integration runs the suite across Python 3.9-3.13 on Linux, macOS,
|
|
300
|
+
and Windows (`.github/workflows/ci.yml`). Publishing is automated via
|
|
301
|
+
`.github/workflows/publish.yml` using PyPI [Trusted Publishing][tp] (OIDC), so
|
|
302
|
+
no API tokens are stored in the repo. To cut a release:
|
|
303
|
+
|
|
304
|
+
1. Bump `version` in `pyproject.toml` and update `CHANGELOG.md`.
|
|
305
|
+
2. Tag and push: `git tag v0.1.0 && git push --tags`.
|
|
306
|
+
3. Publish a GitHub Release for the tag - the workflow builds the sdist/wheel
|
|
307
|
+
and uploads them to PyPI.
|
|
308
|
+
|
|
309
|
+
Run the publish workflow manually (`workflow_dispatch`) to push to TestPyPI
|
|
310
|
+
first as a dry run.
|
|
311
|
+
|
|
312
|
+
[tp]: https://docs.pypi.org/trusted-publishers/
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# pytest-tidy
|
|
2
|
+
|
|
3
|
+
[](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pytest-tidy/)
|
|
5
|
+
[](https://pypi.org/project/pytest-tidy/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**A static, AST-based test-smell linter for pytest suites.** Cross-platform, zero
|
|
9
|
+
runtime execution — it reads your tests, it never runs them.
|
|
10
|
+
|
|
11
|
+
`pytest-tidy` walks the abstract syntax tree of your test files and flags the
|
|
12
|
+
*semantic* smells that let test suites rot: tests that assert nothing,
|
|
13
|
+
`assert True`, `except: pass` swallowing failures, `pytest.raises(Exception)`
|
|
14
|
+
catching everything, `time.sleep(5)` making CI slow and flaky, fixtures with the
|
|
15
|
+
wrong scope, and more. `flake8-pytest-style` catches formatting; `pytest-tidy`
|
|
16
|
+
catches meaning.
|
|
17
|
+
|
|
18
|
+
It ships three ways: a standalone **CLI**, a **pre-commit hook**, and a **pytest
|
|
19
|
+
plugin** that warns during collection.
|
|
20
|
+
|
|
21
|
+
```console
|
|
22
|
+
$ pytest-tidy tests/
|
|
23
|
+
tests/test_orders.py:14:12: PTD002 assertion on a 2-tuple is always true; did you mean `assert <cond>, <message>`? [*]
|
|
24
|
+
tests/test_orders.py:28:5: PTD101 `except Exception:` with no handling body swallows failures
|
|
25
|
+
tests/test_orders.py:41:5: PTD003 test 'test_refund' contains no assertions
|
|
26
|
+
tests/test_billing.py:9:23: PTD203 scope="function" is the default and can be removed [*]
|
|
27
|
+
|
|
28
|
+
Found 4 problems 2 fixable with --fix
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
A test that silently passes is worse than no test: it gives false confidence and
|
|
34
|
+
hides regressions forever. These bugs are invisible in review because the code
|
|
35
|
+
*looks* like it tests something. `pytest-tidy` finds them statically:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# All of these pass no matter what your code does:
|
|
39
|
+
assert (order.total == 99, "wrong total") # PTD002 - a 2-tuple is always truthy
|
|
40
|
+
mock.assert_called_once # PTD501 - never called; missing ()
|
|
41
|
+
try:
|
|
42
|
+
charge(order)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass # PTD101 - swallows AssertionError too
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Highlights
|
|
48
|
+
|
|
49
|
+
- **32 rules** across 7 categories (assertions, exceptions, fixtures, timing,
|
|
50
|
+
structure, mocking, markers) - see the [full catalog](docs/rules.md).
|
|
51
|
+
- **Zero required dependencies.** The core is pure-stdlib `ast`. Works on
|
|
52
|
+
**Python 3.9+**, every OS.
|
|
53
|
+
- **Autofix.** `--fix` rewrites the unambiguous ones (byte-precise range edits
|
|
54
|
+
that preserve your formatting and comments); `--diff` previews them.
|
|
55
|
+
- **Near-zero false positives** by design. Noisier rules are opt-in, not on by
|
|
56
|
+
default.
|
|
57
|
+
- **Three delivery modes:** CLI, `pre-commit`, and a pytest plugin.
|
|
58
|
+
- **CI-friendly** output: `--format github` emits GitHub Actions annotations;
|
|
59
|
+
`--format json` is machine-readable.
|
|
60
|
+
- **Configurable** via `pyproject.toml` or `setup.cfg`, with `# noqa` support.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
pip install pytest-tidy
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On Python 3.11+ TOML config works out of the box. On 3.9 / 3.10, add the `toml`
|
|
69
|
+
extra if you want to configure via `pyproject.toml` (or just use `setup.cfg`,
|
|
70
|
+
which needs nothing extra):
|
|
71
|
+
|
|
72
|
+
```console
|
|
73
|
+
pip install "pytest-tidy[toml]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```console
|
|
79
|
+
pytest-tidy # lint test files under the current directory
|
|
80
|
+
pytest-tidy tests/ pkg/tests # lint specific paths
|
|
81
|
+
pytest-tidy tests/ --fix # apply autofixes in place
|
|
82
|
+
pytest-tidy tests/ --diff # show what --fix would change, write nothing
|
|
83
|
+
pytest-tidy tests/ --statistics # per-rule counts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
When given a **directory**, `pytest-tidy` lints only files matching your
|
|
87
|
+
test-file patterns (`test_*.py`, `*_test.py`, `conftest.py`). When given an
|
|
88
|
+
**explicit file**, it always lints it.
|
|
89
|
+
|
|
90
|
+
### Useful flags
|
|
91
|
+
|
|
92
|
+
| Flag | Purpose |
|
|
93
|
+
| ---- | ------- |
|
|
94
|
+
| `--select CODES` | Enable only these rules/prefixes (replaces defaults). `--select ALL` turns on everything. |
|
|
95
|
+
| `--extend-select CODES` | Enable extra rules **on top of** the defaults (handy for opt-in rules). |
|
|
96
|
+
| `--ignore CODES` | Disable rules/prefixes. |
|
|
97
|
+
| `--fix` / `--diff` | Apply / preview autofixes. |
|
|
98
|
+
| `--format {text,json,github}` | Output format. |
|
|
99
|
+
| `--show-source` | Print the offending line with a caret. |
|
|
100
|
+
| `--statistics` | Per-rule problem counts. |
|
|
101
|
+
| `--list-rules` | Print the whole catalog (respects your config). |
|
|
102
|
+
| `--explain PTD002` | Show a single rule's rationale. |
|
|
103
|
+
| `--no-config` | Ignore any discovered config file. |
|
|
104
|
+
|
|
105
|
+
Codes are matched by prefix, so `--select PTD2` selects the whole fixtures
|
|
106
|
+
category and `--ignore PTD3` silences all timing rules.
|
|
107
|
+
|
|
108
|
+
Exit codes: `0` clean, `1` problems found, `2` usage/internal error.
|
|
109
|
+
|
|
110
|
+
## pre-commit
|
|
111
|
+
|
|
112
|
+
Add to `.pre-commit-config.yaml`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
repos:
|
|
116
|
+
- repo: https://github.com/arizzubair/pytest-tidy
|
|
117
|
+
rev: v0.1.0
|
|
118
|
+
hooks:
|
|
119
|
+
- id: pytest-tidy
|
|
120
|
+
# ...or auto-fix on commit instead:
|
|
121
|
+
# - id: pytest-tidy-fix
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Both hooks only run on `test_*.py`, `*_test.py`, and `conftest.py` files.
|
|
125
|
+
|
|
126
|
+
## pytest plugin
|
|
127
|
+
|
|
128
|
+
The plugin is **opt-in** so it never surprises an existing suite. Enable it per
|
|
129
|
+
run:
|
|
130
|
+
|
|
131
|
+
```console
|
|
132
|
+
pytest --tidy
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
...or persistently in `pyproject.toml`:
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[tool.pytest.ini_options]
|
|
139
|
+
tidy = true
|
|
140
|
+
# optional, mirror the CLI selection flags:
|
|
141
|
+
tidy_extend_select = ["PTD404"]
|
|
142
|
+
tidy_ignore = ["PTD003"]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each finding surfaces as a `PytestTidyWarning` in the warnings summary during
|
|
146
|
+
collection - visible, but it never fails the run.
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
`pytest-tidy` reads `[tool.pytest-tidy]` from the nearest `pyproject.toml`, or
|
|
151
|
+
`[pytest-tidy]` from `setup.cfg` / `tox.ini` (closest file wins):
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
[tool.pytest-tidy]
|
|
155
|
+
# Turn opt-in rules on alongside the defaults:
|
|
156
|
+
extend-select = ["PTD008", "PTD302"]
|
|
157
|
+
|
|
158
|
+
# Silence a rule or a whole category:
|
|
159
|
+
ignore = ["PTD404"]
|
|
160
|
+
|
|
161
|
+
# Or lock to an explicit set (replaces the defaults):
|
|
162
|
+
# select = ["PTD001", "PTD002", "PTD1"]
|
|
163
|
+
|
|
164
|
+
# Don't lint these paths:
|
|
165
|
+
exclude = ["tests/data", "tests/legacy"]
|
|
166
|
+
|
|
167
|
+
# Extra helper calls that count as "an assertion" for PTD003:
|
|
168
|
+
assert-calls = ["assert_matches", "verify"]
|
|
169
|
+
|
|
170
|
+
# Treat functions with this prefix as tests (default: "test"):
|
|
171
|
+
test-function-prefix = "test"
|
|
172
|
+
|
|
173
|
+
# Which files count as test files when a directory is scanned:
|
|
174
|
+
test-file-patterns = ["test_*.py", "*_test.py", "conftest.py"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The equivalent `setup.cfg` (no extra dependency needed):
|
|
178
|
+
|
|
179
|
+
```ini
|
|
180
|
+
[pytest-tidy]
|
|
181
|
+
extend-select = PTD008, PTD302
|
|
182
|
+
ignore = PTD404
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
CLI flags always override file config.
|
|
186
|
+
|
|
187
|
+
### Inline suppression
|
|
188
|
+
|
|
189
|
+
Standard `# noqa` comments are honored:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
assert True # noqa - suppress every rule on this line
|
|
193
|
+
assert value == None # noqa: PTD004 - suppress just PTD004 (comma-separate more)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Rules
|
|
197
|
+
|
|
198
|
+
32 rules, grouped by category. Full rationale, examples, and autofix notes live
|
|
199
|
+
in **[docs/rules.md](docs/rules.md)**; run `pytest-tidy --list-rules` to see the
|
|
200
|
+
set enabled by your config.
|
|
201
|
+
|
|
202
|
+
| Category | Codes | Examples |
|
|
203
|
+
| -------- | ----- | -------- |
|
|
204
|
+
| Assertions | PTD001-PTD009 | assert-on-constant, assert-on-tuple, test-without-assertion, assert-eq-none |
|
|
205
|
+
| Exceptions | PTD101-PTD104 | except-pass, raises-broad-exception, raises-multiple-statements |
|
|
206
|
+
| Fixtures | PTD201-PTD206 | fixture-scope-mismatch, redundant-fixture-scope, yield-fixture-deprecated |
|
|
207
|
+
| Timing & determinism | PTD301-PTD304 | sleep-in-test, datetime-now, random-without-seed, network-call |
|
|
208
|
+
| Structure & collection | PTD401-PTD404 | test-class-init, duplicate-test-name, return-in-test |
|
|
209
|
+
| Mocking | PTD501-PTD502 | mock-uncalled-assert, mock-nonexistent-attr |
|
|
210
|
+
| Markers & parametrize | PTD601-PTD603 | skip-without-reason, unconditional-skip, parametrize-values-mismatch |
|
|
211
|
+
|
|
212
|
+
Opt-in (disabled by default) rules: PTD008, PTD009, PTD103, PTD206, PTD302,
|
|
213
|
+
PTD303, PTD304, PTD404, PTD602. Enable them with `--extend-select` or config.
|
|
214
|
+
|
|
215
|
+
## Philosophy
|
|
216
|
+
|
|
217
|
+
- **Read, never run.** Everything is a pure `ast` walk. No imports of your code,
|
|
218
|
+
no side effects, fully deterministic and cross-platform.
|
|
219
|
+
- **Near-zero false positives.** A linter you have to argue with gets turned off.
|
|
220
|
+
Rules that can't be sure stay opt-in, and default rules bias hard toward "only
|
|
221
|
+
flag it if it's almost certainly a bug."
|
|
222
|
+
- **Fix what's safe.** Autofixes only fire on unambiguous rewrites, and re-lint
|
|
223
|
+
to convergence so chained fixes settle.
|
|
224
|
+
|
|
225
|
+
## Contributing / development
|
|
226
|
+
|
|
227
|
+
```console
|
|
228
|
+
python -m venv .venv
|
|
229
|
+
.venv/Scripts/pip install -e ".[dev]" # POSIX: .venv/bin/pip
|
|
230
|
+
python -m pytest # run the suite
|
|
231
|
+
python -m pytest_tidy tests # dogfood the linter on itself
|
|
232
|
+
python scripts/gen_rules_doc.py # regenerate docs/rules.md
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Each rule is a small class in `src/pytest_tidy/rules/` declaring the AST node
|
|
236
|
+
types it cares about and a `check()` method; the engine walks each file once and
|
|
237
|
+
dispatches nodes to interested rules.
|
|
238
|
+
|
|
239
|
+
### Releasing
|
|
240
|
+
|
|
241
|
+
Continuous integration runs the suite across Python 3.9-3.13 on Linux, macOS,
|
|
242
|
+
and Windows (`.github/workflows/ci.yml`). Publishing is automated via
|
|
243
|
+
`.github/workflows/publish.yml` using PyPI [Trusted Publishing][tp] (OIDC), so
|
|
244
|
+
no API tokens are stored in the repo. To cut a release:
|
|
245
|
+
|
|
246
|
+
1. Bump `version` in `pyproject.toml` and update `CHANGELOG.md`.
|
|
247
|
+
2. Tag and push: `git tag v0.1.0 && git push --tags`.
|
|
248
|
+
3. Publish a GitHub Release for the tag - the workflow builds the sdist/wheel
|
|
249
|
+
and uploads them to PyPI.
|
|
250
|
+
|
|
251
|
+
Run the publish workflow manually (`workflow_dispatch`) to push to TestPyPI
|
|
252
|
+
first as a dry run.
|
|
253
|
+
|
|
254
|
+
[tp]: https://docs.pypi.org/trusted-publishers/
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-tidy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A static, AST-based test-smell linter for pytest suites."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Ariz Zubair" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"pytest",
|
|
15
|
+
"lint",
|
|
16
|
+
"linter",
|
|
17
|
+
"ast",
|
|
18
|
+
"testing",
|
|
19
|
+
"test-smells",
|
|
20
|
+
"static-analysis",
|
|
21
|
+
"pre-commit",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Environment :: Console",
|
|
26
|
+
"Framework :: Pytest",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
33
|
+
"Programming Language :: Python :: 3.9",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
39
|
+
"Topic :: Software Development :: Testing",
|
|
40
|
+
]
|
|
41
|
+
# The core linter has ZERO required third-party dependencies. It is pure-stdlib
|
|
42
|
+
# `ast`. TOML config parsing uses stdlib `tomllib` on 3.11+; on older Pythons it
|
|
43
|
+
# is available via the optional `toml` extra (a `tomli` backport). Config can
|
|
44
|
+
# also be read from setup.cfg with no extra at all.
|
|
45
|
+
dependencies = []
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
toml = ["tomli>=1.1.0; python_version < '3.11'"]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=7.0",
|
|
51
|
+
"tomli>=1.1.0; python_version < '3.11'",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[project.scripts]
|
|
55
|
+
pytest-tidy = "pytest_tidy.cli:main"
|
|
56
|
+
|
|
57
|
+
[project.entry-points.pytest11]
|
|
58
|
+
pytest_tidy = "pytest_tidy.plugin"
|
|
59
|
+
|
|
60
|
+
[project.urls]
|
|
61
|
+
Homepage = "https://github.com/arizzubair/pytest-tidy"
|
|
62
|
+
Documentation = "https://github.com/arizzubair/pytest-tidy/blob/main/docs/rules.md"
|
|
63
|
+
Source = "https://github.com/arizzubair/pytest-tidy"
|
|
64
|
+
Changelog = "https://github.com/arizzubair/pytest-tidy/blob/main/CHANGELOG.md"
|
|
65
|
+
|
|
66
|
+
[tool.setuptools.packages.find]
|
|
67
|
+
where = ["src"]
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.package-data]
|
|
70
|
+
pytest_tidy = ["py.typed"]
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
# Keep our own plugin quiet during our own test run unless a test opts in.
|
|
75
|
+
addopts = "-p no:cacheprovider"
|
|
76
|
+
|
|
77
|
+
# Dogfooding: pytest-tidy linting its own test suite with the default rule set.
|
|
78
|
+
# `fnmatch_lines` is pytester's assertion helper, so it counts as an assertion.
|
|
79
|
+
[tool.pytest-tidy]
|
|
80
|
+
exclude = ["tests/data"]
|
|
81
|
+
assert-calls = ["fnmatch_lines"]
|