From db6f347db6d3f2db5aa2cf2d27630db5ab99d589 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 30 Sep 2013 13:14:16 +0200 Subject: [PATCH] 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. --- CHANGELOG | 3 +++ _pytest/capture.py | 29 +++++++++++++++++-------- _pytest/config.py | 46 ++++++++++++++++++---------------------- _pytest/hookspec.py | 6 +++++- testing/test_capture.py | 10 +++++++++ testing/test_config.py | 15 +++++++++++++ testing/test_parseopt.py | 6 ++++++ 7 files changed, 80 insertions(+), 35 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 23495eefe..27a6cacb6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -75,6 +75,9 @@ new features: - 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 --git a/_pytest/capture.py b/_pytest/capture.py index a372faabb..8fd4cafdf 100644 --- 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 @@ def pytest_addoption(parser): 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 @@ class CaptureManager: 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 --git a/_pytest/config.py b/_pytest/config.py index 4aab1fae6..72276f853 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -141,8 +141,14 @@ class Parser: 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 @@ class Parser: # 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 @@ class Parser: 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 @@ class Config(object): """ 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 @@ class Config(object): 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 @@ class Config(object): 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 @@ class Config(object): "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 --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 9fe302058..44af3b21b 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, args): 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 @@ def pytest_cmdline_main(config): 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 --git a/testing/test_capture.py b/testing/test_capture.py index 8f40d1332..3fc2404ee 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -484,3 +484,13 @@ def test_capture_conftest_runtest_setup(testdir): 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 --git a/testing/test_config.py b/testing/test_config.py index 2c8bb13af..9a8cab20a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -335,3 +335,18 @@ def test_notify_exception(testdir, capfd): 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 --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 99433ecc8..245706332 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -101,6 +101,12 @@ class TestParser: 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([])