Merge pull request #11123 from bluetech/new-style-wrappers

Switch to new-style pluggy hook wrappers
This commit is contained in:
Ran Benita 2023-07-15 10:03:35 +03:00 committed by GitHub
commit 78d81ef865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 334 additions and 275 deletions

View File

@ -0,0 +1,6 @@
``pluggy>=1.2.0`` is now required.
pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0.
See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details.
Plugins which want to use new-style wrappers can do so if they require this version of pytest or later.

View File

@ -808,11 +808,10 @@ case we just write some information out to a ``failures`` file:
import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
rep = yield
# we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed:
@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file:
f.write(rep.nodeid + extra + "\n")
return rep
if you then have failing tests:
@ -899,16 +900,17 @@ here is a little example implemented via a local plugin:
phase_report_key = StashKey[Dict[str, CollectReport]]()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
rep = yield
# store test results for each phase of a call, which can
# be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
return rep
@pytest.fixture
def something(request):

View File

@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
.. _`hookwrapper`:
hookwrapper: executing around other hooks
hook wrappers: executing around other hooks
-------------------------------------------------
.. currentmodule:: _pytest.core
@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes
hook wrappers and passes the same arguments as to the regular hooks.
At the yield point of the hook wrapper pytest will execute the next hook
implementations and return their result to the yield point in the form of
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
exception info. The yield point itself will thus typically not raise
exceptions (unless there are bugs).
implementations and return their result to the yield point, or will
propagate an exception if they raised.
Here is an example definition of a hook wrapper:
@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem):
do_something_before_next_hook_executes()
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
# If the outcome is an exception, will raise the exception.
res = yield
res = outcome.get_result() # will raise if outcome was exception
new_res = post_process_result(res)
post_process_result(res)
# Override the return value to the plugin system.
return new_res
outcome.force_result(new_res) # to override the return value to the plugin system
The hook wrapper needs to return a result for the hook, or raise an exception.
Note that hook wrappers don't return results themselves, they merely
perform tracing or other side effects around the actual hook implementations.
If the result of the underlying hook is a mutable object, they may modify
that result but it's probably better to avoid it.
In many cases, the wrapper only needs to perform tracing or other side effects
around the actual hook implementations, in which case it can return the result
value of the ``yield``. The simplest (though useless) hook wrapper is
``return (yield)``.
In other cases, the wrapper wants the adjust or adapt the result, in which case
it can return a new value. If the result of the underlying hook is a mutable
object, the wrapper may modify that result, but it's probably better to avoid it.
If the hook implementation failed with an exception, the wrapper can handle that
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
supressing it, or raising a different exception entirely.
For more information, consult the
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
.. _plugin-hookorder:
@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
# Plugin 3
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
# will execute even before the tryfirst one above!
outcome = yield
# will execute after all non-hookwrappers executed
try:
return (yield)
finally:
# will execute after all non-wrappers executed
...
Here is the order of execution:
@ -149,12 +159,11 @@ Here is the order of execution:
Plugin1).
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
the result from calling the non-wrappers. Wrappers shall not modify the result.
point. The yield receives the result from calling the non-wrappers, or raises
an exception if the non-wrappers raised.
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
among each other.
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
in which case it will influence the ordering of hook wrappers among each other.
Declaring new hooks

View File

@ -1,5 +1,5 @@
pallets-sphinx-themes
pluggy>=1.0
pluggy>=1.2.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=5,<6

View File

@ -46,7 +46,7 @@ py_modules = py
install_requires =
iniconfig
packaging
pluggy>=0.12,<2.0
pluggy>=1.2.0,<2.0
colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11"

View File

@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None:
assertstate.hook.set_session(session)
@hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
@hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
The rewrite module will use util._reprcompare if it exists to use custom
@ -162,10 +162,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
util._assertion_pass = call_assertion_pass_hook
yield
util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None
try:
return (yield)
finally:
util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None
def pytest_sessionfinish(session: "Session") -> None:

View File

@ -217,12 +217,12 @@ class LFPluginCollWrapper:
self.lfplugin = lfplugin
self._collected_at_least_one_failure = False
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
@hookimpl(wrapper=True)
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Generator[None, CollectReport, CollectReport]:
res = yield
if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
@ -240,19 +240,16 @@ class LFPluginCollWrapper:
key=sort_key,
reverse=True,
)
return
elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
result = res.result
lastfailed = self.lfplugin.lastfailed
# Only filter with known failures.
if not self._collected_at_least_one_failure:
if not any(x.nodeid in lastfailed for x in result):
return
return res
self.lfplugin.config.pluginmanager.register(
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
)
@ -268,8 +265,8 @@ class LFPluginCollWrapper:
# Keep all sub-collectors.
or isinstance(x, nodes.Collector)
]
return
yield
return res
class LFPluginCollSkipfiles:
@ -342,14 +339,14 @@ class LFPlugin:
else:
self.lastfailed[report.nodeid] = True
@hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item]
) -> Generator[None, None, None]:
yield
res = yield
if not self.active:
return
return res
if self.lastfailed:
previously_failed = []
@ -394,6 +391,8 @@ class LFPlugin:
else:
self._report_status += "not deselecting items."
return res
def pytest_sessionfinish(self, session: Session) -> None:
config = self.config
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
@ -414,11 +413,11 @@ class NFPlugin:
assert config.cache is not None
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
@hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, items: List[nodes.Item]
) -> Generator[None, None, None]:
yield
res = yield
if self.active:
new_items: Dict[str, nodes.Item] = {}
@ -436,6 +435,8 @@ class NFPlugin:
else:
self.cached_nodeids.update(item.nodeid for item in items)
return res
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]

View File

@ -36,6 +36,7 @@ from _pytest.fixtures import SubRequest
from _pytest.nodes import Collector
from _pytest.nodes import File
from _pytest.nodes import Item
from _pytest.reports import CollectReport
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@ -130,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
sys.stderr = _reopen_stdio(sys.stderr, "wb")
@hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config: Config):
@hookimpl(wrapper=True)
def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
ns = early_config.known_args_namespace
if ns.capture == "fd":
_windowsconsoleio_workaround(sys.stdout)
@ -145,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config):
# Finally trigger conftest loading but while capturing (issue #93).
capman.start_global_capturing()
outcome = yield
capman.suspend_global_capture()
if outcome.excinfo is not None:
try:
try:
yield
finally:
capman.suspend_global_capture()
except BaseException:
out, err = capman.read_global_capture()
sys.stdout.write(out)
sys.stderr.write(err)
raise
# IO Helpers.
@ -841,41 +846,45 @@ class CaptureManager:
self.deactivate_fixture()
self.suspend_global_capture(in_=False)
out, err = self.read_global_capture()
item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err)
out, err = self.read_global_capture()
item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err)
# Hooks
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: Collector):
@hookimpl(wrapper=True)
def pytest_make_collect_report(
self, collector: Collector
) -> Generator[None, CollectReport, CollectReport]:
if isinstance(collector, File):
self.resume_global_capture()
outcome = yield
self.suspend_global_capture()
try:
rep = yield
finally:
self.suspend_global_capture()
out, err = self.read_global_capture()
rep = outcome.get_result()
if out:
rep.sections.append(("Captured stdout", out))
if err:
rep.sections.append(("Captured stderr", err))
else:
yield
rep = yield
return rep
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("setup", item):
yield
return (yield)
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("call", item):
yield
return (yield)
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("teardown", item):
yield
return (yield)
@hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self) -> None:

View File

@ -1341,12 +1341,14 @@ class Config:
else:
raise
@hookimpl(hookwrapper=True)
def pytest_collection(self) -> Generator[None, None, None]:
@hookimpl(wrapper=True)
def pytest_collection(self) -> Generator[None, object, object]:
# Validate invalid ini keys after collection is done so we take in account
# options added by late-loading conftest files.
yield
self._validate_config_options()
try:
return (yield)
finally:
self._validate_config_options()
def _checkversion(self) -> None:
import pytest
@ -1448,7 +1450,7 @@ class Config:
"""Issue and handle a warning during the "configure" stage.
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
function because it is not possible to have hookwrappers around ``pytest_configure``.
function because it is not possible to have hook wrappers around ``pytest_configure``.
This function is mainly intended for plugins that need to issue warnings during
``pytest_configure`` (or similar stages).

View File

@ -304,10 +304,10 @@ class PdbInvoke:
class PdbTrace:
@hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
@hookimpl(wrapper=True)
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
wrap_pytest_function_for_tracing(pyfuncitem)
yield
return (yield)
def wrap_pytest_function_for_tracing(pyfuncitem):

View File

@ -62,8 +62,8 @@ def get_timeout_config_value(config: Config) -> float:
return float(config.getini("faulthandler_timeout") or 0.0)
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
@pytest.hookimpl(wrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
timeout = get_timeout_config_value(item.config)
if timeout > 0:
import faulthandler
@ -71,11 +71,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield
return (yield)
finally:
faulthandler.cancel_dump_traceback_later()
else:
yield
return (yield)
@pytest.hookimpl(tryfirst=True)

View File

@ -2,6 +2,7 @@
import os
import sys
from argparse import Action
from typing import Generator
from typing import List
from typing import Optional
from typing import Union
@ -97,10 +98,9 @@ def pytest_addoption(parser: Parser) -> None:
)
@pytest.hookimpl(hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config: Config = outcome.get_result()
@pytest.hookimpl(wrapper=True)
def pytest_cmdline_parse() -> Generator[None, Config, Config]:
config = yield
if config.option.debug:
# --debug | --debug <file.log> was provided.
@ -128,6 +128,8 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing)
return config
def showversion(config: Config) -> None:
if config.option.version > 1:

View File

@ -60,7 +60,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
.. note::
This hook is incompatible with ``hookwrapper=True``.
This hook is incompatible with hook wrappers.
"""
@ -74,7 +74,7 @@ def pytest_plugin_registered(
:param pytest.PytestPluginManager manager: pytest plugin manager.
.. note::
This hook is incompatible with ``hookwrapper=True``.
This hook is incompatible with hook wrappers.
"""
@ -113,7 +113,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
attribute or can be retrieved as the ``pytestconfig`` fixture.
.. note::
This hook is incompatible with ``hookwrapper=True``.
This hook is incompatible with hook wrappers.
"""
@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None:
imported.
.. note::
This hook is incompatible with ``hookwrapper=True``.
This hook is incompatible with hook wrappers.
:param pytest.Config config: The pytest config object.
"""

View File

@ -738,27 +738,26 @@ class LoggingPlugin:
return True
@hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionstart(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionstart")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
return (yield)
@hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(wrapper=True, tryfirst=True)
def pytest_collection(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("collection")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
return (yield)
@hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
@hookimpl(wrapper=True)
def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]:
if session.config.option.collectonly:
yield
return
return (yield)
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
# The verbose flag is needed to avoid messy test progress output.
@ -766,7 +765,7 @@ class LoggingPlugin:
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield # Run all the tests.
return (yield) # Run all the tests.
@hookimpl
def pytest_runtest_logstart(self) -> None:
@ -791,12 +790,13 @@ class LoggingPlugin:
item.stash[caplog_records_key][when] = caplog_handler.records
item.stash[caplog_handler_key] = caplog_handler
yield
try:
yield
finally:
log = report_handler.stream.getvalue().strip()
item.add_report_section(when, "log", log)
log = report_handler.stream.getvalue().strip()
item.add_report_section(when, "log", log)
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("setup")
@ -804,31 +804,33 @@ class LoggingPlugin:
item.stash[caplog_records_key] = empty
yield from self._runtest_for(item, "setup")
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("call")
yield from self._runtest_for(item, "call")
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("teardown")
yield from self._runtest_for(item, "teardown")
del item.stash[caplog_records_key]
del item.stash[caplog_handler_key]
try:
yield from self._runtest_for(item, "teardown")
finally:
del item.stash[caplog_records_key]
del item.stash[caplog_handler_key]
@hookimpl
def pytest_runtest_logfinish(self) -> None:
self.log_cli_handler.set_when("finish")
@hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionfinish(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionfinish")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
return (yield)
@hookimpl
def pytest_unconfigure(self) -> None:

View File

@ -160,29 +160,31 @@ class LsofFdLeakChecker:
else:
return True
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]:
@hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]:
lines1 = self.get_open_files()
yield
if hasattr(sys, "pypy_version_info"):
gc.collect()
lines2 = self.get_open_files()
try:
return (yield)
finally:
if hasattr(sys, "pypy_version_info"):
gc.collect()
lines2 = self.get_open_files()
new_fds = {t[0] for t in lines2} - {t[0] for t in lines1}
leaked_files = [t for t in lines2 if t[0] in new_fds]
if leaked_files:
error = [
"***** %s FD leakage detected" % len(leaked_files),
*(str(f) for f in leaked_files),
"*** Before:",
*(str(f) for f in lines1),
"*** After:",
*(str(f) for f in lines2),
"***** %s FD leakage detected" % len(leaked_files),
"*** function %s:%s: %s " % item.location,
"See issue #2366",
]
item.warn(PytestWarning("\n".join(error)))
new_fds = {t[0] for t in lines2} - {t[0] for t in lines1}
leaked_files = [t for t in lines2 if t[0] in new_fds]
if leaked_files:
error = [
"***** %s FD leakage detected" % len(leaked_files),
*(str(f) for f in leaked_files),
"*** Before:",
*(str(f) for f in lines1),
"*** After:",
*(str(f) for f in lines2),
"***** %s FD leakage detected" % len(leaked_files),
"*** function %s:%s: %s " % item.location,
"See issue #2366",
]
item.warn(PytestWarning("\n".join(error)))
# used at least by pytest-xdist plugin

View File

@ -249,6 +249,9 @@ class TestReport(BaseReport):
"""
__test__ = False
# Defined by skipping plugin.
# xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish.
wasxfail: str
def __init__(
self,

View File

@ -28,24 +28,26 @@ def pytest_addoption(parser: Parser) -> None:
)
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_fixture_setup(
fixturedef: FixtureDef[object], request: SubRequest
) -> Generator[None, None, None]:
yield
if request.config.option.setupshow:
if hasattr(request, "param"):
# Save the fixture parameter so ._show_fixture_action() can
# display it now and during the teardown (in .finish()).
if fixturedef.ids:
if callable(fixturedef.ids):
param = fixturedef.ids(request.param)
) -> Generator[None, object, object]:
try:
return (yield)
finally:
if request.config.option.setupshow:
if hasattr(request, "param"):
# Save the fixture parameter so ._show_fixture_action() can
# display it now and during the teardown (in .finish()).
if fixturedef.ids:
if callable(fixturedef.ids):
param = fixturedef.ids(request.param)
else:
param = fixturedef.ids[request.param_index]
else:
param = fixturedef.ids[request.param_index]
else:
param = request.param
fixturedef.cached_param = param # type: ignore[attr-defined]
_show_fixture_action(fixturedef, "SETUP")
param = request.param
fixturedef.cached_param = param # type: ignore[attr-defined]
_show_fixture_action(fixturedef, "SETUP")
def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:

View File

@ -19,6 +19,7 @@ from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import xfail
from _pytest.reports import BaseReport
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from _pytest.stash import StashKey
@ -243,7 +244,7 @@ def pytest_runtest_setup(item: Item) -> None:
xfail("[NOTRUN] " + xfailed.reason)
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
xfailed = item.stash.get(xfailed_key, None)
if xfailed is None:
@ -252,18 +253,20 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
if xfailed and not item.config.option.runxfail and not xfailed.run:
xfail("[NOTRUN] " + xfailed.reason)
yield
# The test run may have added an xfail mark dynamically.
xfailed = item.stash.get(xfailed_key, None)
if xfailed is None:
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
try:
return (yield)
finally:
# The test run may have added an xfail mark dynamically.
xfailed = item.stash.get(xfailed_key, None)
if xfailed is None:
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
@hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
outcome = yield
rep = outcome.get_result()
@hookimpl(wrapper=True)
def pytest_runtest_makereport(
item: Item, call: CallInfo[None]
) -> Generator[None, TestReport, TestReport]:
rep = yield
xfailed = item.stash.get(xfailed_key, None)
if item.config.option.runxfail:
pass # don't interfere
@ -286,6 +289,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
else:
rep.outcome = "passed"
rep.wasxfail = xfailed.reason
return rep
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:

View File

@ -15,7 +15,6 @@ from functools import partial
from pathlib import Path
from typing import Any
from typing import Callable
from typing import cast
from typing import ClassVar
from typing import Dict
from typing import final
@ -849,12 +848,11 @@ class TerminalReporter:
for line in doc.splitlines():
self._tw.line("{}{}".format(indent + " ", line))
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_sessionfinish(
self, session: "Session", exitstatus: Union[int, ExitCode]
):
outcome = yield
outcome.get_result()
) -> Generator[None, None, None]:
result = yield
self._tw.line("")
summary_exit_codes = (
ExitCode.OK,
@ -875,17 +873,20 @@ class TerminalReporter:
elif session.shouldstop:
self.write_sep("!", str(session.shouldstop), red=True)
self.summary_stats()
return result
@hookimpl(hookwrapper=True)
@hookimpl(wrapper=True)
def pytest_terminal_summary(self) -> Generator[None, None, None]:
self.summary_errors()
self.summary_failures()
self.summary_warnings()
self.summary_passes()
yield
self.short_test_summary()
# Display any extra warnings from teardown here (if any).
self.summary_warnings()
try:
return (yield)
finally:
self.short_test_summary()
# Display any extra warnings from teardown here (if any).
self.summary_warnings()
def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None:
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
@ -1466,7 +1467,7 @@ def _get_raw_skip_reason(report: TestReport) -> str:
The string is just the part given by the user.
"""
if hasattr(report, "wasxfail"):
reason = cast(str, report.wasxfail)
reason = report.wasxfail
if reason.startswith("reason: "):
reason = reason[len("reason: ") :]
return reason

View File

@ -59,30 +59,34 @@ class catch_threading_exception:
def thread_exception_runtest_hook() -> Generator[None, None, None]:
with catch_threading_exception() as cm:
yield
if cm.args:
thread_name = "<unknown>" if cm.args.thread is None else cm.args.thread.name
msg = f"Exception in thread {thread_name}\n\n"
msg += "".join(
traceback.format_exception(
cm.args.exc_type,
cm.args.exc_value,
cm.args.exc_traceback,
try:
yield
finally:
if cm.args:
thread_name = (
"<unknown>" if cm.args.thread is None else cm.args.thread.name
)
)
warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
msg = f"Exception in thread {thread_name}\n\n"
msg += "".join(
traceback.format_exception(
cm.args.exc_type,
cm.args.exc_value,
cm.args.exc_traceback,
)
)
warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
@pytest.hookimpl(hookwrapper=True, trylast=True)
@pytest.hookimpl(wrapper=True, trylast=True)
def pytest_runtest_setup() -> Generator[None, None, None]:
yield from thread_exception_runtest_hook()
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_call() -> Generator[None, None, None]:
yield from thread_exception_runtest_hook()
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_teardown() -> Generator[None, None, None]:
yield from thread_exception_runtest_hook()

View File

@ -28,7 +28,7 @@ from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Item
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
from _pytest.stash import StashKey
tmppath_result_key = StashKey[Dict[str, bool]]()
@ -309,10 +309,12 @@ def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
cleanup_dead_symlinks(basetemp)
@hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item: Item, call):
outcome = yield
result: CollectReport = outcome.get_result()
@hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(
item: Item, call
) -> Generator[None, TestReport, TestReport]:
rep = yield
assert rep.when is not None
empty: Dict[str, bool] = {}
item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed
return rep

View File

@ -376,8 +376,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
# Twisted trial support.
@hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
@hookimpl(wrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
ut: Any = sys.modules["twisted.python.failure"]
Failure__init__ = ut.Failure.__init__
@ -400,10 +400,13 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
Failure__init__(self, exc_value, exc_type, exc_tb)
ut.Failure.__init__ = excstore
yield
ut.Failure.__init__ = Failure__init__
try:
res = yield
finally:
ut.Failure.__init__ = Failure__init__
else:
yield
res = yield
return res
def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:

View File

@ -61,33 +61,35 @@ class catch_unraisable_exception:
def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
with catch_unraisable_exception() as cm:
yield
if cm.unraisable:
if cm.unraisable.err_msg is not None:
err_msg = cm.unraisable.err_msg
else:
err_msg = "Exception ignored in"
msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
msg += "".join(
traceback.format_exception(
cm.unraisable.exc_type,
cm.unraisable.exc_value,
cm.unraisable.exc_traceback,
try:
yield
finally:
if cm.unraisable:
if cm.unraisable.err_msg is not None:
err_msg = cm.unraisable.err_msg
else:
err_msg = "Exception ignored in"
msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
msg += "".join(
traceback.format_exception(
cm.unraisable.exc_type,
cm.unraisable.exc_value,
cm.unraisable.exc_traceback,
)
)
)
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_setup() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook()
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_call() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook()
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_teardown() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook()

View File

@ -60,17 +60,18 @@ def catch_warnings_for_item(
for arg in mark.args:
warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
yield
for warning_message in log:
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
nodeid=nodeid,
when=when,
location=None,
try:
yield
finally:
for warning_message in log:
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
nodeid=nodeid,
when=when,
location=None,
)
)
)
def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
@ -103,24 +104,24 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
return msg
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
with catch_warnings_for_item(
config=item.config, ihook=item.ihook, when="runtest", item=item
):
yield
return (yield)
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection(session: Session) -> Generator[None, None, None]:
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_collection(session: Session) -> Generator[None, object, object]:
config = session.config
with catch_warnings_for_item(
config=config, ihook=config.hook, when="collect", item=None
):
yield
return (yield)
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_terminal_summary(
terminalreporter: TerminalReporter,
) -> Generator[None, None, None]:
@ -128,23 +129,23 @@ def pytest_terminal_summary(
with catch_warnings_for_item(
config=config, ihook=config.hook, when="config", item=None
):
yield
return (yield)
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
config = session.config
with catch_warnings_for_item(
config=config, ihook=config.hook, when="config", item=None
):
yield
return (yield)
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_load_initial_conftests(
early_config: "Config",
) -> Generator[None, None, None]:
with catch_warnings_for_item(
config=early_config, ihook=early_config.hook, when="config", item=None
):
yield
return (yield)

View File

@ -1,6 +1,7 @@
import dataclasses
import re
import sys
from typing import Generator
from typing import List
import pytest
@ -21,11 +22,11 @@ if sys.gettrace():
sys.settrace(orig_trace)
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items):
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items) -> Generator[None, None, None]:
"""Prefer faster tests.
Use a hookwrapper to do this in the beginning, so e.g. --ff still works
Use a hook wrapper to do this in the beginning, so e.g. --ff still works
correctly.
"""
fast_items = []
@ -62,7 +63,7 @@ def pytest_collection_modifyitems(items):
items[:] = fast_items + neutral_items + slow_items + slowest_items
yield
return (yield)
@pytest.fixture

View File

@ -1040,13 +1040,13 @@ def test_log_set_path(pytester: Pytester) -> None:
"""
import os
import pytest
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_setup(item):
config = item.config
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
report_file = os.path.join({}, item._request.node.name)
logging_plugin.set_log_path(report_file)
yield
return (yield)
""".format(
repr(report_dir_base)
)

View File

@ -827,11 +827,11 @@ class TestConftestCustomization:
textwrap.dedent(
"""\
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_pycollect_makemodule():
outcome = yield
mod = outcome.get_result()
mod = yield
mod.obj.hello = "world"
return mod
"""
),
encoding="utf-8",
@ -855,14 +855,13 @@ class TestConftestCustomization:
textwrap.dedent(
"""\
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_pycollect_makeitem():
outcome = yield
if outcome.excinfo is None:
result = outcome.get_result()
if result:
for func in result:
func._some123 = "world"
result = yield
if result:
for func in result:
func._some123 = "world"
return result
"""
),
encoding="utf-8",

View File

@ -334,12 +334,11 @@ class TestPrunetraceback:
pytester.makeconftest(
"""
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_make_collect_report():
outcome = yield
rep = outcome.get_result()
rep = yield
rep.headerlines += ["header1"]
outcome.force_result(rep)
return rep
"""
)
result = pytester.runpytest(p)

View File

@ -1317,7 +1317,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
hookimpls = [
(
hookimpl.function.__module__,
"wrapper" if hookimpl.hookwrapper else "nonwrapper",
"wrapper" if (hookimpl.wrapper or hookimpl.hookwrapper) else "nonwrapper",
)
for hookimpl in hc.get_hookimpls()
]

View File

@ -806,12 +806,12 @@ class TestKeywordSelection:
pytester.makepyfile(
conftest="""
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_pycollect_makeitem(name):
outcome = yield
item = yield
if name == "TestClass":
item = outcome.get_result()
item.extra_keyword_matches.add("xxx")
return item
"""
)
reprec = pytester.inline_run(p.parent, "-s", "-k", keyword)

View File

@ -85,8 +85,8 @@ def test_clean_up(pytester: Pytester) -> None:
# This is tough to test behaviorally because the cleanup really runs last.
# So the test make several implementation assumptions:
# - Cleanup is done in pytest_unconfigure().
# - Not a hookwrapper.
# So we can add a hookwrapper ourselves to test what it does.
# - Not a hook wrapper.
# So we can add a hook wrapper ourselves to test what it does.
pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n")
pytester.makepyfile(test_foo="""def test_foo(): pass""")
@ -94,12 +94,14 @@ def test_clean_up(pytester: Pytester) -> None:
after: Optional[List[str]] = None
class Plugin:
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_unconfigure(self) -> Generator[None, None, None]:
nonlocal before, after
before = sys.path.copy()
yield
after = sys.path.copy()
try:
return (yield)
finally:
after = sys.path.copy()
result = pytester.runpytest_inprocess(plugins=[Plugin()])
assert result.ret == 0

View File

@ -542,10 +542,10 @@ def test_runtest_in_module_ordering(pytester: Pytester) -> None:
@pytest.fixture
def mylist(self, request):
return request.function.mylist
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_runtest_call(self, item):
try:
(yield).get_result()
yield
except ValueError:
pass
def test_hello1(self, mylist):
@ -826,12 +826,12 @@ def test_unicode_in_longrepr(pytester: Pytester) -> None:
pytester.makeconftest(
"""\
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_runtest_makereport():
outcome = yield
rep = outcome.get_result()
rep = yield
if rep.when == "call":
rep.longrepr = 'ä'
return rep
"""
)
pytester.makepyfile(

View File

@ -725,12 +725,12 @@ class TestTerminalFunctional:
)
assert result.ret == 0
def test_deselected_with_hookwrapper(self, pytester: Pytester) -> None:
def test_deselected_with_hook_wrapper(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(config, items):
yield
deselected = items.pop()

View File

@ -952,7 +952,7 @@ def test_issue333_result_clearing(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.hookimpl(hookwrapper=True)
@pytest.hookimpl(wrapper=True)
def pytest_runtest_call(item):
yield
assert 0