allow to register plugins with hooks that are only added later
--HG-- branch : more_plugin
This commit is contained in:
parent
d8e91d9fee
commit
b03c1342ac
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue