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" %(
|
raise ValueError("Plugin already registered: %s=%s\n%s" %(
|
||||||
name, plugin, self._name2plugin))
|
name, plugin, self._name2plugin))
|
||||||
#self.trace("registering", name, plugin)
|
#self.trace("registering", name, plugin)
|
||||||
self._name2plugin[name] = plugin
|
|
||||||
reg = getattr(self, "_registercallback", None)
|
reg = getattr(self, "_registercallback", None)
|
||||||
if reg is not 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:
|
if conftest:
|
||||||
self._conftestplugins.append(plugin)
|
self._conftestplugins.append(plugin)
|
||||||
else:
|
else:
|
||||||
|
@ -403,41 +404,86 @@ class HookRelay:
|
||||||
def _getcaller(self, name, plugins):
|
def _getcaller(self, name, plugins):
|
||||||
caller = getattr(self, name)
|
caller = getattr(self, name)
|
||||||
methods = self._pm.listattr(name, plugins=plugins)
|
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:
|
if methods:
|
||||||
all = all + methods
|
return caller.new_cached_caller(methods)
|
||||||
return self.hookmethod._docall(all, kwargs)
|
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:
|
class HookCaller:
|
||||||
def __init__(self, hookrelay, name, firstresult):
|
def __init__(self, hookrelay, name, firstresult, methods=None):
|
||||||
self.hookrelay = hookrelay
|
self.hookrelay = hookrelay
|
||||||
self.name = name
|
self.name = name
|
||||||
self.firstresult = firstresult
|
self.firstresult = firstresult
|
||||||
self.trace = self.hookrelay.trace
|
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):
|
def __repr__(self):
|
||||||
return "<HookCaller %r>" %(self.name,)
|
return "<HookCaller %r>" %(self.name,)
|
||||||
|
|
||||||
|
def clear_method_cache(self):
|
||||||
|
self.methods = None
|
||||||
|
|
||||||
def __call__(self, **kwargs):
|
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)
|
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):
|
def _docall(self, methods, kwargs):
|
||||||
self.trace(self.name, kwargs)
|
self.trace(self.name, kwargs)
|
||||||
self.trace.root.indent += 1
|
self.trace.root.indent += 1
|
||||||
|
@ -450,3 +496,25 @@ class HookCaller:
|
||||||
self.trace.root.indent -= 1
|
self.trace.root.indent -= 1
|
||||||
return res
|
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
|
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
|
self.__dict__[name] = x
|
||||||
return 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 compatproperty(name):
|
||||||
def fget(self):
|
def fget(self):
|
||||||
|
|
|
@ -47,7 +47,6 @@ class PytestArg:
|
||||||
|
|
||||||
def gethookrecorder(self, hook):
|
def gethookrecorder(self, hook):
|
||||||
hookrecorder = HookRecorder(hook._pm)
|
hookrecorder = HookRecorder(hook._pm)
|
||||||
hookrecorder.start_recording(hook._hookspecs)
|
|
||||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||||
return hookrecorder
|
return hookrecorder
|
||||||
|
|
||||||
|
@ -69,9 +68,7 @@ class HookRecorder:
|
||||||
self.calls = []
|
self.calls = []
|
||||||
self._recorders = {}
|
self._recorders = {}
|
||||||
|
|
||||||
def start_recording(self, hookspecs):
|
hookspecs = self._pluginmanager.hook._hookspecs
|
||||||
if not isinstance(hookspecs, (list, tuple)):
|
|
||||||
hookspecs = [hookspecs]
|
|
||||||
for hookspec in hookspecs:
|
for hookspec in hookspecs:
|
||||||
assert hookspec not in self._recorders
|
assert hookspec not in self._recorders
|
||||||
class RecordCalls:
|
class RecordCalls:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import py, pytest
|
import py, pytest
|
||||||
from _pytest.helpconfig import collectattr
|
from _pytest.core import collectattr
|
||||||
|
|
||||||
def test_version(testdir, pytestconfig):
|
def test_version(testdir, pytestconfig):
|
||||||
result = testdir.runpytest("--version")
|
result = testdir.runpytest("--version")
|
||||||
|
|
|
@ -71,28 +71,38 @@ def test_testdir_runs_with_plugin(testdir):
|
||||||
"*1 passed*"
|
"*1 passed*"
|
||||||
])
|
])
|
||||||
|
|
||||||
def test_hookrecorder_basic():
|
|
||||||
rec = HookRecorder(PluginManager())
|
def make_holder():
|
||||||
class ApiClass:
|
class apiclass:
|
||||||
def pytest_xyz(self, arg):
|
def pytest_xyz(self, arg):
|
||||||
"x"
|
"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)
|
rec.hook.pytest_xyz(arg=123)
|
||||||
call = rec.popcall("pytest_xyz")
|
call = rec.popcall("pytest_xyz")
|
||||||
assert call.arg == 123
|
assert call.arg == 123
|
||||||
assert call._name == "pytest_xyz"
|
assert call._name == "pytest_xyz"
|
||||||
pytest.raises(pytest.fail.Exception, "rec.popcall('abc')")
|
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):
|
def test_functional(testdir, linecomp):
|
||||||
reprec = testdir.inline_runsource("""
|
reprec = testdir.inline_runsource("""
|
||||||
|
@ -102,8 +112,9 @@ def test_functional(testdir, linecomp):
|
||||||
def test_func(_pytest):
|
def test_func(_pytest):
|
||||||
class ApiClass:
|
class ApiClass:
|
||||||
def pytest_xyz(self, arg): "x"
|
def pytest_xyz(self, arg): "x"
|
||||||
hook = HookRelay([ApiClass], PluginManager())
|
pm = PluginManager()
|
||||||
rec = _pytest.gethookrecorder(hook)
|
pm.hook._addhooks(ApiClass, "pytest_")
|
||||||
|
rec = _pytest.gethookrecorder(pm.hook)
|
||||||
class Plugin:
|
class Plugin:
|
||||||
def pytest_xyz(self, arg):
|
def pytest_xyz(self, arg):
|
||||||
return arg + 1
|
return arg + 1
|
||||||
|
|
Loading…
Reference in New Issue