11
11
from typing import NoReturn
12
12
import warnings
13
13
14
- from ._hooks import HookImpl
15
- from ._result import HookCallError
14
+ from ._hook_callers import HookImpl
15
+ from ._hook_callers import WrapperImpl
16
16
from ._result import Result
17
17
from ._warnings import PluggyTeardownRaisedWarning
18
18
23
23
24
24
25
25
def run_old_style_hookwrapper (
26
- hook_impl : HookImpl , hook_name : str , args : Sequence [object ]
26
+ hook_impl : WrapperImpl , hook_name : str , args : Sequence [object ]
27
27
) -> Teardown :
28
28
"""
29
29
backward compatibility wrapper to run a old style hookwrapper as a wrapper
@@ -64,18 +64,19 @@ def _raise_wrapfail(
64
64
65
65
66
66
def _warn_teardown_exception (
67
- hook_name : str , hook_impl : HookImpl , e : BaseException
67
+ hook_name : str , hook_impl : WrapperImpl , e : BaseException
68
68
) -> None :
69
69
msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n "
70
70
msg += f"Plugin: { hook_impl .plugin_name } , Hook: { hook_name } \n "
71
71
msg += f"{ type (e ).__name__ } : { e } \n "
72
72
msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
73
- warnings .warn (PluggyTeardownRaisedWarning (msg ), stacklevel = 6 )
73
+ warnings .warn (PluggyTeardownRaisedWarning (msg ), stacklevel = 7 )
74
74
75
75
76
76
def _multicall (
77
77
hook_name : str ,
78
- hook_impls : Sequence [HookImpl ],
78
+ normal_impls : Sequence [HookImpl ],
79
+ wrapper_impls : Sequence [WrapperImpl ],
79
80
caller_kwargs : Mapping [str , object ],
80
81
firstresult : bool ,
81
82
) -> object | list [object ]:
@@ -87,81 +88,41 @@ def _multicall(
87
88
__tracebackhide__ = True
88
89
results : list [object ] = []
89
90
exception = None
90
- try : # run impl and wrapper setup functions in a loop
91
- teardowns : list [Teardown ] = []
92
- try :
93
- for hook_impl in reversed (hook_impls ):
94
- try :
95
- args = [caller_kwargs [argname ] 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
99
- if argname not in caller_kwargs :
100
- raise HookCallError (
101
- f"hook call must provide argument { argname !r} "
102
- ) from e
103
-
104
- if hook_impl .hookwrapper :
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
-
110
- elif hook_impl .wrapper :
111
- try :
112
- # If this cast is not valid, a type error is raised below,
113
- # which is the desired response.
114
- res = hook_impl .function (* args )
115
- function_gen = cast (Generator [None , object , object ], res )
116
- next (function_gen ) # first yield
117
- teardowns .append (function_gen )
118
- except StopIteration :
119
- _raise_wrapfail (function_gen , "did not yield" )
120
- else :
121
- res = hook_impl .function (* args )
122
- if res is not None :
123
- results .append (res )
124
- if firstresult : # halt further impl calls
125
- break
126
- except BaseException as exc :
127
- exception = exc
128
- finally :
129
- if firstresult : # first result hooks return a single value
130
- result = results [0 ] if results else None
131
- 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 :
138
- try :
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
149
- else :
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" )
91
+
92
+ # Set up wrapper completion hooks
93
+ from ._hook_callers import CompletionHook
94
+
95
+ completion_hooks : list [CompletionHook ] = []
96
+
97
+ try :
98
+ # Set up all wrappers and collect their completion hooks
99
+ for wrapper_impl in reversed (wrapper_impls ):
100
+ completion_hook = wrapper_impl .setup_and_get_completion_hook (
101
+ hook_name , caller_kwargs
102
+ )
103
+ completion_hooks .append (completion_hook )
104
+
105
+ # Process normal implementations (in reverse order for correct execution)
106
+ # Caller ensures normal_impls contains only non-wrapper implementations
107
+ for normal_impl in reversed (normal_impls ):
108
+ args = normal_impl ._get_call_args (caller_kwargs )
109
+ res = normal_impl .function (* args )
110
+ if res is not None :
111
+ results .append (res )
112
+ if firstresult : # halt further impl calls
113
+ break
114
+ except BaseException as exc :
115
+ exception = exc
116
+
117
+ # Determine final result before teardown
118
+ if firstresult : # first result hooks return a single value
119
+ result = results [0 ] if results else None
120
+ else :
121
+ result = results
122
+
123
+ # Run completion hooks in reverse order (LIFO - Last In, First Out)
124
+ for completion_hook in reversed (completion_hooks ):
125
+ result , exception = completion_hook (result , exception )
165
126
166
127
if exception is not None :
167
128
raise exception
0 commit comments