diff --git a/CHANGELOG b/CHANGELOG index 0d326ef48..49af784b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,10 @@ Thanks Daniel Hahler, Ashley C Straw, Philippe Gauthier and Pavel Savchenko 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 ----- diff --git a/_pytest/doctest.py b/_pytest/doctest.py index ddbe5fb8e..fd4a24790 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -5,6 +5,7 @@ import pytest, py from _pytest.python import FixtureRequest from py._code.code import TerminalRepr, ReprFileLocation + def pytest_addoption(parser): parser.addini('doctest_optionflags', 'option flags for doctests', type="args", default=["ELLIPSIS"]) @@ -22,6 +23,7 @@ def pytest_addoption(parser): help="ignore doctest ImportErrors", dest="doctest_ignore_import_errors") + def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": @@ -31,20 +33,33 @@ def pytest_collect_file(path, parent): path.check(fnmatch=config.getvalue("doctestglob")): return DoctestTextfile(path, parent) + class ReprFailDoctest(TerminalRepr): + def __init__(self, reprlocation, lines): self.reprlocation = reprlocation self.lines = lines + def toterminal(self, tw): for line in self.lines: tw.line(line) self.reprlocation.toterminal(tw) + class DoctestItem(pytest.Item): + def __init__(self, name, parent, runner=None, dtest=None): super(DoctestItem, self).__init__(name, parent) self.runner = runner 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): _check_all_skipped(self.dtest) @@ -94,6 +109,7 @@ class DoctestItem(pytest.Item): def reportinfo(self): return self.fspath, None, "[doctest] %s" % self.name + def _get_flag_lookup(): import doctest 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, ALLOW_UNICODE=_get_allow_unicode_flag()) + def get_optionflags(parent): optionflags_str = parent.config.getini("doctest_optionflags") flag_lookup_table = _get_flag_lookup() @@ -113,7 +130,7 @@ def get_optionflags(parent): return flag_acc -class DoctestTextfile(DoctestItem, pytest.File): +class DoctestTextfile(DoctestItem, pytest.Module): def runtest(self): import doctest @@ -148,7 +165,7 @@ def _check_all_skipped(test): pytest.skip('all tests skipped by +SKIP option') -class DoctestModule(pytest.File): +class DoctestModule(pytest.Module): def collect(self): import doctest if self.fspath.basename == "conftest.py": @@ -161,23 +178,19 @@ class DoctestModule(pytest.File): pytest.skip('unable to import module %r' % self.fspath) else: raise - # satisfy `FixtureRequest` constructor... - fixture_request = _setup_fixtures(self) - doctest_globals = dict(getfixture=fixture_request.getfuncargvalue) # uses internal doctest module parsing mechanism finder = doctest.DocTestFinder() optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, checker=_get_unicode_checker()) - for test in finder.find(module, module.__name__, - extraglobs=doctest_globals): + for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests yield DoctestItem(test.name, self, runner, test) 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(): pass diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 25a995add..88d90a7bf 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -371,38 +371,6 @@ class TestDoctests: "--junit-xml=junit.xml") 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']) def test_allow_unicode(self, testdir, config_mode): """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)) -class TestDocTestSkips: +class TestDoctestSkips: """ If all examples in a doctest are skipped due to the SKIP option, then the tests should be SKIPPED rather than PASSED. (#957) @@ -493,3 +461,122 @@ class TestDocTestSkips: """) reprec = testdir.inline_run("--doctest-modules") 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 *']) \ No newline at end of file