refine and test new hook registration, now it is called "pytest_addhooks"

similar to pytest_addoption and raises on bogus input.

--HG--
branch : trunk
This commit is contained in:
holger krekel 2010-05-02 16:36:53 +02:00
parent 45e10f4c48
commit fd473d4002
5 changed files with 102 additions and 41 deletions

View File

@ -1,9 +1,26 @@
Changes between 1.2.1 and 1.3.0 (release pending)
==================================================
- new mechanism to allow external plugins to register new hooks
(a recent pytest-xdist plugin for distributed and looponfailing
testing requires this feature)
- allow external plugins to register new hooks via the new
pytest_addhooks(pluginmanager) hook. The new release of
the pytest-xdist plugin for distributed and looponfailing
testing requires this feature.
- add a new pytest_ignore_collect(path, config) hook to allow projects and
plugins to define exclusion behaviour for their directory structure -
for example you may define in a conftest.py this method:
def pytest_ignore_collect(path):
return path.check(link=1)
to prevent even a collection try of any tests in symlinked dirs.
- new pytest_pycollect_makemodule(path, parent) hook for
allowing customization of the Module collection object for a
matching test module.
- expose (previously internal) commonly useful methods:
py.io.get_terminal_with() -> return terminal width
py.io.ansi_print(...) -> print colored/bold text on linux/win32
py.io.saferepr(obj) -> return limited representation string
- expose test outcome related exceptions as py.test.skip.Exception,
py.test.raises.Exception etc., useful mostly for plugins
doing special outcome interpretation/tweaking
- (issue85) fix junitxml plugin to handle tests with non-ascii output
- fix/refine python3 compatibility (thanks Benjamin Peterson)
- fixes for making the jython/win32 combination work, note however:
@ -13,22 +30,6 @@ Changes between 1.2.1 and 1.3.0 (release pending)
- fixes for handling of unicode exception values and unprintable objects
- (issue87) fix unboundlocal error in assertionold code
- (issue86) improve documentation for looponfailing
- add a new pytest_ignore_collect(path, config) hook to allow projects and
plugins to define exclusion behaviour for their directory structure -
for example you may define in a conftest.py this method:
def pytest_ignore_collect(path):
return path.check(link=1)
to prevent even a collection try of any tests in symlinked dirs.
- new pytest_pycollect_makemodule(path, parent) hook for
allowing customization of the Module collection object for a
matching test module.
- expose (previously internal) commonly useful methods:
py.io.get_terminal_with() -> return terminal width
py.io.ansi_print(...) -> print colored/bold text on linux/win32
py.io.saferepr(obj) -> return limited representation string
- expose test outcome related exceptions as py.test.skip.Exception,
py.test.raises.Exception etc., useful mostly for plugins
doing special outcome interpretation/tweaking
- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method
- ship distribute_setup.py version 0.6.10
- added links to the new capturelog and coverage plugins

View File

@ -6,14 +6,14 @@ hook specifications for py.test plugins
# Command line and configuration
# -------------------------------------------------------------------------
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"""
"return dict of name->object which will get stored at py.test. namespace"
def pytest_addoption(parser):
"add optparse-style options via parser.addoption."
def pytest_addhooks(pluginmanager):
"add hooks via pluginmanager.registerhooks(module)"
def pytest_configure(config):
""" called after command line options have been parsed.

View File

@ -21,4 +21,3 @@ def main(args=None):
e = sys.exc_info()[1]
sys.stderr.write("ERROR: %s\n" %(e.args[0],))
raise SystemExit(3)

View File

@ -39,8 +39,7 @@ class PluginManager(object):
if name in self._name2plugin:
return False
self._name2plugin[name] = plugin
self.call_plugin(plugin, "pytest_registerhooks",
{'pluginmanager': self})
self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self})
self.hook.pytest_plugin_registered(manager=self, plugin=plugin)
self.registry.register(plugin)
return True
@ -59,8 +58,8 @@ class PluginManager(object):
if plugin == val:
return True
def registerhooks(self, spec):
self.hook._registerhooks(spec)
def addhooks(self, spec):
self.hook._addhooks(spec, prefix="pytest_")
def getplugins(self):
return list(self.registry)
@ -304,22 +303,31 @@ class Registry:
return l
class HookRelay:
def __init__(self, hookspecs, registry):
def __init__(self, hookspecs, registry, prefix="pytest_"):
if not isinstance(hookspecs, list):
hookspecs = [hookspecs]
self._hookspecs = []
self._registry = registry
for hookspec in hookspecs:
self._registerhooks(hookspec)
self._addhooks(hookspec, prefix)
def _registerhooks(self, hookspecs):
def _addhooks(self, hookspecs, prefix):
self._hookspecs.append(hookspecs)
added = False
for name, method in vars(hookspecs).items():
if name[:1] != "_":
if name.startswith(prefix):
if not method.__doc__:
raise ValueError("docstring required for hook %r, in %r"
% (method, hookspecs))
firstresult = getattr(method, 'firstresult', False)
hc = HookCaller(self, name, firstresult=firstresult)
setattr(self, name, hc)
added = True
#print ("setting new hook", name)
if not added:
raise ValueError("did not find new %r hooks in %r" %(
prefix, hookspecs,))
def _performcall(self, name, multicall):
return multicall.execute()

View File

@ -202,6 +202,58 @@ class TestBootstrapping:
impname = canonical_importname(name)
class TestPytestPluginInteractions:
def test_addhooks_conftestplugin(self, testdir):
from py._test.config import Config
newhooks = testdir.makepyfile(newhooks="""
def pytest_myhook(xyz):
"new hook"
""")
conf = testdir.makeconftest("""
import sys ; sys.path.insert(0, '.')
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.addhooks(newhooks)
def pytest_myhook(xyz):
return xyz + 1
""")
config = Config()
config._conftest.importconftest(conf)
print(config.pluginmanager.getplugins())
res = config.hook.pytest_myhook(xyz=10)
assert res == [11]
def test_addhooks_docstring_error(self, testdir):
newhooks = testdir.makepyfile(newhooks="""
class A: # no pytest_ prefix
pass
def pytest_myhook(xyz):
pass
""")
conf = testdir.makeconftest("""
import sys ; sys.path.insert(0, '.')
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.addhooks(newhooks)
""")
res = testdir.runpytest()
assert res.ret != 0
res.stderr.fnmatch_lines([
"*docstring*pytest_myhook*newhooks*"
])
def test_addhooks_nohooks(self, testdir):
conf = testdir.makeconftest("""
import sys
def pytest_addhooks(pluginmanager):
pluginmanager.addhooks(sys)
""")
res = testdir.runpytest()
assert res.ret != 0
res.stderr.fnmatch_lines([
"*did not find*sys*"
])
def test_do_option_conftestplugin(self, testdir):
from py._test.config import Config
p = testdir.makepyfile("""
@ -401,9 +453,9 @@ class TestHookRelay:
registry = Registry()
class Api:
def hello(self, arg):
pass
"api hook 1"
mcm = HookRelay(hookspecs=Api, registry=registry)
mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he")
assert hasattr(mcm, 'hello')
assert repr(mcm.hello).find("hello") != -1
class Plugin:
@ -418,17 +470,18 @@ class TestHookRelay:
registry = Registry()
class Api:
def hello(self, arg):
pass
mcm = HookRelay(hookspecs=Api, registry=registry)
"api hook 1"
mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he")
py.test.raises(TypeError, "mcm.hello(3)")
def test_firstresult_definition(self):
registry = Registry()
class Api:
def hello(self, arg): pass
def hello(self, arg):
"api hook 1"
hello.firstresult = True
mcm = HookRelay(hookspecs=Api, registry=registry)
mcm = HookRelay(hookspecs=Api, registry=registry, prefix="he")
class Plugin:
def hello(self, arg):
return arg + 1