Repository: pytest-dev/pytest
Pull request: Fix doctest collection of `functools.cached_property` objects.
I'll be first to admit that the proposed solution is _very_ hacky but felt inspired by some adjacent code to do things this way. Suggestions for improvement are welcomed. 🙂

closes #11237


Problem statement:
@cached_property is not checked for doctests
- [x] a detailed description of the bug or problem you are having
If I have doctests in a function decorated with `@cached_property`, they will not be run.
- [x] output of `pip list` from the virtual environment you are using
```
Package           Version Editable project location
----------------- ------- ---------------------------------------------
coverage          6.5.0
exceptiongroup    1.1.2
iniconfig         2.0.0
nodeenv           1.8.0
packaging         23.1
pip               23.2
pluggy            1.2.0
pyright           1.1.318
pytest            7.4.0
setuptools        68.0.0
tomli             2.0.1
typing_extensions 4.7.1
wheel             0.38.4
```
- [x] pytest and operating system versions
pytest 7.4.0
Pop!_OS 22.04 LTS
- [x] minimal example if possible
```python
from functools import cached_property

class Test:
    @cached_property
    def test_me(self):
        """
        A function that returns the string `ok`

        Examples
        --------
        >>> Test().test_me
        'ok'
        """
        return "ok"

    @property
    def test_me_2(self):
        """
        A function that returns the string `ok`

        Examples
        --------
        >>> Test().test_me_2
        'ok'
        """
        return "ok"
```
Run `python -m pytest --doctest-modules`, only one test will be captured and run.

Hints from issue discussion:

Commit message: Move _from_module override to pre-existsing DocTestFinder subclass

Patch to review:
diff --git a/AUTHORS b/AUTHORS
index 313e507f204..18db7808f13 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 00000000000..d054fc18d1c
--- /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 e6f666dda03..c99ccbbb1c8 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,18 @@ def _find(
                         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
+
+                # 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,
diff --git a/testing/test_doctest.py b/testing/test_doctest.py
index f189e8645fa..f4d3155c435 100644
--- a/testing/test_doctest.py
+++ b/testing/test_doctest.py
@@ -482,6 +482,24 @@ def test_doctestmodule(self, pytester: Pytester):
         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(


Code review comments:
diff_hunk: @@ -536,6 +537,18 @@ def _find(
                         tests, obj, name, module, source_lines, globs, seen
                     )
 
+            def _from_module(self, module, object):
+                """`cached_property` objects will are never considered a part
path: src/_pytest/doctest.py
text: @user1:
```suggestion
                """`cached_property` objects are never considered a part
```

@author:
Fixed

diff_hunk: @@ -536,6 +537,18 @@ def _find(
                         tests, obj, name, module, source_lines, globs, seen
                     )
 
+            def _from_module(self, module, object):
path: src/_pytest/doctest.py
text: @user1:
Can we wrap this in `if sys.version_info < (3, 13):`?

@author:
Great call 👍