From 4b709037abf86e59f4227a8aad4ba8e1c64a0634 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 30 Sep 2013 13:14:14 +0200 Subject: [PATCH] some more separation of core pluginmanager from pytest specific functionality. Idea is to have the PluginManager be re-useable from other projects at some point. --- _pytest/config.py | 106 ++++++++++++++++++++++++++++++++++++- _pytest/core.py | 110 ++++----------------------------------- _pytest/main.py | 2 +- _pytest/pytester.py | 4 +- _pytest/terminal.py | 6 +++ pytest.py | 14 ++--- testing/test_config.py | 15 ++++++ testing/test_core.py | 32 ++++-------- testing/test_pytester.py | 2 +- 9 files changed, 157 insertions(+), 134 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index f52a635bc..4aab1fae6 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -2,6 +2,87 @@ import py import sys, os +from _pytest import hookspec # the extension point definitions +from _pytest.core import PluginManager + +# pytest startup + +def main(args=None, plugins=None): + """ return exit code, after performing an in-process test run. + + :arg args: list of command line arguments. + + :arg plugins: list of plugin objects to be auto-registered during + initialization. + """ + config = _prepareconfig(args, plugins) + exitstatus = config.hook.pytest_cmdline_main(config=config) + return exitstatus + +class cmdline: # compatibility namespace + main = staticmethod(main) + +class UsageError(Exception): + """ error in py.test usage or invocation""" + +_preinit = [] + +default_plugins = ( + "mark main terminal runner python pdb unittest capture skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml resultlog doctest").split() + +def _preloadplugins(): + assert not _preinit + _preinit.append(get_plugin_manager()) + +def get_plugin_manager(): + if _preinit: + return _preinit.pop(0) + # subsequent calls to main will create a fresh instance + pluginmanager = PytestPluginManager() + pluginmanager.config = config = Config(pluginmanager) # XXX attr needed? + for spec in default_plugins: + pluginmanager.import_plugin(spec) + return pluginmanager + +def _prepareconfig(args=None, plugins=None): + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] + elif not isinstance(args, (tuple, list)): + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) + pluginmanager = get_plugin_manager() + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + +class PytestPluginManager(PluginManager): + def __init__(self, hookspecs=[hookspec]): + super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + self.register(self) + if os.environ.get('PYTEST_DEBUG'): + err = sys.stderr + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass + self.trace.root.setwriter(err.write) + + def pytest_configure(self, config): + config.addinivalue_line("markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible.") + config.addinivalue_line("markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible.") + class Parser: """ Parser for command line arguments and ini-file values. """ @@ -494,10 +575,15 @@ class Config(object): self._opt2dest = {} self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") + self.pluginmanager.set_register_callback(self._register_plugin) self._configured = False - def pytest_plugin_registered(self, plugin): + def _register_plugin(self, plugin, name): call_plugin = self.pluginmanager.call_plugin + call_plugin(plugin, "pytest_addhooks", + {'pluginmanager': self.pluginmanager}) + self.hook.pytest_plugin_registered(plugin=plugin, + manager=self.pluginmanager) dic = call_plugin(plugin, "pytest_namespace", {}) or {} if dic: import pytest @@ -527,10 +613,26 @@ class Config(object): fin = config._cleanup.pop() fin() + def notify_exception(self, excinfo, option=None): + if option and option.fulltrace: + style = "long" + else: + style = "native" + excrepr = excinfo.getrepr(funcargs=True, + showlocals=getattr(option, 'showlocals', False), + style=style, + ) + res = self.hook.pytest_internalerror(excrepr=excrepr, + excinfo=excinfo) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ - from _pytest.core import get_plugin_manager pluginmanager = get_plugin_manager() config = pluginmanager.config # XXX slightly crude way to initialize capturing diff --git a/_pytest/core.py b/_pytest/core.py index fae32228d..61a8228d3 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -4,17 +4,10 @@ pytest PluginManager, basic initialization and tracing. import sys, os import inspect import py -from _pytest import hookspec # the extension point definitions -from _pytest.config import Config assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) -default_plugins = ( - "mark main terminal runner python pdb unittest capture skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml resultlog doctest").split() - class TagTracer: def __init__(self): self._tag2proc = {} @@ -73,7 +66,7 @@ class TagTracerSub: return self.__class__(self.root, self.tags + (name,)) class PluginManager(object): - def __init__(self, load=False): + def __init__(self, hookspecs=None): self._name2plugin = {} self._listattrcache = {} self._plugins = [] @@ -81,20 +74,11 @@ class PluginManager(object): self.trace = TagTracer().get("pluginmanage") self._plugin_distinfo = [] self._shutdown = [] - if os.environ.get('PYTEST_DEBUG'): - err = sys.stderr - encoding = getattr(err, 'encoding', 'utf8') - try: - err = py.io.dupfile(err, encoding=encoding) - except Exception: - pass - self.trace.root.setwriter(err.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - self.config = Config(self) # XXX unclear if the attr is needed - if load: - for spec in default_plugins: - self.import_plugin(spec) + self.hook = HookRelay(hookspecs or [], pm=self) + + def set_register_callback(self, callback): + assert not hasattr(self, "_registercallback") + self._registercallback = callback def register(self, plugin, name=None, prepend=False): if self._name2plugin.get(name, None) == -1: @@ -105,8 +89,9 @@ class PluginManager(object): name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + reg = getattr(self, "_registercallback", None) + if reg is not None: + reg(plugin, name) if not prepend: self._plugins.append(plugin) else: @@ -139,8 +124,8 @@ class PluginManager(object): if plugin == val: return True - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") + def addhooks(self, spec, prefix="pytest_"): + self.hook._addhooks(spec, prefix=prefix) def getplugins(self): return list(self._plugins) @@ -240,36 +225,6 @@ class PluginManager(object): self.register(mod, modname) self.consider_module(mod) - def pytest_configure(self, config): - config.addinivalue_line("markers", - "tryfirst: mark a hook implementation function such that the " - "plugin machinery will try to call it first/as early as possible.") - config.addinivalue_line("markers", - "trylast: mark a hook implementation function such that the " - "plugin machinery will try to call it last/as late as possible.") - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def notify_exception(self, excinfo, option=None): - if option and option.fulltrace: - style = "long" - else: - style = "native" - excrepr = excinfo.getrepr(funcargs=True, - showlocals=getattr(option, 'showlocals', False), - style=style, - ) - res = self.hook.pytest_internalerror(excrepr=excrepr, - excinfo=excinfo) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - def listattr(self, attrname, plugins=None): if plugins is None: plugins = self._plugins @@ -424,46 +379,3 @@ class HookCaller: self.trace.root.indent -= 1 return res -_preinit = [] - -def _preloadplugins(): - assert not _preinit - _preinit.append(PluginManager(load=True)) - -def get_plugin_manager(): - if _preinit: - return _preinit.pop(0) - else: # subsequent calls to main will create a fresh instance - return PluginManager(load=True) - -def _prepareconfig(args=None, plugins=None): - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] - elif not isinstance(args, (tuple, list)): - if not isinstance(args, str): - raise ValueError("not a string or argument list: %r" % (args,)) - args = py.std.shlex.split(args) - pluginmanager = get_plugin_manager() - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - -def main(args=None, plugins=None): - """ return exit code, after performing an in-process test run. - - :arg args: list of command line arguments. - - :arg plugins: list of plugin objects to be auto-registered during - initialization. - """ - config = _prepareconfig(args, plugins) - exitstatus = config.hook.pytest_cmdline_main(config=config) - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" - diff --git a/_pytest/main.py b/_pytest/main.py index 39c7dba3f..16d89fef5 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -91,7 +91,7 @@ def wrap_session(config, doit): session.exitstatus = EXIT_INTERRUPTED except: excinfo = py.code.ExceptionInfo() - config.pluginmanager.notify_exception(excinfo, config.option) + config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught Spurious SystemExit!\n") diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 3d6ff2933..dc9236089 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -375,8 +375,8 @@ class TmpTestdir: break else: args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp')) - import _pytest.core - config = _pytest.core._prepareconfig(args, self.plugins) + import _pytest.config + config = _pytest.config._prepareconfig(args, self.plugins) # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 82ec17885..e55390a23 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -342,6 +342,7 @@ class TerminalReporter: if exitstatus in (0, 1, 2, 4): self.summary_errors() self.summary_failures() + self.summary_hints() self.config.hook.pytest_terminal_summary(terminalreporter=self) if exitstatus == 2: self._report_keyboardinterrupt() @@ -407,6 +408,11 @@ class TerminalReporter: l.append(x) return l + def summary_hints(self): + if self.config.option.traceconfig: + for hint in self.config.pluginmanager._hints: + self._tw.line("hint: %s" % hint) + def summary_failures(self): if self.config.option.tbstyle != "no": reports = self.getreports('failed') diff --git a/pytest.py b/pytest.py index 9897780b2..6c25c6195 100644 --- a/pytest.py +++ b/pytest.py @@ -8,9 +8,11 @@ if __name__ == '__main__': # if run as a script or by 'python -m pytest' # we trigger the below "else" condition by the following import import pytest raise SystemExit(pytest.main()) -else: - # we are simply imported - from _pytest.core import main, UsageError, _preloadplugins - from _pytest import core as cmdline - from _pytest import __version__ - _preloadplugins() # to populate pytest.* namespace so help(pytest) works + +# else we are imported + +from _pytest.config import main, UsageError, _preloadplugins, cmdline +from _pytest import __version__ + +_preloadplugins() # to populate pytest.* namespace so help(pytest) works + diff --git a/testing/test_config.py b/testing/test_config.py index 912ebbe83..2c8bb13af 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -320,3 +320,18 @@ def test_cmdline_processargs_simple(testdir): def test_toolongargs_issue224(testdir): result = testdir.runpytest("-m", "hello" * 500) assert result.ret == 0 + +def test_notify_exception(testdir, capfd): + config = testdir.parseconfig() + excinfo = pytest.raises(ValueError, "raise ValueError(1)") + config.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + config.pluginmanager.register(A()) + config.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + diff --git a/testing/test_core.py b/testing/test_core.py index 3948b4695..a347e19a3 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,6 +1,7 @@ import pytest, py, os from _pytest.core import PluginManager from _pytest.core import MultiCall, HookRelay, varnames +from _pytest.config import get_plugin_manager class TestBootstrapping: @@ -149,7 +150,7 @@ class TestBootstrapping: mod = py.std.types.ModuleType("x") mod.pytest_plugins = "pytest_a" aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = PluginManager() + pluginmanager = get_plugin_manager() reprec = testdir.getreportrecorder(pluginmanager) #syspath.prepend(aplugin.dirpath()) py.std.sys.path.insert(0, str(aplugin.dirpath())) @@ -224,36 +225,21 @@ class TestBootstrapping: assert pp.isregistered(mod) def test_register_mismatch_method(self): - pp = PluginManager(load=True) + pp = get_plugin_manager() class hello: def pytest_gurgel(self): pass pytest.raises(Exception, "pp.register(hello())") def test_register_mismatch_arg(self): - pp = PluginManager(load=True) + pp = get_plugin_manager() class hello: def pytest_configure(self, asd): pass excinfo = pytest.raises(Exception, "pp.register(hello())") - - def test_notify_exception(self, capfd): - pp = PluginManager() - excinfo = pytest.raises(ValueError, "raise ValueError(1)") - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert "ValueError" in err - class A: - def pytest_internalerror(self, excrepr): - return True - pp.register(A()) - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert not err - def test_register(self): - pm = PluginManager(load=False) + pm = get_plugin_manager() class MyPlugin: pass my = MyPlugin() @@ -261,13 +247,13 @@ class TestBootstrapping: assert pm.getplugins() my2 = MyPlugin() pm.register(my2) - assert pm.getplugins()[2:] == [my, my2] + assert pm.getplugins()[-2:] == [my, my2] assert pm.isregistered(my) assert pm.isregistered(my2) pm.unregister(my) assert not pm.isregistered(my) - assert pm.getplugins()[2:] == [my2] + assert pm.getplugins()[-1:] == [my2] def test_listattr(self): plugins = PluginManager() @@ -284,7 +270,7 @@ class TestBootstrapping: assert l == [41, 42, 43] def test_hook_tracing(self): - pm = PluginManager() + pm = get_plugin_manager() saveindent = [] class api1: x = 41 @@ -319,7 +305,7 @@ class TestPytestPluginInteractions: def pytest_myhook(xyz): return xyz + 1 """) - config = PluginManager(load=True).config + config = get_plugin_manager().config config._conftest.importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 64600f686..ff745062f 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -104,7 +104,7 @@ def test_functional(testdir, linecomp): def test_func(_pytest): class ApiClass: def pytest_xyz(self, arg): "x" - hook = HookRelay([ApiClass], PluginManager(load=False)) + hook = HookRelay([ApiClass], PluginManager()) rec = _pytest.gethookrecorder(hook) class Plugin: def pytest_xyz(self, arg):