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 import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
outcome = yield rep = yield
rep = outcome.get_result()
# we only look at actual failing test calls, not setup/teardown # we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed: 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") f.write(rep.nodeid + extra + "\n")
return rep
if you then have failing tests: 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]]() 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): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
outcome = yield rep = yield
rep = outcome.get_result()
# store test results for each phase of a call, which can # store test results for each phase of a call, which can
# be "setup", "call", "teardown" # be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep item.stash.setdefault(phase_report_key, {})[rep.when] = rep
return rep
@pytest.fixture @pytest.fixture
def something(request): def something(request):

View File

@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
.. _`hookwrapper`: .. _`hookwrapper`:
hookwrapper: executing around other hooks hook wrappers: executing around other hooks
------------------------------------------------- -------------------------------------------------
.. currentmodule:: _pytest.core .. 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. 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 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 implementations and return their result to the yield point, or will
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or propagate an exception if they raised.
exception info. The yield point itself will thus typically not raise
exceptions (unless there are bugs).
Here is an example definition of a hook wrapper: Here is an example definition of a hook wrapper:
@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
import pytest import pytest
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
do_something_before_next_hook_executes() do_something_before_next_hook_executes()
outcome = yield # If the outcome is an exception, will raise the exception.
# outcome.excinfo may be None or a (cls, val, tb) tuple 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 In many cases, the wrapper only needs to perform tracing or other side effects
perform tracing or other side effects around the actual hook implementations. around the actual hook implementations, in which case it can return the result
If the result of the underlying hook is a mutable object, they may modify value of the ``yield``. The simplest (though useless) hook wrapper is
that result but it's probably better to avoid it. ``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 For more information, consult the
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`. :ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
.. _plugin-hookorder: .. _plugin-hookorder:
@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
# Plugin 3 # Plugin 3
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items): def pytest_collection_modifyitems(items):
# will execute even before the tryfirst one above! # will execute even before the tryfirst one above!
outcome = yield try:
# will execute after all non-hookwrappers executed return (yield)
finally:
# will execute after all non-wrappers executed
...
Here is the order of execution: Here is the order of execution:
@ -149,12 +159,11 @@ Here is the order of execution:
Plugin1). Plugin1).
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield 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 point. The yield receives the result from calling the non-wrappers, or raises
the result from calling the non-wrappers. Wrappers shall not modify the result. an exception if the non-wrappers raised.
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers in which case it will influence the ordering of hook wrappers among each other.
among each other.
Declaring new hooks Declaring new hooks

View File

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

View File

@ -46,7 +46,7 @@ py_modules = py
install_requires = install_requires =
iniconfig iniconfig
packaging packaging
pluggy>=0.12,<2.0 pluggy>=1.2.0,<2.0
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;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) assertstate.hook.set_session(session)
@hookimpl(tryfirst=True, hookwrapper=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
The rewrite module will use util._reprcompare if it exists to use custom 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 util._assertion_pass = call_assertion_pass_hook
yield try:
return (yield)
util._reprcompare, util._assertion_pass = saved_assert_hooks finally:
util._config = None util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None
def pytest_sessionfinish(session: "Session") -> None: def pytest_sessionfinish(session: "Session") -> None:

View File

@ -217,12 +217,12 @@ class LFPluginCollWrapper:
self.lfplugin = lfplugin self.lfplugin = lfplugin
self._collected_at_least_one_failure = False self._collected_at_least_one_failure = False
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector): def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Generator[None, CollectReport, CollectReport]:
res = yield
if isinstance(collector, (Session, Package)): if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths lf_paths = self.lfplugin._last_failed_paths
@ -240,19 +240,16 @@ class LFPluginCollWrapper:
key=sort_key, key=sort_key,
reverse=True, reverse=True,
) )
return
elif isinstance(collector, File): elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths: if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
result = res.result result = res.result
lastfailed = self.lfplugin.lastfailed lastfailed = self.lfplugin.lastfailed
# Only filter with known failures. # Only filter with known failures.
if not self._collected_at_least_one_failure: if not self._collected_at_least_one_failure:
if not any(x.nodeid in lastfailed for x in result): if not any(x.nodeid in lastfailed for x in result):
return return res
self.lfplugin.config.pluginmanager.register( self.lfplugin.config.pluginmanager.register(
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
) )
@ -268,8 +265,8 @@ class LFPluginCollWrapper:
# Keep all sub-collectors. # Keep all sub-collectors.
or isinstance(x, nodes.Collector) or isinstance(x, nodes.Collector)
] ]
return
yield return res
class LFPluginCollSkipfiles: class LFPluginCollSkipfiles:
@ -342,14 +339,14 @@ class LFPlugin:
else: else:
self.lastfailed[report.nodeid] = True self.lastfailed[report.nodeid] = True
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems( def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item] self, config: Config, items: List[nodes.Item]
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
yield res = yield
if not self.active: if not self.active:
return return res
if self.lastfailed: if self.lastfailed:
previously_failed = [] previously_failed = []
@ -394,6 +391,8 @@ class LFPlugin:
else: else:
self._report_status += "not deselecting items." self._report_status += "not deselecting items."
return res
def pytest_sessionfinish(self, session: Session) -> None: def pytest_sessionfinish(self, session: Session) -> None:
config = self.config config = self.config
if config.getoption("cacheshow") or hasattr(config, "workerinput"): if config.getoption("cacheshow") or hasattr(config, "workerinput"):
@ -414,11 +413,11 @@ class NFPlugin:
assert config.cache is not None assert config.cache is not None
self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems( def pytest_collection_modifyitems(
self, items: List[nodes.Item] self, items: List[nodes.Item]
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
yield res = yield
if self.active: if self.active:
new_items: Dict[str, nodes.Item] = {} new_items: Dict[str, nodes.Item] = {}
@ -436,6 +435,8 @@ class NFPlugin:
else: else:
self.cached_nodeids.update(item.nodeid for item in items) self.cached_nodeids.update(item.nodeid for item in items)
return res
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: 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] 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 Collector
from _pytest.nodes import File from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.reports import CollectReport
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@ -130,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
sys.stderr = _reopen_stdio(sys.stderr, "wb") sys.stderr = _reopen_stdio(sys.stderr, "wb")
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_load_initial_conftests(early_config: Config): def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
ns = early_config.known_args_namespace ns = early_config.known_args_namespace
if ns.capture == "fd": if ns.capture == "fd":
_windowsconsoleio_workaround(sys.stdout) _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). # Finally trigger conftest loading but while capturing (issue #93).
capman.start_global_capturing() capman.start_global_capturing()
outcome = yield try:
capman.suspend_global_capture() try:
if outcome.excinfo is not None: yield
finally:
capman.suspend_global_capture()
except BaseException:
out, err = capman.read_global_capture() out, err = capman.read_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
raise
# IO Helpers. # IO Helpers.
@ -841,41 +846,45 @@ class CaptureManager:
self.deactivate_fixture() self.deactivate_fixture()
self.suspend_global_capture(in_=False) self.suspend_global_capture(in_=False)
out, err = self.read_global_capture() out, err = self.read_global_capture()
item.add_report_section(when, "stdout", out) item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err) item.add_report_section(when, "stderr", err)
# Hooks # Hooks
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_make_collect_report(self, collector: Collector): def pytest_make_collect_report(
self, collector: Collector
) -> Generator[None, CollectReport, CollectReport]:
if isinstance(collector, File): if isinstance(collector, File):
self.resume_global_capture() self.resume_global_capture()
outcome = yield try:
self.suspend_global_capture() rep = yield
finally:
self.suspend_global_capture()
out, err = self.read_global_capture() out, err = self.read_global_capture()
rep = outcome.get_result()
if out: if out:
rep.sections.append(("Captured stdout", out)) rep.sections.append(("Captured stdout", out))
if err: if err:
rep.sections.append(("Captured stderr", err)) rep.sections.append(("Captured stderr", err))
else: else:
yield rep = yield
return rep
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("setup", item): 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]: def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("call", item): 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]: def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("teardown", item): with self.item_capture("teardown", item):
yield return (yield)
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self) -> None: def pytest_keyboard_interrupt(self) -> None:

View File

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

View File

@ -304,10 +304,10 @@ class PdbInvoke:
class PdbTrace: class PdbTrace:
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
wrap_pytest_function_for_tracing(pyfuncitem) wrap_pytest_function_for_tracing(pyfuncitem)
yield return (yield)
def wrap_pytest_function_for_tracing(pyfuncitem): 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) return float(config.getini("faulthandler_timeout") or 0.0)
@pytest.hookimpl(hookwrapper=True, trylast=True) @pytest.hookimpl(wrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
timeout = get_timeout_config_value(item.config) timeout = get_timeout_config_value(item.config)
if timeout > 0: if timeout > 0:
import faulthandler 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] stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr) faulthandler.dump_traceback_later(timeout, file=stderr)
try: try:
yield return (yield)
finally: finally:
faulthandler.cancel_dump_traceback_later() faulthandler.cancel_dump_traceback_later()
else: else:
yield return (yield)
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
from argparse import Action from argparse import Action
from typing import Generator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Union from typing import Union
@ -97,10 +98,9 @@ def pytest_addoption(parser: Parser) -> None:
) )
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_cmdline_parse(): def pytest_cmdline_parse() -> Generator[None, Config, Config]:
outcome = yield config = yield
config: Config = outcome.get_result()
if config.option.debug: if config.option.debug:
# --debug | --debug <file.log> was provided. # --debug | --debug <file.log> was provided.
@ -128,6 +128,8 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing) config.add_cleanup(unset_tracing)
return config
def showversion(config: Config) -> None: def showversion(config: Config) -> None:
if config.option.version > 1: 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. :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
.. note:: .. 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. :param pytest.PytestPluginManager manager: pytest plugin manager.
.. note:: .. 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. attribute or can be retrieved as the ``pytestconfig`` fixture.
.. note:: .. 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. imported.
.. note:: .. note::
This hook is incompatible with ``hookwrapper=True``. This hook is incompatible with hook wrappers.
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
""" """

View File

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

View File

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

View File

@ -249,6 +249,9 @@ class TestReport(BaseReport):
""" """
__test__ = False __test__ = False
# Defined by skipping plugin.
# xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish.
wasxfail: str
def __init__( def __init__(
self, 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( def pytest_fixture_setup(
fixturedef: FixtureDef[object], request: SubRequest fixturedef: FixtureDef[object], request: SubRequest
) -> Generator[None, None, None]: ) -> Generator[None, object, object]:
yield try:
if request.config.option.setupshow: return (yield)
if hasattr(request, "param"): finally:
# Save the fixture parameter so ._show_fixture_action() can if request.config.option.setupshow:
# display it now and during the teardown (in .finish()). if hasattr(request, "param"):
if fixturedef.ids: # Save the fixture parameter so ._show_fixture_action() can
if callable(fixturedef.ids): # display it now and during the teardown (in .finish()).
param = fixturedef.ids(request.param) if fixturedef.ids:
if callable(fixturedef.ids):
param = fixturedef.ids(request.param)
else:
param = fixturedef.ids[request.param_index]
else: else:
param = fixturedef.ids[request.param_index] param = request.param
else: fixturedef.cached_param = param # type: ignore[attr-defined]
param = request.param _show_fixture_action(fixturedef, "SETUP")
fixturedef.cached_param = param # type: ignore[attr-defined]
_show_fixture_action(fixturedef, "SETUP")
def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: 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 skip
from _pytest.outcomes import xfail from _pytest.outcomes import xfail
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.stash import StashKey from _pytest.stash import StashKey
@ -243,7 +244,7 @@ def pytest_runtest_setup(item: Item) -> None:
xfail("[NOTRUN] " + xfailed.reason) xfail("[NOTRUN] " + xfailed.reason)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_call(item: Item) -> Generator[None, None, None]: def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
xfailed = item.stash.get(xfailed_key, None) xfailed = item.stash.get(xfailed_key, None)
if xfailed is 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: if xfailed and not item.config.option.runxfail and not xfailed.run:
xfail("[NOTRUN] " + xfailed.reason) xfail("[NOTRUN] " + xfailed.reason)
yield try:
return (yield)
# The test run may have added an xfail mark dynamically. finally:
xfailed = item.stash.get(xfailed_key, None) # The test run may have added an xfail mark dynamically.
if xfailed is None: xfailed = item.stash.get(xfailed_key, None)
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed is None:
item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo[None]): def pytest_runtest_makereport(
outcome = yield item: Item, call: CallInfo[None]
rep = outcome.get_result() ) -> Generator[None, TestReport, TestReport]:
rep = yield
xfailed = item.stash.get(xfailed_key, None) xfailed = item.stash.get(xfailed_key, None)
if item.config.option.runxfail: if item.config.option.runxfail:
pass # don't interfere pass # don't interfere
@ -286,6 +289,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
else: else:
rep.outcome = "passed" rep.outcome = "passed"
rep.wasxfail = xfailed.reason rep.wasxfail = xfailed.reason
return rep
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: 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 pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import ClassVar from typing import ClassVar
from typing import Dict from typing import Dict
from typing import final from typing import final
@ -849,12 +848,11 @@ class TerminalReporter:
for line in doc.splitlines(): for line in doc.splitlines():
self._tw.line("{}{}".format(indent + " ", line)) self._tw.line("{}{}".format(indent + " ", line))
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_sessionfinish( def pytest_sessionfinish(
self, session: "Session", exitstatus: Union[int, ExitCode] self, session: "Session", exitstatus: Union[int, ExitCode]
): ) -> Generator[None, None, None]:
outcome = yield result = yield
outcome.get_result()
self._tw.line("") self._tw.line("")
summary_exit_codes = ( summary_exit_codes = (
ExitCode.OK, ExitCode.OK,
@ -875,17 +873,20 @@ class TerminalReporter:
elif session.shouldstop: elif session.shouldstop:
self.write_sep("!", str(session.shouldstop), red=True) self.write_sep("!", str(session.shouldstop), red=True)
self.summary_stats() self.summary_stats()
return result
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_terminal_summary(self) -> Generator[None, None, None]: def pytest_terminal_summary(self) -> Generator[None, None, None]:
self.summary_errors() self.summary_errors()
self.summary_failures() self.summary_failures()
self.summary_warnings() self.summary_warnings()
self.summary_passes() self.summary_passes()
yield try:
self.short_test_summary() return (yield)
# Display any extra warnings from teardown here (if any). finally:
self.summary_warnings() 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: def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None:
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) 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. The string is just the part given by the user.
""" """
if hasattr(report, "wasxfail"): if hasattr(report, "wasxfail"):
reason = cast(str, report.wasxfail) reason = report.wasxfail
if reason.startswith("reason: "): if reason.startswith("reason: "):
reason = reason[len("reason: ") :] reason = reason[len("reason: ") :]
return reason return reason

View File

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

View File

@ -28,7 +28,7 @@ from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.reports import CollectReport from _pytest.reports import TestReport
from _pytest.stash import StashKey from _pytest.stash import StashKey
tmppath_result_key = StashKey[Dict[str, bool]]() tmppath_result_key = StashKey[Dict[str, bool]]()
@ -309,10 +309,12 @@ def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
cleanup_dead_symlinks(basetemp) cleanup_dead_symlinks(basetemp)
@hookimpl(tryfirst=True, hookwrapper=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item: Item, call): def pytest_runtest_makereport(
outcome = yield item: Item, call
result: CollectReport = outcome.get_result() ) -> Generator[None, TestReport, TestReport]:
rep = yield
assert rep.when is not None
empty: Dict[str, bool] = {} 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. # Twisted trial support.
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
ut: Any = sys.modules["twisted.python.failure"] ut: Any = sys.modules["twisted.python.failure"]
Failure__init__ = ut.Failure.__init__ 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) Failure__init__(self, exc_value, exc_type, exc_tb)
ut.Failure.__init__ = excstore ut.Failure.__init__ = excstore
yield try:
ut.Failure.__init__ = Failure__init__ res = yield
finally:
ut.Failure.__init__ = Failure__init__
else: else:
yield res = yield
return res
def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: 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]: def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
with catch_unraisable_exception() as cm: with catch_unraisable_exception() as cm:
yield try:
if cm.unraisable: yield
if cm.unraisable.err_msg is not None: finally:
err_msg = cm.unraisable.err_msg if cm.unraisable:
else: if cm.unraisable.err_msg is not None:
err_msg = "Exception ignored in" err_msg = cm.unraisable.err_msg
msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" else:
msg += "".join( err_msg = "Exception ignored in"
traceback.format_exception( msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
cm.unraisable.exc_type, msg += "".join(
cm.unraisable.exc_value, traceback.format_exception(
cm.unraisable.exc_traceback, 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]: def pytest_runtest_setup() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook() 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]: def pytest_runtest_call() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook() 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]: def pytest_runtest_teardown() -> Generator[None, None, None]:
yield from unraisable_exception_runtest_hook() yield from unraisable_exception_runtest_hook()

View File

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

View File

@ -1,6 +1,7 @@
import dataclasses import dataclasses
import re import re
import sys import sys
from typing import Generator
from typing import List from typing import List
import pytest import pytest
@ -21,11 +22,11 @@ if sys.gettrace():
sys.settrace(orig_trace) sys.settrace(orig_trace)
@pytest.hookimpl(hookwrapper=True, tryfirst=True) @pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items): def pytest_collection_modifyitems(items) -> Generator[None, None, None]:
"""Prefer faster tests. """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. correctly.
""" """
fast_items = [] fast_items = []
@ -62,7 +63,7 @@ def pytest_collection_modifyitems(items):
items[:] = fast_items + neutral_items + slow_items + slowest_items items[:] = fast_items + neutral_items + slow_items + slowest_items
yield return (yield)
@pytest.fixture @pytest.fixture

View File

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

View File

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

View File

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

View File

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

View File

@ -806,12 +806,12 @@ class TestKeywordSelection:
pytester.makepyfile( pytester.makepyfile(
conftest=""" conftest="""
import pytest import pytest
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_pycollect_makeitem(name): def pytest_pycollect_makeitem(name):
outcome = yield item = yield
if name == "TestClass": if name == "TestClass":
item = outcome.get_result()
item.extra_keyword_matches.add("xxx") item.extra_keyword_matches.add("xxx")
return item
""" """
) )
reprec = pytester.inline_run(p.parent, "-s", "-k", keyword) 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. # This is tough to test behaviorally because the cleanup really runs last.
# So the test make several implementation assumptions: # So the test make several implementation assumptions:
# - Cleanup is done in pytest_unconfigure(). # - Cleanup is done in pytest_unconfigure().
# - Not a hookwrapper. # - Not a hook wrapper.
# So we can add a hookwrapper ourselves to test what it does. # 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.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n")
pytester.makepyfile(test_foo="""def test_foo(): pass""") pytester.makepyfile(test_foo="""def test_foo(): pass""")
@ -94,12 +94,14 @@ def test_clean_up(pytester: Pytester) -> None:
after: Optional[List[str]] = None after: Optional[List[str]] = None
class Plugin: class Plugin:
@pytest.hookimpl(hookwrapper=True, tryfirst=True) @pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_unconfigure(self) -> Generator[None, None, None]: def pytest_unconfigure(self) -> Generator[None, None, None]:
nonlocal before, after nonlocal before, after
before = sys.path.copy() before = sys.path.copy()
yield try:
after = sys.path.copy() return (yield)
finally:
after = sys.path.copy()
result = pytester.runpytest_inprocess(plugins=[Plugin()]) result = pytester.runpytest_inprocess(plugins=[Plugin()])
assert result.ret == 0 assert result.ret == 0

View File

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

View File

@ -725,12 +725,12 @@ class TestTerminalFunctional:
) )
assert result.ret == 0 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( pytester.makeconftest(
""" """
import pytest import pytest
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(config, items): def pytest_collection_modifyitems(config, items):
yield yield
deselected = items.pop() deselected = items.pop()

View File

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