Merge pull request #1102 from nicoddemus/doctest-fixtures-fix

Fix autouse fixtures and doctest modules
This commit is contained in:
Ronny Pfannschmidt 2015-10-10 14:21:53 +02:00
commit db077555f6
3 changed files with 145 additions and 41 deletions

View File

@ -20,6 +20,10 @@
Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko
for contributing and Bruno Oliveira for the PR. for contributing and Bruno Oliveira for the PR.
- fix #1100 and #1057: errors when using autouse fixtures and doctest modules.
Thanks Sergey B Kirpichev and Vital Kudzelka for contributing and Bruno
Oliveira for the PR.
2.8.1 2.8.1
----- -----

View File

@ -5,6 +5,7 @@ import pytest, py
from _pytest.python import FixtureRequest from _pytest.python import FixtureRequest
from py._code.code import TerminalRepr, ReprFileLocation from py._code.code import TerminalRepr, ReprFileLocation
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addini('doctest_optionflags', 'option flags for doctests', parser.addini('doctest_optionflags', 'option flags for doctests',
type="args", default=["ELLIPSIS"]) type="args", default=["ELLIPSIS"])
@ -22,6 +23,7 @@ def pytest_addoption(parser):
help="ignore doctest ImportErrors", help="ignore doctest ImportErrors",
dest="doctest_ignore_import_errors") dest="doctest_ignore_import_errors")
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
config = parent.config config = parent.config
if path.ext == ".py": if path.ext == ".py":
@ -31,20 +33,33 @@ def pytest_collect_file(path, parent):
path.check(fnmatch=config.getvalue("doctestglob")): path.check(fnmatch=config.getvalue("doctestglob")):
return DoctestTextfile(path, parent) return DoctestTextfile(path, parent)
class ReprFailDoctest(TerminalRepr): class ReprFailDoctest(TerminalRepr):
def __init__(self, reprlocation, lines): def __init__(self, reprlocation, lines):
self.reprlocation = reprlocation self.reprlocation = reprlocation
self.lines = lines self.lines = lines
def toterminal(self, tw): def toterminal(self, tw):
for line in self.lines: for line in self.lines:
tw.line(line) tw.line(line)
self.reprlocation.toterminal(tw) self.reprlocation.toterminal(tw)
class DoctestItem(pytest.Item): class DoctestItem(pytest.Item):
def __init__(self, name, parent, runner=None, dtest=None): def __init__(self, name, parent, runner=None, dtest=None):
super(DoctestItem, self).__init__(name, parent) super(DoctestItem, self).__init__(name, parent)
self.runner = runner self.runner = runner
self.dtest = dtest self.dtest = dtest
self.obj = None
self.fixture_request = None
def setup(self):
if self.dtest is not None:
self.fixture_request = _setup_fixtures(self)
globs = dict(getfixture=self.fixture_request.getfuncargvalue)
self.dtest.globs.update(globs)
def runtest(self): def runtest(self):
_check_all_skipped(self.dtest) _check_all_skipped(self.dtest)
@ -94,6 +109,7 @@ class DoctestItem(pytest.Item):
def reportinfo(self): def reportinfo(self):
return self.fspath, None, "[doctest] %s" % self.name return self.fspath, None, "[doctest] %s" % self.name
def _get_flag_lookup(): def _get_flag_lookup():
import doctest import doctest
return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
@ -104,6 +120,7 @@ def _get_flag_lookup():
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
ALLOW_UNICODE=_get_allow_unicode_flag()) ALLOW_UNICODE=_get_allow_unicode_flag())
def get_optionflags(parent): def get_optionflags(parent):
optionflags_str = parent.config.getini("doctest_optionflags") optionflags_str = parent.config.getini("doctest_optionflags")
flag_lookup_table = _get_flag_lookup() flag_lookup_table = _get_flag_lookup()
@ -113,7 +130,7 @@ def get_optionflags(parent):
return flag_acc return flag_acc
class DoctestTextfile(DoctestItem, pytest.File): class DoctestTextfile(DoctestItem, pytest.Module):
def runtest(self): def runtest(self):
import doctest import doctest
@ -148,7 +165,7 @@ def _check_all_skipped(test):
pytest.skip('all tests skipped by +SKIP option') pytest.skip('all tests skipped by +SKIP option')
class DoctestModule(pytest.File): class DoctestModule(pytest.Module):
def collect(self): def collect(self):
import doctest import doctest
if self.fspath.basename == "conftest.py": if self.fspath.basename == "conftest.py":
@ -161,23 +178,19 @@ class DoctestModule(pytest.File):
pytest.skip('unable to import module %r' % self.fspath) pytest.skip('unable to import module %r' % self.fspath)
else: else:
raise raise
# satisfy `FixtureRequest` constructor...
fixture_request = _setup_fixtures(self)
doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
# uses internal doctest module parsing mechanism # uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder() finder = doctest.DocTestFinder()
optionflags = get_optionflags(self) optionflags = get_optionflags(self)
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
checker=_get_unicode_checker()) checker=_get_unicode_checker())
for test in finder.find(module, module.__name__, for test in finder.find(module, module.__name__):
extraglobs=doctest_globals):
if test.examples: # skip empty doctests if test.examples: # skip empty doctests
yield DoctestItem(test.name, self, runner, test) yield DoctestItem(test.name, self, runner, test)
def _setup_fixtures(doctest_item): def _setup_fixtures(doctest_item):
""" """
Used by DoctestTextfile and DoctestModule to setup fixture information. Used by DoctestTextfile and DoctestItem to setup fixture information.
""" """
def func(): def func():
pass pass

View File

@ -371,38 +371,6 @@ class TestDoctests:
"--junit-xml=junit.xml") "--junit-xml=junit.xml")
reprec.assertoutcome(failed=1) reprec.assertoutcome(failed=1)
def test_doctest_module_session_fixture(self, testdir):
"""Test that session fixtures are initialized for doctest modules (#768)
"""
# session fixture which changes some global data, which will
# be accessed by doctests in a module
testdir.makeconftest("""
import pytest
import sys
@pytest.yield_fixture(autouse=True, scope='session')
def myfixture():
assert not hasattr(sys, 'pytest_session_data')
sys.pytest_session_data = 1
yield
del sys.pytest_session_data
""")
testdir.makepyfile(foo="""
import sys
def foo():
'''
>>> assert sys.pytest_session_data == 1
'''
def bar():
'''
>>> assert sys.pytest_session_data == 1
'''
""")
result = testdir.runpytest("--doctest-modules")
result.stdout.fnmatch_lines('*2 passed*')
@pytest.mark.parametrize('config_mode', ['ini', 'comment']) @pytest.mark.parametrize('config_mode', ['ini', 'comment'])
def test_allow_unicode(self, testdir, config_mode): def test_allow_unicode(self, testdir, config_mode):
"""Test that doctests which output unicode work in all python versions """Test that doctests which output unicode work in all python versions
@ -446,7 +414,7 @@ class TestDoctests:
reprec.assertoutcome(passed=passed, failed=int(not passed)) reprec.assertoutcome(passed=passed, failed=int(not passed))
class TestDocTestSkips: class TestDoctestSkips:
""" """
If all examples in a doctest are skipped due to the SKIP option, then If all examples in a doctest are skipped due to the SKIP option, then
the tests should be SKIPPED rather than PASSED. (#957) the tests should be SKIPPED rather than PASSED. (#957)
@ -493,3 +461,122 @@ class TestDocTestSkips:
""") """)
reprec = testdir.inline_run("--doctest-modules") reprec = testdir.inline_run("--doctest-modules")
reprec.assertoutcome(skipped=1) reprec.assertoutcome(skipped=1)
class TestDoctestAutoUseFixtures:
SCOPES = ['module', 'session', 'class', 'function']
def test_doctest_module_session_fixture(self, testdir):
"""Test that session fixtures are initialized for doctest modules (#768)
"""
# session fixture which changes some global data, which will
# be accessed by doctests in a module
testdir.makeconftest("""
import pytest
import sys
@pytest.yield_fixture(autouse=True, scope='session')
def myfixture():
assert not hasattr(sys, 'pytest_session_data')
sys.pytest_session_data = 1
yield
del sys.pytest_session_data
""")
testdir.makepyfile(foo="""
import sys
def foo():
'''
>>> assert sys.pytest_session_data == 1
'''
def bar():
'''
>>> assert sys.pytest_session_data == 1
'''
""")
result = testdir.runpytest("--doctest-modules")
result.stdout.fnmatch_lines('*2 passed*')
@pytest.mark.parametrize('scope', SCOPES)
@pytest.mark.parametrize('enable_doctest', [True, False])
def test_fixture_scopes(self, testdir, scope, enable_doctest):
"""Test that auto-use fixtures work properly with doctest modules.
See #1057 and #1100.
"""
testdir.makeconftest('''
import pytest
@pytest.fixture(autouse=True, scope="{scope}")
def auto(request):
return 99
'''.format(scope=scope))
testdir.makepyfile(test_1='''
def test_foo():
"""
>>> getfixture('auto') + 1
100
"""
def test_bar():
assert 1
''')
params = ('--doctest-modules',) if enable_doctest else ()
passes = 3 if enable_doctest else 2
result = testdir.runpytest(*params)
result.stdout.fnmatch_lines(['*=== %d passed in *' % passes])
@pytest.mark.parametrize('scope', SCOPES)
@pytest.mark.parametrize('autouse', [True, False])
@pytest.mark.parametrize('use_fixture_in_doctest', [True, False])
def test_fixture_module_doctest_scopes(self, testdir, scope, autouse,
use_fixture_in_doctest):
"""Test that auto-use fixtures work properly with doctest files.
See #1057 and #1100.
"""
testdir.makeconftest('''
import pytest
@pytest.fixture(autouse={autouse}, scope="{scope}")
def auto(request):
return 99
'''.format(scope=scope, autouse=autouse))
if use_fixture_in_doctest:
testdir.maketxtfile(test_doc="""
>>> getfixture('auto')
99
""")
else:
testdir.maketxtfile(test_doc="""
>>> 1 + 1
2
""")
result = testdir.runpytest('--doctest-modules')
assert 'FAILURES' not in str(result.stdout.str())
result.stdout.fnmatch_lines(['*=== 1 passed in *'])
@pytest.mark.parametrize('scope', SCOPES)
def test_auto_use_request_attributes(self, testdir, scope):
"""Check that all attributes of a request in an autouse fixture
behave as expected when requested for a doctest item.
"""
testdir.makeconftest('''
import pytest
@pytest.fixture(autouse=True, scope="{scope}")
def auto(request):
if "{scope}" == 'module':
assert request.module is None
if "{scope}" == 'class':
assert request.cls is None
if "{scope}" == 'function':
assert request.function is None
return 99
'''.format(scope=scope))
testdir.maketxtfile(test_doc="""
>>> 1 + 1
2
""")
result = testdir.runpytest('--doctest-modules')
assert 'FAILURES' not in str(result.stdout.str())
result.stdout.fnmatch_lines(['*=== 1 passed in *'])