Skip to content

Commit 86a3acd

Browse files
committed
feat: support pytest 9's new config
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 38a581e commit 86a3acd

File tree

5 files changed

+315
-86
lines changed

5 files changed

+315
-86
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,10 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1
329329
- [`PP005`](https://learn.scientific-python.org/development/guides/packaging-simple#PP005): Using SPDX project.license should not use deprecated trove classifiers
330330
- [`PP006`](https://learn.scientific-python.org/development/guides/packaging-simple#PP006): The dev dependency group should be defined
331331
- [`PP301`](https://learn.scientific-python.org/development/guides/pytest#PP301): Has pytest in pyproject
332-
- [`PP302`](https://learn.scientific-python.org/development/guides/pytest#PP302): Sets a minimum pytest to at least 6
332+
- [`PP302`](https://learn.scientific-python.org/development/guides/pytest#PP302): Sets a minimum pytest to at least 6 or 9
333333
- [`PP303`](https://learn.scientific-python.org/development/guides/pytest#PP303): Sets the test paths
334334
- [`PP304`](https://learn.scientific-python.org/development/guides/pytest#PP304): Sets the log level in pytest
335-
- [`PP305`](https://learn.scientific-python.org/development/guides/pytest#PP305): Specifies xfail_strict
335+
- [`PP305`](https://learn.scientific-python.org/development/guides/pytest#PP305): Specifies strict xfail
336336
- [`PP306`](https://learn.scientific-python.org/development/guides/pytest#PP306): Specifies strict config
337337
- [`PP307`](https://learn.scientific-python.org/development/guides/pytest#PP307): Specifies strict markers
338338
- [`PP308`](https://learn.scientific-python.org/development/guides/pytest#PP308): Specifies useful pytest summary

docs/pages/guides/pytest.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ This looks simple, but it is doing several things:
6666
### Configuring pytest
6767

6868
pytest supports configuration in `pytest.ini`, `setup.cfg`, or, since version 6,
69-
`pyproject.toml` {% rr PP301 %}. Remember, pytest is a developer requirement,
70-
not a user one, so always require 6+ (or 7+) and use `pyproject.toml`. This is
71-
an example configuration:
69+
`pyproject.toml` {% rr PP301 %}, or, since version 9, `pytest.toml` or
70+
`.pytest.toml`. Remember, pytest is a developer requirement, not a user one, so
71+
always require 6+ (or 9+) and use `pyproject.toml` or the pytest TOML ones. This
72+
is an example configuration:
73+
74+
{% tabs %} {% tab classic "Pytest 6+" %}
7275

7376
```toml
7477
[tool.pytest.ini_options]
@@ -82,23 +85,40 @@ testpaths = [
8285
]
8386
```
8487

88+
{% endtab %} {% tab modern "Pytest 9+" %}
89+
90+
```toml
91+
[tool.pytest]
92+
minversion = "9.0"
93+
addopts = ["-ra", "--showlocals"]
94+
strict = true
95+
filterwarnings = ["error"]
96+
log_level = "INFO"
97+
testpaths = [
98+
"tests",
99+
]
100+
```
101+
102+
{% endtab %} {% endtabs %}
103+
85104
{% rr PP302 %} The `minversion` will print a nicer error if your `pytest` is too
86105
old (though, ironically, it won't read this if the version is too old, so
87-
setting "6" or less in `pyproject.toml` is rather pointless). The `addopts`
88-
setting will add whatever you put there to the command line when you run;
89-
{% rr PP308 %} `-ra` will print a summary "r"eport of "a"ll results, which gives
90-
you a quick way to review what tests failed and were skipped, and why.
91-
`--showlocals` will print locals in tracebacks. {% rr PP307 %}
92-
`--strict-markers` will make sure you don't try to use an unspecified fixture.
93-
{% rr PP306 %} And `--strict-config` will error if you make a mistake in your
94-
config. {% rr PP305 %} `xfail_strict` will change the default for `xfail` to
95-
fail the tests if it doesn't fail - you can still override locally in a specific
96-
xfail for a flaky failure. {% rr PP309 %} `filter_warnings` will cause all
97-
warnings to be errors (you can add allowed warnings here too, see below).
98-
{% rr PP304 %} `log_level` will report `INFO` and above log messages on a
99-
failure. {% rr PP303 %} Finally, `testpaths` will limit `pytest` to just looking
100-
in the folders given - useful if it tries to pick up things that are not tests
101-
from other directories.
106+
setting "6" or less in `pyproject.toml` is rather pointless, similarly for 9 if
107+
using the new config location). The `addopts` setting will add whatever you put
108+
there to the command line when you run; {% rr PP308 %} `-ra` will print a
109+
summary "r"eport of "a"ll results, which gives you a quick way to review what
110+
tests failed and were skipped, and why. `--showlocals` will print locals in
111+
tracebacks - depending on your tests, you might or might not like this one.
112+
{% rr PP307 %} `--strict-markers` will make sure you don't try to use an
113+
unspecified fixture. {% rr PP306 %} And `--strict-config` will error if you make
114+
a mistake in your config. {% rr PP305 %} `xfail_strict` will change the default
115+
for `xfail` to fail the tests if it doesn't fail - you can still override
116+
locally in a specific xfail for a flaky failure. {% rr PP309 %}
117+
`filter_warnings` will cause all warnings to be errors (you can add allowed
118+
warnings here too, see below). {% rr PP304 %} `log_level` will report `INFO` and
119+
above log messages on a failure. {% rr PP303 %} Finally, `testpaths` will limit
120+
`pytest` to just looking in the folders given - useful if it tries to pick up
121+
things that are not tests from other directories.
102122
[See the docs](https://docs.pytest.org/en/stable/customize.html) for more
103123
options.
104124

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,14 @@ setupcfg = "sp_repo_review.checks.setupcfg:repo_review_checks"
7070
noxfile = "sp_repo_review.checks.noxfile:repo_review_checks"
7171

7272
[project.entry-points."repo_review.fixtures"]
73-
workflows = "sp_repo_review.checks.github:workflows"
7473
dependabot = "sp_repo_review.checks.github:dependabot"
74+
noxfile = "sp_repo_review.checks.noxfile:noxfile"
7575
precommit = "sp_repo_review.checks.precommit:precommit"
76+
pytest = "sp_repo_review.checks.pyproject:pytest"
7677
readthedocs = "sp_repo_review.checks.readthedocs:readthedocs"
7778
ruff = "sp_repo_review.checks.ruff:ruff"
7879
setupcfg = "sp_repo_review.checks.setupcfg:setupcfg"
79-
noxfile = "sp_repo_review.checks.noxfile:noxfile"
80+
workflows = "sp_repo_review.checks.github:workflows"
8081

8182
[project.entry-points."repo_review.families"]
8283
scikit-hep = "sp_repo_review.families:get_families"

src/sp_repo_review/checks/pyproject.py

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
11
from __future__ import annotations
22

3+
import enum
34
from typing import TYPE_CHECKING, Any
45

6+
from .._compat import tomllib
57
from . import mk_url
68

79
if TYPE_CHECKING:
810
from configparser import ConfigParser
911

12+
from .._compat.importlib.resources.abc import Traversable
13+
14+
15+
class PytestFile(enum.Enum):
16+
PYTEST_TOML = enum.auto()
17+
MODERN_PYPROJECT = enum.auto()
18+
LEGACY_PYPROJECT = enum.auto()
19+
NONE = enum.auto()
20+
21+
22+
def pytest(
23+
pyproject: dict[str, Any], root: Traversable
24+
) -> tuple[PytestFile, dict[str, Any]]:
25+
"""
26+
Returns the pytest configuration, or None if the configuration doesn't exist.
27+
Respects toml configurations only.
28+
"""
29+
paths = [root.joinpath("pytest.toml"), root.joinpath(".pytest.toml")]
30+
for path in paths:
31+
if path.is_file():
32+
with path.open("rb") as f:
33+
contents = tomllib.load(f)
34+
return (PytestFile.PYTEST_TOML, contents.get("pytest", {}))
35+
36+
match pyproject:
37+
case {"tool": {"pytest": {"ini_options": config}}}:
38+
return (PytestFile.LEGACY_PYPROJECT, config)
39+
case {"tool": {"pytest": config}}:
40+
return (PytestFile.MODERN_PYPROJECT, config)
41+
case _:
42+
return (PytestFile.NONE, {})
43+
1044

1145
def get_requires_python(
1246
pyproject: dict[str, Any], setupcfg: ConfigParser | None
@@ -149,45 +183,45 @@ def check(pyproject: dict[str, Any]) -> bool:
149183
class PP301(PyProject):
150184
"Has pytest in pyproject"
151185

152-
requires = {"PY001"}
186+
requires = set[str]()
153187
url = mk_url("pytest")
154188

155189
@staticmethod
156-
def check(pyproject: dict[str, Any]) -> bool:
190+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
157191
"""
158-
Must have a `[tool.pytest.ini_options]` configuration section in
159-
pyproject.toml. If you must have it somewhere else (such as to support
160-
`pytest<6`), ignore this check.
192+
Must have a `[tool.pytest]` (pytest 9+) or `[tool.pytest.ini_options]`
193+
(pytest 6+) configuration section in pyproject.toml. If you must have it
194+
in ini format, ignore this check. pytest.toml and .pytest.toml files
195+
(pytest 9+) are also supported.
161196
"""
162197

163-
match pyproject:
164-
case {"tool": {"pytest": {"ini_options": {}}}}:
165-
return True
166-
case _:
167-
return False
198+
loc, _ = pytest
199+
return loc is not PytestFile.NONE
168200

169201

170202
class PP302(PyProject):
171-
"Sets a minimum pytest to at least 6"
203+
"Sets a minimum pytest to at least 6 or 9"
172204

173205
requires = {"PP301"}
174206
url = mk_url("pytest")
175207

176208
@staticmethod
177-
def check(pyproject: dict[str, Any]) -> bool:
209+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
178210
"""
179211
Must have a `minversion=`, and must be at least 6 (first version to
180-
support `pyproject.toml` configuration).
212+
support `pyproject.toml` ini configuration) or 9 (first version to
213+
support native configuration and toml config files).
181214
182215
```toml
183216
[tool.pytest.ini_options]
184-
minversion = "7"
217+
minversion = "9"
185218
```
186219
"""
187-
options = pyproject["tool"]["pytest"]["ini_options"]
220+
loc, options = pytest
221+
minversion = 6 if loc is PytestFile.LEGACY_PYPROJECT else 9
188222
return (
189223
"minversion" in options
190-
and int(str(options["minversion"]).split(".", maxsplit=1)[0]) >= 6
224+
and int(str(options["minversion"]).split(".", maxsplit=1)[0]) >= minversion
191225
)
192226

193227

@@ -198,7 +232,7 @@ class PP303(PyProject):
198232
url = mk_url("pytest")
199233

200234
@staticmethod
201-
def check(pyproject: dict[str, Any]) -> bool:
235+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
202236
"""
203237
The `testpaths` setting should be set to a reasonable default.
204238
@@ -207,7 +241,7 @@ def check(pyproject: dict[str, Any]) -> bool:
207241
testpaths = ["tests"]
208242
```
209243
"""
210-
options = pyproject["tool"]["pytest"]["ini_options"]
244+
_, options = pytest
211245
return "testpaths" in options
212246

213247

@@ -218,7 +252,7 @@ class PP304(PyProject):
218252
url = mk_url("pytest")
219253

220254
@staticmethod
221-
def check(pyproject: dict[str, Any]) -> bool:
255+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
222256
"""
223257
`log_level` should be set. This will allow logs to be displayed on
224258
failures.
@@ -228,29 +262,34 @@ def check(pyproject: dict[str, Any]) -> bool:
228262
log_level = "INFO"
229263
```
230264
"""
231-
options = pyproject["tool"]["pytest"]["ini_options"]
265+
_, options = pytest
232266
return "log_cli_level" in options or "log_level" in options
233267

234268

235269
class PP305(PyProject):
236-
"Specifies xfail_strict"
270+
"Specifies strict xfail"
237271

238272
requires = {"PP301"}
239273
url = mk_url("pytest")
240274

241275
@staticmethod
242-
def check(pyproject: dict[str, Any]) -> bool:
276+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
243277
"""
244-
`xfail_strict` should be set. You can manually specify if a check
245-
should be strict when setting each xfail.
278+
`xfail_strict`, or if using pytest 9+, `strict_xfail` or `strict` should
279+
be set. You can manually specify if a check should be strict when
280+
setting each xfail.
246281
247282
```toml
248283
[tool.pytest.ini_options]
249284
xfail_strict = true
250285
```
251286
"""
252-
options = pyproject["tool"]["pytest"]["ini_options"]
253-
return "xfail_strict" in options
287+
_, options = pytest
288+
return (
289+
"xfail_strict" in options
290+
or "strict_xfail" in options
291+
or "strict" in options
292+
)
254293

255294

256295
class PP306(PyProject):
@@ -260,18 +299,23 @@ class PP306(PyProject):
260299
url = mk_url("pytest")
261300

262301
@staticmethod
263-
def check(pyproject: dict[str, Any]) -> bool:
302+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
264303
"""
265-
`--strict-config` should be in `addopts = [...]`. This forces an error
266-
if a config setting is misspelled.
304+
`--strict-config` should be in `addopts = [...]` or (pytest 9+)
305+
`strict_config` or `strict` should be set. This forces an error if a
306+
config setting is misspelled.
267307
268308
```toml
269309
[tool.pytest.ini_options]
270310
addopts = ["-ra", "--strict-config", "--strict-markers"]
271311
```
272312
"""
273-
options = pyproject["tool"]["pytest"]["ini_options"]
274-
return "--strict-config" in options.get("addopts", [])
313+
_, options = pytest
314+
return (
315+
"strict" in options
316+
or "strict_config" in options
317+
or "--strict-config" in options.get("addopts", [])
318+
)
275319

276320

277321
class PP307(PyProject):
@@ -281,18 +325,23 @@ class PP307(PyProject):
281325
url = mk_url("pytest")
282326

283327
@staticmethod
284-
def check(pyproject: dict[str, Any]) -> bool:
328+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
285329
"""
286-
`--strict-markers` should be in `addopts = [...]`. This forces all
287-
markers to be specified in config, avoiding misspellings.
330+
`--strict-markers` should be in `addopts = [...]` or (pytest 9+)
331+
`strict_markers` or `strict` should be set. This forces test markers to
332+
be specified in config, avoiding misspellings.
288333
289334
```toml
290335
[tool.pytest.ini_options]
291336
addopts = ["-ra", "--strict-config", "--strict-markers"]
292337
```
293338
"""
294-
options = pyproject["tool"]["pytest"]["ini_options"]
295-
return "--strict-markers" in options.get("addopts", [])
339+
_, options = pytest
340+
return (
341+
"strict" in options
342+
or "strict_markers" in options
343+
or "--strict-markers" in options.get("addopts", [])
344+
)
296345

297346

298347
class PP308(PyProject):
@@ -302,7 +351,7 @@ class PP308(PyProject):
302351
url = mk_url("pytest")
303352

304353
@staticmethod
305-
def check(pyproject: dict[str, Any]) -> bool:
354+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
306355
"""
307356
An explicit summary flag like `-ra` should be in `addopts = [...]`
308357
(print summary of all fails/errors).
@@ -312,9 +361,9 @@ def check(pyproject: dict[str, Any]) -> bool:
312361
addopts = ["-ra", "--strict-config", "--strict-markers"]
313362
```
314363
"""
315-
options = pyproject["tool"]["pytest"]["ini_options"]
364+
loc, options = pytest
316365
addopts = options.get("addopts", [])
317-
if isinstance(addopts, str):
366+
if loc is PytestFile.LEGACY_PYPROJECT and isinstance(addopts, str):
318367
addopts = addopts.split()
319368
return any(opt.startswith("-r") for opt in addopts)
320369

@@ -326,7 +375,7 @@ class PP309(PyProject):
326375
url = mk_url("pytest")
327376

328377
@staticmethod
329-
def check(pyproject: dict[str, Any]) -> bool:
378+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
330379
"""
331380
`filterwarnings` must be set (probably to at least `["error"]`). Python
332381
will hide important warnings otherwise, like deprecations.
@@ -336,7 +385,7 @@ def check(pyproject: dict[str, Any]) -> bool:
336385
filterwarnings = ["error"]
337386
```
338387
"""
339-
options = pyproject["tool"]["pytest"]["ini_options"]
388+
_, options = pytest
340389
return "filterwarnings" in options
341390

342391

0 commit comments

Comments
 (0)