From 02a4042dca240f5011f08daffe0d85223520622c Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 25 Apr 2015 11:29:11 +0200 Subject: [PATCH] incrementally update hook call lists instead of regenerating the whole list on each registered plugin --HG-- branch : more_plugin --- _pytest/core.py | 87 +++++++++--------- testing/test_config.py | 3 +- testing/test_core.py | 195 ++++++++++++++++++++++++----------------- 3 files changed, 164 insertions(+), 121 deletions(-) diff --git a/_pytest/core.py b/_pytest/core.py index 91353801b..3bdb41b14 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -180,9 +180,13 @@ class PluginManager(object): def make_hook_caller(self, name, plugins): caller = getattr(self.hook, name) - methods = self.listattr(name, plugins=plugins) - return HookCaller(caller.name, [plugins], firstresult=caller.firstresult, - argnames=caller.argnames, methods=methods) + hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, + argnames=caller.argnames) + for plugin in hc.plugins: + meth = getattr(plugin, name, None) + if meth is not None: + hc._add_method(meth) + return hc def register(self, plugin, name=None): """ Register a plugin with the given name and ensure that all its @@ -216,7 +220,7 @@ class PluginManager(object): hookcallers = self._plugin2hookcallers.pop(plugin) for hookcaller in hookcallers: hookcaller.plugins.remove(plugin) - self._scan_methods(hookcaller) + hookcaller._scan_methods() def addhooks(self, module_or_class): """ add new hook definitions from the given module_or_class using @@ -231,14 +235,14 @@ class PluginManager(object): argnames = varnames(specfunc, startindex=isclass) if hc is None: hc = HookCaller(name, [], firstresult=firstresult, - argnames=argnames, methods=[]) + argnames=argnames) setattr(self.hook, name, hc) else: # plugins registered this hook without knowing the spec hc.setspec(firstresult=firstresult, argnames=argnames) - self._scan_methods(hc) for plugin in hc.plugins: self._verify_hook(hc, specfunc, plugin) + hc._add_method(getattr(plugin, name)) names.append(name) if not names: raise ValueError("did not find new %r hooks in %r" @@ -264,35 +268,10 @@ class PluginManager(object): """ Return a plugin or None for the given name. """ return self._name2plugin.get(name) - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - last = [] - wrappers = [] - for plugin in plugins: - try: - meth = getattr(plugin, attrname) - except AttributeError: - continue - if hasattr(meth, 'hookwrapper'): - wrappers.append(meth) - elif hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) - l.extend(last) - l.extend(wrappers) - return l - - def _scan_methods(self, hookcaller): - hookcaller.methods = self.listattr(hookcaller.name, hookcaller.plugins) - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() + meth = getattr(plugin, methname, None) + if meth is not None: + return MultiCall(methods=[meth], kwargs=kwargs, firstresult=True).execute() def _scan_plugin(self, plugin): hookcallers = [] @@ -313,7 +292,7 @@ class PluginManager(object): # we have a hook spec, can verify early self._verify_hook(hook, method, plugin) hook.plugins.append(plugin) - self._scan_methods(hook) + hook._add_method(method) hookcallers.append(hook) return hookcallers @@ -348,7 +327,7 @@ class MultiCall: """ execute a call into multiple python functions/methods. """ def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) + self.methods = methods self.kwargs = kwargs self.kwargs["__multicall__"] = self self.results = [] @@ -421,14 +400,15 @@ class HookRelay: class HookCaller: - def __init__(self, name, plugins, argnames=None, firstresult=None, methods=None): + def __init__(self, name, plugins, argnames=None, firstresult=None): self.name = name self.plugins = plugins - self.methods = methods if argnames is not None: argnames = ["__multicall__"] + list(argnames) self.argnames = argnames self.firstresult = firstresult + self.wrappers = [] + self.nonwrappers = [] @property def pre(self): @@ -440,14 +420,41 @@ class HookCaller: self.argnames = ["__multicall__"] + list(argnames) self.firstresult = firstresult + def _scan_methods(self): + self.wrappers[:] = [] + self.nonwrappers[:] = [] + for plugin in self.plugins: + self._add_method(getattr(plugin, self.name)) + + def _add_method(self, meth): + assert not self.pre + if hasattr(meth, 'hookwrapper'): + self.wrappers.append(meth) + elif hasattr(meth, 'trylast'): + self.nonwrappers.insert(0, meth) + elif hasattr(meth, 'tryfirst'): + self.nonwrappers.append(meth) + else: + if not self.nonwrappers or not hasattr(self.nonwrappers[-1], "tryfirst"): + self.nonwrappers.append(meth) + else: + for i in reversed(range(len(self.nonwrappers)-1)): + if hasattr(self.nonwrappers[i], "tryfirst"): + continue + self.nonwrappers.insert(i+1, meth) + break + else: + self.nonwrappers.insert(0, meth) + def __repr__(self): return "" %(self.name,) def __call__(self, **kwargs): - return self._docall(self.methods, kwargs) + return self._docall(self.nonwrappers + self.wrappers, kwargs) def callextra(self, methods, **kwargs): - return self._docall(self.methods + methods, kwargs) + return self._docall(self.nonwrappers + methods + self.wrappers, + kwargs) def _docall(self, methods, kwargs): assert not self.pre, self.name diff --git a/testing/test_config.py b/testing/test_config.py index f1e3c5eb9..cf8a70b6f 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -355,7 +355,8 @@ def test_load_initial_conftest_last_ordering(testdir): pass m = My() pm.register(m) - l = pm.listattr("pytest_load_initial_conftests") + hc = pm.hook.pytest_load_initial_conftests + l = hc.nonwrappers + hc.wrappers assert l[-1].__module__ == "_pytest.capture" assert l[-2] == m.pytest_load_initial_conftests assert l[-3].__module__ == "_pytest.config" diff --git a/testing/test_core.py b/testing/test_core.py index dd5f0a833..c413385ed 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -64,20 +64,6 @@ class TestPluginManager: assert not pm.isregistered(my) assert pm.getplugins()[-1:] == [my2] - def test_listattr(self): - plugins = PluginManager("xyz") - class api1: - x = 41 - class api2: - x = 42 - class api3: - x = 43 - plugins.register(api1()) - plugins.register(api2()) - plugins.register(api3()) - l = list(plugins.listattr('x')) - assert l == [41, 42, 43] - def test_register_unknown_hooks(self, pm): class Plugin1: def he_method1(self, arg): @@ -91,6 +77,121 @@ class TestPluginManager: #assert not pm._unverified_hooks assert pm.hook.he_method1(arg=1) == [2] +class TestAddMethodOrdering: + @pytest.fixture + def hc(self, pm): + class Hooks: + def he_method1(self, arg): + pass + pm.addhooks(Hooks) + return pm.hook.he_method1 + + @pytest.fixture + def addmeth(self, hc): + def addmeth(tryfirst=False, trylast=False, hookwrapper=False): + def wrap(func): + if tryfirst: + func.tryfirst = True + if trylast: + func.trylast = True + if hookwrapper: + func.hookwrapper = True + hc._add_method(func) + return func + return wrap + return addmeth + + def test_adding_nonwrappers(self, hc, addmeth): + @addmeth() + def he_method1(): + pass + + @addmeth() + def he_method2(): + pass + + @addmeth() + def he_method3(): + pass + assert hc.nonwrappers == [he_method1, he_method2, he_method3] + + def test_adding_nonwrappers_trylast(self, hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + + @addmeth() + def he_method1_b(): + pass + assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + + def test_adding_nonwrappers_trylast2(self, hc, addmeth): + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + + @addmeth(trylast=True) + def he_method1(): + pass + assert hc.nonwrappers == [he_method1, he_method1_middle, he_method1_b] + + def test_adding_nonwrappers_tryfirst(self, hc, addmeth): + @addmeth(tryfirst=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth() + def he_method1_b(): + pass + assert hc.nonwrappers == [he_method1_middle, he_method1_b, he_method1] + + def test_adding_nonwrappers_trylast(self, hc, addmeth): + @addmeth() + def he_method1_a(): + pass + + @addmeth(trylast=True) + def he_method1_b(): + pass + + @addmeth() + def he_method1_c(): + pass + + @addmeth(trylast=True) + def he_method1_d(): + pass + assert hc.nonwrappers == [he_method1_d, he_method1_b, he_method1_a, he_method1_c] + + def test_adding_wrappers_ordering(self, hc, addmeth): + @addmeth(hookwrapper=True) + def he_method1(): + pass + + @addmeth() + def he_method1_middle(): + pass + + @addmeth(hookwrapper=True) + def he_method3(): + pass + + assert hc.nonwrappers == [he_method1_middle] + assert hc.wrappers == [he_method1, he_method3] + + class TestPytestPluginInteractions: def test_addhooks_conftestplugin(self, testdir): @@ -201,43 +302,6 @@ class TestPytestPluginInteractions: assert pytestpm.trace.root.indent == indent assert saveindent[0] > indent - # lower level API - - def test_listattr(self): - pluginmanager = PluginManager("xyz") - class My2: - x = 42 - pluginmanager.register(My2()) - assert not pluginmanager.listattr("hello") - assert pluginmanager.listattr("x") == [42] - - def test_listattr_tryfirst(self): - class P1: - @pytest.mark.tryfirst - def m(self): - return 17 - - class P2: - def m(self): - return 23 - class P3: - def m(self): - return 19 - - pluginmanager = PluginManager("xyz") - p1 = P1() - p2 = P2() - p3 = P3() - pluginmanager.register(p1) - pluginmanager.register(p2) - pluginmanager.register(p3) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p3.m, p1.m] - del P1.m.__dict__['tryfirst'] - pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m)) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p1.m, p3.m] - def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile(""" @@ -386,35 +450,6 @@ class TestMultiCall: assert res == [] assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] - def test_listattr_hookwrapper_ordering(self): - class P1: - @pytest.mark.hookwrapper - def m(self): - return 17 - - class P2: - def m(self): - return 23 - - class P3: - @pytest.mark.tryfirst - def m(self): - return 19 - - pluginmanager = PluginManager("xyz") - p1 = P1() - p2 = P2() - p3 = P3() - pluginmanager.register(p1) - pluginmanager.register(p2) - pluginmanager.register(p3) - methods = pluginmanager.listattr('m') - assert methods == [p2.m, p3.m, p1.m] - ## listattr keeps a cache and deleting - ## a function attribute requires clearing it - #pluginmanager._listattrcache.clear() - #del P1.m.__dict__['tryfirst'] - def test_hookwrapper_not_yield(self): def m1(): pass