Skip to content

Commit 0ceb558

Browse files
Merge pull request #546 from RonnyPfannschmidt/ronny/hookwrapper-wrap-legacy
refactor: implement legacy hookwrappers in terms of a modern hook"
2 parents 2acc644 + 1f4872e commit 0ceb558

17 files changed

+331
-198
lines changed

.coveragerc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[run]
22
include =
33
pluggy/*
4+
src/pluggy/*
45
testing/*
56
*/lib/python*/site-packages/pluggy/*
67
*/pypy*/site-packages/pluggy/*
@@ -27,3 +28,8 @@ exclude_lines =
2728

2829
# Ignore coverage on lines solely with `...`
2930
^\s*\.\.\.\s*$
31+
# ignore coverage on ruff line continued
32+
^\s*def.*:\ \.\.\.\s*$
33+
.*: ...$
34+
# ignore coverage on pass lines
35+
^\s*passs*$

changelog/573.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix python 3.14 SyntaxWrror by rearranging code.

pyproject.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ requires-python = ">=3.9"
3737
dynamic = ["version"]
3838
[project.optional-dependencies]
3939
dev = ["pre-commit", "tox"]
40-
testing = ["pytest", "pytest-benchmark"]
40+
testing = ["pytest", "pytest-benchmark", "coverage"]
4141

4242
[tool.setuptools]
4343
packages = ["pluggy"]
@@ -48,8 +48,14 @@ package-data = {"pluggy" = ["py.typed"]}
4848
[tool.ruff.lint]
4949
extend-select = [
5050
"I", # isort
51-
"UP",
51+
"F","E", "W",
52+
"UP", "ANN",
5253
]
54+
extend-ignore = ["ANN401"]
55+
56+
[tool.ruff.lint.extend-per-file-ignores]
57+
"testing/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,]
58+
"docs/*.py" = ["ANN001", "ANN002", "ANN003", "ANN201", "ANN202","ANN204" ,]
5359

5460
[tool.ruff.lint.isort]
5561
force-single-line = true

scripts/release.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from git import Repo
1313

1414

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

3838

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

4949

50-
def changelog(version, write_out=False):
50+
def changelog(version: str, write_out: bool = False) -> None:
5151
if write_out:
5252
addopts = []
5353
else:
@@ -56,7 +56,7 @@ def changelog(version, write_out=False):
5656
check_call(["towncrier", "build", "--yes", "--version", version] + addopts)
5757

5858

59-
def main():
59+
def main() -> int:
6060
init(autoreset=True)
6161
parser = argparse.ArgumentParser()
6262
parser.add_argument("version", help="Release version")
@@ -66,6 +66,8 @@ def main():
6666
except RuntimeError as e:
6767
print(f"{Fore.RED}ERROR: {e}")
6868
return 1
69+
else:
70+
return 0
6971

7072

7173
if __name__ == "__main__":

scripts/towncrier-draft-to-file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33

44

5-
def main():
5+
def main() -> int:
66
"""
77
Platform agnostic wrapper script for towncrier.
88
Fixes the issue (pytest#7251) where windows users are unable to natively

src/pluggy/__init__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
try:
2-
from ._version import version as __version__
3-
except ImportError:
4-
# broken installation, we don't even try
5-
# unknown only works because we do poor mans version compare
6-
__version__ = "unknown"
7-
81
__all__ = [
92
"__version__",
103
"PluginManager",
@@ -21,7 +14,6 @@
2114
"PluggyWarning",
2215
"PluggyTeardownRaisedWarning",
2316
]
24-
2517
from ._hooks import HookCaller
2618
from ._hooks import HookImpl
2719
from ._hooks import HookimplMarker
@@ -33,5 +25,6 @@
3325
from ._manager import PluginValidationError
3426
from ._result import HookCallError
3527
from ._result import Result
28+
from ._version import version as __version__
3629
from ._warnings import PluggyTeardownRaisedWarning
3730
from ._warnings import PluggyWarning

src/pluggy/_callers.py

Lines changed: 82 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from collections.abc import Sequence
1010
from typing import cast
1111
from typing import NoReturn
12-
from typing import Union
1312
import warnings
1413

1514
from ._hooks import HookImpl
@@ -20,19 +19,45 @@
2019

2120
# Need to distinguish between old- and new-style hook wrappers.
2221
# Wrapping with a tuple is the fastest type-safe way I found to do it.
23-
Teardown = Union[
24-
tuple[Generator[None, Result[object], None], HookImpl],
25-
Generator[None, object, object],
26-
]
22+
Teardown = Generator[None, object, object]
23+
24+
25+
def run_old_style_hookwrapper(
26+
hook_impl: HookImpl, hook_name: str, args: Sequence[object]
27+
) -> Teardown:
28+
"""
29+
backward compatibility wrapper to run a old style hookwrapper as a wrapper
30+
"""
31+
32+
teardown: Teardown = cast(Teardown, hook_impl.function(*args))
33+
try:
34+
next(teardown)
35+
except StopIteration:
36+
_raise_wrapfail(teardown, "did not yield")
37+
try:
38+
res = yield
39+
result = Result(res, None)
40+
except BaseException as exc:
41+
result = Result(None, exc)
42+
try:
43+
teardown.send(result)
44+
except StopIteration:
45+
pass
46+
except BaseException as e:
47+
_warn_teardown_exception(hook_name, hook_impl, e)
48+
raise
49+
else:
50+
_raise_wrapfail(teardown, "has second yield")
51+
finally:
52+
teardown.close()
53+
return result.get_result()
2754

2855

2956
def _raise_wrapfail(
30-
wrap_controller: (
31-
Generator[None, Result[object], None] | Generator[None, object, object]
32-
),
57+
wrap_controller: Generator[None, object, object],
3358
msg: str,
3459
) -> NoReturn:
35-
co = wrap_controller.gi_code # type: ignore[union-attr]
60+
co = wrap_controller.gi_code # type: ignore[attr-defined]
3661
raise RuntimeError(
3762
f"wrap_controller at {co.co_name!r} {co.co_filename}:{co.co_firstlineno} {msg}"
3863
)
@@ -45,7 +70,7 @@ def _warn_teardown_exception(
4570
msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n"
4671
msg += f"{type(e).__name__}: {e}\n"
4772
msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
48-
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5)
73+
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)
4974

5075

5176
def _multicall(
@@ -62,31 +87,26 @@ def _multicall(
6287
__tracebackhide__ = True
6388
results: list[object] = []
6489
exception = None
65-
only_new_style_wrappers = True
6690
try: # run impl and wrapper setup functions in a loop
6791
teardowns: list[Teardown] = []
6892
try:
6993
for hook_impl in reversed(hook_impls):
7094
try:
7195
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
72-
except KeyError:
73-
for argname in hook_impl.argnames:
96+
except KeyError as e:
97+
# coverage bug - this is tested
98+
for argname in hook_impl.argnames: # pragma: no cover
7499
if argname not in caller_kwargs:
75100
raise HookCallError(
76101
f"hook call must provide argument {argname!r}"
77-
)
102+
) from e
78103

79104
if hook_impl.hookwrapper:
80-
only_new_style_wrappers = False
81-
try:
82-
# If this cast is not valid, a type error is raised below,
83-
# which is the desired response.
84-
res = hook_impl.function(*args)
85-
wrapper_gen = cast(Generator[None, Result[object], None], res)
86-
next(wrapper_gen) # first yield
87-
teardowns.append((wrapper_gen, hook_impl))
88-
except StopIteration:
89-
_raise_wrapfail(wrapper_gen, "did not yield")
105+
function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args)
106+
107+
next(function_gen) # first yield
108+
teardowns.append(function_gen)
109+
90110
elif hook_impl.wrapper:
91111
try:
92112
# If this cast is not valid, a type error is raised below,
@@ -106,99 +126,44 @@ def _multicall(
106126
except BaseException as exc:
107127
exception = exc
108128
finally:
109-
# Fast path - only new-style wrappers, no Result.
110-
if only_new_style_wrappers:
111-
if firstresult: # first result hooks return a single value
112-
result = results[0] if results else None
113-
else:
114-
result = results
115-
116-
# run all wrapper post-yield blocks
117-
for teardown in reversed(teardowns):
118-
try:
119-
if exception is not None:
120-
try:
121-
teardown.throw(exception) # type: ignore[union-attr]
122-
except RuntimeError as re:
123-
# StopIteration from generator causes RuntimeError
124-
# even for coroutine usage - see #544
125-
if (
126-
isinstance(exception, StopIteration)
127-
and re.__cause__ is exception
128-
):
129-
teardown.close() # type: ignore[union-attr]
130-
continue
131-
else:
132-
raise
133-
else:
134-
teardown.send(result) # type: ignore[union-attr]
135-
# Following is unreachable for a well behaved hook wrapper.
136-
# Try to force finalizers otherwise postponed till GC action.
137-
# Note: close() may raise if generator handles GeneratorExit.
138-
teardown.close() # type: ignore[union-attr]
139-
except StopIteration as si:
140-
result = si.value
141-
exception = None
142-
continue
143-
except BaseException as e:
144-
exception = e
145-
continue
146-
_raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type]
147-
148-
if exception is not None:
149-
raise exception
150-
else:
151-
return result
152-
153-
# Slow path - need to support old-style wrappers.
129+
if firstresult: # first result hooks return a single value
130+
result = results[0] if results else None
154131
else:
155-
if firstresult: # first result hooks return a single value
156-
outcome: Result[object | list[object]] = Result(
157-
results[0] if results else None, exception
158-
)
159-
else:
160-
outcome = Result(results, exception)
161-
162-
# run all wrapper post-yield blocks
163-
for teardown in reversed(teardowns):
164-
if isinstance(teardown, tuple):
165-
try:
166-
teardown[0].send(outcome)
167-
except StopIteration:
168-
pass
169-
except BaseException as e:
170-
_warn_teardown_exception(hook_name, teardown[1], e)
171-
raise
172-
else:
173-
_raise_wrapfail(teardown[0], "has second yield")
174-
else:
132+
result = results
133+
134+
# run all wrapper post-yield blocks
135+
for teardown in reversed(teardowns):
136+
try:
137+
if exception is not None:
175138
try:
176-
if outcome._exception is not None:
177-
try:
178-
teardown.throw(outcome._exception)
179-
except RuntimeError as re:
180-
# StopIteration from generator causes RuntimeError
181-
# even for coroutine usage - see #544
182-
if (
183-
isinstance(outcome._exception, StopIteration)
184-
and re.__cause__ is outcome._exception
185-
):
186-
teardown.close()
187-
continue
188-
else:
189-
raise
139+
teardown.throw(exception)
140+
except RuntimeError as re:
141+
# StopIteration from generator causes RuntimeError
142+
# even for coroutine usage - see #544
143+
if (
144+
isinstance(exception, StopIteration)
145+
and re.__cause__ is exception
146+
):
147+
teardown.close()
148+
continue
190149
else:
191-
teardown.send(outcome._result)
192-
# Following is unreachable for a well behaved hook wrapper.
193-
# Try to force finalizers otherwise postponed till GC action.
194-
# Note: close() may raise if generator handles GeneratorExit.
195-
teardown.close()
196-
except StopIteration as si:
197-
outcome.force_result(si.value)
198-
continue
199-
except BaseException as e:
200-
outcome.force_exception(e)
201-
continue
202-
_raise_wrapfail(teardown, "has second yield")
203-
204-
return outcome.get_result()
150+
raise
151+
else:
152+
teardown.send(result)
153+
# Following is unreachable for a well behaved hook wrapper.
154+
# Try to force finalizers otherwise postponed till GC action.
155+
# Note: close() may raise if generator handles GeneratorExit.
156+
teardown.close()
157+
except StopIteration as si:
158+
result = si.value
159+
exception = None
160+
continue
161+
except BaseException as e:
162+
exception = e
163+
continue
164+
_raise_wrapfail(teardown, "has second yield")
165+
166+
if exception is not None:
167+
raise exception
168+
else:
169+
return result

0 commit comments

Comments
 (0)