Skip to content

Commit 5f54615

Browse files
author
Tyler Goodlet
committed
WIP: Draft iterable hooks implementation
1 parent 4fb708b commit 5f54615

File tree

2 files changed

+83
-6
lines changed

2 files changed

+83
-6
lines changed

pluggy/__init__.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inspect
22
import warnings
3-
from .callers import _multicall, HookCallError, _Result, _legacymulticall
3+
from .callers import (
4+
_multicall, _itercall, HookCallError, _Result, _legacymulticall)
45

56
__version__ = '0.5.3.dev'
67

@@ -209,6 +210,8 @@ def __init__(self, project_name, implprefix=None):
209210
self._plugin_distinfo = []
210211
self.trace = _TagTracer().get("pluginmanage")
211212
self.hook = _HookRelay(self.trace.root.get("hook"))
213+
# alternative set of lazily executed hook calls
214+
self.ihook = _HookRelay(self.trace.root.get("hook"))
212215
self._implprefix = implprefix
213216
self._inner_hookexec = lambda hook, methods, kwargs: \
214217
hook.multicall(
@@ -245,14 +248,25 @@ def register(self, plugin, name=None):
245248
method = getattr(plugin, name)
246249
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
247250
hook = getattr(self.hook, name, None)
251+
ihook = getattr(self.ihook, name, None)
252+
248253
if hook is None:
249254
hook = _HookCaller(name, self._hookexec)
255+
ihook = _HookCaller(name, self._hookexec, iterate=True)
250256
setattr(self.hook, name, hook)
257+
setattr(self.ihook, name, ihook)
258+
251259
elif hook.has_spec():
252260
self._verify_hook(hook, hookimpl)
253261
hook._maybe_apply_history(hookimpl)
262+
263+
self._verify_hook(ihook, hookimpl)
264+
ihook._maybe_apply_history(hookimpl)
265+
254266
hook._add_hookimpl(hookimpl)
267+
ihook._add_hookimpl(hookimpl)
255268
hookcallers.append(hook)
269+
hookcallers.append(ihook)
256270
return plugin_name
257271

258272
def parse_hookimpl_opts(self, plugin, name):
@@ -306,14 +320,19 @@ def add_hookspecs(self, module_or_class):
306320
spec_opts = self.parse_hookspec_opts(module_or_class, name)
307321
if spec_opts is not None:
308322
hc = getattr(self.hook, name, None)
323+
hi = getattr(self.ihook, name, None)
309324
if hc is None:
310325
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
311326
setattr(self.hook, name, hc)
327+
hi = _HookCaller(name, self._hookexec, module_or_class,
328+
spec_opts, iterate=True)
329+
setattr(self.ihook, name, hi)
312330
else:
313331
# plugins registered this hook without knowing the spec
314-
hc.set_specification(module_or_class, spec_opts)
315-
for hookfunction in (hc._wrappers + hc._nonwrappers):
316-
self._verify_hook(hc, hookfunction)
332+
for h in [hc, hi]:
333+
h.set_specification(module_or_class, spec_opts)
334+
for hookfunction in (h._wrappers + h._nonwrappers):
335+
self._verify_hook(h, hookfunction)
317336
names.append(name)
318337

319338
if not names:
@@ -528,14 +547,15 @@ def __init__(self, trace):
528547

529548

530549
class _HookCaller(object):
531-
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
550+
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None,
551+
iterate=False):
532552
self.name = name
533553
self._wrappers = []
534554
self._nonwrappers = []
535555
self._hookexec = hook_execute
536556
self.argnames = None
537557
self.kwargnames = None
538-
self.multicall = _multicall
558+
self.multicall = _multicall if not iterate else _itercall
539559
if specmodule_or_class is not None:
540560
assert spec_opts is not None
541561
self.set_specification(specmodule_or_class, spec_opts)

pluggy/callers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,60 @@ def _multicall(hook_impls, caller_kwargs, specopts={}, hook=None):
202202
pass
203203

204204
return outcome.get_result()
205+
206+
207+
def _itercall(hook_impls, caller_kwargs, specopts={}, hook=None):
208+
"""Execute a calls into multiple python functions/methods and yield
209+
the result(s) lazily.
210+
211+
``caller_kwargs`` comes from _HookCaller.__call__().
212+
"""
213+
__tracebackhide__ = True
214+
specopts = hook.spec_opts if hook else specopts
215+
results = []
216+
firstresult = specopts.get("firstresult")
217+
excinfo = None
218+
try: # run impl and wrapper setup functions in a loop
219+
teardowns = []
220+
try:
221+
for hook_impl in reversed(hook_impls):
222+
try:
223+
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
224+
except KeyError:
225+
for argname in hook_impl.argnames:
226+
if argname not in caller_kwargs:
227+
raise HookCallError(
228+
"hook call must provide argument %r" % (argname,))
229+
230+
if hook_impl.hookwrapper:
231+
try:
232+
gen = hook_impl.function(*args)
233+
next(gen) # first yield
234+
teardowns.append(gen)
235+
except StopIteration:
236+
_raise_wrapfail(gen, "did not yield")
237+
else:
238+
res = hook_impl.function(*args)
239+
if res is not None:
240+
results.append(res)
241+
yield res
242+
if firstresult: # halt further impl calls
243+
break
244+
except BaseException:
245+
excinfo = sys.exc_info()
246+
finally:
247+
if firstresult: # first result hooks return a single value
248+
outcome = _Result(results[0] if results else None, excinfo)
249+
else:
250+
outcome = _Result(results, excinfo)
251+
252+
# run all wrapper post-yield blocks
253+
for gen in reversed(teardowns):
254+
try:
255+
gen.send(outcome)
256+
_raise_wrapfail(gen, "has second yield")
257+
except StopIteration:
258+
pass
259+
260+
# raise any exceptions
261+
outcome.get_result()

0 commit comments

Comments
 (0)