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.
Files changed (47) hide show
  1. pytest_tidy-0.1.0/LICENSE +21 -0
  2. pytest_tidy-0.1.0/PKG-INFO +316 -0
  3. pytest_tidy-0.1.0/README.md +258 -0
  4. pytest_tidy-0.1.0/pyproject.toml +81 -0
  5. pytest_tidy-0.1.0/setup.cfg +4 -0
  6. pytest_tidy-0.1.0/src/pytest_tidy/__init__.py +44 -0
  7. pytest_tidy-0.1.0/src/pytest_tidy/__main__.py +4 -0
  8. pytest_tidy-0.1.0/src/pytest_tidy/astutils.py +259 -0
  9. pytest_tidy-0.1.0/src/pytest_tidy/autofix.py +100 -0
  10. pytest_tidy-0.1.0/src/pytest_tidy/cli.py +383 -0
  11. pytest_tidy-0.1.0/src/pytest_tidy/config.py +186 -0
  12. pytest_tidy-0.1.0/src/pytest_tidy/context.py +97 -0
  13. pytest_tidy-0.1.0/src/pytest_tidy/discovery.py +102 -0
  14. pytest_tidy-0.1.0/src/pytest_tidy/engine.py +135 -0
  15. pytest_tidy-0.1.0/src/pytest_tidy/models.py +79 -0
  16. pytest_tidy-0.1.0/src/pytest_tidy/plugin.py +137 -0
  17. pytest_tidy-0.1.0/src/pytest_tidy/py.typed +0 -0
  18. pytest_tidy-0.1.0/src/pytest_tidy/reporting.py +178 -0
  19. pytest_tidy-0.1.0/src/pytest_tidy/rules/__init__.py +45 -0
  20. pytest_tidy-0.1.0/src/pytest_tidy/rules/assertions.py +548 -0
  21. pytest_tidy-0.1.0/src/pytest_tidy/rules/base.py +92 -0
  22. pytest_tidy-0.1.0/src/pytest_tidy/rules/exceptions.py +178 -0
  23. pytest_tidy-0.1.0/src/pytest_tidy/rules/fixtures.py +275 -0
  24. pytest_tidy-0.1.0/src/pytest_tidy/rules/markers.py +168 -0
  25. pytest_tidy-0.1.0/src/pytest_tidy/rules/mocking.py +104 -0
  26. pytest_tidy-0.1.0/src/pytest_tidy/rules/structure.py +163 -0
  27. pytest_tidy-0.1.0/src/pytest_tidy/rules/timing.py +170 -0
  28. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/PKG-INFO +316 -0
  29. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/SOURCES.txt +45 -0
  30. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/dependency_links.txt +1 -0
  31. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/entry_points.txt +5 -0
  32. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/requires.txt +11 -0
  33. pytest_tidy-0.1.0/src/pytest_tidy.egg-info/top_level.txt +1 -0
  34. pytest_tidy-0.1.0/tests/test_autofix.py +78 -0
  35. pytest_tidy-0.1.0/tests/test_cli.py +167 -0
  36. pytest_tidy-0.1.0/tests/test_config.py +130 -0
  37. pytest_tidy-0.1.0/tests/test_discovery.py +62 -0
  38. pytest_tidy-0.1.0/tests/test_engine.py +66 -0
  39. pytest_tidy-0.1.0/tests/test_plugin.py +56 -0
  40. pytest_tidy-0.1.0/tests/test_rules_assertions.py +215 -0
  41. pytest_tidy-0.1.0/tests/test_rules_catalog.py +68 -0
  42. pytest_tidy-0.1.0/tests/test_rules_exceptions.py +165 -0
  43. pytest_tidy-0.1.0/tests/test_rules_fixtures.py +153 -0
  44. pytest_tidy-0.1.0/tests/test_rules_markers.py +101 -0
  45. pytest_tidy-0.1.0/tests/test_rules_mocking.py +45 -0
  46. pytest_tidy-0.1.0/tests/test_rules_structure.py +95 -0
  47. 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
+ [![CI](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml/badge.svg)](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml)
62
+ [![PyPI](https://img.shields.io/pypi/v/pytest-tidy.svg)](https://pypi.org/project/pytest-tidy/)
63
+ [![Python versions](https://img.shields.io/pypi/pyversions/pytest-tidy.svg)](https://pypi.org/project/pytest-tidy/)
64
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml/badge.svg)](https://github.com/arizzubair/pytest-tidy/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pytest-tidy.svg)](https://pypi.org/project/pytest-tidy/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/pytest-tidy.svg)](https://pypi.org/project/pytest-tidy/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+