diff --git a/CHANGES.rst b/CHANGES.rst index 9286d81..a753c47 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,8 @@ 1.6.0 (unreleased) ================== - +- Ensure that tests skipped with `__doctest_skip__` and `__doctest_requires__` + show up as skipped tests in Pytest's output. [#312] 1.5.0 (2025-10-17) ================== diff --git a/pytest_doctestplus/plugin.py b/pytest_doctestplus/plugin.py index b94446b..8f74a09 100644 --- a/pytest_doctestplus/plugin.py +++ b/pytest_doctestplus/plugin.py @@ -879,14 +879,17 @@ def find(self, obj, name=None, module=None, globs=None, extraglobs=None): if hasattr(obj, '__doctest_skip__') or hasattr(obj, '__doctest_requires__'): - def test_filter(test): + def conditionally_insert_skip(test): + """ + Insert skip statement if `test` matches `__doctest_(skip|requires)__`. + """ for pat in getattr(obj, '__doctest_skip__', []): if pat == '*': - return False + self._prepend_skip(test) elif pat == '.' and test.name == name: - return False + self._prepend_skip(test) elif fnmatch.fnmatch(test.name, '.'.join((name, pat))): - return False + self._prepend_skip(test) reqs = getattr(obj, '__doctest_requires__', {}) for pats, mods in reqs.items(): @@ -903,14 +906,38 @@ def test_filter(test): else: continue # The pattern does not apply - if not self.check_required_modules(mods): - return False + for mod in mods: + self._prepend_importorskip(test, module=mod) return True - tests = list(filter(test_filter, tests)) + for _test in tests: + conditionally_insert_skip(_test) return tests + def _prepend_skip(self, test): + """Prepends `pytest.skip` before the doctest.""" + source = ( + "import pytest; " + "pytest.skip('listed in `__doctest_skip__`'); " + # Don't impact what's available in the namespace + "del pytest" + ) + importorskip = doctest.Example(source=source, want="") + test.examples.insert(0, importorskip) + + def _prepend_importorskip(self, test, *, module): + """Prepends `pytest.importorskip` before the doctest.""" + source = ( + "import pytest; " + # Hide output of this statement in `___`, otherwise doctests fail + f"___ = pytest.importorskip({module!r}); " + # Don't impact what's available in the namespace + "del pytest; del ___" + ) + importorskip = doctest.Example(source=source, want="") + test.examples.insert(0, importorskip) + def write_modified_file(fname, new_fname, changes, encoding=None): # Sort in reversed order to edit the lines: diff --git a/tests/test_doctestplus.py b/tests/test_doctestplus.py index 932ec94..71d0380 100644 --- a/tests/test_doctestplus.py +++ b/tests/test_doctestplus.py @@ -1542,3 +1542,51 @@ def f(): original_fixed = original.replace("1\n 2", "\n ".join(["0", "1", "2", "3"])) assert result == original_fixed + + +def test_skip_module_variable(testdir): + p = testdir.makepyfile(""" + __doctest_skip__ = ["f"] + + def f(): + ''' + >>> 1 + 2 + 5 + ''' + pass + + def g(): + ''' + >>> 1 + 1 + 2 + ''' + pass + """) + testdir.inline_run(p, '--doctest-plus').assertoutcome(passed=1, skipped=1) + + +def test_requires_module_variable(testdir): + p = testdir.makepyfile(""" + __doctest_requires__ = { + ("f",): ["module_that_is_not_availabe"], + ("g",): ["pytest"], + } + + def f(): + ''' + >>> import module_that_is_not_availabe + ''' + pass + + def g(): + ''' + Test that call to `pytest.importorskip` is not visible + + >>> assert "pytest" not in locals() + >>> assert "___" not in locals() + >>> 1 + 1 + 2 + ''' + pass + """) + testdir.inline_run(p, '--doctest-plus').assertoutcome(passed=1, skipped=1)