Skip to content

Commit a14c77a

Browse files
committed
Fix problems when mixing autouse fixtures and doctest modules
The main problem was that previously DoctestModule was setting up its fixtures during collection, instead of letting each DoctestItem make its own fixture setup Fix #1100 Fix #1057
1 parent 5171d16 commit a14c77a

File tree

3 files changed

+145
-41
lines changed

3 files changed

+145
-41
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko
1515
for contributing and Bruno Oliveira for the PR.
1616

17+
- fix #1100 and #1057: errors when using autouse fixtures and doctest modules.
18+
Thanks Sergey B Kirpichev and Vital Kudzelka for contributing and Bruno
19+
Oliveira for the PR.
20+
1721
2.8.1
1822
-----
1923

_pytest/doctest.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from _pytest.python import FixtureRequest
66
from py._code.code import TerminalRepr, ReprFileLocation
77

8+
89
def pytest_addoption(parser):
910
parser.addini('doctest_optionflags', 'option flags for doctests',
1011
type="args", default=["ELLIPSIS"])
@@ -22,6 +23,7 @@ def pytest_addoption(parser):
2223
help="ignore doctest ImportErrors",
2324
dest="doctest_ignore_import_errors")
2425

26+
2527
def pytest_collect_file(path, parent):
2628
config = parent.config
2729
if path.ext == ".py":
@@ -31,20 +33,33 @@ def pytest_collect_file(path, parent):
3133
path.check(fnmatch=config.getvalue("doctestglob")):
3234
return DoctestTextfile(path, parent)
3335

36+
3437
class ReprFailDoctest(TerminalRepr):
38+
3539
def __init__(self, reprlocation, lines):
3640
self.reprlocation = reprlocation
3741
self.lines = lines
42+
3843
def toterminal(self, tw):
3944
for line in self.lines:
4045
tw.line(line)
4146
self.reprlocation.toterminal(tw)
4247

48+
4349
class DoctestItem(pytest.Item):
50+
4451
def __init__(self, name, parent, runner=None, dtest=None):
4552
super(DoctestItem, self).__init__(name, parent)
4653
self.runner = runner
4754
self.dtest = dtest
55+
self.obj = None
56+
self.fixture_request = None
57+
58+
def setup(self):
59+
if self.dtest is not None:
60+
self.fixture_request = _setup_fixtures(self)
61+
globs = dict(getfixture=self.fixture_request.getfuncargvalue)
62+
self.dtest.globs.update(globs)
4863

4964
def runtest(self):
5065
_check_all_skipped(self.dtest)
@@ -94,6 +109,7 @@ def repr_failure(self, excinfo):
94109
def reportinfo(self):
95110
return self.fspath, None, "[doctest] %s" % self.name
96111

112+
97113
def _get_flag_lookup():
98114
import doctest
99115
return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
@@ -104,6 +120,7 @@ def _get_flag_lookup():
104120
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
105121
ALLOW_UNICODE=_get_allow_unicode_flag())
106122

123+
107124
def get_optionflags(parent):
108125
optionflags_str = parent.config.getini("doctest_optionflags")
109126
flag_lookup_table = _get_flag_lookup()
@@ -113,7 +130,7 @@ def get_optionflags(parent):
113130
return flag_acc
114131

115132

116-
class DoctestTextfile(DoctestItem, pytest.File):
133+
class DoctestTextfile(DoctestItem, pytest.Module):
117134

118135
def runtest(self):
119136
import doctest
@@ -148,7 +165,7 @@ def _check_all_skipped(test):
148165
pytest.skip('all tests skipped by +SKIP option')
149166

150167

151-
class DoctestModule(pytest.File):
168+
class DoctestModule(pytest.Module):
152169
def collect(self):
153170
import doctest
154171
if self.fspath.basename == "conftest.py":
@@ -161,23 +178,19 @@ def collect(self):
161178
pytest.skip('unable to import module %r' % self.fspath)
162179
else:
163180
raise
164-
# satisfy `FixtureRequest` constructor...
165-
fixture_request = _setup_fixtures(self)
166-
doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
167181
# uses internal doctest module parsing mechanism
168182
finder = doctest.DocTestFinder()
169183
optionflags = get_optionflags(self)
170184
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
171185
checker=_get_unicode_checker())
172-
for test in finder.find(module, module.__name__,
173-
extraglobs=doctest_globals):
186+
for test in finder.find(module, module.__name__):
174187
if test.examples: # skip empty doctests
175188
yield DoctestItem(test.name, self, runner, test)
176189

177190

178191
def _setup_fixtures(doctest_item):
179192
"""
180-
Used by DoctestTextfile and DoctestModule to setup fixture information.
193+
Used by DoctestTextfile and DoctestItem to setup fixture information.
181194
"""
182195
def func():
183196
pass

testing/test_doctest.py

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -371,38 +371,6 @@ def foo():
371371
"--junit-xml=junit.xml")
372372
reprec.assertoutcome(failed=1)
373373

374-
def test_doctest_module_session_fixture(self, testdir):
375-
"""Test that session fixtures are initialized for doctest modules (#768)
376-
"""
377-
# session fixture which changes some global data, which will
378-
# be accessed by doctests in a module
379-
testdir.makeconftest("""
380-
import pytest
381-
import sys
382-
383-
@pytest.yield_fixture(autouse=True, scope='session')
384-
def myfixture():
385-
assert not hasattr(sys, 'pytest_session_data')
386-
sys.pytest_session_data = 1
387-
yield
388-
del sys.pytest_session_data
389-
""")
390-
testdir.makepyfile(foo="""
391-
import sys
392-
393-
def foo():
394-
'''
395-
>>> assert sys.pytest_session_data == 1
396-
'''
397-
398-
def bar():
399-
'''
400-
>>> assert sys.pytest_session_data == 1
401-
'''
402-
""")
403-
result = testdir.runpytest("--doctest-modules")
404-
result.stdout.fnmatch_lines('*2 passed*')
405-
406374
@pytest.mark.parametrize('config_mode', ['ini', 'comment'])
407375
def test_allow_unicode(self, testdir, config_mode):
408376
"""Test that doctests which output unicode work in all python versions
@@ -446,7 +414,7 @@ def test_unicode_string(self, testdir):
446414
reprec.assertoutcome(passed=passed, failed=int(not passed))
447415

448416

449-
class TestDocTestSkips:
417+
class TestDoctestSkips:
450418
"""
451419
If all examples in a doctest are skipped due to the SKIP option, then
452420
the tests should be SKIPPED rather than PASSED. (#957)
@@ -493,3 +461,122 @@ def test_all_skipped(self, testdir, makedoctest):
493461
""")
494462
reprec = testdir.inline_run("--doctest-modules")
495463
reprec.assertoutcome(skipped=1)
464+
465+
466+
class TestDoctestAutoUseFixtures:
467+
468+
SCOPES = ['module', 'session', 'class', 'function']
469+
470+
def test_doctest_module_session_fixture(self, testdir):
471+
"""Test that session fixtures are initialized for doctest modules (#768)
472+
"""
473+
# session fixture which changes some global data, which will
474+
# be accessed by doctests in a module
475+
testdir.makeconftest("""
476+
import pytest
477+
import sys
478+
479+
@pytest.yield_fixture(autouse=True, scope='session')
480+
def myfixture():
481+
assert not hasattr(sys, 'pytest_session_data')
482+
sys.pytest_session_data = 1
483+
yield
484+
del sys.pytest_session_data
485+
""")
486+
testdir.makepyfile(foo="""
487+
import sys
488+
489+
def foo():
490+
'''
491+
>>> assert sys.pytest_session_data == 1
492+
'''
493+
494+
def bar():
495+
'''
496+
>>> assert sys.pytest_session_data == 1
497+
'''
498+
""")
499+
result = testdir.runpytest("--doctest-modules")
500+
result.stdout.fnmatch_lines('*2 passed*')
501+
502+
@pytest.mark.parametrize('scope', SCOPES)
503+
@pytest.mark.parametrize('enable_doctest', [True, False])
504+
def test_fixture_scopes(self, testdir, scope, enable_doctest):
505+
"""Test that auto-use fixtures work properly with doctest modules.
506+
See #1057 and #1100.
507+
"""
508+
testdir.makeconftest('''
509+
import pytest
510+
511+
@pytest.fixture(autouse=True, scope="{scope}")
512+
def auto(request):
513+
return 99
514+
'''.format(scope=scope))
515+
testdir.makepyfile(test_1='''
516+
def test_foo():
517+
"""
518+
>>> getfixture('auto') + 1
519+
100
520+
"""
521+
def test_bar():
522+
assert 1
523+
''')
524+
params = ('--doctest-modules',) if enable_doctest else ()
525+
passes = 3 if enable_doctest else 2
526+
result = testdir.runpytest(*params)
527+
result.stdout.fnmatch_lines(['*=== %d passed in *' % passes])
528+
529+
@pytest.mark.parametrize('scope', SCOPES)
530+
@pytest.mark.parametrize('autouse', [True, False])
531+
@pytest.mark.parametrize('use_fixture_in_doctest', [True, False])
532+
def test_fixture_module_doctest_scopes(self, testdir, scope, autouse,
533+
use_fixture_in_doctest):
534+
"""Test that auto-use fixtures work properly with doctest files.
535+
See #1057 and #1100.
536+
"""
537+
testdir.makeconftest('''
538+
import pytest
539+
540+
@pytest.fixture(autouse={autouse}, scope="{scope}")
541+
def auto(request):
542+
return 99
543+
'''.format(scope=scope, autouse=autouse))
544+
if use_fixture_in_doctest:
545+
testdir.maketxtfile(test_doc="""
546+
>>> getfixture('auto')
547+
99
548+
""")
549+
else:
550+
testdir.maketxtfile(test_doc="""
551+
>>> 1 + 1
552+
2
553+
""")
554+
result = testdir.runpytest('--doctest-modules')
555+
assert 'FAILURES' not in str(result.stdout.str())
556+
result.stdout.fnmatch_lines(['*=== 1 passed in *'])
557+
558+
@pytest.mark.parametrize('scope', SCOPES)
559+
def test_auto_use_request_attributes(self, testdir, scope):
560+
"""Check that all attributes of a request in an autouse fixture
561+
behave as expected when requested for a doctest item.
562+
"""
563+
testdir.makeconftest('''
564+
import pytest
565+
566+
@pytest.fixture(autouse=True, scope="{scope}")
567+
def auto(request):
568+
if "{scope}" == 'module':
569+
assert request.module is None
570+
if "{scope}" == 'class':
571+
assert request.cls is None
572+
if "{scope}" == 'function':
573+
assert request.function is None
574+
return 99
575+
'''.format(scope=scope))
576+
testdir.maketxtfile(test_doc="""
577+
>>> 1 + 1
578+
2
579+
""")
580+
result = testdir.runpytest('--doctest-modules')
581+
assert 'FAILURES' not in str(result.stdout.str())
582+
result.stdout.fnmatch_lines(['*=== 1 passed in *'])

0 commit comments

Comments
 (0)