Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3875ea5
fix #544: Correctly pass StopIteration trough wrappers
RonnyPfannschmidt Dec 21, 2024
85e1d74
WIP: move legacy hook result trigger outside of the except block
RonnyPfannschmidt Nov 4, 2024
85a6c24
expand ruff linting
RonnyPfannschmidt Nov 30, 2024
5a12973
implment review: bettern name for hookwrapper helper
RonnyPfannschmidt Mar 16, 2025
25af101
fixup: correct type ignore comment
RonnyPfannschmidt Mar 16, 2025
2f428b9
chore: add coverage to testing extra
RonnyPfannschmidt Apr 9, 2025
6b044a6
chore: drop deprecated ruff rules from config
RonnyPfannschmidt Apr 9, 2025
3202c88
chore: coverage: expand default ignores to pass lines and ellipsis de…
RonnyPfannschmidt Apr 9, 2025
5d1ffb9
chore: expand coverage by pragmas and test changes
RonnyPfannschmidt Apr 9, 2025
d0ab361
add test to cover nonspec hook arg missing
RonnyPfannschmidt Apr 9, 2025
eb70fb9
epxand test coverage and drop dead code for wrappers
RonnyPfannschmidt Apr 10, 2025
cfa7ed1
add test to cover unregistration of a blocked plugin
RonnyPfannschmidt Apr 10, 2025
6a417de
add coverage for optional hook validation and error cases
RonnyPfannschmidt Apr 10, 2025
d443764
add no cover for special varnames cases
RonnyPfannschmidt Apr 10, 2025
0df16f4
Merge remote-tracking branch 'upstream/main' into ronny/hookwrapper-w…
RonnyPfannschmidt Apr 10, 2025
615c6c5
Merge branch 'main' into hookwrapper-wrap-legacy
RonnyPfannschmidt May 9, 2025
4be0c55
add changelog
RonnyPfannschmidt May 9, 2025
1f4872e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[run]
include =
pluggy/*
src/pluggy/*
testing/*
*/lib/python*/site-packages/pluggy/*
*/pypy*/site-packages/pluggy/*
Expand All @@ -27,3 +28,8 @@ exclude_lines =

# Ignore coverage on lines solely with `...`
^\s*\.\.\.\s*$
# ignore coverage on ruff line continued
^\s*def.*:\ \.\.\.\s*$
.*: ...$
# ignore coverage on pass lines
^\s*passs*$
1 change: 1 addition & 0 deletions changelog/573.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix python 3.14 SyntaxWrror by rearranging code.
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ requires-python = ">=3.9"
dynamic = ["version"]
[project.optional-dependencies]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
testing = ["pytest", "pytest-benchmark", "coverage"]

[tool.setuptools]
packages = ["pluggy"]
Expand All @@ -48,8 +48,14 @@ package-data = {"pluggy" = ["py.typed"]}
[tool.ruff.lint]
extend-select = [
"I", # isort
"UP",
"F","E", "W",
"UP", "ANN",
]
extend-ignore = ["ANN401"]

[tool.ruff.lint.extend-per-file-ignores]
"testing/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,]
"docs/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,]

[tool.ruff.lint.isort]
force-single-line = true
Expand Down
10 changes: 6 additions & 4 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from git import Repo


def create_branch(version):
def create_branch(version: str) -> Repo:
"""Create a fresh branch from upstream/main"""
repo = Repo.init(".")
if repo.is_dirty(untracked_files=True):
Expand All @@ -36,7 +36,7 @@ def get_upstream(repo: Repo) -> Remote:
raise RuntimeError("could not find pytest-dev/pluggy remote")


def pre_release(version):
def pre_release(version: str) -> None:
"""Generates new docs, release announcements and creates a local tag."""
create_branch(version)
changelog(version, write_out=True)
Expand All @@ -47,7 +47,7 @@ def pre_release(version):
print(f"{Fore.GREEN}Please push your branch to your fork and open a PR.")


def changelog(version, write_out=False):
def changelog(version: str, write_out: bool = False) -> None:
if write_out:
addopts = []
else:
Expand All @@ -56,7 +56,7 @@ def changelog(version, write_out=False):
check_call(["towncrier", "build", "--yes", "--version", version] + addopts)


def main():
def main() -> int:
init(autoreset=True)
parser = argparse.ArgumentParser()
parser.add_argument("version", help="Release version")
Expand All @@ -66,6 +66,8 @@ def main():
except RuntimeError as e:
print(f"{Fore.RED}ERROR: {e}")
return 1
else:
return 0


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion scripts/towncrier-draft-to-file.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys


def main():
def main() -> int:
"""
Platform agnostic wrapper script for towncrier.
Fixes the issue (pytest#7251) where windows users are unable to natively
Expand Down
9 changes: 1 addition & 8 deletions src/pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
try:
from ._version import version as __version__
except ImportError:
# broken installation, we don't even try
# unknown only works because we do poor mans version compare
__version__ = "unknown"

__all__ = [
"__version__",
"PluginManager",
Expand All @@ -21,7 +14,6 @@
"PluggyWarning",
"PluggyTeardownRaisedWarning",
]

from ._hooks import HookCaller
from ._hooks import HookImpl
from ._hooks import HookimplMarker
Expand All @@ -33,5 +25,6 @@
from ._manager import PluginValidationError
from ._result import HookCallError
from ._result import Result
from ._version import version as __version__
from ._warnings import PluggyTeardownRaisedWarning
from ._warnings import PluggyWarning
199 changes: 82 additions & 117 deletions src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from collections.abc import Sequence
from typing import cast
from typing import NoReturn
from typing import Union
import warnings

from ._hooks import HookImpl
Expand All @@ -20,19 +19,45 @@

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


def run_old_style_hookwrapper(
hook_impl: HookImpl, hook_name: str, args: Sequence[object]
) -> Teardown:
"""
backward compatibility wrapper to run a old style hookwrapper as a wrapper
"""

teardown: Teardown = cast(Teardown, hook_impl.function(*args))
try:
next(teardown)
except StopIteration:
_raise_wrapfail(teardown, "did not yield")
try:
res = yield
result = Result(res, None)
except BaseException as exc:
result = Result(None, exc)
try:
teardown.send(result)
except StopIteration:
pass
except BaseException as e:
_warn_teardown_exception(hook_name, hook_impl, e)
raise
else:
_raise_wrapfail(teardown, "has second yield")
finally:
teardown.close()
return result.get_result()


def _raise_wrapfail(
wrap_controller: (
Generator[None, Result[object], None] | Generator[None, object, object]
),
wrap_controller: Generator[None, object, object],
msg: str,
) -> NoReturn:
co = wrap_controller.gi_code # type: ignore[union-attr]
co = wrap_controller.gi_code # type: ignore[attr-defined]
raise RuntimeError(
f"wrap_controller at {co.co_name!r} {co.co_filename}:{co.co_firstlineno} {msg}"
)
Expand All @@ -45,7 +70,7 @@ def _warn_teardown_exception(
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)
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)


def _multicall(
Expand All @@ -62,31 +87,26 @@ def _multicall(
__tracebackhide__ = True
results: list[object] = []
exception = None
only_new_style_wrappers = True
try: # run impl and wrapper setup functions in a loop
teardowns: list[Teardown] = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
except KeyError as e:
# coverage bug - this is tested
for argname in hook_impl.argnames: # pragma: no cover
if argname not in caller_kwargs:
raise HookCallError(
f"hook call must provide argument {argname!r}"
)
) from e

if hook_impl.hookwrapper:
only_new_style_wrappers = False
try:
# If this cast is not valid, a type error is raised below,
# which is the desired response.
res = hook_impl.function(*args)
wrapper_gen = cast(Generator[None, Result[object], None], res)
next(wrapper_gen) # first yield
teardowns.append((wrapper_gen, hook_impl))
except StopIteration:
_raise_wrapfail(wrapper_gen, "did not yield")
function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args)

next(function_gen) # first yield
teardowns.append(function_gen)

elif hook_impl.wrapper:
try:
# If this cast is not valid, a type error is raised below,
Expand All @@ -106,99 +126,44 @@ def _multicall(
except BaseException as exc:
exception = exc
finally:
# Fast path - only new-style wrappers, no Result.
if only_new_style_wrappers:
if firstresult: # first result hooks return a single value
result = results[0] if results else None
else:
result = results

# run all wrapper post-yield blocks
for teardown in reversed(teardowns):
try:
if exception is not None:
try:
teardown.throw(exception) # type: ignore[union-attr]
except RuntimeError as re:
# StopIteration from generator causes RuntimeError
# even for coroutine usage - see #544
if (
isinstance(exception, StopIteration)
and re.__cause__ is exception
):
teardown.close() # type: ignore[union-attr]
continue
else:
raise
else:
teardown.send(result) # type: ignore[union-attr]
# Following is unreachable for a well behaved hook wrapper.
# Try to force finalizers otherwise postponed till GC action.
# Note: close() may raise if generator handles GeneratorExit.
teardown.close() # type: ignore[union-attr]
except StopIteration as si:
result = si.value
exception = None
continue
except BaseException as e:
exception = e
continue
_raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type]

if exception is not None:
raise exception
else:
return result

# Slow path - need to support old-style wrappers.
if firstresult: # first result hooks return a single value
result = results[0] if results else None
else:
if firstresult: # first result hooks return a single value
outcome: Result[object | list[object]] = Result(
results[0] if results else None, exception
)
else:
outcome = Result(results, exception)

# run all wrapper post-yield blocks
for teardown in reversed(teardowns):
if isinstance(teardown, tuple):
try:
teardown[0].send(outcome)
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:
result = results

# run all wrapper post-yield blocks
for teardown in reversed(teardowns):
try:
if exception is not None:
try:
if outcome._exception is not None:
try:
teardown.throw(outcome._exception)
except RuntimeError as re:
# StopIteration from generator causes RuntimeError
# even for coroutine usage - see #544
if (
isinstance(outcome._exception, StopIteration)
and re.__cause__ is outcome._exception
):
teardown.close()
continue
else:
raise
teardown.throw(exception)
except RuntimeError as re:
# StopIteration from generator causes RuntimeError
# even for coroutine usage - see #544
if (
isinstance(exception, StopIteration)
and re.__cause__ is exception
):
teardown.close()
continue
else:
teardown.send(outcome._result)
# Following is unreachable for a well behaved hook wrapper.
# Try to force finalizers otherwise postponed till GC action.
# Note: close() may raise if generator handles GeneratorExit.
teardown.close()
except StopIteration as si:
outcome.force_result(si.value)
continue
except BaseException as e:
outcome.force_exception(e)
continue
_raise_wrapfail(teardown, "has second yield")

return outcome.get_result()
raise
else:
teardown.send(result)
# Following is unreachable for a well behaved hook wrapper.
# Try to force finalizers otherwise postponed till GC action.
# Note: close() may raise if generator handles GeneratorExit.
teardown.close()
except StopIteration as si:
result = si.value
exception = None
continue
except BaseException as e:
exception = e
continue
_raise_wrapfail(teardown, "has second yield")

if exception is not None:
raise exception
else:
return result
Loading