[py-svn] py-trunk commit a1bbe5eefc21: introduce an experimental approach for allowing dynamic addition of hooks from plugin. Plugins may register new hooks by implementing the new
commits-noreply at bitbucket.org
commits-noreply at bitbucket.org
Thu Apr 22 11:58:40 CEST 2010
# HG changeset patch -- Bitbucket.org
# Project py-trunk
# URL http://bitbucket.org/hpk42/py-trunk/overview
# User holger krekel <holger at merlinux.eu>
# Date 1271930277 -7200
# Node ID a1bbe5eefc21377e8ac5e714019c74088cb96d28
# Parent 540361eb4d03cbcbe24bd0bffeab915d04576de3
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.
--- a/py/_test/pluginmanager.py
+++ b/py/_test/pluginmanager.py
@@ -7,7 +7,7 @@ from py._plugin import hookspec
from py._test.outcome import Skipped
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 "
"junitxml doctest").split()
@@ -20,7 +20,7 @@ class PluginManager(object):
self.registry = Registry()
self._name2plugin = {}
self._hints = []
- self.hook = HookRelay(hookspecs=hookspec, registry=self.registry)
+ self.hook = HookRelay([hookspec], registry=self.registry)
self.register(self)
for spec in default_plugins:
self.import_plugin(spec)
@@ -40,6 +40,8 @@ class PluginManager(object):
if name in self._name2plugin:
return False
self._name2plugin[name] = plugin
+ self.call_plugin(plugin, "pytest_registerhooks",
+ {'pluginmanager': self})
self.hook.pytest_plugin_registered(manager=self, plugin=plugin)
self.registry.register(plugin)
return True
@@ -58,6 +60,9 @@ class PluginManager(object):
if plugin == val:
return True
+ def registerhooks(self, spec):
+ self.hook._registerhooks(spec)
+
def getplugins(self):
return list(self.registry)
@@ -301,13 +306,21 @@ class Registry:
class HookRelay:
def __init__(self, hookspecs, registry):
- self._hookspecs = hookspecs
+ if not isinstance(hookspecs, list):
+ hookspecs = [hookspecs]
+ self._hookspecs = []
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():
if name[:1] != "_":
firstresult = getattr(method, 'firstresult', False)
hc = HookCaller(self, name, firstresult=firstresult)
setattr(self, name, hc)
+ #print ("setting new hook", name)
def _performcall(self, name, multicall):
return multicall.execute()
--- a/py/_plugin/hookspec.py
+++ b/py/_plugin/hookspec.py
@@ -9,6 +9,9 @@ hook specifications for py.test plugins
def pytest_addoption(parser):
""" called before commandline parsing. """
+def pytest_registerhooks(pluginmanager):
+ """ called after commandline parsing before pytest_configure. """
+
def pytest_namespace():
""" return dict of name->object which will get stored at py.test. namespace"""
@@ -133,31 +136,6 @@ def pytest_doctest_prepare_content(conte
""" return processed content for a given doctest"""
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
--- a/py/_plugin/pytest_terminal.py
+++ b/py/_plugin/pytest_terminal.py
@@ -6,6 +6,8 @@ This is a good source for looking at the
import py
import sys
+optionalhook = py.test.mark.optionalhook
+
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general")
group._addoption('-v', '--verbose', action="count",
@@ -130,6 +132,15 @@ class TerminalReporter:
for line in str(excrepr).split("\n"):
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):
#self.write_line("%s instantiated gateway from spec %r" %(gateway.id, gateway.spec._spec))
d = {}
@@ -149,30 +160,37 @@ class TerminalReporter:
self.write_line(infoline)
self.gateway2info[gateway] = infoline
- 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_testnodeready(self, node):
self.write_line("[%s] txnode ready to receive tests" %(node.gateway.id,))
+ @optionalhook
def pytest_testnodedown(self, node, error):
if 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):
if self.config.option.debug or \
self.config.option.traceconfig and category.find("config") != -1:
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):
self.stats.setdefault('deselected', []).append(items)
@@ -274,16 +292,6 @@ class TerminalReporter:
else:
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):
try:
return report.longrepr.reprcrash
--- a/py/__init__.py
+++ b/py/__init__.py
@@ -8,9 +8,8 @@ dictionary or an import path.
(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
py.apipkg.initpkg(__name__, dict(
--- a/testing/plugin/test_pytest_helpconfig.py
+++ b/testing/plugin/test_pytest_helpconfig.py
@@ -29,3 +29,24 @@ def test_collectattr():
methods = py.builtin.sorted(collectattr(B()))
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
+
--- a/testing/plugin/test_pytest__pytest.py
+++ b/testing/plugin/test_pytest__pytest.py
@@ -34,7 +34,7 @@ def test_functional(testdir, linecomp):
def test_func(_pytest):
class ApiClass:
def xyz(self, arg): pass
- hook = HookRelay(ApiClass, Registry())
+ hook = HookRelay([ApiClass], Registry())
rec = _pytest.gethookrecorder(hook)
class Plugin:
def xyz(self, arg):
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
- (issue87) fix unboundlocal error in assertionold code
- (issue86) improve documentation for looponfailing
--- a/py/_plugin/pytest__pytest.py
+++ b/py/_plugin/pytest__pytest.py
@@ -34,15 +34,18 @@ class HookRecorder:
self._recorders = {}
def start_recording(self, hookspecs):
- assert hookspecs not in self._recorders
- class RecordCalls:
- _recorder = self
- for name, method in vars(hookspecs).items():
- if name[0] != "_":
- setattr(RecordCalls, name, self._makecallparser(method))
- recorder = RecordCalls()
- self._recorders[hookspecs] = recorder
- self._registry.register(recorder)
+ if not isinstance(hookspecs, (list, tuple)):
+ hookspecs = [hookspecs]
+ for hookspec in hookspecs:
+ assert hookspec not in self._recorders
+ class RecordCalls:
+ _recorder = self
+ for name, method in vars(hookspec).items():
+ if name[0] != "_":
+ setattr(RecordCalls, name, self._makecallparser(method))
+ recorder = RecordCalls()
+ self._recorders[hookspec] = recorder
+ self._registry.register(recorder)
self.hook = HookRelay(hookspecs, registry=self._registry)
def finish_recording(self):
--- a/py/_plugin/pytest_helpconfig.py
+++ b/py/_plugin/pytest_helpconfig.py
@@ -93,9 +93,11 @@ def pytest_report_header(config):
# =====================================================
def pytest_plugin_registered(manager, plugin):
- hookspec = manager.hook._hookspecs
methods = collectattr(plugin)
- hooks = collectattr(hookspec)
+ hooks = {}
+ for hookspec in manager.hook._hookspecs:
+ hooks.update(collectattr(hookspec))
+
stringio = py.io.TextIO()
def Print(*args):
if args:
@@ -109,10 +111,13 @@ def pytest_plugin_registered(manager, pl
if isgenerichook(name):
continue
if name not in hooks:
- Print("found unknown hook:", name)
- fail = True
+ if not getattr(method, 'optionalhook', False):
+ Print("found unknown hook:", name)
+ fail = True
else:
+ #print "checking", method
method_args = getargs(method)
+ #print "method_args", method_args
if '__multicall__' in method_args:
method_args.remove('__multicall__')
hook = hooks[name]
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,7 @@ def main():
name='py',
description='py.test and pylib: rapid testing and development utils.',
long_description = long_description,
- version= trunk or '1.2.1',
+ version= trunk or '1.2.2',
url='http://pylib.org',
license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
More information about the pytest-commit
mailing list