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
This commit is contained in:
parent
5171d167ce
commit
a14c77aeba
|
@ -14,6 +14,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
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 *'])
|
Loading…
Reference in New Issue