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):
|
||||
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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue