incrementally update hook call lists instead of regenerating the whole

list on each registered plugin

--HG--
branch : more_plugin
This commit is contained in:
holger krekel 2015-04-25 11:29:11 +02:00
parent b03c1342ac
commit 02a4042dca
3 changed files with 164 additions and 121 deletions

View File

@ -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 "<HookCaller %r>" %(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

View File

@ -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"

View File

@ -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