diff --git a/AUTHORS b/AUTHORS index 313e507f2..18db7808f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -379,6 +379,7 @@ Tor Colvin Trevor Bekolay Tushar Sadhwani Tyler Goodlet +Tyler Smart Tzu-ping Chung Vasily Kuznetsov Victor Maryama diff --git a/changelog/11237.bugfix.rst b/changelog/11237.bugfix.rst new file mode 100644 index 000000000..d054fc18d --- /dev/null +++ b/changelog/11237.bugfix.rst @@ -0,0 +1 @@ +Fix doctest collection of `functools.cached_property` objects. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index e6f666dda..e8bf92d95 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -1,5 +1,6 @@ """Discover and run doctests in modules and test files.""" import bdb +import functools import inspect import os import platform @@ -536,6 +537,21 @@ class DoctestModule(Module): tests, obj, name, module, source_lines, globs, seen ) + class CachedPropertyAwareDocTestFinder(MockAwareDocTestFinder): + def _from_module(self, module, object): + """Doctest code does not take into account `@cached_property`, + this is a hackish way to fix it. https://github.com/python/cpython/issues/107995 + + Wrap Doctest finder so that when it calls `_from_module` for + a cached_property it uses the underlying function instead of the + wrapped cached_property object. + """ + if isinstance(object, functools.cached_property): + object = object.func + + # Type ignored because this is a private function. + return super()._from_module(module, object) # type: ignore[misc] + if self.path.name == "conftest.py": module = self.config.pluginmanager._importconftest( self.path, @@ -555,7 +571,7 @@ class DoctestModule(Module): else: raise # Uses internal doctest module parsing mechanism. - finder = MockAwareDocTestFinder() + finder = CachedPropertyAwareDocTestFinder() optionflags = get_optionflags(self) runner = _get_runner( verbose=False, diff --git a/testing/test_doctest.py b/testing/test_doctest.py index f189e8645..f4d3155c4 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -482,6 +482,24 @@ class TestDoctests: reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) + def test_doctest_cached_property(self, pytester: Pytester): + p = pytester.makepyfile( + """ + import functools + + class Foo: + @functools.cached_property + def foo(self): + ''' + >>> assert False, "Tacos!" + ''' + ... + """ + ) + result = pytester.runpytest(p, "--doctest-modules") + result.assert_outcomes(failed=1) + assert "Tacos!" in result.stdout.str() + def test_doctestmodule_external_and_issue116(self, pytester: Pytester): p = pytester.mkpydir("hello") p.joinpath("__init__.py").write_text(