From d4fb6ac9f7875b762a6bd1f179ef5df4997d6182 Mon Sep 17 00:00:00 2001 From: Tyler Smart Date: Wed, 16 Aug 2023 00:54:38 -0600 Subject: [PATCH 1/4] Fix doctest collection of `functools.cached_property` objects. --- AUTHORS | 1 + changelog/11237.bugfix.rst | 1 + src/_pytest/doctest.py | 18 +++++++++++++++++- testing/test_doctest.py | 18 ++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelog/11237.bugfix.rst 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( From ebd571bb18d2cf00ebc70db8090217fc62d00e98 Mon Sep 17 00:00:00 2001 From: Tyler Smart Date: Sat, 19 Aug 2023 12:04:59 -0600 Subject: [PATCH 2/4] Move _from_module override to pre-existsing DocTestFinder subclass --- src/_pytest/doctest.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index e8bf92d95..c99ccbbb1 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -537,14 +537,11 @@ 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. + """`cached_property` objects will are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 """ if isinstance(object, functools.cached_property): object = object.func @@ -571,7 +568,7 @@ class DoctestModule(Module): else: raise # Uses internal doctest module parsing mechanism. - finder = CachedPropertyAwareDocTestFinder() + finder = MockAwareDocTestFinder() optionflags = get_optionflags(self) runner = _get_runner( verbose=False, From 7a625481dae2380d8f9d1cf53f6a59a2977b49c8 Mon Sep 17 00:00:00 2001 From: Tyler Smart Date: Sat, 19 Aug 2023 22:20:40 -0600 Subject: [PATCH 3/4] PR suggestions --- src/_pytest/doctest.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index c99ccbbb1..fcd48f893 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -537,17 +537,19 @@ class DoctestModule(Module): tests, obj, name, module, source_lines, globs, seen ) - def _from_module(self, module, object): - """`cached_property` objects will are never considered a part - of the 'current module'. As such they are skipped by doctest. - Here we override `_from_module` to check the underlying - function instead. https://github.com/python/cpython/issues/107995 - """ - if isinstance(object, functools.cached_property): - object = object.func + if sys.version_info < (3, 13): - # Type ignored because this is a private function. - return super()._from_module(module, object) # type: ignore[misc] + def _from_module(self, module, object): + """`cached_property` objects are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 + """ + 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( From a357c7abc84e0382894a9b53d88ebb7ed2680686 Mon Sep 17 00:00:00 2001 From: Tyler Smart Date: Sun, 20 Aug 2023 20:55:30 -0600 Subject: [PATCH 4/4] Ignore dip in branch coverage (since py3.13+ isn't tested in CI) --- src/_pytest/doctest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index fcd48f893..49f8c9bfe 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -551,6 +551,9 @@ class DoctestModule(Module): # Type ignored because this is a private function. return super()._from_module(module, object) # type: ignore[misc] + else: # pragma: no cover + pass + if self.path.name == "conftest.py": module = self.config.pluginmanager._importconftest( self.path,