From dee4fd903b9fb8d43c08b4b7e1271f5f65c42b2d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 20 Sep 2017 01:01:52 -0400 Subject: [PATCH 1/5] Move legacy multicall components to callers module --- pluggy/__init__.py | 69 +--------------------------------------------- pluggy/callers.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/pluggy/__init__.py b/pluggy/__init__.py index eb4cdd30..9b81be1f 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -1,6 +1,6 @@ import inspect import warnings -from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result +from .callers import _MultiCall, HookCallError, _Result, _LegacyMultiCall __version__ = '0.5.3.dev' @@ -166,25 +166,6 @@ def get(self, name): return self.__class__(self.root, self.tags + (name,)) -def _wrapped_call(wrap_controller, func): - """ Wrap calling to a function with a generator which needs to yield - exactly once. The yield point will trigger calling the wrapped function - and return its ``_Result`` to the yield point. The generator then needs - to finish (raise StopIteration) in order for the wrapped call to complete. - """ - try: - next(wrap_controller) # first yield - except StopIteration: - _raise_wrapfail(wrap_controller, "did not yield") - call_outcome = _Result.from_call(func) - try: - wrap_controller.send(call_outcome) - _raise_wrapfail(wrap_controller, "has second yield") - except StopIteration: - pass - return call_outcome.get_result() - - class _TracedHookExecution(object): def __init__(self, pluginmanager, before, after): self.pluginmanager = pluginmanager @@ -485,54 +466,6 @@ def subset_hook_caller(self, name, remove_plugins): return orig -class _LegacyMultiCall(object): - """ execute a call into multiple python functions/methods. """ - - # XXX note that the __multicall__ argument is supported only - # for pytest compatibility reasons. It was never officially - # supported there and is explicitely deprecated since 2.8 - # so we can remove it soon, allowing to avoid the below recursion - # in execute() and simplify/speed up the execute loop. - - def __init__(self, hook_impls, kwargs, specopts={}, hook=None): - self.hook = hook - self.hook_impls = hook_impls - self.caller_kwargs = kwargs # come from _HookCaller.__call__() - self.caller_kwargs["__multicall__"] = self - self.specopts = hook.spec_opts if hook else specopts - - def execute(self): - caller_kwargs = self.caller_kwargs - self.results = results = [] - firstresult = self.specopts.get("firstresult") - - while self.hook_impls: - hook_impl = self.hook_impls.pop() - 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: - return _wrapped_call(hook_impl.function(*args), self.execute) - res = hook_impl.function(*args) - if res is not None: - if firstresult: - return res - results.append(res) - - if not firstresult: - return results - - def __repr__(self): - status = "%d meths" % (len(self.hook_impls),) - if hasattr(self, "results"): - status = ("%d results, " % len(self.results)) + status - return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) - - def varnames(func): """Return tuple of positional and keywrord argument names for a function, method, class or callable. diff --git a/pluggy/callers.py b/pluggy/callers.py index 58b47eb5..5ddbbf18 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -77,6 +77,73 @@ def get_result(self): _reraise(*ex) # noqa +def _wrapped_call(wrap_controller, func): + """ Wrap calling to a function with a generator which needs to yield + exactly once. The yield point will trigger calling the wrapped function + and return its ``_Result`` to the yield point. The generator then needs + to finish (raise StopIteration) in order for the wrapped call to complete. + """ + try: + next(wrap_controller) # first yield + except StopIteration: + _raise_wrapfail(wrap_controller, "did not yield") + call_outcome = _Result.from_call(func) + try: + wrap_controller.send(call_outcome) + _raise_wrapfail(wrap_controller, "has second yield") + except StopIteration: + pass + return call_outcome.get_result() + + +class _LegacyMultiCall(object): + """ execute a call into multiple python functions/methods. """ + + # XXX note that the __multicall__ argument is supported only + # for pytest compatibility reasons. It was never officially + # supported there and is explicitely deprecated since 2.8 + # so we can remove it soon, allowing to avoid the below recursion + # in execute() and simplify/speed up the execute loop. + + def __init__(self, hook_impls, kwargs, specopts={}, hook=None): + self.hook = hook + self.hook_impls = hook_impls + self.caller_kwargs = kwargs # come from _HookCaller.__call__() + self.caller_kwargs["__multicall__"] = self + self.specopts = hook.spec_opts if hook else specopts + + def execute(self): + caller_kwargs = self.caller_kwargs + self.results = results = [] + firstresult = self.specopts.get("firstresult") + + while self.hook_impls: + hook_impl = self.hook_impls.pop() + 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: + return _wrapped_call(hook_impl.function(*args), self.execute) + res = hook_impl.function(*args) + if res is not None: + if firstresult: + return res + results.append(res) + + if not firstresult: + return results + + def __repr__(self): + status = "%d meths" % (len(self.hook_impls),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) + + class _MultiCall(object): """Execute a call into multiple python functions/methods. """ From fa9c5aec4c8ae31356a2f3f8cdcf9982834b0ab4 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 21 Sep 2017 17:56:04 -0400 Subject: [PATCH 2/5] Convert `_MultiCall` into a function Avoids a class with a single method `execute()` at the expense of dropping the `__repr__()` support. This should make cython-izing that much simpler. Resolves #84 --- pluggy/__init__.py | 8 ++-- pluggy/callers.py | 114 +++++++++++++++++++++------------------------ 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/pluggy/__init__.py b/pluggy/__init__.py index 9b81be1f..46011b8e 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -1,6 +1,6 @@ import inspect import warnings -from .callers import _MultiCall, HookCallError, _Result, _LegacyMultiCall +from .callers import _multicall, HookCallError, _Result, _legacymulticall __version__ = '0.5.3.dev' @@ -213,7 +213,7 @@ def __init__(self, project_name, implprefix=None): self._inner_hookexec = lambda hook, methods, kwargs: \ hook.multicall( methods, kwargs, specopts=hook.spec_opts, hook=hook - ).execute() + ) def _hookexec(self, hook, methods, kwargs): # called from all hookcaller instances. @@ -535,7 +535,7 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None) self._hookexec = hook_execute self.argnames = None self.kwargnames = None - self.multicall = _MultiCall + self.multicall = _multicall if specmodule_or_class is not None: assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) @@ -592,7 +592,7 @@ def _add_hookimpl(self, hookimpl): "removed in an upcoming release.", DeprecationWarning ) - self.multicall = _LegacyMultiCall + self.multicall = _legacymulticall def __repr__(self): return "<_HookCaller %r>" % (self.name,) diff --git a/pluggy/callers.py b/pluggy/callers.py index 5ddbbf18..3189f8aa 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -144,67 +144,61 @@ def __repr__(self): return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) -class _MultiCall(object): - """Execute a call into multiple python functions/methods. - """ - def __init__(self, hook_impls, kwargs, specopts={}, hook=None): - self.hook = hook - self.hook_impls = hook_impls - self.caller_kwargs = kwargs # come from _HookCaller.__call__() - self.specopts = hook.spec_opts if hook else specopts +def _legacymulticall(hook_impls, caller_kwargs, specopts={}, hook=None): + return _LegacyMultiCall( + hook_impls, caller_kwargs, specopts=specopts, hook=hook).execute() - def execute(self): - __tracebackhide__ = True - caller_kwargs = self.caller_kwargs - self.results = results = [] - firstresult = self.specopts.get("firstresult") - excinfo = None - try: # run impl and wrapper setup functions in a loop - teardowns = [] - try: - for hook_impl in reversed(self.hook_impls): - try: - args = [caller_kwargs[argname] for argname in hook_impl.argnames] - # args = operator.itemgetter(hookimpl.argnames)(caller_kwargs) - 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) - 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): + +def _multicall(hook_impls, caller_kwargs, specopts={}, hook=None): + """Execute a call into multiple python functions/methods and return the + result(s). + + ``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: - gen.send(outcome) - _raise_wrapfail(gen, "has second yield") - except StopIteration: - pass + 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) + 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) - return outcome.get_result() + # run all wrapper post-yield blocks + for gen in reversed(teardowns): + try: + gen.send(outcome) + _raise_wrapfail(gen, "has second yield") + except StopIteration: + pass - def __repr__(self): - status = "%d meths" % (len(self.hook_impls),) - if hasattr(self, "results"): - status = ("%d results, " % len(self.results)) + status - return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) + return outcome.get_result() From 1ec0408cb9f6caca3ea4fbae896c7f6ed3dd6774 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 21 Sep 2017 18:01:31 -0400 Subject: [PATCH 3/5] Update unit tests --- testing/test_multicall.py | 48 ++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/testing/test_multicall.py b/testing/test_multicall.py index 898fc21d..bbc7921f 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -1,6 +1,7 @@ import pytest -from pluggy import _MultiCall, HookImpl, HookCallError, _LegacyMultiCall +from pluggy import _multicall, _legacymulticall, HookImpl, HookCallError +from pluggy.callers import _LegacyMultiCall from pluggy import HookspecMarker, HookimplMarker @@ -10,7 +11,7 @@ def test_uses_copy_of_methods(): l = [lambda: 42] - mc = _MultiCall(l, {}) + mc = _LegacyMultiCall(l, {}) repr(mc) l[:] = [] res = mc.execute() @@ -18,14 +19,14 @@ def test_uses_copy_of_methods(): def MC(methods, kwargs, firstresult=False): - caller = _MultiCall + caller = _multicall hookfuncs = [] for method in methods: f = HookImpl(None, "", method, method.example_impl) hookfuncs.append(f) if '__multicall__' in f.argnames: - caller = _LegacyMultiCall - return caller(hookfuncs, kwargs, {"firstresult": firstresult}) + caller = _legacymulticall + return caller(hookfuncs, kwargs, specopts={"firstresult": firstresult}) def test_call_passing(): @@ -45,9 +46,7 @@ def m(self, __multicall__, x): p1 = P1() p2 = P2() - multicall = MC([p1.m, p2.m], {"x": 23}) - assert "23" in repr(multicall) - reslist = multicall.execute() + reslist = MC([p1.m, p2.m], {"x": 23}) assert len(reslist) == 2 # ensure reversed order assert reslist == [23, 17] @@ -63,19 +62,15 @@ class A(object): def f(self, x, y): return x + y - multicall = MC([f, A().f], dict(x=23, y=24)) - assert "'x': 23" in repr(multicall) - assert "'y': 24" in repr(multicall) - reslist = multicall.execute() + reslist = MC([f, A().f], dict(x=23, y=24)) assert reslist == [24 + 23, 24] - assert "2 results" in repr(multicall) def test_keyword_args_with_defaultargs(): @hookimpl def f(x, z=1): return x + z - reslist = MC([f], dict(x=23, y=24)).execute() + reslist = MC([f], dict(x=23, y=24)) assert reslist == [24] @@ -83,8 +78,8 @@ def test_tags_call_error(): @hookimpl def f(x): return x - multicall = MC([f], {}) - pytest.raises(HookCallError, multicall.execute) + with pytest.raises(HookCallError): + MC([f], {}) def test_call_subexecute(): @@ -97,8 +92,7 @@ def m(__multicall__): def n(): return 1 - call = MC([n, m], {}, firstresult=True) - res = call.execute() + res = MC([n, m], {}, firstresult=True) assert res == 2 @@ -111,9 +105,9 @@ def m1(): def m2(): return None - res = MC([m1, m2], {}, {"firstresult": True}).execute() + res = MC([m1, m2], {}, {"firstresult": True}) assert res == 1 - res = MC([m1, m2], {}, {}).execute() + res = MC([m1, m2], {}, {}) assert res == [1] @@ -131,11 +125,11 @@ def m2(): l.append("m2") return 2 - res = MC([m2, m1], {}).execute() + res = MC([m2, m1], {}) assert res == [2] assert l == ["m1 init", "m2", "m1 finish"] l[:] = [] - res = MC([m2, m1], {}, {"firstresult": True}).execute() + res = MC([m2, m1], {}, {"firstresult": True}) assert res == 2 assert l == ["m1 init", "m2", "m1 finish"] @@ -155,7 +149,7 @@ def m2(): yield 2 l.append("m2 finish") - res = MC([m2, m1], {}).execute() + res = MC([m2, m1], {}) assert res == [] assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] @@ -165,9 +159,8 @@ def test_hookwrapper_not_yield(): def m1(): pass - mc = MC([m1], {}) with pytest.raises(TypeError): - mc.execute() + MC([m1], {}) def test_hookwrapper_too_many_yield(): @@ -176,9 +169,8 @@ def m1(): yield 1 yield 2 - mc = MC([m1], {}) with pytest.raises(RuntimeError) as ex: - mc.execute() + MC([m1], {}) assert "m1" in str(ex.value) assert (__file__ + ':') in str(ex.value) @@ -198,5 +190,5 @@ def m2(): raise exc with pytest.raises(exc): - MC([m2, m1], {}).execute() + MC([m2, m1], {}) assert l == ["m1 init", "m1 finish"] From 2f5109ac339353e7018711374843a4b5909d594a Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 22 Sep 2017 11:40:19 -0400 Subject: [PATCH 4/5] Update benchmark tests --- testing/benchmark.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/benchmark.py b/testing/benchmark.py index d2609b86..41f83543 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -2,7 +2,7 @@ Benchmarking and performance tests. """ import pytest -from pluggy import (_MultiCall, _LegacyMultiCall, HookImpl, HookspecMarker, +from pluggy import (_multicall, _legacymulticall, HookImpl, HookspecMarker, HookimplMarker) hookspec = HookspecMarker("example") @@ -44,7 +44,7 @@ def wrappers(request): @pytest.fixture( - params=[_MultiCall, _LegacyMultiCall], + params=[_multicall, _legacymulticall], ids=lambda item: item.__name__ ) def callertype(request): @@ -52,7 +52,7 @@ def callertype(request): def inner_exec(methods, callertype): - return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype).execute() + return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype) def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype): From 7791b9c3ae22f76fcdd869a0db2426df14475a88 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 23 Sep 2017 15:17:57 -0400 Subject: [PATCH 5/5] Use a smaller range of hooks when benchmarking --- testing/benchmark.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/benchmark.py b/testing/benchmark.py index 41f83543..5a913e9d 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -28,7 +28,7 @@ def wrapper(arg1, arg2, arg3): @pytest.fixture( - params=[0, 1, 10, 100], + params=[10, 100], ids="hooks={}".format, ) def hooks(request): @@ -36,7 +36,7 @@ def hooks(request): @pytest.fixture( - params=[0, 1, 10, 100], + params=[10, 100], ids="wrappers={}".format, ) def wrappers(request):