11from __future__ import annotations
22
3+ import enum
34from typing import TYPE_CHECKING , Any
45
6+ from .._compat import tomllib
57from . import mk_url
68
79if 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
1145def get_requires_python (
1246 pyproject : dict [str , Any ], setupcfg : ConfigParser | None
@@ -149,45 +183,45 @@ def check(pyproject: dict[str, Any]) -> bool:
149183class 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
170202class 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
235269class 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
256295class 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
277321class 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
298347class 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