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)
|
||||
return 4
|
||||
else:
|
||||
config.pluginmanager.check_pending()
|
||||
return config.hook.pytest_cmdline_main(config=config)
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
|
|
103
_pytest/core.py
103
_pytest/core.py
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in New Issue