Skip to content

Commit

Permalink
WIP: Draft iterable hooks implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyler Goodlet committed Nov 12, 2017
1 parent 4fb708b commit 5f54615
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 6 deletions.
32 changes: 26 additions & 6 deletions pluggy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import inspect
import warnings
from .callers import _multicall, HookCallError, _Result, _legacymulticall
from .callers import (
_multicall, _itercall, HookCallError, _Result, _legacymulticall)

__version__ = '0.5.3.dev'

Expand Down Expand Up @@ -209,6 +210,8 @@ def __init__(self, project_name, implprefix=None):
self._plugin_distinfo = []
self.trace = _TagTracer().get("pluginmanage")
self.hook = _HookRelay(self.trace.root.get("hook"))
# alternative set of lazily executed hook calls
self.ihook = _HookRelay(self.trace.root.get("hook"))
self._implprefix = implprefix
self._inner_hookexec = lambda hook, methods, kwargs: \
hook.multicall(
Expand Down Expand Up @@ -245,14 +248,25 @@ def register(self, plugin, name=None):
method = getattr(plugin, name)
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
hook = getattr(self.hook, name, None)
ihook = getattr(self.ihook, name, None)

if hook is None:
hook = _HookCaller(name, self._hookexec)
ihook = _HookCaller(name, self._hookexec, iterate=True)
setattr(self.hook, name, hook)
setattr(self.ihook, name, ihook)

elif hook.has_spec():
self._verify_hook(hook, hookimpl)
hook._maybe_apply_history(hookimpl)

self._verify_hook(ihook, hookimpl)
ihook._maybe_apply_history(hookimpl)

hook._add_hookimpl(hookimpl)
ihook._add_hookimpl(hookimpl)
hookcallers.append(hook)
hookcallers.append(ihook)
return plugin_name

def parse_hookimpl_opts(self, plugin, name):
Expand Down Expand Up @@ -306,14 +320,19 @@ def add_hookspecs(self, module_or_class):
spec_opts = self.parse_hookspec_opts(module_or_class, name)
if spec_opts is not None:
hc = getattr(self.hook, name, None)
hi = getattr(self.ihook, name, None)
if hc is None:
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
setattr(self.hook, name, hc)
hi = _HookCaller(name, self._hookexec, module_or_class,
spec_opts, iterate=True)
setattr(self.ihook, name, hi)
else:
# plugins registered this hook without knowing the spec
hc.set_specification(module_or_class, spec_opts)
for hookfunction in (hc._wrappers + hc._nonwrappers):
self._verify_hook(hc, hookfunction)
for h in [hc, hi]:
h.set_specification(module_or_class, spec_opts)
for hookfunction in (h._wrappers + h._nonwrappers):
self._verify_hook(h, hookfunction)
names.append(name)

if not names:
Expand Down Expand Up @@ -528,14 +547,15 @@ def __init__(self, trace):


class _HookCaller(object):
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None,
iterate=False):
self.name = name
self._wrappers = []
self._nonwrappers = []
self._hookexec = hook_execute
self.argnames = None
self.kwargnames = None
self.multicall = _multicall
self.multicall = _multicall if not iterate else _itercall
if specmodule_or_class is not None:
assert spec_opts is not None
self.set_specification(specmodule_or_class, spec_opts)
Expand Down
57 changes: 57 additions & 0 deletions pluggy/callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,60 @@ def _multicall(hook_impls, caller_kwargs, specopts={}, hook=None):
pass

return outcome.get_result()


def _itercall(hook_impls, caller_kwargs, specopts={}, hook=None):
"""Execute a calls into multiple python functions/methods and yield
the result(s) lazily.
``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
specopts = hook.spec_opts if hook else specopts
results = []
firstresult = specopts.get("firstresult")
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
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:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,))

if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
yield res
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)

# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass

# raise any exceptions
outcome.get_result()

0 comments on commit 5f54615

Please sign in to comment.