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
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

View File

@ -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'):

View File

@ -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:

View File

@ -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.

View File

@ -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()

View File

@ -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"

View File

@ -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([])