From 64b939a2e422e582f0f182af687b88f514b97526 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Thu, 17 Sep 2026 21:02:51 -0700 Subject: [PATCH] Resolve lazy imports before using them as a mock spec unittest.mock.patch() reads the attribute it replaces out of target.__dict__, which for an unresolved lazy import holds a types.LazyImportType placeholder. spec=True, spec_set=True and autospec=True then specced the placeholder, so patching a function produced a NonCallableMagicMock. Reimplementation of python/cpython#153977, open since July 2026: - Resolve in _patch.__enter__(), and only when the spec is taken from the original, rather than unconditionally in get_original(). Resolving on every patch() breaks patch("mod.dep", new=...) for a lazy import of a module that is not installed. - Test spec and spec_set as well as autospec, as asked for in review. - Build the lazy binding in the test instead of importing json lazily at module level: "lazy from" binds eagerly when the module is already imported, so that test passed without the fix too. Reported as gh-153888. --- Lib/test/test_unittest/testmock/testpatch.py | 35 +++++++++++++++++++ Lib/unittest/mock.py | 6 +++- ...-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index bd85fdcfc472a6..bd2a7fb6d6731d 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -5,6 +5,7 @@ import os import sys from collections import OrderedDict +from types import LazyImportType, ModuleType import unittest import test @@ -2101,5 +2102,39 @@ def test(_): test() +class PatchLazyImportTest(unittest.TestCase): + + def lazy_module(self): + # `lazy from` binds eagerly when the module it imports from is already + # imported, so publish the target only once the statement has run. + self.enterContext(uncache('lazy_patch_user', 'lazy_patch_target')) + user = ModuleType('lazy_patch_user') + exec('lazy from lazy_patch_target import function', user.__dict__) + sys.modules['lazy_patch_user'] = user + target = ModuleType('lazy_patch_target') + exec('def function(arg): pass', target.__dict__) + sys.modules['lazy_patch_target'] = target + + self.assertIsInstance(user.__dict__['function'], LazyImportType) + return user + + def test_autospec(self): + module = self.lazy_module() + with patch.object(module, 'function', autospec=True) as mock_function: + mock_function('arg') + with self.assertRaises(TypeError): + mock_function('arg', 'extra') + + def test_spec(self): + module = self.lazy_module() + with patch.object(module, 'function', spec=True) as mock_function: + mock_function('arg') + + def test_spec_set(self): + module = self.lazy_module() + with patch.object(module, 'function', spec_set=True) as mock_function: + mock_function('arg') + + if __name__ == '__main__': unittest.main() diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 1effc70b532313..d3d47ff81e8596 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -35,7 +35,7 @@ import threading from annotationlib import Format from dataclasses import fields, is_dataclass -from types import CodeType, ModuleType, MethodType +from types import CodeType, LazyImportType, ModuleType, MethodType from unittest.util import safe_repr from functools import wraps, partial from threading import RLock @@ -1507,6 +1507,10 @@ def __enter__(self): original, local = self.get_original() + if (isinstance(original, LazyImportType) + and (spec is True or spec_set is True or autospec is True)): + original = original.resolve() + if new is DEFAULT and autospec is None: inherit = False if spec is True: diff --git a/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst b/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst new file mode 100644 index 00000000000000..278c9560cec216 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst @@ -0,0 +1,2 @@ +Fix :func:`unittest.mock.patch` with ``autospec``, ``spec``, or ``spec_set`` +when used with lazy-imported objects.