call scanning of plugins directly, code is shifted from helpconfig.py to core.py
This commit is contained in:
parent
ea5fb0c153
commit
28c785a0d1
112
_pytest/core.py
112
_pytest/core.py
|
@ -95,10 +95,11 @@ class PluginManager(object):
|
|||
raise ValueError("Plugin already registered: %s=%s\n%s" %(
|
||||
name, plugin, self._name2plugin))
|
||||
#self.trace("registering", name, plugin)
|
||||
self._name2plugin[name] = plugin
|
||||
reg = getattr(self, "_registercallback", None)
|
||||
if reg is not None:
|
||||
reg(plugin, name)
|
||||
reg(plugin, name) # may call addhooks
|
||||
self.hook._scan_plugin(plugin)
|
||||
self._name2plugin[name] = plugin
|
||||
if conftest:
|
||||
self._conftestplugins.append(plugin)
|
||||
else:
|
||||
|
@ -403,41 +404,86 @@ class HookRelay:
|
|||
def _getcaller(self, name, plugins):
|
||||
caller = getattr(self, name)
|
||||
methods = self._pm.listattr(name, plugins=plugins)
|
||||
return CachedHookCaller(caller, methods)
|
||||
|
||||
|
||||
class CachedHookCaller:
|
||||
def __init__(self, hookmethod, methods):
|
||||
self.hookmethod = hookmethod
|
||||
self.methods = methods
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
return self.hookmethod._docall(self.methods, kwargs)
|
||||
|
||||
def callextra(self, methods, **kwargs):
|
||||
# XXX in theory we should respect "tryfirst/trylast" if set
|
||||
# on the added methods but we currently only use it for
|
||||
# pytest_generate_tests and it doesn't make sense there i'd think
|
||||
all = self.methods
|
||||
if methods:
|
||||
all = all + methods
|
||||
return self.hookmethod._docall(all, kwargs)
|
||||
return caller.new_cached_caller(methods)
|
||||
return caller
|
||||
|
||||
def _scan_plugin(self, plugin):
|
||||
methods = collectattr(plugin)
|
||||
hooks = {}
|
||||
for hookspec in self._hookspecs:
|
||||
hooks.update(collectattr(hookspec))
|
||||
|
||||
stringio = py.io.TextIO()
|
||||
def Print(*args):
|
||||
if args:
|
||||
stringio.write(" ".join(map(str, args)))
|
||||
stringio.write("\n")
|
||||
|
||||
fail = False
|
||||
while methods:
|
||||
name, method = methods.popitem()
|
||||
#print "checking", name
|
||||
if isgenerichook(name):
|
||||
continue
|
||||
if name not in hooks:
|
||||
if not getattr(method, 'optionalhook', False):
|
||||
Print("found unknown hook:", name)
|
||||
fail = True
|
||||
else:
|
||||
#print "checking", method
|
||||
method_args = list(varnames(method))
|
||||
if '__multicall__' in method_args:
|
||||
method_args.remove('__multicall__')
|
||||
hook = hooks[name]
|
||||
hookargs = varnames(hook)
|
||||
for arg in method_args:
|
||||
if arg not in hookargs:
|
||||
Print("argument %r not available" %(arg, ))
|
||||
Print("actual definition: %s" %(formatdef(method)))
|
||||
Print("available hook arguments: %s" %
|
||||
", ".join(hookargs))
|
||||
fail = True
|
||||
break
|
||||
#if not fail:
|
||||
# print "matching hook:", formatdef(method)
|
||||
getattr(self, name).clear_method_cache()
|
||||
|
||||
if fail:
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue()))
|
||||
|
||||
|
||||
class HookCaller:
|
||||
def __init__(self, hookrelay, name, firstresult):
|
||||
def __init__(self, hookrelay, name, firstresult, methods=None):
|
||||
self.hookrelay = hookrelay
|
||||
self.name = name
|
||||
self.firstresult = firstresult
|
||||
self.trace = self.hookrelay.trace
|
||||
self.methods = methods
|
||||
|
||||
def new_cached_caller(self, methods):
|
||||
return HookCaller(self.hookrelay, self.name, self.firstresult,
|
||||
methods=methods)
|
||||
|
||||
def __repr__(self):
|
||||
return "<HookCaller %r>" %(self.name,)
|
||||
|
||||
def clear_method_cache(self):
|
||||
self.methods = None
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
methods = self.hookrelay._pm.listattr(self.name)
|
||||
methods = self.methods
|
||||
if self.methods is None:
|
||||
self.methods = methods = self.hookrelay._pm.listattr(self.name)
|
||||
methods = self.methods
|
||||
return self._docall(methods, kwargs)
|
||||
|
||||
def callextra(self, methods, **kwargs):
|
||||
if self.methods is None:
|
||||
self.reload_methods()
|
||||
return self._docall(self.methods + methods, kwargs)
|
||||
|
||||
def _docall(self, methods, kwargs):
|
||||
self.trace(self.name, kwargs)
|
||||
self.trace.root.indent += 1
|
||||
|
@ -450,3 +496,25 @@ class HookCaller:
|
|||
self.trace.root.indent -= 1
|
||||
return res
|
||||
|
||||
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def collectattr(obj):
|
||||
methods = {}
|
||||
for apiname in dir(obj):
|
||||
if apiname.startswith("pytest_"):
|
||||
methods[apiname] = getattr(obj, apiname)
|
||||
return methods
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
|
|
|
@ -127,70 +127,3 @@ def pytest_report_header(config):
|
|||
return lines
|
||||
|
||||
|
||||
# =====================================================
|
||||
# validate plugin syntax and hooks
|
||||
# =====================================================
|
||||
|
||||
def pytest_plugin_registered(manager, plugin):
|
||||
methods = collectattr(plugin)
|
||||
hooks = {}
|
||||
for hookspec in manager.hook._hookspecs:
|
||||
hooks.update(collectattr(hookspec))
|
||||
|
||||
stringio = py.io.TextIO()
|
||||
def Print(*args):
|
||||
if args:
|
||||
stringio.write(" ".join(map(str, args)))
|
||||
stringio.write("\n")
|
||||
|
||||
fail = False
|
||||
while methods:
|
||||
name, method = methods.popitem()
|
||||
#print "checking", name
|
||||
if isgenerichook(name):
|
||||
continue
|
||||
if name not in hooks:
|
||||
if not getattr(method, 'optionalhook', False):
|
||||
Print("found unknown hook:", name)
|
||||
fail = True
|
||||
else:
|
||||
#print "checking", method
|
||||
method_args = list(varnames(method))
|
||||
if '__multicall__' in method_args:
|
||||
method_args.remove('__multicall__')
|
||||
hook = hooks[name]
|
||||
hookargs = varnames(hook)
|
||||
for arg in method_args:
|
||||
if arg not in hookargs:
|
||||
Print("argument %r not available" %(arg, ))
|
||||
Print("actual definition: %s" %(formatdef(method)))
|
||||
Print("available hook arguments: %s" %
|
||||
", ".join(hookargs))
|
||||
fail = True
|
||||
break
|
||||
#if not fail:
|
||||
# print "matching hook:", formatdef(method)
|
||||
if fail:
|
||||
name = getattr(plugin, '__name__', plugin)
|
||||
raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue()))
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
def isgenerichook(name):
|
||||
return name == "pytest_plugins" or \
|
||||
name.startswith("pytest_funcarg__")
|
||||
|
||||
def collectattr(obj):
|
||||
methods = {}
|
||||
for apiname in dir(obj):
|
||||
if apiname.startswith("pytest_"):
|
||||
methods[apiname] = getattr(obj, apiname)
|
||||
return methods
|
||||
|
||||
def formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
|
||||
|
|
|
@ -164,28 +164,6 @@ class FSHookProxy(object):
|
|||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
hookmethod = getattr(self.config.hook, name)
|
||||
methods = self.config.pluginmanager.listattr(name, plugins=plugins)
|
||||
self.__dict__[name] = x = HookCaller(hookmethod, methods)
|
||||
return x
|
||||
|
||||
|
||||
class HookCaller:
|
||||
def __init__(self, hookmethod, methods):
|
||||
self.hookmethod = hookmethod
|
||||
self.methods = methods
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
return self.hookmethod._docall(self.methods, kwargs)
|
||||
|
||||
def callextra(self, methods, **kwargs):
|
||||
# XXX in theory we should respect "tryfirst/trylast" if set
|
||||
# on the added methods but we currently only use it for
|
||||
# pytest_generate_tests and it doesn't make sense there i'd think
|
||||
all = self.methods
|
||||
if methods:
|
||||
all = all + methods
|
||||
return self.hookmethod._docall(all, kwargs)
|
||||
|
||||
def compatproperty(name):
|
||||
def fget(self):
|
||||
|
|
|
@ -47,7 +47,6 @@ class PytestArg:
|
|||
|
||||
def gethookrecorder(self, hook):
|
||||
hookrecorder = HookRecorder(hook._pm)
|
||||
hookrecorder.start_recording(hook._hookspecs)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
|
@ -69,9 +68,7 @@ class HookRecorder:
|
|||
self.calls = []
|
||||
self._recorders = {}
|
||||
|
||||
def start_recording(self, hookspecs):
|
||||
if not isinstance(hookspecs, (list, tuple)):
|
||||
hookspecs = [hookspecs]
|
||||
hookspecs = self._pluginmanager.hook._hookspecs
|
||||
for hookspec in hookspecs:
|
||||
assert hookspec not in self._recorders
|
||||
class RecordCalls:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import py, pytest
|
||||
from _pytest.helpconfig import collectattr
|
||||
from _pytest.core import collectattr
|
||||
|
||||
def test_version(testdir, pytestconfig):
|
||||
result = testdir.runpytest("--version")
|
||||
|
|
|
@ -71,28 +71,38 @@ def test_testdir_runs_with_plugin(testdir):
|
|||
"*1 passed*"
|
||||
])
|
||||
|
||||
def test_hookrecorder_basic():
|
||||
rec = HookRecorder(PluginManager())
|
||||
class ApiClass:
|
||||
|
||||
def make_holder():
|
||||
class apiclass:
|
||||
def pytest_xyz(self, arg):
|
||||
"x"
|
||||
rec.start_recording(ApiClass)
|
||||
def pytest_xyz_noarg(self):
|
||||
"x"
|
||||
|
||||
apimod = type(os)('api')
|
||||
def pytest_xyz(arg):
|
||||
"x"
|
||||
def pytest_xyz_noarg():
|
||||
"x"
|
||||
apimod.pytest_xyz = pytest_xyz
|
||||
apimod.pytest_xyz_noarg = pytest_xyz_noarg
|
||||
return apiclass, apimod
|
||||
|
||||
|
||||
@pytest.mark.parametrize("holder", make_holder())
|
||||
def test_hookrecorder_basic(holder):
|
||||
pm = PluginManager()
|
||||
pm.hook._addhooks(holder, "pytest_")
|
||||
rec = HookRecorder(pm)
|
||||
rec.hook.pytest_xyz(arg=123)
|
||||
call = rec.popcall("pytest_xyz")
|
||||
assert call.arg == 123
|
||||
assert call._name == "pytest_xyz"
|
||||
pytest.raises(pytest.fail.Exception, "rec.popcall('abc')")
|
||||
rec.hook.pytest_xyz_noarg()
|
||||
call = rec.popcall("pytest_xyz_noarg")
|
||||
assert call._name == "pytest_xyz_noarg"
|
||||
|
||||
def test_hookrecorder_basic_no_args_hook():
|
||||
rec = HookRecorder(PluginManager())
|
||||
apimod = type(os)('api')
|
||||
def pytest_xyz():
|
||||
"x"
|
||||
apimod.pytest_xyz = pytest_xyz
|
||||
rec.start_recording(apimod)
|
||||
rec.hook.pytest_xyz()
|
||||
call = rec.popcall("pytest_xyz")
|
||||
assert call._name == "pytest_xyz"
|
||||
|
||||
def test_functional(testdir, linecomp):
|
||||
reprec = testdir.inline_runsource("""
|
||||
|
@ -102,8 +112,9 @@ def test_functional(testdir, linecomp):
|
|||
def test_func(_pytest):
|
||||
class ApiClass:
|
||||
def pytest_xyz(self, arg): "x"
|
||||
hook = HookRelay([ApiClass], PluginManager())
|
||||
rec = _pytest.gethookrecorder(hook)
|
||||
pm = PluginManager()
|
||||
pm.hook._addhooks(ApiClass, "pytest_")
|
||||
rec = _pytest.gethookrecorder(pm.hook)
|
||||
class Plugin:
|
||||
def pytest_xyz(self, arg):
|
||||
return arg + 1
|
||||
|
|
Loading…
Reference in New Issue