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 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
1146def get_requires_python (
1247 pyproject : dict [str , Any ], setupcfg : ConfigParser | None
@@ -149,45 +184,45 @@ def check(pyproject: dict[str, Any]) -> bool:
149184class 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
170203class 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
235270class 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
256296class 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
277322class 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
298348class 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