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`. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy