allow to register plugins with hooks that are only added later

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

View File

@ -38,6 +38,7 @@ def main(args=None, plugins=None):
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
return 4
else:
config.pluginmanager.check_pending()
return config.hook.pytest_cmdline_main(config=config)
class cmdline: # compatibility namespace

View File

@ -181,7 +181,7 @@ 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, caller.firstresult,
return HookCaller(caller.name, [plugins], firstresult=caller.firstresult,
argnames=caller.argnames, methods=methods)
def register(self, plugin, name=None):
@ -201,13 +201,9 @@ class PluginManager(object):
return self._do_register(plugin, name)
def _do_register(self, plugin, name):
hookcallers = list(self._scan_plugin(plugin))
self._plugin2hookcallers[plugin] = hookcallers
self._plugin2hookcallers[plugin] = self._scan_plugin(plugin)
self._name2plugin[name] = plugin
self._plugins.append(plugin)
# rescan all methods for the hookcallers we found
for hookcaller in hookcallers:
self._scan_methods(hookcaller)
return True
def unregister(self, plugin):
@ -219,6 +215,7 @@ class PluginManager(object):
del self._name2plugin[name]
hookcallers = self._plugin2hookcallers.pop(plugin)
for hookcaller in hookcallers:
hookcaller.plugins.remove(plugin)
self._scan_methods(hookcaller)
def addhooks(self, module_or_class):
@ -228,11 +225,20 @@ class PluginManager(object):
names = []
for name in dir(module_or_class):
if name.startswith(self._prefix):
method = module_or_class.__dict__[name]
firstresult = getattr(method, 'firstresult', False)
hc = HookCaller(name, firstresult=firstresult,
argnames=varnames(method, startindex=isclass))
setattr(self.hook, name, hc)
specfunc = module_or_class.__dict__[name]
firstresult = getattr(specfunc, 'firstresult', False)
hc = getattr(self.hook, name, None)
argnames = varnames(specfunc, startindex=isclass)
if hc is None:
hc = HookCaller(name, [], firstresult=firstresult,
argnames=argnames, methods=[])
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)
names.append(name)
if not names:
raise ValueError("did not find new %r hooks in %r"
@ -282,18 +288,14 @@ class PluginManager(object):
return l
def _scan_methods(self, hookcaller):
hookcaller.methods = self.listattr(hookcaller.name)
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()
def _scan_plugin(self, plugin):
def fail(msg, *args):
name = getattr(plugin, '__name__', plugin)
raise PluginValidationError("plugin %r\n%s" %(name, msg % args))
hookcallers = []
for name in dir(plugin):
if name[0] == "_" or not name.startswith(self._prefix):
continue
@ -302,17 +304,40 @@ class PluginManager(object):
if hook is None:
if self._excludefunc is not None and self._excludefunc(name):
continue
if getattr(method, 'optionalhook', False):
continue
fail("found unknown hook: %r", name)
for arg in varnames(method):
if arg not in hook.argnames:
fail("argument %r not available\n"
"actual definition: %s\n"
"available hookargs: %s",
arg, formatdef(method),
", ".join(hook.argnames))
yield hook
hook = HookCaller(name, [plugin])
setattr(self.hook, name, hook)
elif hook.pre:
# there is only a pre non-specced stub
hook.plugins.append(plugin)
else:
# we have a hook spec, can verify early
self._verify_hook(hook, method, plugin)
hook.plugins.append(plugin)
self._scan_methods(hook)
hookcallers.append(hook)
return hookcallers
def _verify_hook(self, hook, method, plugin):
for arg in varnames(method):
if arg not in hook.argnames:
pluginname = self._get_canonical_name(plugin)
raise PluginValidationError(
"Plugin %r\nhook %r\nargument %r not available\n"
"plugin definition: %s\n"
"available hookargs: %s" %(
pluginname, hook.name, arg, formatdef(method),
", ".join(hook.argnames)))
def check_pending(self):
for name in self.hook.__dict__:
if name.startswith(self._prefix):
hook = getattr(self.hook, name)
if hook.pre:
for plugin in hook.plugins:
method = getattr(plugin, hook.name)
if not getattr(method, "optionalhook", False):
raise PluginValidationError(
"unknown hook %r in plugin %r" %(name, plugin))
def _get_canonical_name(self, plugin):
return getattr(plugin, "__name__", None) or str(id(plugin))
@ -396,13 +421,24 @@ class HookRelay:
class HookCaller:
def __init__(self, name, firstresult, argnames, methods=()):
def __init__(self, name, plugins, argnames=None, firstresult=None, methods=None):
self.name = name
self.firstresult = firstresult
self.argnames = ["__multicall__"]
self.argnames.extend(argnames)
assert "self" not in argnames # sanity check
self.plugins = plugins
self.methods = methods
if argnames is not None:
argnames = ["__multicall__"] + list(argnames)
self.argnames = argnames
self.firstresult = firstresult
@property
def pre(self):
return self.argnames is None
def setspec(self, argnames, firstresult):
assert self.pre
assert "self" not in argnames # sanity check
self.argnames = ["__multicall__"] + list(argnames)
self.firstresult = firstresult
def __repr__(self):
return "<HookCaller %r>" %(self.name,)
@ -414,6 +450,7 @@ class HookCaller:
return self._docall(self.methods + methods, kwargs)
def _docall(self, methods, kwargs):
assert not self.pre, self.name
return MultiCall(methods, kwargs,
firstresult=self.firstresult).execute()

View File

@ -32,12 +32,13 @@ class TestPluginManager:
pm.unregister(a1)
assert not pm.isregistered(a1)
def test_register_mismatch_method(self):
pm = get_plugin_manager()
def test_register_mismatch_method(self, pytestpm):
class hello:
def pytest_gurgel(self):
pass
pytest.raises(Exception, lambda: pm.register(hello()))
pytestpm.register(hello())
with pytest.raises(PluginValidationError):
pytestpm.check_pending()
def test_register_mismatch_arg(self):
pm = get_plugin_manager()
@ -77,6 +78,18 @@ class TestPluginManager:
l = list(plugins.listattr('x'))
assert l == [41, 42, 43]
def test_register_unknown_hooks(self, pm):
class Plugin1:
def he_method1(self, arg):
return arg + 1
pm.register(Plugin1())
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
#assert not pm._unverified_hooks
assert pm.hook.he_method1(arg=1) == [2]
class TestPytestPluginInteractions: