From ddf3ab12f3e733fcd4e38c80c71389fdff2f4266 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Tue, 15 Jul 2025 11:34:13 +0200 Subject: [PATCH 1/2] Use inspect.isroutine() in DocTest's lineno computation Previously, DocTest's lineno of `functools.cache()`-decorated functions was not properly returned (None was returned) because the underlying computation, in `DocTest._find_lineno()`, relied on `inspect.isfunction()` which does not consider the decorated result as a function. We now use the more generic `inspect.isroutine()`, as elsewhere in doctest's logic, thus fixing lineno computation for functools.cache()-decorated functions. --- Lib/doctest.py | 2 +- Lib/test/test_doctest/doctest_lineno.py | 18 ++++++++++++++++++ Lib/test/test_doctest/test_doctest.py | 2 ++ ...5-07-21-15-40-00.gh-issue-136914.-GNG-d.rst | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst diff --git a/Lib/doctest.py b/Lib/doctest.py index e77823f64b67e4..e08d5ce028e9c9 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1141,7 +1141,7 @@ def _find_lineno(self, obj, source_lines): if inspect.ismethod(obj): obj = obj.__func__ if isinstance(obj, property): obj = obj.fget - if inspect.isfunction(obj) and getattr(obj, '__doc__', None): + if inspect.isroutine(obj) and getattr(obj, '__doc__', None): # We don't use `docstring` var here, because `obj` can be changed. obj = inspect.unwrap(obj) try: diff --git a/Lib/test/test_doctest/doctest_lineno.py b/Lib/test/test_doctest/doctest_lineno.py index 0dbcd9a11eaba2..2565650012d0ab 100644 --- a/Lib/test/test_doctest/doctest_lineno.py +++ b/Lib/test/test_doctest/doctest_lineno.py @@ -76,3 +76,21 @@ def property_with_doctest(self): @decorator def func_with_docstring_wrapped(): """Some unrelated info.""" + + +# https://github.com/python/cpython/issues/136914 +import functools + + +@functools.cache +def cached_func_with_doctest(value): + """ + >>> cached_func_with_doctest(1) + -1 + """ + return -value + + +@functools.cache +def cached_func_without_docstring(value): + return value + 1 diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 72763d4a0132d0..6274bb8a4fafec 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -687,6 +687,8 @@ def basics(): r""" 45 test.test_doctest.doctest_lineno.MethodWrapper.method_with_doctest None test.test_doctest.doctest_lineno.MethodWrapper.method_without_docstring 61 test.test_doctest.doctest_lineno.MethodWrapper.property_with_doctest + 86 test.test_doctest.doctest_lineno.cached_func_with_doctest + None test.test_doctest.doctest_lineno.cached_func_without_docstring 4 test.test_doctest.doctest_lineno.func_with_docstring 77 test.test_doctest.doctest_lineno.func_with_docstring_wrapped 12 test.test_doctest.doctest_lineno.func_with_doctest diff --git a/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst b/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst new file mode 100644 index 00000000000000..21edc0e3c639cf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst @@ -0,0 +1 @@ +Fix retrieval of :attr:`doctest.DocTest.lineno` for :func:`functools.cache`-decorated functions. From 553fb134b2c4810be1c7b56f1ccf165b9b80e270 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Fri, 25 Jul 2025 09:04:05 +0200 Subject: [PATCH 2/2] Detect line numbers for cached properties in doctests --- Lib/doctest.py | 3 +++ Lib/test/test_doctest/doctest_lineno.py | 11 +++++++++++ Lib/test/test_doctest/test_doctest.py | 2 ++ .../2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst | 3 ++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/doctest.py b/Lib/doctest.py index e08d5ce028e9c9..92a2ab4f7e66f8 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -94,6 +94,7 @@ def _test(): import __future__ import difflib +import functools import inspect import linecache import os @@ -1141,6 +1142,8 @@ def _find_lineno(self, obj, source_lines): if inspect.ismethod(obj): obj = obj.__func__ if isinstance(obj, property): obj = obj.fget + if isinstance(obj, functools.cached_property): + obj = obj.func if inspect.isroutine(obj) and getattr(obj, '__doc__', None): # We don't use `docstring` var here, because `obj` can be changed. obj = inspect.unwrap(obj) diff --git a/Lib/test/test_doctest/doctest_lineno.py b/Lib/test/test_doctest/doctest_lineno.py index 2565650012d0ab..0bd402e98288d0 100644 --- a/Lib/test/test_doctest/doctest_lineno.py +++ b/Lib/test/test_doctest/doctest_lineno.py @@ -94,3 +94,14 @@ def cached_func_with_doctest(value): @functools.cache def cached_func_without_docstring(value): return value + 1 + + +class ClassWithACachedProperty: + + @functools.cached_property + def cached(self): + """ + >>> X().cached + -1 + """ + return 0 diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 6274bb8a4fafec..0fa74407e3c436 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -678,6 +678,8 @@ def basics(): r""" >>> for t in tests: ... print('%5s %s' % (t.lineno, t.name)) None test.test_doctest.doctest_lineno + None test.test_doctest.doctest_lineno.ClassWithACachedProperty + 102 test.test_doctest.doctest_lineno.ClassWithACachedProperty.cached 22 test.test_doctest.doctest_lineno.ClassWithDocstring 30 test.test_doctest.doctest_lineno.ClassWithDoctest None test.test_doctest.doctest_lineno.ClassWithoutDocstring diff --git a/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst b/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst index 21edc0e3c639cf..78ec8025fbc0fd 100644 --- a/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst +++ b/Misc/NEWS.d/next/Library/2025-07-21-15-40-00.gh-issue-136914.-GNG-d.rst @@ -1 +1,2 @@ -Fix retrieval of :attr:`doctest.DocTest.lineno` for :func:`functools.cache`-decorated functions. +Fix retrieval of :attr:`doctest.DocTest.lineno` for objects decorated with +:func:`functools.cache` or :class:`functools.cached_property`.