From fb00c4e7619d5b75415a917871a607cdba4ffc16 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 31 Jan 2017 21:16:38 -0500 Subject: [PATCH 1/5] Convert pluggy.py into a package module --- pluggy.py => pluggy/__init__.py | 0 setup.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename pluggy.py => pluggy/__init__.py (100%) diff --git a/pluggy.py b/pluggy/__init__.py similarity index 100% rename from pluggy.py rename to pluggy/__init__.py diff --git a/setup.py b/setup.py index 8d74319d..836fe5e6 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ def get_version(): p = os.path.join(os.path.dirname( - os.path.abspath(__file__)), "pluggy.py") + os.path.abspath(__file__)), "pluggy/__init__.py") with open(p) as f: for line in f.readlines(): if "__version__" in line: @@ -40,7 +40,7 @@ def main(): author_email='holger at merlinux.eu', url='https://github.com/pytest-dev/pluggy', classifiers=classifiers, - py_modules=['pluggy'], + packages=['pluggy'], ) From 845c502532d4b9e2dca42bfc451df5f0e0d6383f Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 9 Jul 2017 18:43:33 -0400 Subject: [PATCH 2/5] Deprecate __multicall__ support Add a new `pluggy.callers._MultiCall` implementation which removes support the implicit `__multicall__` special argument and drops the recursion required to handle hook wrappers. Rename the original implementation `_LegacyMultiCall` and load it only when the `__multicall__` argument is detected in a hookimpl function signature. Add a deprecation warning whenever the legacy fallback occurs. Resolves #23 --- pluggy/__init__.py | 24 +++++----- pluggy/callers.py | 108 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 pluggy/callers.py diff --git a/pluggy/__init__.py b/pluggy/__init__.py index fa30fcdb..c60c384c 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -1,6 +1,7 @@ import sys import inspect import warnings +from .callers import _MultiCall, HookCallError, _raise_wrapfail __version__ = '0.5.0' @@ -14,10 +15,6 @@ class PluginValidationError(Exception): """ plugin failed validation. """ -class HookCallError(Exception): - """ Hook was called wrongly. """ - - class HookspecMarker(object): """ Decorator helper class for marking functions as hook specifications. @@ -172,12 +169,6 @@ def get(self, name): return self.__class__(self.root, self.tags + (name,)) -def _raise_wrapfail(wrap_controller, msg): - co = wrap_controller.gi_code - raise RuntimeError("wrap_controller at %r %s:%d %s" % - (co.co_name, co.co_filename, co.co_firstlineno, msg)) - - 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 @@ -275,7 +266,7 @@ def __init__(self, project_name, implprefix=None): self.hook = _HookRelay(self.trace.root.get("hook")) self._implprefix = implprefix self._inner_hookexec = lambda hook, methods, kwargs: \ - _MultiCall( + hook.multicall( methods, kwargs, specopts=hook.spec_opts, hook=hook ).execute() @@ -530,7 +521,7 @@ def subset_hook_caller(self, name, remove_plugins): return orig -class _MultiCall(object): +class _LegacyMultiCall(object): """ execute a call into multiple python functions/methods. """ # XXX note that the __multicall__ argument is supported only @@ -647,6 +638,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 if specmodule_or_class is not None: assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) @@ -697,6 +689,14 @@ def _add_hookimpl(self, hookimpl): i -= 1 methods.insert(i + 1, hookimpl) + if '__multicall__' in hookimpl.argnames: + warnings.warn( + "Support for __multicall__ is now deprecated and will be" + "removed in an upcoming release.", + warnings.DeprecationWarning + ) + self.multicall = _LegacyMultiCall + def __repr__(self): return "<_HookCaller %r>" % (self.name,) diff --git a/pluggy/callers.py b/pluggy/callers.py new file mode 100644 index 00000000..c7eb0111 --- /dev/null +++ b/pluggy/callers.py @@ -0,0 +1,108 @@ +''' +Call loop machinery +''' +import sys + + +_py3 = sys.version_info > (3, 0) + + +if not _py3: + exec(""" +def _reraise(cls, val, tb): + raise cls, val, tb +""") + + +def _raise_wrapfail(wrap_controller, msg): + co = wrap_controller.gi_code + raise RuntimeError("wrap_controller at %r %s:%d %s" % + (co.co_name, co.co_filename, co.co_firstlineno, msg)) + + +class HookCallError(Exception): + """ Hook was called wrongly. """ + + +class Result(object): + def __init__(self, result, excinfo): + self.result = result + self.excinfo = excinfo + + def force_result(self, result): + self.result = result + self.excinfo = None + + def get_result(self): + if self.excinfo is None: + return self.result + else: + ex = self.excinfo + if _py3: + raise ex[1].with_traceback(ex[2]) + _reraise(*ex) # noqa + + +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 execute(self): + 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: + 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 + + if firstresult: + return outcome.get_result()[0] + + return outcome.get_result() + + 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) From 09d75893ccab20ffea9528682575e7f2f1fcccd3 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 10 Jul 2017 00:55:47 -0400 Subject: [PATCH 3/5] Adjust tests to use legacy multicall when necessary --- testing/test_multicall.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/test_multicall.py b/testing/test_multicall.py index 7aff8c33..898fc21d 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -1,6 +1,6 @@ import pytest -from pluggy import _MultiCall, HookImpl, HookCallError +from pluggy import _MultiCall, HookImpl, HookCallError, _LegacyMultiCall from pluggy import HookspecMarker, HookimplMarker @@ -18,11 +18,14 @@ def test_uses_copy_of_methods(): def MC(methods, kwargs, firstresult=False): + caller = _MultiCall hookfuncs = [] for method in methods: f = HookImpl(None, "", method, method.example_impl) hookfuncs.append(f) - return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult}) + if '__multicall__' in f.argnames: + caller = _LegacyMultiCall + return caller(hookfuncs, kwargs, {"firstresult": firstresult}) def test_call_passing(): From 21fd6c4d681191b5399ed4bc57f69ba0455d4e67 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 10 Jul 2017 01:10:33 -0400 Subject: [PATCH 4/5] Parametrize over legacy versus new multicall types --- testing/benchmark.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/testing/benchmark.py b/testing/benchmark.py index e5888a5f..d2609b86 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -2,18 +2,19 @@ Benchmarking and performance tests. """ import pytest -from pluggy import _MultiCall, HookImpl, HookspecMarker, HookimplMarker +from pluggy import (_MultiCall, _LegacyMultiCall, HookImpl, HookspecMarker, + HookimplMarker) hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") -def MC(methods, kwargs, firstresult=False): +def MC(methods, kwargs, callertype, firstresult=False): hookfuncs = [] for method in methods: f = HookImpl(None, "", method, method.example_impl) hookfuncs.append(f) - return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult}) + return callertype(hookfuncs, kwargs, {"firstresult": firstresult}) @hookimpl @@ -42,9 +43,17 @@ def wrappers(request): return [wrapper for i in range(request.param)] -def inner_exec(methods): - return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}).execute() +@pytest.fixture( + params=[_MultiCall, _LegacyMultiCall], + ids=lambda item: item.__name__ +) +def callertype(request): + return request.param + + +def inner_exec(methods, callertype): + return MC(methods, {'arg1': 1, 'arg2': 2, 'arg3': 3}, callertype).execute() -def test_hook_and_wrappers_speed(benchmark, hooks, wrappers): - benchmark(inner_exec, hooks + wrappers) +def test_hook_and_wrappers_speed(benchmark, hooks, wrappers, callertype): + benchmark(inner_exec, hooks + wrappers, callertype) From 88f1b73fda287571df3d92e46ae0098793c5a2e9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 11 Jul 2017 13:46:06 -0400 Subject: [PATCH 5/5] Remove _CallOutcome; embrace _Result `_CallOutcome` limits usage due the constructor calling a function input. Instead add a `classmethod` constructor `_Result.from_call()` which can be used to get the same behaviour and avoids duplicate types. --- pluggy/__init__.py | 48 ++++++---------------------------------------- pluggy/callers.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/pluggy/__init__.py b/pluggy/__init__.py index c60c384c..9eff1ac9 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -1,15 +1,12 @@ -import sys import inspect import warnings -from .callers import _MultiCall, HookCallError, _raise_wrapfail +from .callers import _MultiCall, HookCallError, _raise_wrapfail, _Result __version__ = '0.5.0' __all__ = ["PluginManager", "PluginValidationError", "HookCallError", "HookspecMarker", "HookimplMarker"] -_py3 = sys.version_info > (3, 0) - class PluginValidationError(Exception): """ plugin failed validation. """ @@ -83,7 +80,7 @@ def __call__(self, function=None, hookwrapper=False, optionalhook=False, If hookwrapper is True the hook implementations needs to execute exactly one "yield". The code before the yield is run early before any non-hookwrapper function is run. The code after the yield is run after all non-hookwrapper - function have run. The yield receives an ``_CallOutcome`` object representing + function have run. The yield receives a ``_Result`` object representing the exception or result outcome of the inner calls (including other hookwrapper calls). @@ -172,14 +169,14 @@ def get(self, 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 _CallOutcome to the yield point. The generator then needs + 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 = _CallOutcome(func) + call_outcome = _Result.from_call(func) try: wrap_controller.send(call_outcome) _raise_wrapfail(wrap_controller, "has second yield") @@ -188,39 +185,6 @@ def _wrapped_call(wrap_controller, func): return call_outcome.get_result() -class _CallOutcome(object): - """ Outcome of a function call, either an exception or a proper result. - Calling the ``get_result`` method will return the result or reraise - the exception raised when the function was called. """ - excinfo = None - - def __init__(self, func): - try: - self.result = func() - except BaseException: - self.excinfo = sys.exc_info() - - def force_result(self, result): - self.result = result - self.excinfo = None - - def get_result(self): - if self.excinfo is None: - return self.result - else: - ex = self.excinfo - if _py3: - raise ex[1].with_traceback(ex[2]) - _reraise(*ex) # noqa - - -if not _py3: - exec(""" -def _reraise(cls, val, tb): - raise cls, val, tb -""") - - class _TracedHookExecution(object): def __init__(self, pluginmanager, before, after): self.pluginmanager = pluginmanager @@ -232,7 +196,7 @@ def __init__(self, pluginmanager, before, after): def __call__(self, hook, hook_impls, kwargs): self.before(hook.name, hook_impls, kwargs) - outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs)) + outcome = _Result.from_call(lambda: self.oldcall(hook, hook_impls, kwargs)) self.after(outcome, hook.name, hook_impls, kwargs) return outcome.get_result() @@ -481,7 +445,7 @@ def add_hookcall_monitoring(self, before, after): of HookImpl instances and the keyword arguments for the hook call. ``after(outcome, hook_name, hook_impls, kwargs)`` receives the - same arguments as ``before`` but also a :py:class:`_CallOutcome`` object + same arguments as ``before`` but also a :py:class:`_Result`` object which represents the result of the overall hook call. """ return _TracedHookExecution(self, before, after).undo diff --git a/pluggy/callers.py b/pluggy/callers.py index c7eb0111..abe36e78 100644 --- a/pluggy/callers.py +++ b/pluggy/callers.py @@ -24,11 +24,21 @@ class HookCallError(Exception): """ Hook was called wrongly. """ -class Result(object): +class _Result(object): def __init__(self, result, excinfo): self.result = result self.excinfo = excinfo + @classmethod + def from_call(cls, func): + result = excinfo = None + try: + result = func() + except BaseException: + excinfo = sys.exc_info() + + return cls(result, excinfo) + def force_result(self, result): self.result = result self.excinfo = None @@ -86,7 +96,7 @@ def execute(self): except BaseException: excinfo = sys.exc_info() finally: - outcome = Result(results, excinfo) + outcome = _Result(results, excinfo) # run all wrapper post-yield blocks for gen in reversed(teardowns):