From 14e84a54d518c5af968658af2c03dd29ea27ca67 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Nov 2017 13:50:24 -0500 Subject: [PATCH 1/4] WIP: Draft iterable hooks implementation --- pluggy/callers.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ pluggy/hooks.py | 6 ++--- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/pluggy/callers.py b/pluggy/callers.py index 3ff67bec..e6c79654 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -199,3 +199,60 @@ def _multicall(hook_impls, caller_kwargs, firstresult=False): 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() diff --git a/pluggy/hooks.py b/pluggy/hooks.py index ae7c321f..2bc25b48 100644 --- a/pluggy/hooks.py +++ b/pluggy/hooks.py @@ -168,8 +168,8 @@ 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 = [] @@ -177,7 +177,7 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, self._specmodule_or_class = None self.argnames = None self.kwargnames = None - self.multicall = _multicall + self.multicall = _multicall if not iterate else _itercall self.spec_opts = spec_opts or {} if specmodule_or_class is not None: self.set_specification(specmodule_or_class, spec_opts) From 9d1bab066019178727f74595c58fce71895685a6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Nov 2017 18:19:35 -0500 Subject: [PATCH 2/4] Add baseline test to verify iterable hooks --- testing/test_pluginmanager.py | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 6b35814b..550818c0 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -393,3 +393,60 @@ def example_hook(): assert getattr(pm.hook, 'example_hook', None) # conftest.example_hook should be collected assert pm.parse_hookimpl_opts(conftest, 'example_blah') is None assert pm.parse_hookimpl_opts(conftest, 'example_hook') == {} + + +def test_iterable_hooks(pm): + class Hooks(object): + @hookspec + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + l = [] + + class Plugin1(object): + @hookimpl + def he_method1(self, arg): + l.append(1) + return 1 + + class Plugin2(object): + @hookimpl + def he_method1(self, arg): + l.append(2) + return 2 + + class Plugin3(object): + @hookimpl + def he_method1(self, arg): + l.append(3) + return 3 + + class Plugin4(object): + @hookimpl + def he_method1(self, arg): + l.append(4) + return 4 + + class PluginWrapper(object): + @hookimpl(hookwrapper=True) + def he_method1(self, arg): + assert not l + outcome = yield + res = outcome.get_result() + assert res + assert res == [1, 2, 3] == l + + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + pm.register(Plugin4()) + pm.register(PluginWrapper()) + + for result, i in zip(pm.ihook.he_method1(arg=None), reversed(range(1, 5))): + assert result == i + if result == 2: # stop before the final iteration + break + + assert l == [4, 3, 2] From 49786961c786b0ea978c8485dc5238d8b4661818 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Nov 2017 18:29:17 -0500 Subject: [PATCH 3/4] Handle gen exit properly --- pluggy/callers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pluggy/callers.py b/pluggy/callers.py index e6c79654..9950dc6e 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -238,6 +238,8 @@ def _itercall(hook_impls, caller_kwargs, specopts={}, hook=None): yield res if firstresult: # halt further impl calls break + except GeneratorExit: + pass # loop was terminated prematurely by caller except BaseException: excinfo = sys.exc_info() finally: From 4a8ea9fd8a1c615a190f10aa77c68a4d97ad5d7c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 12 Nov 2017 18:29:41 -0500 Subject: [PATCH 4/4] Fix test --- testing/test_pluginmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 550818c0..06fff9a7 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -436,7 +436,7 @@ def he_method1(self, arg): outcome = yield res = outcome.get_result() assert res - assert res == [1, 2, 3] == l + assert res == [4, 3, 2] == l pm.register(Plugin1()) pm.register(Plugin2())