From fd473d400226e847cd255510240d3df37baa9737 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 2 May 2010 16:36:53 +0200 Subject: [PATCH] refine and test new hook registration, now it is called "pytest_addhooks" similar to pytest_addoption and raises on bogus input. --HG-- branch : trunk --- CHANGELOG | 39 +++++++++++---------- py/_plugin/hookspec.py | 14 ++++---- py/_test/cmdline.py | 1 - py/_test/pluginmanager.py | 24 ++++++++----- testing/test_pluginmanager.py | 65 +++++++++++++++++++++++++++++++---- 5 files changed, 102 insertions(+), 41 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2d66258ab..aac4b391b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/py/_plugin/hookspec.py b/py/_plugin/hookspec.py index 691070d32..37230671b 100644 --- a/py/_plugin/hookspec.py +++ b/py/_plugin/hookspec.py @@ -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. diff --git a/py/_test/cmdline.py b/py/_test/cmdline.py index 92aab633e..804c42fc1 100644 --- a/py/_test/cmdline.py +++ b/py/_test/cmdline.py @@ -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) - diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index 6392ff911..d1b673421 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -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() diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index ad80cc4f6..425333768 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -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