|
14 | 14 | from functools import lru_cache |
15 | 15 | from pathlib import Path |
16 | 16 | from textwrap import dedent |
| 17 | +from types import FunctionType |
17 | 18 | from types import TracebackType |
18 | 19 | from typing import Any |
19 | 20 | from typing import Callable |
|
58 | 59 | from _pytest.pathlib import resolve_package_path |
59 | 60 | from _pytest.stash import Stash |
60 | 61 | from _pytest.warning_types import PytestConfigWarning |
| 62 | +from _pytest.warning_types import warn_explicit_for |
61 | 63 |
|
62 | 64 | if TYPE_CHECKING: |
63 | 65 |
|
@@ -341,6 +343,38 @@ def _get_directory(path: Path) -> Path: |
341 | 343 | return path |
342 | 344 |
|
343 | 345 |
|
| 346 | +def _get_legacy_hook_marks( |
| 347 | + method: Any, |
| 348 | + hook_type: str, |
| 349 | + opt_names: Tuple[str, ...], |
| 350 | +) -> Dict[str, bool]: |
| 351 | + if TYPE_CHECKING: |
| 352 | + # abuse typeguard from importlib to avoid massive method type union thats lacking a alias |
| 353 | + assert inspect.isroutine(method) |
| 354 | + known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])} |
| 355 | + must_warn: list[str] = [] |
| 356 | + opts: dict[str, bool] = {} |
| 357 | + for opt_name in opt_names: |
| 358 | + opt_attr = getattr(method, opt_name, AttributeError) |
| 359 | + if opt_attr is not AttributeError: |
| 360 | + must_warn.append(f"{opt_name}={opt_attr}") |
| 361 | + opts[opt_name] = True |
| 362 | + elif opt_name in known_marks: |
| 363 | + must_warn.append(f"{opt_name}=True") |
| 364 | + opts[opt_name] = True |
| 365 | + else: |
| 366 | + opts[opt_name] = False |
| 367 | + if must_warn: |
| 368 | + hook_opts = ", ".join(must_warn) |
| 369 | + message = _pytest.deprecated.HOOK_LEGACY_MARKING.format( |
| 370 | + type=hook_type, |
| 371 | + fullname=method.__qualname__, |
| 372 | + hook_opts=hook_opts, |
| 373 | + ) |
| 374 | + warn_explicit_for(cast(FunctionType, method), message) |
| 375 | + return opts |
| 376 | + |
| 377 | + |
344 | 378 | @final |
345 | 379 | class PytestPluginManager(PluginManager): |
346 | 380 | """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with |
@@ -414,40 +448,29 @@ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): |
414 | 448 | if name == "pytest_plugins": |
415 | 449 | return |
416 | 450 |
|
417 | | - method = getattr(plugin, name) |
418 | 451 | opts = super().parse_hookimpl_opts(plugin, name) |
| 452 | + if opts is not None: |
| 453 | + return opts |
419 | 454 |
|
| 455 | + method = getattr(plugin, name) |
420 | 456 | # Consider only actual functions for hooks (#3775). |
421 | 457 | if not inspect.isroutine(method): |
422 | 458 | return |
423 | | - |
424 | 459 | # Collect unmarked hooks as long as they have the `pytest_' prefix. |
425 | | - if opts is None and name.startswith("pytest_"): |
426 | | - opts = {} |
427 | | - if opts is not None: |
428 | | - # TODO: DeprecationWarning, people should use hookimpl |
429 | | - # https://github.com/pytest-dev/pytest/issues/4562 |
430 | | - known_marks = {m.name for m in getattr(method, "pytestmark", [])} |
431 | | - |
432 | | - for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): |
433 | | - opts.setdefault(name, hasattr(method, name) or name in known_marks) |
434 | | - return opts |
| 460 | + return _get_legacy_hook_marks( |
| 461 | + method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") |
| 462 | + ) |
435 | 463 |
|
436 | 464 | def parse_hookspec_opts(self, module_or_class, name: str): |
437 | 465 | opts = super().parse_hookspec_opts(module_or_class, name) |
438 | 466 | if opts is None: |
439 | 467 | method = getattr(module_or_class, name) |
440 | | - |
441 | 468 | if name.startswith("pytest_"): |
442 | | - # todo: deprecate hookspec hacks |
443 | | - # https://github.com/pytest-dev/pytest/issues/4562 |
444 | | - known_marks = {m.name for m in getattr(method, "pytestmark", [])} |
445 | | - opts = { |
446 | | - "firstresult": hasattr(method, "firstresult") |
447 | | - or "firstresult" in known_marks, |
448 | | - "historic": hasattr(method, "historic") |
449 | | - or "historic" in known_marks, |
450 | | - } |
| 469 | + opts = _get_legacy_hook_marks( |
| 470 | + method, |
| 471 | + "spec", |
| 472 | + ("firstresult", "historic"), |
| 473 | + ) |
451 | 474 | return opts |
452 | 475 |
|
453 | 476 | def register( |
|
0 commit comments