Skip to content

Commit

Permalink
Warn when old-style hookwrapper raises during teardown
Browse files Browse the repository at this point in the history
Fix #463.
  • Loading branch information
bluetech committed Dec 22, 2023
1 parent d3877d4 commit 3bc3aab
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 4 deletions.
2 changes: 2 additions & 0 deletions changelog/463.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
A warning :class:`~pluggy.PluggyTeardownRaisedWarning` is now issued when an old-style hookwrapper raises an exception during teardown.
See the warning documentation for more details.
12 changes: 12 additions & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ API Reference
.. autoclass:: pluggy.HookimplOpts()
:show-inheritance:
:members:


Warnings
--------

Custom warnings generated in some situations such as improper usage or deprecated features.

.. autoclass:: pluggy.PluggyWarning()
:show-inheritance:

.. autoclass:: pluggy.PluggyTeardownRaisedWarning()
:show-inheritance:
6 changes: 6 additions & 0 deletions src/pluggy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"HookspecMarker",
"HookimplMarker",
"Result",
"PluggyWarning",
"PluggyTeardownRaisedWarning",
]

from ._manager import PluginManager, PluginValidationError
Expand All @@ -31,3 +33,7 @@
HookimplOpts,
HookImpl,
)
from ._warnings import (
PluggyWarning,
PluggyTeardownRaisedWarning,
)
24 changes: 20 additions & 4 deletions src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import annotations

import warnings
from typing import cast
from typing import Generator
from typing import Mapping
Expand All @@ -14,12 +15,13 @@
from ._hooks import HookImpl
from ._result import HookCallError
from ._result import Result
from ._warnings import PluggyTeardownRaisedWarning


# Need to distinguish between old- and new-style hook wrappers.
# Wrapping one a singleton tuple is the fastest type-safe way I found to do it.
# Wrapping with a tuple is the fastest type-safe way I found to do it.
Teardown = Union[
Tuple[Generator[None, Result[object], None]],
Tuple[Generator[None, Result[object], None], HookImpl],
Generator[None, object, object],
]

Expand All @@ -37,6 +39,16 @@ def _raise_wrapfail(
)


Check warning on line 41 in src/pluggy/_callers.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_callers.py#L39-L41

Added lines #L39 - L41 were not covered by tests
def _warn_teardown_exception(
hook_name: str, hook_impl: HookImpl, e: BaseException
) -> None:
msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n"
msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n"
msg += f"{type(e).__name__}: {e}\n"
msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5)


def _multicall(
hook_name: str,
hook_impls: Sequence[HookImpl],
Expand Down Expand Up @@ -73,7 +85,7 @@ def _multicall(
res = hook_impl.function(*args)
wrapper_gen = cast(Generator[None, Result[object], None], res)
next(wrapper_gen) # first yield
teardowns.append((wrapper_gen,))
teardowns.append((wrapper_gen, hook_impl))
except StopIteration:
_raise_wrapfail(wrapper_gen, "did not yield")
elif hook_impl.wrapper:
Expand Down Expand Up @@ -141,9 +153,13 @@ def _multicall(
if isinstance(teardown, tuple):
try:
teardown[0].send(outcome)
_raise_wrapfail(teardown[0], "has second yield")
except StopIteration:
pass
except BaseException as e:
_warn_teardown_exception(hook_name, teardown[1], e)
raise
else:
_raise_wrapfail(teardown[0], "has second yield")
else:
try:
if outcome._exception is not None:
Expand Down
27 changes: 27 additions & 0 deletions src/pluggy/_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import final


class PluggyWarning(UserWarning):
"""Base class for all warnings emitted by pluggy."""

__module__ = "pluggy"


@final
class PluggyTeardownRaisedWarning(PluggyWarning):
"""A plugin raised an exception during an :ref:`old-style hookwrapper
<old_style_hookwrappers>` teardown.
Such exceptions are not handled by pluggy, and may cause subsequent
teardowns to be executed at unexpected times, or be skipped entirely.
This is an issue in the plugin implementation.
If the exception is unintended, fix the underlying cause.
If the exception is intended, switch to :ref:`new-style hook wrappers
<hookwrappers>`, or use :func:`result.force_exception()
<pluggy.Result.force_exception>` to set the exception instead of raising.
"""

__module__ = "pluggy"
49 changes: 49 additions & 0 deletions testing/test_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pathlib import Path

import pytest

from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluggyTeardownRaisedWarning
from pluggy import PluginManager


hookspec = HookspecMarker("example")
hookimpl = HookimplMarker("example")


def test_teardown_raised_warning(pm: PluginManager) -> None:
class Api:
@hookspec
def my_hook(self):
raise NotImplementedError()

pm.add_hookspecs(Api)

class Plugin1:
@hookimpl
def my_hook(self):
pass

class Plugin2:
@hookimpl(hookwrapper=True)
def my_hook(self):
yield
1 / 0

class Plugin3:
@hookimpl(hookwrapper=True)
def my_hook(self):
yield

pm.register(Plugin1(), "plugin1")
pm.register(Plugin2(), "plugin2")
pm.register(Plugin3(), "plugin3")
with pytest.warns(
PluggyTeardownRaisedWarning,
match=r"\bplugin2\b.*\bmy_hook\b.*\n.*ZeroDivisionError",
) as wc:
with pytest.raises(ZeroDivisionError):
pm.hook.my_hook()
assert len(wc.list) == 1
assert Path(wc.list[0].filename).name == "test_warnings.py"

0 comments on commit 3bc3aab

Please sign in to comment.