Skip to content

Commit db07755

Browse files
Merge pull request #1102 from nicoddemus/doctest-fixtures-fix
Fix autouse fixtures and doctest modules
2 parents b052bec + a14c77a commit db07755

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
@@ -20,6 +20,10 @@
2020
Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko
2121
for contributing and Bruno Oliveira for the PR.
2222

23+
- fix #1100 and #1057: errors when using autouse fixtures and doctest modules.
24+
Thanks Sergey B Kirpichev and Vital Kudzelka for contributing and Bruno
25+
Oliveira for the PR.
26+
2327
2.8.1
2428
-----
2529

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