Skip to content

Commit 430ac90

Browse files
partial application of review suggestions + externalize the helper
1 parent 3d47e38 commit 430ac90

File tree

5 files changed

+45
-33
lines changed

5 files changed

+45
-33
lines changed

changelog/4562.deprecation.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
Deprecate configureing hook specs/impls using attribute/marks.
2-
Instead ``use pytest.hookimpl`` and ``pytest.hookspec``.
1+
Deprecate configuring hook specs/impls using attribute/marks.
2+
3+
Instead use :ref:`pytest.hookimpl` and :ref:`pytest.hookspec`.
4+
For more details, see the :ref:`docs <deprecate-hook-marks>`.

doc/en/deprecations.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ have been available since years and should be used.
4141
4242
pytest_runtest_call.tryfirst = True
4343
44-
# becomes
44+
This now is declared as:
4545

46+
.. code-block:: python
4647
4748
@pytest.hookimpl(tryfirst=True)
4849
def pytest_runtest_call():

src/_pytest/config/__init__.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import warnings
1414
from functools import lru_cache
1515
from pathlib import Path
16+
from types import FunctionType
1617
from types import TracebackType
1718
from typing import Any
1819
from typing import Callable
@@ -35,6 +36,7 @@
3536
from pluggy import HookimplMarker
3637
from pluggy import HookspecMarker
3738
from pluggy import PluginManager
39+
from typing_extensions import Literal
3840

3941
import _pytest._code
4042
import _pytest.deprecated
@@ -330,6 +332,32 @@ def _prepareconfig(
330332
raise
331333

332334

335+
def _get_legacy_hook_marks(
336+
method: FunctionType,
337+
hook_type: Literal["spec", "impl"],
338+
opt_names: Tuple[str, ...],
339+
) -> Dict[str, bool]:
340+
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
341+
must_warn = False
342+
opts = {}
343+
for opt_name in opt_names:
344+
if hasattr(method, opt_name) or opt_name in known_marks:
345+
opts[opt_name] = True
346+
must_warn = True
347+
else:
348+
opts[opt_name] = False
349+
if must_warn:
350+
351+
hook_opts = ", ".join(f"{name}=True" for name, val in opts.items() if val)
352+
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
353+
type=hook_type,
354+
fullname=method.__qualname__,
355+
hook_opts=hook_opts,
356+
)
357+
warn_explicit_for(method, message)
358+
return opts
359+
360+
333361
@final
334362
class PytestPluginManager(PluginManager):
335363
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
@@ -402,39 +430,20 @@ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
402430
if not inspect.isroutine(method):
403431
return
404432
# Collect unmarked hooks as long as they have the `pytest_' prefix.
405-
406-
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
407-
opts = {
408-
name: hasattr(method, name) or name in known_marks
409-
for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper")
410-
}
411-
if any(opts.values()):
412-
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
413-
type="spec", fullname=method.__qualname__
414-
)
415-
warn_explicit_for(method, message)
416-
417-
return opts
433+
return _get_legacy_hook_marks(
434+
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
435+
)
418436

419437
def parse_hookspec_opts(self, module_or_class, name: str):
420438
opts = super().parse_hookspec_opts(module_or_class, name)
421439
if opts is None:
422440
method = getattr(module_or_class, name)
423441
if name.startswith("pytest_"):
424-
425-
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
426-
opts = {
427-
"firstresult": hasattr(method, "firstresult")
428-
or "firstresult" in known_marks,
429-
"historic": hasattr(method, "historic")
430-
or "historic" in known_marks,
431-
}
432-
# hook from xdist, fixing in ...
433-
if any(opts.values()) and module_or_class.__name__ != "xdist.newhooks":
434-
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
435-
type="spec", fullname=method.__qualname__
436-
)
437-
warn_explicit_for(method, message)
442+
opts = _get_legacy_hook_marks(
443+
method,
444+
"spec",
445+
("firstresult", "historic"),
446+
)
438447
return opts
439448

440449
def register(

src/_pytest/deprecated.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
HOOK_LEGACY_MARKING = UnformattedWarning(
111111
PytestDeprecationWarning,
112112
"The hook {type} {fullname} is not marked using pytest.hook{type}.\n"
113-
" please use the pytest.hook{type}(...) decorator instead of pytest.mark \n"
113+
" please use the pytest.hook{type}({hook_opts}) decorator instead of pytest.mark \n"
114114
" to correctly mark hooks as pytest hooks.\n"
115115
" see URL",
116116
)

testing/deprecated_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_fillfixtures_is_deprecated() -> None:
5151
_pytest.fixtures.fillfixtures(mock.Mock())
5252

5353

54-
def test_hooksec_marks_are_deprecated():
54+
def test_hookspec_via_function_attributes_are_deprecated():
5555
from _pytest.config import PytestPluginManager
5656

5757
pm = PytestPluginManager()
@@ -74,7 +74,7 @@ def pytest_bad_hook(self):
7474
assert record.filename == __file__
7575

7676

77-
def test_hookimpl_marks_are_deprecated():
77+
def test_hookimpl_via_function_attributes_are_deprecated():
7878
from _pytest.config import PytestPluginManager
7979

8080
pm = PytestPluginManager()

0 commit comments

Comments
 (0)