Skip to content

Commit db6f347

Browse files
committed
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.
1 parent 4b70903 commit db6f347

File tree

7 files changed

+80
-35
lines changed

7 files changed

+80
-35
lines changed

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ new features:
7575
- fix issue 308 - allow to mark/xfail/skip individual parameter sets
7676
when parametrizing. Thanks Brianna Laugher.
7777

78+
- call new experimental pytest_load_initial_conftests hook to allow
79+
3rd party plugins to do something before a conftest is loaded.
80+
7881
Bug fixes:
7982

8083
- pytest now uses argparse instead of optparse (thanks Anthon) which

_pytest/capture.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """
22

33
import pytest, py
4+
import sys
45
import os
56

67
def pytest_addoption(parser):
@@ -12,23 +13,34 @@ def pytest_addoption(parser):
1213
help="shortcut for --capture=no.")
1314

1415
@pytest.mark.tryfirst
15-
def pytest_cmdline_parse(pluginmanager, args):
16-
# we want to perform capturing already for plugin/conftest loading
17-
if '-s' in args or "--capture=no" in args:
18-
method = "no"
19-
elif hasattr(os, 'dup') and '--capture=sys' not in args:
16+
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
17+
ns = parser.parse_known_args(args)
18+
method = ns.capture
19+
if not method:
2020
method = "fd"
21-
else:
21+
if method == "fd" and not hasattr(os, "dup"):
2222
method = "sys"
2323
capman = CaptureManager(method)
24-
pluginmanager.register(capman, "capturemanager")
24+
early_config.pluginmanager.register(capman, "capturemanager")
2525
# make sure that capturemanager is properly reset at final shutdown
2626
def teardown():
2727
try:
2828
capman.reset_capturings()
2929
except ValueError:
3030
pass
31-
pluginmanager.add_shutdown(teardown)
31+
early_config.pluginmanager.add_shutdown(teardown)
32+
33+
# finally trigger conftest loading but while capturing (issue93)
34+
capman.resumecapture()
35+
try:
36+
try:
37+
return __multicall__.execute()
38+
finally:
39+
out, err = capman.suspendcapture()
40+
except:
41+
sys.stdout.write(out)
42+
sys.stderr.write(err)
43+
raise
3244

3345
def addouterr(rep, outerr):
3446
for secname, content in zip(["out", "err"], outerr):
@@ -89,7 +101,6 @@ def reset_capturings(self):
89101
for name, cap in self._method2capture.items():
90102
cap.reset()
91103

92-
93104
def resumecapture_item(self, item):
94105
method = self._getmethod(item.config, item.fspath)
95106
if not hasattr(item, 'outerr'):

_pytest/config.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,14 @@ def addoption(self, *opts, **attrs):
141141
self._anonymous.addoption(*opts, **attrs)
142142

143143
def parse(self, args):
144-
from _pytest._argcomplete import try_argcomplete, filescompleter
145-
self.optparser = optparser = MyOptionParser(self)
144+
from _pytest._argcomplete import try_argcomplete
145+
self.optparser = self._getparser()
146+
try_argcomplete(self.optparser)
147+
return self.optparser.parse_args([str(x) for x in args])
148+
149+
def _getparser(self):
150+
from _pytest._argcomplete import filescompleter
151+
optparser = MyOptionParser(self)
146152
groups = self._groups + [self._anonymous]
147153
for group in groups:
148154
if group.options:
@@ -155,15 +161,19 @@ def parse(self, args):
155161
# bash like autocompletion for dirs (appending '/')
156162
optparser.add_argument(FILE_OR_DIR, nargs='*'
157163
).completer=filescompleter
158-
try_argcomplete(self.optparser)
159-
return self.optparser.parse_args([str(x) for x in args])
164+
return optparser
160165

161166
def parse_setoption(self, args, option):
162167
parsedoption = self.parse(args)
163168
for name, value in parsedoption.__dict__.items():
164169
setattr(option, name, value)
165170
return getattr(parsedoption, FILE_OR_DIR)
166171

172+
def parse_known_args(self, args):
173+
optparser = self._getparser()
174+
args = [str(x) for x in args]
175+
return optparser.parse_known_args(args)[0]
176+
167177
def addini(self, name, help, type=None, default=None):
168178
""" register an ini-file option.
169179
@@ -635,9 +645,6 @@ def fromdictargs(cls, option_dict, args):
635645
""" constructor useable for subprocesses. """
636646
pluginmanager = get_plugin_manager()
637647
config = pluginmanager.config
638-
# XXX slightly crude way to initialize capturing
639-
import _pytest.capture
640-
_pytest.capture.pytest_cmdline_parse(config.pluginmanager, args)
641648
config._preparse(args, addopts=False)
642649
config.option.__dict__.update(option_dict)
643650
for x in config.option.plugins:
@@ -663,21 +670,9 @@ def _getmatchingplugins(self, fspath):
663670
plugins += self._conftest.getconftestmodules(fspath)
664671
return plugins
665672

666-
def _setinitialconftest(self, args):
667-
# capture output during conftest init (#issue93)
668-
# XXX introduce load_conftest hook to avoid needing to know
669-
# about capturing plugin here
670-
capman = self.pluginmanager.getplugin("capturemanager")
671-
capman.resumecapture()
672-
try:
673-
try:
674-
self._conftest.setinitial(args)
675-
finally:
676-
out, err = capman.suspendcapture() # logging might have got it
677-
except:
678-
sys.stdout.write(out)
679-
sys.stderr.write(err)
680-
raise
673+
def pytest_load_initial_conftests(self, parser, args):
674+
self._conftest.setinitial(args)
675+
pytest_load_initial_conftests.trylast = True
681676

682677
def _initini(self, args):
683678
self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
@@ -692,9 +687,8 @@ def _preparse(self, args, addopts=True):
692687
self.pluginmanager.consider_preparse(args)
693688
self.pluginmanager.consider_setuptools_entrypoints()
694689
self.pluginmanager.consider_env()
695-
self._setinitialconftest(args)
696-
if addopts:
697-
self.hook.pytest_cmdline_preparse(config=self, args=args)
690+
self.hook.pytest_load_initial_conftests(early_config=self,
691+
args=args, parser=self._parser)
698692

699693
def _checkversion(self):
700694
import pytest
@@ -715,6 +709,8 @@ def parse(self, args):
715709
"can only parse cmdline args at most once per Config object")
716710
self._origargs = args
717711
self._preparse(args)
712+
# XXX deprecated hook:
713+
self.hook.pytest_cmdline_preparse(config=self, args=args)
718714
self._parser.hints.extend(self.pluginmanager._hints)
719715
args = self._parser.parse_setoption(args, self.option)
720716
if not args:

_pytest/hookspec.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, args):
2020
pytest_cmdline_parse.firstresult = True
2121

2222
def pytest_cmdline_preparse(config, args):
23-
"""modify command line arguments before option parsing. """
23+
"""(deprecated) modify command line arguments before option parsing. """
2424

2525
def pytest_addoption(parser):
2626
"""register argparse-style options and ini-style config values.
@@ -52,6 +52,10 @@ def pytest_cmdline_main(config):
5252
implementation will invoke the configure hooks and runtest_mainloop. """
5353
pytest_cmdline_main.firstresult = True
5454

55+
def pytest_load_initial_conftests(args, early_config, parser):
56+
""" implements loading initial conftests.
57+
"""
58+
5559
def pytest_configure(config):
5660
""" called after command line options have been parsed
5761
and all plugins and initial conftest files been loaded.

testing/test_capture.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,13 @@ def pytest_runtest_setup():
484484
result = testdir.runpytest()
485485
assert result.ret == 0
486486
assert 'hello19' not in result.stdout.str()
487+
488+
def test_capture_early_option_parsing(testdir):
489+
testdir.makeconftest("""
490+
def pytest_runtest_setup():
491+
print ("hello19")
492+
""")
493+
testdir.makepyfile("def test_func(): pass")
494+
result = testdir.runpytest("-vs")
495+
assert result.ret == 0
496+
assert 'hello19' in result.stdout.str()

testing/test_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,18 @@ def pytest_internalerror(self, excrepr):
335335
out, err = capfd.readouterr()
336336
assert not err
337337

338+
339+
def test_load_initial_conftest_last_ordering(testdir):
340+
from _pytest.config import get_plugin_manager
341+
pm = get_plugin_manager()
342+
class My:
343+
def pytest_load_initial_conftests(self):
344+
pass
345+
m = My()
346+
pm.register(m)
347+
l = pm.listattr("pytest_load_initial_conftests")
348+
assert l[-1].__module__ == "_pytest.capture"
349+
assert l[-2] == m.pytest_load_initial_conftests
350+
assert l[-3].__module__ == "_pytest.config"
351+
352+

testing/test_parseopt.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def test_parse2(self, parser):
101101
args = parser.parse([py.path.local()])
102102
assert getattr(args, parseopt.FILE_OR_DIR)[0] == py.path.local()
103103

104+
def test_parse_known_args(self, parser):
105+
args = parser.parse_known_args([py.path.local()])
106+
parser.addoption("--hello", action="store_true")
107+
ns = parser.parse_known_args(["x", "--y", "--hello", "this"])
108+
assert ns.hello
109+
104110
def test_parse_will_set_default(self, parser):
105111
parser.addoption("--hello", dest="hello", default="x", action="store")
106112
option = parser.parse([])

0 commit comments

Comments
 (0)