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.
This commit is contained in:
holger krekel 2013-09-30 13:14:16 +02:00
parent 4b709037ab
commit db6f347db6
7 changed files with 80 additions and 35 deletions

View File

@ -75,6 +75,9 @@ new features:
- fix issue 308 - allow to mark/xfail/skip individual parameter sets - fix issue 308 - allow to mark/xfail/skip individual parameter sets
when parametrizing. Thanks Brianna Laugher. 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: Bug fixes:
- pytest now uses argparse instead of optparse (thanks Anthon) which - pytest now uses argparse instead of optparse (thanks Anthon) which

View File

@ -1,6 +1,7 @@
""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ """ per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """
import pytest, py import pytest, py
import sys
import os import os
def pytest_addoption(parser): def pytest_addoption(parser):
@ -12,23 +13,34 @@ def pytest_addoption(parser):
help="shortcut for --capture=no.") help="shortcut for --capture=no.")
@pytest.mark.tryfirst @pytest.mark.tryfirst
def pytest_cmdline_parse(pluginmanager, args): def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
# we want to perform capturing already for plugin/conftest loading ns = parser.parse_known_args(args)
if '-s' in args or "--capture=no" in args: method = ns.capture
method = "no" if not method:
elif hasattr(os, 'dup') and '--capture=sys' not in args:
method = "fd" method = "fd"
else: if method == "fd" and not hasattr(os, "dup"):
method = "sys" method = "sys"
capman = CaptureManager(method) capman = CaptureManager(method)
pluginmanager.register(capman, "capturemanager") early_config.pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown # make sure that capturemanager is properly reset at final shutdown
def teardown(): def teardown():
try: try:
capman.reset_capturings() capman.reset_capturings()
except ValueError: except ValueError:
pass 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): def addouterr(rep, outerr):
for secname, content in zip(["out", "err"], outerr): for secname, content in zip(["out", "err"], outerr):
@ -89,7 +101,6 @@ class CaptureManager:
for name, cap in self._method2capture.items(): for name, cap in self._method2capture.items():
cap.reset() cap.reset()
def resumecapture_item(self, item): def resumecapture_item(self, item):
method = self._getmethod(item.config, item.fspath) method = self._getmethod(item.config, item.fspath)
if not hasattr(item, 'outerr'): if not hasattr(item, 'outerr'):

View File

@ -141,8 +141,14 @@ class Parser:
self._anonymous.addoption(*opts, **attrs) self._anonymous.addoption(*opts, **attrs)
def parse(self, args): def parse(self, args):
from _pytest._argcomplete import try_argcomplete, filescompleter from _pytest._argcomplete import try_argcomplete
self.optparser = optparser = MyOptionParser(self) 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] groups = self._groups + [self._anonymous]
for group in groups: for group in groups:
if group.options: if group.options:
@ -155,8 +161,7 @@ class Parser:
# bash like autocompletion for dirs (appending '/') # bash like autocompletion for dirs (appending '/')
optparser.add_argument(FILE_OR_DIR, nargs='*' optparser.add_argument(FILE_OR_DIR, nargs='*'
).completer=filescompleter ).completer=filescompleter
try_argcomplete(self.optparser) return optparser
return self.optparser.parse_args([str(x) for x in args])
def parse_setoption(self, args, option): def parse_setoption(self, args, option):
parsedoption = self.parse(args) parsedoption = self.parse(args)
@ -164,6 +169,11 @@ class Parser:
setattr(option, name, value) setattr(option, name, value)
return getattr(parsedoption, FILE_OR_DIR) 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): def addini(self, name, help, type=None, default=None):
""" register an ini-file option. """ register an ini-file option.
@ -635,9 +645,6 @@ class Config(object):
""" constructor useable for subprocesses. """ """ constructor useable for subprocesses. """
pluginmanager = get_plugin_manager() pluginmanager = get_plugin_manager()
config = pluginmanager.config 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._preparse(args, addopts=False)
config.option.__dict__.update(option_dict) config.option.__dict__.update(option_dict)
for x in config.option.plugins: for x in config.option.plugins:
@ -663,21 +670,9 @@ class Config(object):
plugins += self._conftest.getconftestmodules(fspath) plugins += self._conftest.getconftestmodules(fspath)
return plugins return plugins
def _setinitialconftest(self, args): def pytest_load_initial_conftests(self, parser, args):
# capture output during conftest init (#issue93) self._conftest.setinitial(args)
# XXX introduce load_conftest hook to avoid needing to know pytest_load_initial_conftests.trylast = True
# 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 _initini(self, args): def _initini(self, args):
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) 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_preparse(args)
self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_setuptools_entrypoints()
self.pluginmanager.consider_env() self.pluginmanager.consider_env()
self._setinitialconftest(args) self.hook.pytest_load_initial_conftests(early_config=self,
if addopts: args=args, parser=self._parser)
self.hook.pytest_cmdline_preparse(config=self, args=args)
def _checkversion(self): def _checkversion(self):
import pytest import pytest
@ -715,6 +709,8 @@ class Config(object):
"can only parse cmdline args at most once per Config object") "can only parse cmdline args at most once per Config object")
self._origargs = args self._origargs = args
self._preparse(args) self._preparse(args)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.hints.extend(self.pluginmanager._hints) self._parser.hints.extend(self.pluginmanager._hints)
args = self._parser.parse_setoption(args, self.option) args = self._parser.parse_setoption(args, self.option)
if not args: if not args:

View File

@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, args):
pytest_cmdline_parse.firstresult = True pytest_cmdline_parse.firstresult = True
def pytest_cmdline_preparse(config, args): 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): def pytest_addoption(parser):
"""register argparse-style options and ini-style config values. """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. """ implementation will invoke the configure hooks and runtest_mainloop. """
pytest_cmdline_main.firstresult = True pytest_cmdline_main.firstresult = True
def pytest_load_initial_conftests(args, early_config, parser):
""" implements loading initial conftests.
"""
def pytest_configure(config): def pytest_configure(config):
""" called after command line options have been parsed """ called after command line options have been parsed
and all plugins and initial conftest files been loaded. and all plugins and initial conftest files been loaded.

View File

@ -484,3 +484,13 @@ def test_capture_conftest_runtest_setup(testdir):
result = testdir.runpytest() result = testdir.runpytest()
assert result.ret == 0 assert result.ret == 0
assert 'hello19' not in result.stdout.str() 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()

View File

@ -335,3 +335,18 @@ def test_notify_exception(testdir, capfd):
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert not err 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"

View File

@ -101,6 +101,12 @@ class TestParser:
args = parser.parse([py.path.local()]) args = parser.parse([py.path.local()])
assert getattr(args, parseopt.FILE_OR_DIR)[0] == 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): def test_parse_will_set_default(self, parser):
parser.addoption("--hello", dest="hello", default="x", action="store") parser.addoption("--hello", dest="hello", default="x", action="store")
option = parser.parse([]) option = parser.parse([])