From 565a31804c1139fe7886f38af3b3923653b0c1b3 Mon Sep 17 00:00:00 2001 From: Alfred Perlstein Date: Wed, 5 May 2021 10:33:17 -0700 Subject: [PATCH] bpo-35753: Fix crash in doctest with unwrap-able functions (#22981) Ignore objects that inspect.unwrap throws due to too many wrappers. This is a very rare case, however it can easily be surfaced when a module under doctest imports unitest.mock.call into its namespace. We simply skip any object that throws this exception. This should handle the majority of cases. --- Lib/doctest.py | 15 +++++++++++++-- Lib/test/test_doctest.py | 15 ++++++++++++++- .../2020-10-25-19-20-26.bpo-35753.2LT-hO.rst | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2020-10-25-19-20-26.bpo-35753.2LT-hO.rst diff --git a/Lib/doctest.py b/Lib/doctest.py index e95c333f48a..ba898f65403 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -973,6 +973,17 @@ class DocTestFinder: 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): """ Find tests for the given object and any contained objects, and @@ -995,9 +1006,9 @@ class DocTestFinder: if inspect.ismodule(obj) and self._recurse: for valname, val in obj.__dict__.items(): valname = '%s.%s' % (name, valname) + # Recurse to functions & classes. - if ((inspect.isroutine(inspect.unwrap(val)) - or inspect.isclass(val)) and + if ((self._is_routine(val) or inspect.isclass(val)) and self._from_module(module, val)): self._find(tests, val, valname, module, source_lines, globs, seen) diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py index 6f51b1bc4f0..828a0ff5676 100644 --- a/Lib/test/test_doctest.py +++ b/Lib/test/test_doctest.py @@ -15,6 +15,7 @@ import importlib.util import unittest import tempfile import shutil +import types import contextlib # NOTE: There are some additional tests relating to interaction with @@ -443,7 +444,7 @@ We'll simulate a __file__ attr that ends in pyc: >>> tests = finder.find(sample_func) >>> print(tests) # doctest: +ELLIPSIS - [] + [] The exact name depends on how test_doctest was invoked, so allow for leading path components. @@ -698,6 +699,18 @@ and 'int' is a type. class TestDocTestFinder(unittest.TestCase): + def test_issue35753(self): + # This import of `call` should trigger issue35753 when + # `support.run_doctest` is called due to unwrap failing, + # however with a patched doctest this should succeed. + from unittest.mock import call + dummy_module = types.ModuleType("dummy") + dummy_module.__dict__['inject_call'] = call + try: + support.run_doctest(dummy_module, verbosity=True) + except ValueError as e: + raise support.TestFailed("Doctest unwrap failed") from e + def test_empty_namespace_package(self): pkg_name = 'doctest_empty_pkg' with tempfile.TemporaryDirectory() as parent_dir: diff --git a/Misc/NEWS.d/next/Tests/2020-10-25-19-20-26.bpo-35753.2LT-hO.rst b/Misc/NEWS.d/next/Tests/2020-10-25-19-20-26.bpo-35753.2LT-hO.rst new file mode 100644 index 00000000000..eddfc25906d --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2020-10-25-19-20-26.bpo-35753.2LT-hO.rst @@ -0,0 +1,2 @@ +Fix crash in doctest when doctest parses modules that include unwrappable +functions by skipping those functions.