Skip to content

Commit 4763cf2

Browse files
henryiiiCopilot
andauthored
feat: support pytest 9's new config (#681)
* feat: support pytest 9's new config Signed-off-by: Henry Schreiner <[email protected]> * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 37dcd7d commit 4763cf2

File tree

5 files changed

+316
-86
lines changed

5 files changed

+316
-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: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,47 @@
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 a tuple containing the pytest configuration file type and the configuration dictionary.
27+
Returns (PytestFile.NONE, {}) if the configuration doesn't exist.
28+
Respects toml configurations only.
29+
"""
30+
paths = [root.joinpath("pytest.toml"), root.joinpath(".pytest.toml")]
31+
for path in paths:
32+
if path.is_file():
33+
with path.open("rb") as f:
34+
contents = tomllib.load(f)
35+
return (PytestFile.PYTEST_TOML, contents.get("pytest", {}))
36+
37+
match pyproject:
38+
case {"tool": {"pytest": {"ini_options": config}}}:
39+
return (PytestFile.LEGACY_PYPROJECT, config)
40+
case {"tool": {"pytest": config}}:
41+
return (PytestFile.MODERN_PYPROJECT, config)
42+
case _:
43+
return (PytestFile.NONE, {})
44+
1045

1146
def get_requires_python(
1247
pyproject: dict[str, Any], setupcfg: ConfigParser | None
@@ -149,45 +184,45 @@ def check(pyproject: dict[str, Any]) -> bool:
149184
class PP301(PyProject):
150185
"Has pytest in pyproject"
151186

152-
requires = {"PY001"}
187+
requires = set[str]()
153188
url = mk_url("pytest")
154189

155190
@staticmethod
156-
def check(pyproject: dict[str, Any]) -> bool:
191+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
157192
"""
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.
193+
Must have a `[tool.pytest]` (pytest 9+) or `[tool.pytest.ini_options]`
194+
(pytest 6+) configuration section in pyproject.toml. If you must have it
195+
in ini format, ignore this check. pytest.toml and .pytest.toml files
196+
(pytest 9+) are also supported.
161197
"""
162198

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

169202

170203
class PP302(PyProject):
171-
"Sets a minimum pytest to at least 6"
204+
"Sets a minimum pytest to at least 6 or 9"
172205

173206
requires = {"PP301"}
174207
url = mk_url("pytest")
175208

176209
@staticmethod
177-
def check(pyproject: dict[str, Any]) -> bool:
210+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
178211
"""
179212
Must have a `minversion=`, and must be at least 6 (first version to
180-
support `pyproject.toml` configuration).
213+
support `pyproject.toml` ini configuration) or 9 (first version to
214+
support native configuration and toml config files).
181215
182216
```toml
183217
[tool.pytest.ini_options]
184-
minversion = "7"
218+
minversion = "9"
185219
```
186220
"""
187-
options = pyproject["tool"]["pytest"]["ini_options"]
221+
loc, options = pytest
222+
minversion = 6 if loc is PytestFile.LEGACY_PYPROJECT else 9
188223
return (
189224
"minversion" in options
190-
and int(str(options["minversion"]).split(".", maxsplit=1)[0]) >= 6
225+
and int(str(options["minversion"]).split(".", maxsplit=1)[0]) >= minversion
191226
)
192227

193228

@@ -198,7 +233,7 @@ class PP303(PyProject):
198233
url = mk_url("pytest")
199234

200235
@staticmethod
201-
def check(pyproject: dict[str, Any]) -> bool:
236+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
202237
"""
203238
The `testpaths` setting should be set to a reasonable default.
204239
@@ -207,7 +242,7 @@ def check(pyproject: dict[str, Any]) -> bool:
207242
testpaths = ["tests"]
208243
```
209244
"""
210-
options = pyproject["tool"]["pytest"]["ini_options"]
245+
_, options = pytest
211246
return "testpaths" in options
212247

213248

@@ -218,7 +253,7 @@ class PP304(PyProject):
218253
url = mk_url("pytest")
219254

220255
@staticmethod
221-
def check(pyproject: dict[str, Any]) -> bool:
256+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
222257
"""
223258
`log_level` should be set. This will allow logs to be displayed on
224259
failures.
@@ -228,29 +263,34 @@ def check(pyproject: dict[str, Any]) -> bool:
228263
log_level = "INFO"
229264
```
230265
"""
231-
options = pyproject["tool"]["pytest"]["ini_options"]
266+
_, options = pytest
232267
return "log_cli_level" in options or "log_level" in options
233268

234269

235270
class PP305(PyProject):
236-
"Specifies xfail_strict"
271+
"Specifies strict xfail"
237272

238273
requires = {"PP301"}
239274
url = mk_url("pytest")
240275

241276
@staticmethod
242-
def check(pyproject: dict[str, Any]) -> bool:
277+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
243278
"""
244-
`xfail_strict` should be set. You can manually specify if a check
245-
should be strict when setting each xfail.
279+
`xfail_strict`, or if using pytest 9+, `strict_xfail` or `strict` should
280+
be set. You can manually specify if a check should be strict when
281+
setting each xfail.
246282
247283
```toml
248284
[tool.pytest.ini_options]
249285
xfail_strict = true
250286
```
251287
"""
252-
options = pyproject["tool"]["pytest"]["ini_options"]
253-
return "xfail_strict" in options
288+
_, options = pytest
289+
return (
290+
"xfail_strict" in options
291+
or "strict_xfail" in options
292+
or "strict" in options
293+
)
254294

255295

256296
class PP306(PyProject):
@@ -260,18 +300,23 @@ class PP306(PyProject):
260300
url = mk_url("pytest")
261301

262302
@staticmethod
263-
def check(pyproject: dict[str, Any]) -> bool:
303+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
264304
"""
265-
`--strict-config` should be in `addopts = [...]`. This forces an error
266-
if a config setting is misspelled.
305+
`--strict-config` should be in `addopts = [...]` or (pytest 9+)
306+
`strict_config` or `strict` should be set. This forces an error if a
307+
config setting is misspelled.
267308
268309
```toml
269310
[tool.pytest.ini_options]
270311
addopts = ["-ra", "--strict-config", "--strict-markers"]
271312
```
272313
"""
273-
options = pyproject["tool"]["pytest"]["ini_options"]
274-
return "--strict-config" in options.get("addopts", [])
314+
_, options = pytest
315+
return (
316+
"strict" in options
317+
or "strict_config" in options
318+
or "--strict-config" in options.get("addopts", [])
319+
)
275320

276321

277322
class PP307(PyProject):
@@ -281,18 +326,23 @@ class PP307(PyProject):
281326
url = mk_url("pytest")
282327

283328
@staticmethod
284-
def check(pyproject: dict[str, Any]) -> bool:
329+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
285330
"""
286-
`--strict-markers` should be in `addopts = [...]`. This forces all
287-
markers to be specified in config, avoiding misspellings.
331+
`--strict-markers` should be in `addopts = [...]` or (pytest 9+)
332+
`strict_markers` or `strict` should be set. This forces test markers to
333+
be specified in config, avoiding misspellings.
288334
289335
```toml
290336
[tool.pytest.ini_options]
291337
addopts = ["-ra", "--strict-config", "--strict-markers"]
292338
```
293339
"""
294-
options = pyproject["tool"]["pytest"]["ini_options"]
295-
return "--strict-markers" in options.get("addopts", [])
340+
_, options = pytest
341+
return (
342+
"strict" in options
343+
or "strict_markers" in options
344+
or "--strict-markers" in options.get("addopts", [])
345+
)
296346

297347

298348
class PP308(PyProject):
@@ -302,7 +352,7 @@ class PP308(PyProject):
302352
url = mk_url("pytest")
303353

304354
@staticmethod
305-
def check(pyproject: dict[str, Any]) -> bool:
355+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
306356
"""
307357
An explicit summary flag like `-ra` should be in `addopts = [...]`
308358
(print summary of all fails/errors).
@@ -312,9 +362,9 @@ def check(pyproject: dict[str, Any]) -> bool:
312362
addopts = ["-ra", "--strict-config", "--strict-markers"]
313363
```
314364
"""
315-
options = pyproject["tool"]["pytest"]["ini_options"]
365+
loc, options = pytest
316366
addopts = options.get("addopts", [])
317-
if isinstance(addopts, str):
367+
if loc is PytestFile.LEGACY_PYPROJECT and isinstance(addopts, str):
318368
addopts = addopts.split()
319369
return any(opt.startswith("-r") for opt in addopts)
320370

@@ -326,7 +376,7 @@ class PP309(PyProject):
326376
url = mk_url("pytest")
327377

328378
@staticmethod
329-
def check(pyproject: dict[str, Any]) -> bool:
379+
def check(pytest: tuple[PytestFile, dict[str, Any]]) -> bool:
330380
"""
331381
`filterwarnings` must be set (probably to at least `["error"]`). Python
332382
will hide important warnings otherwise, like deprecations.
@@ -336,7 +386,7 @@ def check(pyproject: dict[str, Any]) -> bool:
336386
filterwarnings = ["error"]
337387
```
338388
"""
339-
options = pyproject["tool"]["pytest"]["ini_options"]
389+
_, options = pytest
340390
return "filterwarnings" in options
341391

342392

0 commit comments

Comments
 (0)