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) tw.line("ERROR: could not load %s\n" % (e.path), red=True)
return 4 return 4
else: else:
config.pluginmanager.check_pending()
return config.hook.pytest_cmdline_main(config=config) return config.hook.pytest_cmdline_main(config=config)
class cmdline: # compatibility namespace class cmdline: # compatibility namespace

View File

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

View File

@ -32,12 +32,13 @@ class TestPluginManager:
pm.unregister(a1) pm.unregister(a1)
assert not pm.isregistered(a1) assert not pm.isregistered(a1)
def test_register_mismatch_method(self): def test_register_mismatch_method(self, pytestpm):
pm = get_plugin_manager()
class hello: class hello:
def pytest_gurgel(self): def pytest_gurgel(self):
pass pass
pytest.raises(Exception, lambda: pm.register(hello())) pytestpm.register(hello())
with pytest.raises(PluginValidationError):
pytestpm.check_pending()
def test_register_mismatch_arg(self): def test_register_mismatch_arg(self):
pm = get_plugin_manager() pm = get_plugin_manager()
@ -77,6 +78,18 @@ class TestPluginManager:
l = list(plugins.listattr('x')) l = list(plugins.listattr('x'))
assert l == [41, 42, 43] 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: class TestPytestPluginInteractions: