introduce an experimental approach for allowing dynamic addition of hooks from plugin. Plugins may register new hooks by implementing the new
pytest_registerhooks(pluginmanager) and call pluginmanager.registerhooks(module) with the referenced 'module' object containing the hooks. The new pytest_registerhooks is called after pytest_addoption and before pytest_configure. --HG-- branch : trunk
This commit is contained in:
parent
cbb4c0dadc
commit
85d35f7418
|
@ -1,6 +1,7 @@
|
||||||
Changes between 1.2.1 and XXX
|
Changes between 1.2.1 and 1.2.2 (release pending)
|
||||||
=====================================
|
==================================================
|
||||||
|
|
||||||
|
- new mechanism to allow plugins to register new hooks
|
||||||
- added links to the new capturelog and coverage plugins
|
- added links to the new capturelog and coverage plugins
|
||||||
- (issue87) fix unboundlocal error in assertionold code
|
- (issue87) fix unboundlocal error in assertionold code
|
||||||
- (issue86) improve documentation for looponfailing
|
- (issue86) improve documentation for looponfailing
|
||||||
|
|
|
@ -8,9 +8,8 @@ dictionary or an import path.
|
||||||
|
|
||||||
(c) Holger Krekel and others, 2004-2010
|
(c) Holger Krekel and others, 2004-2010
|
||||||
"""
|
"""
|
||||||
version = "1.2.1"
|
_version__ = version = "1.2.2"
|
||||||
|
|
||||||
__version__ = version = version or "1.2.x"
|
|
||||||
import py.apipkg
|
import py.apipkg
|
||||||
|
|
||||||
py.apipkg.initpkg(__name__, dict(
|
py.apipkg.initpkg(__name__, dict(
|
||||||
|
|
|
@ -9,6 +9,9 @@ hook specifications for py.test plugins
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
""" called before commandline parsing. """
|
""" called before commandline parsing. """
|
||||||
|
|
||||||
|
def pytest_registerhooks(pluginmanager):
|
||||||
|
""" called after commandline parsing before pytest_configure. """
|
||||||
|
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
""" return dict of name->object which will get stored at py.test. namespace"""
|
""" return dict of name->object which will get stored at py.test. namespace"""
|
||||||
|
|
||||||
|
@ -133,31 +136,6 @@ def pytest_doctest_prepare_content(content):
|
||||||
""" return processed content for a given doctest"""
|
""" return processed content for a given doctest"""
|
||||||
pytest_doctest_prepare_content.firstresult = True
|
pytest_doctest_prepare_content.firstresult = True
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# distributed testing
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def pytest_gwmanage_newgateway(gateway, platinfo):
|
|
||||||
""" called on new raw gateway creation. """
|
|
||||||
|
|
||||||
def pytest_gwmanage_rsyncstart(source, gateways):
|
|
||||||
""" called before rsyncing a directory to remote gateways takes place. """
|
|
||||||
|
|
||||||
def pytest_gwmanage_rsyncfinish(source, gateways):
|
|
||||||
""" called after rsyncing a directory to remote gateways takes place. """
|
|
||||||
|
|
||||||
def pytest_testnodeready(node):
|
|
||||||
""" Test Node is ready to operate. """
|
|
||||||
|
|
||||||
def pytest_testnodedown(node, error):
|
|
||||||
""" Test Node is down. """
|
|
||||||
|
|
||||||
def pytest_rescheduleitems(items):
|
|
||||||
""" reschedule Items from a node that went down. """
|
|
||||||
|
|
||||||
def pytest_looponfailinfo(failreports, rootdirs):
|
|
||||||
""" info for repeating failing tests. """
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# error handling and internal debugging hooks
|
# error handling and internal debugging hooks
|
||||||
|
|
|
@ -34,15 +34,18 @@ class HookRecorder:
|
||||||
self._recorders = {}
|
self._recorders = {}
|
||||||
|
|
||||||
def start_recording(self, hookspecs):
|
def start_recording(self, hookspecs):
|
||||||
assert hookspecs not in self._recorders
|
if not isinstance(hookspecs, (list, tuple)):
|
||||||
class RecordCalls:
|
hookspecs = [hookspecs]
|
||||||
_recorder = self
|
for hookspec in hookspecs:
|
||||||
for name, method in vars(hookspecs).items():
|
assert hookspec not in self._recorders
|
||||||
if name[0] != "_":
|
class RecordCalls:
|
||||||
setattr(RecordCalls, name, self._makecallparser(method))
|
_recorder = self
|
||||||
recorder = RecordCalls()
|
for name, method in vars(hookspec).items():
|
||||||
self._recorders[hookspecs] = recorder
|
if name[0] != "_":
|
||||||
self._registry.register(recorder)
|
setattr(RecordCalls, name, self._makecallparser(method))
|
||||||
|
recorder = RecordCalls()
|
||||||
|
self._recorders[hookspec] = recorder
|
||||||
|
self._registry.register(recorder)
|
||||||
self.hook = HookRelay(hookspecs, registry=self._registry)
|
self.hook = HookRelay(hookspecs, registry=self._registry)
|
||||||
|
|
||||||
def finish_recording(self):
|
def finish_recording(self):
|
||||||
|
|
|
@ -93,9 +93,11 @@ def pytest_report_header(config):
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
||||||
def pytest_plugin_registered(manager, plugin):
|
def pytest_plugin_registered(manager, plugin):
|
||||||
hookspec = manager.hook._hookspecs
|
|
||||||
methods = collectattr(plugin)
|
methods = collectattr(plugin)
|
||||||
hooks = collectattr(hookspec)
|
hooks = {}
|
||||||
|
for hookspec in manager.hook._hookspecs:
|
||||||
|
hooks.update(collectattr(hookspec))
|
||||||
|
|
||||||
stringio = py.io.TextIO()
|
stringio = py.io.TextIO()
|
||||||
def Print(*args):
|
def Print(*args):
|
||||||
if args:
|
if args:
|
||||||
|
@ -109,10 +111,13 @@ def pytest_plugin_registered(manager, plugin):
|
||||||
if isgenerichook(name):
|
if isgenerichook(name):
|
||||||
continue
|
continue
|
||||||
if name not in hooks:
|
if name not in hooks:
|
||||||
Print("found unknown hook:", name)
|
if not getattr(method, 'optionalhook', False):
|
||||||
fail = True
|
Print("found unknown hook:", name)
|
||||||
|
fail = True
|
||||||
else:
|
else:
|
||||||
|
#print "checking", method
|
||||||
method_args = getargs(method)
|
method_args = getargs(method)
|
||||||
|
#print "method_args", method_args
|
||||||
if '__multicall__' in method_args:
|
if '__multicall__' in method_args:
|
||||||
method_args.remove('__multicall__')
|
method_args.remove('__multicall__')
|
||||||
hook = hooks[name]
|
hook = hooks[name]
|
||||||
|
|
|
@ -6,6 +6,8 @@ This is a good source for looking at the various reporting hooks.
|
||||||
import py
|
import py
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
optionalhook = py.test.mark.optionalhook
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||||
group._addoption('-v', '--verbose', action="count",
|
group._addoption('-v', '--verbose', action="count",
|
||||||
|
@ -130,6 +132,15 @@ class TerminalReporter:
|
||||||
for line in str(excrepr).split("\n"):
|
for line in str(excrepr).split("\n"):
|
||||||
self.write_line("INTERNALERROR> " + line)
|
self.write_line("INTERNALERROR> " + line)
|
||||||
|
|
||||||
|
def pytest_plugin_registered(self, plugin):
|
||||||
|
if self.config.option.traceconfig:
|
||||||
|
msg = "PLUGIN registered: %s" %(plugin,)
|
||||||
|
# XXX this event may happen during setup/teardown time
|
||||||
|
# which unfortunately captures our output here
|
||||||
|
# which garbles our output if we use self.write_line
|
||||||
|
self.write_line(msg)
|
||||||
|
|
||||||
|
@optionalhook
|
||||||
def pytest_gwmanage_newgateway(self, gateway, platinfo):
|
def pytest_gwmanage_newgateway(self, gateway, platinfo):
|
||||||
#self.write_line("%s instantiated gateway from spec %r" %(gateway.id, gateway.spec._spec))
|
#self.write_line("%s instantiated gateway from spec %r" %(gateway.id, gateway.spec._spec))
|
||||||
d = {}
|
d = {}
|
||||||
|
@ -149,30 +160,37 @@ class TerminalReporter:
|
||||||
self.write_line(infoline)
|
self.write_line(infoline)
|
||||||
self.gateway2info[gateway] = infoline
|
self.gateway2info[gateway] = infoline
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin):
|
@optionalhook
|
||||||
if self.config.option.traceconfig:
|
|
||||||
msg = "PLUGIN registered: %s" %(plugin,)
|
|
||||||
# XXX this event may happen during setup/teardown time
|
|
||||||
# which unfortunately captures our output here
|
|
||||||
# which garbles our output if we use self.write_line
|
|
||||||
self.write_line(msg)
|
|
||||||
|
|
||||||
def pytest_testnodeready(self, node):
|
def pytest_testnodeready(self, node):
|
||||||
self.write_line("[%s] txnode ready to receive tests" %(node.gateway.id,))
|
self.write_line("[%s] txnode ready to receive tests" %(node.gateway.id,))
|
||||||
|
|
||||||
|
@optionalhook
|
||||||
def pytest_testnodedown(self, node, error):
|
def pytest_testnodedown(self, node, error):
|
||||||
if error:
|
if error:
|
||||||
self.write_line("[%s] node down, error: %s" %(node.gateway.id, error))
|
self.write_line("[%s] node down, error: %s" %(node.gateway.id, error))
|
||||||
|
|
||||||
|
@optionalhook
|
||||||
|
def pytest_rescheduleitems(self, items):
|
||||||
|
if self.config.option.debug:
|
||||||
|
self.write_sep("!", "RESCHEDULING %s " %(items,))
|
||||||
|
|
||||||
|
@optionalhook
|
||||||
|
def pytest_looponfailinfo(self, failreports, rootdirs):
|
||||||
|
if failreports:
|
||||||
|
self.write_sep("#", "LOOPONFAILING", red=True)
|
||||||
|
for report in failreports:
|
||||||
|
loc = self._getcrashline(report)
|
||||||
|
self.write_line(loc, red=True)
|
||||||
|
self.write_sep("#", "waiting for changes")
|
||||||
|
for rootdir in rootdirs:
|
||||||
|
self.write_line("### Watching: %s" %(rootdir,), bold=True)
|
||||||
|
|
||||||
|
|
||||||
def pytest_trace(self, category, msg):
|
def pytest_trace(self, category, msg):
|
||||||
if self.config.option.debug or \
|
if self.config.option.debug or \
|
||||||
self.config.option.traceconfig and category.find("config") != -1:
|
self.config.option.traceconfig and category.find("config") != -1:
|
||||||
self.write_line("[%s] %s" %(category, msg))
|
self.write_line("[%s] %s" %(category, msg))
|
||||||
|
|
||||||
def pytest_rescheduleitems(self, items):
|
|
||||||
if self.config.option.debug:
|
|
||||||
self.write_sep("!", "RESCHEDULING %s " %(items,))
|
|
||||||
|
|
||||||
def pytest_deselected(self, items):
|
def pytest_deselected(self, items):
|
||||||
self.stats.setdefault('deselected', []).append(items)
|
self.stats.setdefault('deselected', []).append(items)
|
||||||
|
|
||||||
|
@ -274,16 +292,6 @@ class TerminalReporter:
|
||||||
else:
|
else:
|
||||||
excrepr.reprcrash.toterminal(self._tw)
|
excrepr.reprcrash.toterminal(self._tw)
|
||||||
|
|
||||||
def pytest_looponfailinfo(self, failreports, rootdirs):
|
|
||||||
if failreports:
|
|
||||||
self.write_sep("#", "LOOPONFAILING", red=True)
|
|
||||||
for report in failreports:
|
|
||||||
loc = self._getcrashline(report)
|
|
||||||
self.write_line(loc, red=True)
|
|
||||||
self.write_sep("#", "waiting for changes")
|
|
||||||
for rootdir in rootdirs:
|
|
||||||
self.write_line("### Watching: %s" %(rootdir,), bold=True)
|
|
||||||
|
|
||||||
def _getcrashline(self, report):
|
def _getcrashline(self, report):
|
||||||
try:
|
try:
|
||||||
return report.longrepr.reprcrash
|
return report.longrepr.reprcrash
|
||||||
|
|
|
@ -7,7 +7,7 @@ from py._plugin import hookspec
|
||||||
from py._test.outcome import Skipped
|
from py._test.outcome import Skipped
|
||||||
|
|
||||||
default_plugins = (
|
default_plugins = (
|
||||||
"default runner capture terminal mark skipping tmpdir monkeypatch "
|
"default runner capture mark terminal skipping tmpdir monkeypatch "
|
||||||
"recwarn pdb pastebin unittest helpconfig nose assertion genscript "
|
"recwarn pdb pastebin unittest helpconfig nose assertion genscript "
|
||||||
"junitxml doctest").split()
|
"junitxml doctest").split()
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class PluginManager(object):
|
||||||
self.registry = Registry()
|
self.registry = Registry()
|
||||||
self._name2plugin = {}
|
self._name2plugin = {}
|
||||||
self._hints = []
|
self._hints = []
|
||||||
self.hook = HookRelay(hookspecs=hookspec, registry=self.registry)
|
self.hook = HookRelay([hookspec], registry=self.registry)
|
||||||
self.register(self)
|
self.register(self)
|
||||||
for spec in default_plugins:
|
for spec in default_plugins:
|
||||||
self.import_plugin(spec)
|
self.import_plugin(spec)
|
||||||
|
@ -40,6 +40,8 @@ class PluginManager(object):
|
||||||
if name in self._name2plugin:
|
if name in self._name2plugin:
|
||||||
return False
|
return False
|
||||||
self._name2plugin[name] = plugin
|
self._name2plugin[name] = plugin
|
||||||
|
self.call_plugin(plugin, "pytest_registerhooks",
|
||||||
|
{'pluginmanager': self})
|
||||||
self.hook.pytest_plugin_registered(manager=self, plugin=plugin)
|
self.hook.pytest_plugin_registered(manager=self, plugin=plugin)
|
||||||
self.registry.register(plugin)
|
self.registry.register(plugin)
|
||||||
return True
|
return True
|
||||||
|
@ -58,6 +60,9 @@ class PluginManager(object):
|
||||||
if plugin == val:
|
if plugin == val:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def registerhooks(self, spec):
|
||||||
|
self.hook._registerhooks(spec)
|
||||||
|
|
||||||
def getplugins(self):
|
def getplugins(self):
|
||||||
return list(self.registry)
|
return list(self.registry)
|
||||||
|
|
||||||
|
@ -301,13 +306,21 @@ class Registry:
|
||||||
|
|
||||||
class HookRelay:
|
class HookRelay:
|
||||||
def __init__(self, hookspecs, registry):
|
def __init__(self, hookspecs, registry):
|
||||||
self._hookspecs = hookspecs
|
if not isinstance(hookspecs, list):
|
||||||
|
hookspecs = [hookspecs]
|
||||||
|
self._hookspecs = []
|
||||||
self._registry = registry
|
self._registry = registry
|
||||||
|
for hookspec in hookspecs:
|
||||||
|
self._registerhooks(hookspec)
|
||||||
|
|
||||||
|
def _registerhooks(self, hookspecs):
|
||||||
|
self._hookspecs.append(hookspecs)
|
||||||
for name, method in vars(hookspecs).items():
|
for name, method in vars(hookspecs).items():
|
||||||
if name[:1] != "_":
|
if name[:1] != "_":
|
||||||
firstresult = getattr(method, 'firstresult', False)
|
firstresult = getattr(method, 'firstresult', False)
|
||||||
hc = HookCaller(self, name, firstresult=firstresult)
|
hc = HookCaller(self, name, firstresult=firstresult)
|
||||||
setattr(self, name, hc)
|
setattr(self, name, hc)
|
||||||
|
#print ("setting new hook", name)
|
||||||
|
|
||||||
def _performcall(self, name, multicall):
|
def _performcall(self, name, multicall):
|
||||||
return multicall.execute()
|
return multicall.execute()
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -27,7 +27,7 @@ def main():
|
||||||
name='py',
|
name='py',
|
||||||
description='py.test and pylib: rapid testing and development utils.',
|
description='py.test and pylib: rapid testing and development utils.',
|
||||||
long_description = long_description,
|
long_description = long_description,
|
||||||
version= trunk or '1.2.1',
|
version= trunk or '1.2.2',
|
||||||
url='http://pylib.org',
|
url='http://pylib.org',
|
||||||
license='MIT license',
|
license='MIT license',
|
||||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||||
|
|
|
@ -34,7 +34,7 @@ def test_functional(testdir, linecomp):
|
||||||
def test_func(_pytest):
|
def test_func(_pytest):
|
||||||
class ApiClass:
|
class ApiClass:
|
||||||
def xyz(self, arg): pass
|
def xyz(self, arg): pass
|
||||||
hook = HookRelay(ApiClass, Registry())
|
hook = HookRelay([ApiClass], Registry())
|
||||||
rec = _pytest.gethookrecorder(hook)
|
rec = _pytest.gethookrecorder(hook)
|
||||||
class Plugin:
|
class Plugin:
|
||||||
def xyz(self, arg):
|
def xyz(self, arg):
|
||||||
|
|
|
@ -29,3 +29,24 @@ def test_collectattr():
|
||||||
methods = py.builtin.sorted(collectattr(B()))
|
methods = py.builtin.sorted(collectattr(B()))
|
||||||
assert list(methods) == ['pytest_hello', 'pytest_world']
|
assert list(methods) == ['pytest_hello', 'pytest_world']
|
||||||
|
|
||||||
|
def test_hookvalidation_unknown(testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
def pytest_hello(xyz):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret != 0
|
||||||
|
assert result.stderr.fnmatch_lines([
|
||||||
|
'*unknown hook*pytest_hello*'
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_hookvalidation_optional(testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
import py
|
||||||
|
@py.test.mark.optionalhook
|
||||||
|
def pytest_hello(xyz):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue