4 new commits in pytest: https://bitbucket.org/hpk42/pytest/commits/2d3ddee7ba00/ Changeset: 2d3ddee7ba00 User: hpk42 Date: 2013-09-30 10:19:06 Summary: localize some argcomplete-related functionality Affected #: 1 file
diff -r 1fe40df1127840c4aaf946c8dc955c2d7d6e1912 -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -2,15 +2,7 @@ import py import sys, os -from _pytest.core import PluginManager import pytest -from _pytest._argcomplete import try_argcomplete, filescompleter - -# enable after some grace period for plugin writers -TYPE_WARN = False -if TYPE_WARN: - import warnings - def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) @@ -82,6 +74,7 @@ self._anonymous.addoption(*opts, **attrs) def parse(self, args): + from _pytest._argcomplete import try_argcomplete, filescompleter self.optparser = optparser = MyOptionParser(self) groups = self._groups + [self._anonymous] for group in groups: @@ -142,6 +135,8 @@ 'int': int, 'string': str, } + # enable after some grace period for plugin writers + TYPE_WARN = False def __init__(self, *names, **attrs): """store parms in private vars for use in add_argument""" @@ -149,11 +144,11 @@ self._short_opts = [] self._long_opts = [] self.dest = attrs.get('dest') - if TYPE_WARN: + if self.TYPE_WARN: try: help = attrs['help'] if '%default' in help: - warnings.warn( + py.std.warnings.warn( 'py.test now uses argparse. "%default" should be' ' changed to "%(default)s" ', FutureWarning, @@ -168,8 +163,8 @@ # this might raise a keyerror as well, don't want to catch that if isinstance(typ, str): if typ == 'choice': - if TYPE_WARN: - warnings.warn( + if self.TYPE_WARN: + py.std.warnings.warn( 'type argument to addoption() is a string %r.' ' For parsearg this is optional and when supplied ' ' should be a type.' @@ -180,8 +175,8 @@ # the type of the first element attrs['type'] = type(attrs['choices'][0]) else: - if TYPE_WARN: - warnings.warn( + if self.TYPE_WARN: + py.std.warnings.warn( 'type argument to addoption() is a string %r.' ' For parsearg this should be a type.' ' (options: %s)' % (typ, names), https://bitbucket.org/hpk42/pytest/commits/1a7f73bd9982/ Changeset: 1a7f73bd9982 User: hpk42 Date: 2013-09-30 13:14:14 Summary: shift pytest_configure/unconfigure/addoption/namespace hook calling to config object. The _pytest.config module itself is no longer a plugin but the actual config instance is plugin-registered as ``pytestconfig``. This allows to put most pytest specific logic to _pytest.config instead of in the core pluginmanager. Affected #: 8 files diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -2,20 +2,6 @@ import py import sys, os -import pytest - -def pytest_cmdline_parse(pluginmanager, args): - config = Config(pluginmanager) - config.parse(args) - return config - -def pytest_unconfigure(config): - while 1: - try: - fin = config._cleanup.pop() - except IndexError: - break - fin() class Parser: """ Parser for command line arguments and ini-file values. """ @@ -507,13 +493,46 @@ self._inicache = {} self._opt2dest = {} self._cleanup = [] + self.pluginmanager.register(self, "pytestconfig") + self._configured = False + + def pytest_plugin_registered(self, plugin): + call_plugin = self.pluginmanager.call_plugin + dic = call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + import pytest + setns(pytest, dic) + call_plugin(plugin, "pytest_addoption", {'parser': self._parser}) + if self._configured: + call_plugin(plugin, "pytest_configure", {'config': self}) + + def do_configure(self): + assert not self._configured + self._configured = True + self.hook.pytest_configure(config=self) + + def do_unconfigure(self): + assert self._configured + self._configured = False + self.hook.pytest_unconfigure(config=self) + self.pluginmanager.ensure_shutdown() + + def pytest_cmdline_parse(self, pluginmanager, args): + assert self == pluginmanager.config, (self, pluginmanager.config) + self.parse(args) + return self + + def pytest_unconfigure(config): + while config._cleanup: + fin = config._cleanup.pop() + fin() @classmethod def fromdictargs(cls, option_dict, args): """ constructor useable for subprocesses. """ from _pytest.core import get_plugin_manager pluginmanager = get_plugin_manager() - config = cls(pluginmanager) + config = pluginmanager.config # XXX slightly crude way to initialize capturing import _pytest.capture _pytest.capture.pytest_cmdline_parse(config.pluginmanager, args) @@ -572,11 +591,11 @@ self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) if addopts: self.hook.pytest_cmdline_preparse(config=self, args=args) def _checkversion(self): + import pytest minver = self.inicfg.get('minversion', None) if minver: ver = minver.split(".") @@ -723,3 +742,23 @@ return iniconfig['pytest'] return {} + +def setns(obj, dic): + import pytest + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod + mod.__all__ = [] + setattr(obj, name, mod) + obj.__all__.append(name) + setns(mod, value) + else: + setattr(obj, name, value) + obj.__all__.append(name) + #if obj != pytest: + # pytest.__all__.append(name) + setattr(pytest, name, value) diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,17 +1,17 @@ """ pytest PluginManager, basic initialization and tracing. -(c) Holger Krekel 2004-2010 """ 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 = ( - "config mark main terminal runner python pdb unittest capture skipping " + "mark main terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml resultlog doctest").split() @@ -91,6 +91,7 @@ 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) @@ -100,7 +101,8 @@ return name = name or getattr(plugin, '__name__', str(id(plugin))) if self.isregistered(plugin, name): - raise ValueError("Plugin already registered: %s=%s" %(name, plugin)) + raise ValueError("Plugin already registered: %s=%s\n%s" %( + name, plugin, self._name2plugin)) #self.trace("registering", name, plugin) self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) @@ -220,7 +222,6 @@ if self.getplugin(modname) is not None: return try: - #self.trace("importing", modname) mod = importplugin(modname) except KeyboardInterrupt: raise @@ -247,59 +248,12 @@ "trylast: mark a hook implementation function such that the " "plugin machinery will try to call it last/as late as possible.") - def pytest_plugin_registered(self, plugin): - import pytest - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(pytest, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - import pytest - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - modname = "pytest.%s" % name - mod = py.std.types.ModuleType(modname) - sys.modules[modname] = mod - mod.__all__ = [] - setattr(obj, name, mod) - obj.__all__.append(name) - self._setns(mod, value) - else: - setattr(obj, name, value) - obj.__all__.append(name) - #if obj != pytest: - # pytest.__all__.append(name) - setattr(pytest, name, value) - 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 do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.ensure_shutdown() - def notify_exception(self, excinfo, option=None): if option and option.fulltrace: style = "long" @@ -350,6 +304,7 @@ name = importspec try: mod = "_pytest." + name + #print >>sys.stderr, "tryimport", mod __import__(mod) return sys.modules[mod] except ImportError: @@ -358,6 +313,7 @@ # raise pass # try: + #print >>sys.stderr, "tryimport", importspec __import__(importspec) except ImportError: raise ImportError(importspec) diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/helpconfig.py --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -54,9 +54,9 @@ sys.stderr.write(line + "\n") return 0 elif config.option.help: - config.pluginmanager.do_configure(config) + config.do_configure() showhelp(config) - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() return 0 def showhelp(config): diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -76,7 +76,7 @@ initstate = 0 try: try: - config.pluginmanager.do_configure(config) + config.do_configure() initstate = 1 config.hook.pytest_sessionstart(session=session) initstate = 2 @@ -105,7 +105,7 @@ session=session, exitstatus=session.exitstatus) if initstate >= 1: - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() config.pluginmanager.ensure_shutdown() return session.exitstatus diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/mark.py --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -import pytest, py +import py def pytest_namespace(): @@ -39,14 +39,14 @@ def pytest_cmdline_main(config): if config.option.markers: - config.pluginmanager.do_configure(config) + config.do_configure() tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() - config.pluginmanager.do_unconfigure(config) + config.do_unconfigure() return 0 pytest_cmdline_main.tryfirst = True @@ -129,6 +129,7 @@ mapped_names = set() # Add the names of the current item and any parent items + import pytest for item in colitem.listchain(): if not isinstance(item, pytest.Instance): mapped_names.add(item.name) @@ -145,6 +146,7 @@ def pytest_configure(config): + import pytest if config.option.strict: pytest.mark._config = config diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -390,9 +390,9 @@ def parseconfigure(self, *args): config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) + config.do_configure() self.request.addfinalizer(lambda: - config.pluginmanager.do_unconfigure(config)) + config.do_unconfigure()) return config def getitem(self, source, funcname="test_func"): diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -261,13 +261,13 @@ assert pm.getplugins() my2 = MyPlugin() pm.register(my2) - assert pm.getplugins()[1:] == [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()[1:] == [my2] + assert pm.getplugins()[2:] == [my2] def test_listattr(self): plugins = PluginManager() @@ -319,7 +319,7 @@ def pytest_myhook(xyz): return xyz + 1 """) - config = testdir.Config(PluginManager(load=True)) + config = PluginManager(load=True).config config._conftest.importconftest(conf) print(config.pluginmanager.getplugins()) res = config.hook.pytest_myhook(xyz=10) @@ -383,13 +383,13 @@ config.pluginmanager.register(A()) assert len(l) == 0 - config.pluginmanager.do_configure(config=config) + config.do_configure() assert len(l) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin assert len(l) == 2 assert l[0] != l[1] - config.pluginmanager.do_unconfigure(config=config) + config.do_unconfigure() config.pluginmanager.register(A()) assert len(l) == 2 diff -r 2d3ddee7ba00489c9d3da6cd12c4a2ac433e567f -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -208,13 +208,14 @@ testdir.parseconfig("-p", "nqweotexistent") """) #pytest.raises(ImportError, - # "config.pluginmanager.do_configure(config)" + # "config.do_configure(config)" #) def test_plugin_already_exists(testdir): config = testdir.parseconfig("-p", "terminal") assert config.option.plugins == ['terminal'] - config.pluginmanager.do_configure(config) + config.do_configure() + config.do_unconfigure() def test_exclude(testdir): hellodir = testdir.mkdir("hello") https://bitbucket.org/hpk42/pytest/commits/0371f8e21864/ Changeset: 0371f8e21864 User: hpk42 Date: 2013-09-30 13:14:14 Summary: 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. Affected #: 9 files diff -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d _pytest/config.py --- 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 @@ 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 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -4,17 +4,10 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -91,7 +91,7 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -375,8 +375,8 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -342,6 +342,7 @@ 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 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d pytest.py --- a/pytest.py +++ b/pytest.py @@ -8,9 +8,11 @@ # 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -320,3 +320,18 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d testing/test_core.py --- 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -r 1a7f73bd9982815bc9de2c3944171d69aa2658a0 -r 0371f8e218643f9c780189ade8f213016dea974d testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -104,7 +104,7 @@ 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): https://bitbucket.org/hpk42/pytest/commits/cd10ce8eb344/ Changeset: cd10ce8eb344 User: hpk42 Date: 2013-09-30 13:14:16 Summary: fix issue358 -- introduce new pytest_load_initial_conftests hook and make capturing initialization use it, relying on a new (somewhat internal) parser.parse_known_args() method. This also addresses issue359 -- plugins like pytest-django could implement a pytest_load_initial_conftests hook like the capture plugin. Affected #: 7 files diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -75,6 +75,9 @@ - fix issue 308 - allow to mark/xfail/skip individual parameter sets when parametrizing. Thanks Brianna Laugher. +- call new experimental pytest_load_initial_conftests hook to allow + 3rd party plugins to do something before a conftest is loaded. + Bug fixes: - pytest now uses argparse instead of optparse (thanks Anthon) which diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -1,6 +1,7 @@ """ per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ import pytest, py +import sys import os def pytest_addoption(parser): @@ -12,23 +13,34 @@ help="shortcut for --capture=no.") @pytest.mark.tryfirst -def pytest_cmdline_parse(pluginmanager, args): - # we want to perform capturing already for plugin/conftest loading - if '-s' in args or "--capture=no" in args: - method = "no" - elif hasattr(os, 'dup') and '--capture=sys' not in args: +def pytest_load_initial_conftests(early_config, parser, args, __multicall__): + ns = parser.parse_known_args(args) + method = ns.capture + if not method: method = "fd" - else: + if method == "fd" and not hasattr(os, "dup"): method = "sys" capman = CaptureManager(method) - pluginmanager.register(capman, "capturemanager") + early_config.pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown def teardown(): try: capman.reset_capturings() except ValueError: pass - pluginmanager.add_shutdown(teardown) + early_config.pluginmanager.add_shutdown(teardown) + + # finally trigger conftest loading but while capturing (issue93) + capman.resumecapture() + try: + try: + return __multicall__.execute() + finally: + out, err = capman.suspendcapture() + except: + sys.stdout.write(out) + sys.stderr.write(err) + raise def addouterr(rep, outerr): for secname, content in zip(["out", "err"], outerr): @@ -89,7 +101,6 @@ for name, cap in self._method2capture.items(): cap.reset() - def resumecapture_item(self, item): method = self._getmethod(item.config, item.fspath) if not hasattr(item, 'outerr'): diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -141,8 +141,14 @@ self._anonymous.addoption(*opts, **attrs) def parse(self, args): - from _pytest._argcomplete import try_argcomplete, filescompleter - self.optparser = optparser = MyOptionParser(self) + from _pytest._argcomplete import try_argcomplete + self.optparser = self._getparser() + try_argcomplete(self.optparser) + return self.optparser.parse_args([str(x) for x in args]) + + def _getparser(self): + from _pytest._argcomplete import filescompleter + optparser = MyOptionParser(self) groups = self._groups + [self._anonymous] for group in groups: if group.options: @@ -155,8 +161,7 @@ # bash like autocompletion for dirs (appending '/') optparser.add_argument(FILE_OR_DIR, nargs='*' ).completer=filescompleter - try_argcomplete(self.optparser) - return self.optparser.parse_args([str(x) for x in args]) + return optparser def parse_setoption(self, args, option): parsedoption = self.parse(args) @@ -164,6 +169,11 @@ setattr(option, name, value) return getattr(parsedoption, FILE_OR_DIR) + def parse_known_args(self, args): + optparser = self._getparser() + args = [str(x) for x in args] + return optparser.parse_known_args(args)[0] + def addini(self, name, help, type=None, default=None): """ register an ini-file option. @@ -635,9 +645,6 @@ """ constructor useable for subprocesses. """ pluginmanager = get_plugin_manager() config = pluginmanager.config - # XXX slightly crude way to initialize capturing - import _pytest.capture - _pytest.capture.pytest_cmdline_parse(config.pluginmanager, args) config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) for x in config.option.plugins: @@ -663,21 +670,9 @@ plugins += self._conftest.getconftestmodules(fspath) return plugins - def _setinitialconftest(self, args): - # capture output during conftest init (#issue93) - # XXX introduce load_conftest hook to avoid needing to know - # about capturing plugin here - capman = self.pluginmanager.getplugin("capturemanager") - capman.resumecapture() - try: - try: - self._conftest.setinitial(args) - finally: - out, err = capman.suspendcapture() # logging might have got it - except: - sys.stdout.write(out) - sys.stderr.write(err) - raise + def pytest_load_initial_conftests(self, parser, args): + self._conftest.setinitial(args) + pytest_load_initial_conftests.trylast = True def _initini(self, args): self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) @@ -692,9 +687,8 @@ self.pluginmanager.consider_preparse(args) self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() - self._setinitialconftest(args) - if addopts: - self.hook.pytest_cmdline_preparse(config=self, args=args) + self.hook.pytest_load_initial_conftests(early_config=self, + args=args, parser=self._parser) def _checkversion(self): import pytest @@ -715,6 +709,8 @@ "can only parse cmdline args at most once per Config object") self._origargs = args self._preparse(args) + # XXX deprecated hook: + self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.hints.extend(self.pluginmanager._hints) args = self._parser.parse_setoption(args, self.option) if not args: diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f _pytest/hookspec.py --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -20,7 +20,7 @@ pytest_cmdline_parse.firstresult = True def pytest_cmdline_preparse(config, args): - """modify command line arguments before option parsing. """ + """(deprecated) modify command line arguments before option parsing. """ def pytest_addoption(parser): """register argparse-style options and ini-style config values. @@ -52,6 +52,10 @@ implementation will invoke the configure hooks and runtest_mainloop. """ pytest_cmdline_main.firstresult = True +def pytest_load_initial_conftests(args, early_config, parser): + """ implements loading initial conftests. + """ + def pytest_configure(config): """ called after command line options have been parsed and all plugins and initial conftest files been loaded. diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -484,3 +484,13 @@ result = testdir.runpytest() assert result.ret == 0 assert 'hello19' not in result.stdout.str() + +def test_capture_early_option_parsing(testdir): + testdir.makeconftest(""" + def pytest_runtest_setup(): + print ("hello19") + """) + testdir.makepyfile("def test_func(): pass") + result = testdir.runpytest("-vs") + assert result.ret == 0 + assert 'hello19' in result.stdout.str() diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -335,3 +335,18 @@ out, err = capfd.readouterr() assert not err + +def test_load_initial_conftest_last_ordering(testdir): + from _pytest.config import get_plugin_manager + pm = get_plugin_manager() + class My: + def pytest_load_initial_conftests(self): + pass + m = My() + pm.register(m) + l = pm.listattr("pytest_load_initial_conftests") + assert l[-1].__module__ == "_pytest.capture" + assert l[-2] == m.pytest_load_initial_conftests + assert l[-3].__module__ == "_pytest.config" + + diff -r 0371f8e218643f9c780189ade8f213016dea974d -r cd10ce8eb3440922c538b67256cbf92b09a29b6f testing/test_parseopt.py --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -101,6 +101,12 @@ args = parser.parse([py.path.local()]) assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local() + def test_parse_known_args(self, parser): + args = parser.parse_known_args([py.path.local()]) + parser.addoption("--hello", action="store_true") + ns = parser.parse_known_args(["x", "--y", "--hello", "this"]) + assert ns.hello + def test_parse_will_set_default(self, parser): parser.addoption("--hello", dest="hello", default="x", action="store") option = parser.parse([]) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit