incrementally update hook call lists instead of regenerating the whole
list on each registered plugin --HG-- branch : more_plugin
This commit is contained in:
		
							parent
							
								
									b03c1342ac
								
							
						
					
					
						commit
						02a4042dca
					
				|  | @ -180,9 +180,13 @@ class PluginManager(object): | ||||||
| 
 | 
 | ||||||
|     def make_hook_caller(self, name, plugins): |     def make_hook_caller(self, name, plugins): | ||||||
|         caller = getattr(self.hook, name) |         caller = getattr(self.hook, name) | ||||||
|         methods = self.listattr(name, plugins=plugins) |         hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, | ||||||
|         return HookCaller(caller.name, [plugins], firstresult=caller.firstresult, |                         argnames=caller.argnames) | ||||||
|                           argnames=caller.argnames, methods=methods) |         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): |     def register(self, plugin, name=None): | ||||||
|         """ Register a plugin with the given name and ensure that all its |         """ Register a plugin with the given name and ensure that all its | ||||||
|  | @ -216,7 +220,7 @@ class PluginManager(object): | ||||||
|         hookcallers = self._plugin2hookcallers.pop(plugin) |         hookcallers = self._plugin2hookcallers.pop(plugin) | ||||||
|         for hookcaller in hookcallers: |         for hookcaller in hookcallers: | ||||||
|             hookcaller.plugins.remove(plugin) |             hookcaller.plugins.remove(plugin) | ||||||
|             self._scan_methods(hookcaller) |             hookcaller._scan_methods() | ||||||
| 
 | 
 | ||||||
|     def addhooks(self, module_or_class): |     def addhooks(self, module_or_class): | ||||||
|         """ add new hook definitions from the given module_or_class using |         """ add new hook definitions from the given module_or_class using | ||||||
|  | @ -231,14 +235,14 @@ class PluginManager(object): | ||||||
|                 argnames = varnames(specfunc, startindex=isclass) |                 argnames = varnames(specfunc, startindex=isclass) | ||||||
|                 if hc is None: |                 if hc is None: | ||||||
|                     hc = HookCaller(name, [], firstresult=firstresult, |                     hc = HookCaller(name, [], firstresult=firstresult, | ||||||
|                                     argnames=argnames, methods=[]) |                                     argnames=argnames) | ||||||
|                     setattr(self.hook, name, hc) |                     setattr(self.hook, name, hc) | ||||||
|                 else: |                 else: | ||||||
|                     # plugins registered this hook without knowing the spec |                     # plugins registered this hook without knowing the spec | ||||||
|                     hc.setspec(firstresult=firstresult, argnames=argnames) |                     hc.setspec(firstresult=firstresult, argnames=argnames) | ||||||
|                     self._scan_methods(hc) |  | ||||||
|                     for plugin in hc.plugins: |                     for plugin in hc.plugins: | ||||||
|                         self._verify_hook(hc, specfunc, plugin) |                         self._verify_hook(hc, specfunc, plugin) | ||||||
|  |                         hc._add_method(getattr(plugin, name)) | ||||||
|                 names.append(name) |                 names.append(name) | ||||||
|         if not names: |         if not names: | ||||||
|             raise ValueError("did not find new %r hooks in %r" |             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 a plugin or None for the given name. """ | ||||||
|         return self._name2plugin.get(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): |     def call_plugin(self, plugin, methname, kwargs): | ||||||
|         return MultiCall(methods=self.listattr(methname, plugins=[plugin]), |         meth = getattr(plugin, methname, None) | ||||||
|                 kwargs=kwargs, firstresult=True).execute() |         if meth is not None: | ||||||
|  |             return MultiCall(methods=[meth], kwargs=kwargs, firstresult=True).execute() | ||||||
| 
 | 
 | ||||||
|     def _scan_plugin(self, plugin): |     def _scan_plugin(self, plugin): | ||||||
|         hookcallers = [] |         hookcallers = [] | ||||||
|  | @ -313,7 +292,7 @@ class PluginManager(object): | ||||||
|                 # we have a hook spec, can verify early |                 # we have a hook spec, can verify early | ||||||
|                 self._verify_hook(hook, method, plugin) |                 self._verify_hook(hook, method, plugin) | ||||||
|                 hook.plugins.append(plugin) |                 hook.plugins.append(plugin) | ||||||
|                 self._scan_methods(hook) |                 hook._add_method(method) | ||||||
|             hookcallers.append(hook) |             hookcallers.append(hook) | ||||||
|         return hookcallers |         return hookcallers | ||||||
| 
 | 
 | ||||||
|  | @ -348,7 +327,7 @@ class MultiCall: | ||||||
|     """ execute a call into multiple python functions/methods. """ |     """ execute a call into multiple python functions/methods. """ | ||||||
| 
 | 
 | ||||||
|     def __init__(self, methods, kwargs, firstresult=False): |     def __init__(self, methods, kwargs, firstresult=False): | ||||||
|         self.methods = list(methods) |         self.methods = methods | ||||||
|         self.kwargs = kwargs |         self.kwargs = kwargs | ||||||
|         self.kwargs["__multicall__"] = self |         self.kwargs["__multicall__"] = self | ||||||
|         self.results = [] |         self.results = [] | ||||||
|  | @ -421,14 +400,15 @@ class HookRelay: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class HookCaller: | 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.name = name | ||||||
|         self.plugins = plugins |         self.plugins = plugins | ||||||
|         self.methods = methods |  | ||||||
|         if argnames is not None: |         if argnames is not None: | ||||||
|             argnames = ["__multicall__"] + list(argnames) |             argnames = ["__multicall__"] + list(argnames) | ||||||
|         self.argnames = argnames |         self.argnames = argnames | ||||||
|         self.firstresult = firstresult |         self.firstresult = firstresult | ||||||
|  |         self.wrappers = [] | ||||||
|  |         self.nonwrappers = [] | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def pre(self): |     def pre(self): | ||||||
|  | @ -440,14 +420,41 @@ class HookCaller: | ||||||
|         self.argnames = ["__multicall__"] + list(argnames) |         self.argnames = ["__multicall__"] + list(argnames) | ||||||
|         self.firstresult = firstresult |         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): |     def __repr__(self): | ||||||
|         return "<HookCaller %r>" %(self.name,) |         return "<HookCaller %r>" %(self.name,) | ||||||
| 
 | 
 | ||||||
|     def __call__(self, **kwargs): |     def __call__(self, **kwargs): | ||||||
|         return self._docall(self.methods, kwargs) |         return self._docall(self.nonwrappers + self.wrappers, kwargs) | ||||||
| 
 | 
 | ||||||
|     def callextra(self, methods, **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): |     def _docall(self, methods, kwargs): | ||||||
|         assert not self.pre, self.name |         assert not self.pre, self.name | ||||||
|  |  | ||||||
|  | @ -355,7 +355,8 @@ def test_load_initial_conftest_last_ordering(testdir): | ||||||
|             pass |             pass | ||||||
|     m = My() |     m = My() | ||||||
|     pm.register(m) |     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[-1].__module__ == "_pytest.capture" | ||||||
|     assert l[-2] == m.pytest_load_initial_conftests |     assert l[-2] == m.pytest_load_initial_conftests | ||||||
|     assert l[-3].__module__ == "_pytest.config" |     assert l[-3].__module__ == "_pytest.config" | ||||||
|  |  | ||||||
|  | @ -64,20 +64,6 @@ class TestPluginManager: | ||||||
|         assert not pm.isregistered(my) |         assert not pm.isregistered(my) | ||||||
|         assert pm.getplugins()[-1:] == [my2] |         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): |     def test_register_unknown_hooks(self, pm): | ||||||
|         class Plugin1: |         class Plugin1: | ||||||
|             def he_method1(self, arg): |             def he_method1(self, arg): | ||||||
|  | @ -91,6 +77,121 @@ class TestPluginManager: | ||||||
|         #assert not pm._unverified_hooks |         #assert not pm._unverified_hooks | ||||||
|         assert pm.hook.he_method1(arg=1) == [2] |         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: | class TestPytestPluginInteractions: | ||||||
| 
 | 
 | ||||||
|     def test_addhooks_conftestplugin(self, testdir): |     def test_addhooks_conftestplugin(self, testdir): | ||||||
|  | @ -201,43 +302,6 @@ class TestPytestPluginInteractions: | ||||||
|         assert pytestpm.trace.root.indent == indent |         assert pytestpm.trace.root.indent == indent | ||||||
|         assert saveindent[0] > 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): | def test_namespace_has_default_and_env_plugins(testdir): | ||||||
|     p = testdir.makepyfile(""" |     p = testdir.makepyfile(""" | ||||||
|  | @ -386,35 +450,6 @@ class TestMultiCall: | ||||||
|         assert res == [] |         assert res == [] | ||||||
|         assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] |         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 test_hookwrapper_not_yield(self): | ||||||
|         def m1(): |         def m1(): | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue