diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index 106b0a6c95b7be..9f3a766862591d 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -1366,7 +1366,7 @@ DocTestFinder objects :class:`DocTestFinder` defines the following method: - .. method:: find(obj[, name][, module][, globs][, extraglobs]) + .. method:: find(obj[, name][, module][, globs][, extraglobs][, follow_wrapped]) Return a list of the :class:`DocTest`\ s that are defined by *obj*'s docstring, or by any of its contained objects' docstrings. @@ -1402,6 +1402,12 @@ DocTestFinder objects specified, or ``{}`` otherwise. If *extraglobs* is not specified, then it defaults to ``{}``. + If *follow_wrapped* is ``True``, :func:`inspect.unwrap` is used to unwrap + all objects before they are searched for doctests. + + .. versionchanged:: 3.14 + The *follow_wrapped* parameter was added. + Pass ``False`` to search objects for doctests without unwrapping them first. .. _doctest-doctestparser: diff --git a/Lib/doctest.py b/Lib/doctest.py index e02e73ed722f7e..bf33cadb131d4b 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -869,7 +869,7 @@ def __init__(self, verbose=False, parser=DocTestParser(), self._recurse = recurse self._exclude_empty = exclude_empty - def find(self, obj, name=None, module=None, globs=None, extraglobs=None): + def find(self, obj, name=None, module=None, globs=None, extraglobs=None, follow_wrapped=True): """ Return a list of the DocTests that are defined by the given object's docstring, or by any of its contained objects' @@ -904,6 +904,9 @@ def find(self, obj, name=None, module=None, globs=None, extraglobs=None): to {}. """ + if follow_wrapped: + obj = inspect.unwrap(obj) + # If name was not specified, then extract it from the object. if name is None: name = getattr(obj, '__name__', None) @@ -963,7 +966,7 @@ def find(self, obj, name=None, module=None, globs=None, extraglobs=None): # Recursively explore `obj`, extracting DocTests. tests = [] - self._find(tests, obj, name, module, source_lines, globs, {}) + self._find(tests, obj, name, module, source_lines, globs, follow_wrapped, {}) # Sort the tests by alpha order of names, for consistency in # verbose-mode output. This was a feature of doctest in Pythons # <= 2.3 that got lost by accident in 2.4. It was repaired in @@ -1000,18 +1003,7 @@ def _from_module(self, module, object): else: raise ValueError("object must be a class or function") - def _is_routine(self, obj): - """ - Safely unwrap objects and determine if they are functions. - """ - maybe_routine = obj - try: - maybe_routine = inspect.unwrap(maybe_routine) - except ValueError: - pass - return inspect.isroutine(maybe_routine) - - def _find(self, tests, obj, name, module, source_lines, globs, seen): + def _find(self, tests, obj, name, module, source_lines, globs, follow_wrapped, seen): """ Find tests for the given object and any contained objects, and add them to `tests`. @@ -1019,6 +1011,9 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): if self._verbose: print('Finding tests in %s' % name) + if follow_wrapped: + obj = inspect.unwrap(obj) + # If we've already processed this object, then ignore it. if id(obj) in seen: return @@ -1035,10 +1030,10 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): valname = '%s.%s' % (name, valname) # Recurse to functions & classes. - if ((self._is_routine(val) or inspect.isclass(val)) and + if ((inspect.isroutine(val) or inspect.isclass(val)) and self._from_module(module, val)): self._find(tests, val, valname, module, source_lines, - globs, seen) + globs, follow_wrapped, seen) # Look for tests in a module's __test__ dictionary. if inspect.ismodule(obj) and self._recurse: @@ -1055,7 +1050,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): (type(val),)) valname = '%s.__test__.%s' % (name, valname) self._find(tests, val, valname, module, source_lines, - globs, seen) + globs, follow_wrapped, seen) # Look for tests in a class's contained objects. if inspect.isclass(obj) and self._recurse: @@ -1070,7 +1065,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): self._from_module(module, val)): valname = '%s.%s' % (name, valname) self._find(tests, val, valname, module, source_lines, - globs, seen) + globs, follow_wrapped, seen) def _get_test(self, obj, name, module, globs, source_lines): """ @@ -1142,7 +1137,6 @@ def _find_lineno(self, obj, source_lines): obj = obj.fget if inspect.isfunction(obj) and getattr(obj, '__doc__', None): # We don't use `docstring` var here, because `obj` can be changed. - obj = inspect.unwrap(obj) try: obj = obj.__code__ except AttributeError: diff --git a/Lib/functools.py b/Lib/functools.py index fd33f0ae479ddc..15433f6aad7cfc 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -1029,6 +1029,8 @@ def __init__(self, func): import weakref # see comment in singledispatch function self._method_cache = weakref.WeakKeyDictionary() + update_wrapper(self, func) + def register(self, cls, method=None): """generic_method.register(cls, func) -> func diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index a4a49298bab3be..548575ac19fa6a 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -123,6 +123,22 @@ def a_cached_property(self): """ return "hello" + @functools.singledispatchmethod + def a_singledispatchmethod(self, arg): + """ + >>> print(SampleClass(30).a_singledispatchmethod(30)) + int + >>> print(SampleClass(30).a_singledispatchmethod("30")) + base + """ + return "base" + + @a_singledispatchmethod.register(int) + def _(self, arg): + return "int" + + del _ + class NestedClass: """ >>> x = SampleClass.NestedClass(5) @@ -528,6 +544,7 @@ def basics(): r""" 1 SampleClass.a_cached_property 2 SampleClass.a_classmethod 1 SampleClass.a_property + 2 SampleClass.a_singledispatchmethod 1 SampleClass.a_staticmethod 1 SampleClass.double 1 SampleClass.get @@ -585,6 +602,7 @@ def basics(): r""" 1 some_module.SampleClass.a_cached_property 2 some_module.SampleClass.a_classmethod 1 some_module.SampleClass.a_property + 2 some_module.SampleClass.a_singledispatchmethod 1 some_module.SampleClass.a_staticmethod 1 some_module.SampleClass.double 1 some_module.SampleClass.get @@ -642,6 +660,7 @@ def basics(): r""" 1 SampleClass.a_cached_property 2 SampleClass.a_classmethod 1 SampleClass.a_property + 2 SampleClass.a_singledispatchmethod 1 SampleClass.a_staticmethod 1 SampleClass.double 1 SampleClass.get @@ -664,6 +683,7 @@ def basics(): r""" 1 SampleClass.a_cached_property 2 SampleClass.a_classmethod 1 SampleClass.a_property + 2 SampleClass.a_singledispatchmethod 1 SampleClass.a_staticmethod 1 SampleClass.double 1 SampleClass.get diff --git a/Misc/NEWS.d/next/Library/2025-02-02-14-06-32.gh-issue-129578.t_mZ1C.rst b/Misc/NEWS.d/next/Library/2025-02-02-14-06-32.gh-issue-129578.t_mZ1C.rst new file mode 100644 index 00000000000000..9e2908ecdcbebd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-02-14-06-32.gh-issue-129578.t_mZ1C.rst @@ -0,0 +1,5 @@ +Add *follow_wrapped* parameter to :func:`doctest.DocTestFinder.find` to +unwrap objects before searching them for doctests. + +Add :func:`functools.update_wrapper` call to constructor of +:class:`functools.singledispatchmethod` to let it be searched by doctest.