https://github.com/python/cpython/commit/cac016736e5742395fff20b73692876f05ae312c
commit: cac016736e5742395fff20b73692876f05ae312c
branch: main
author: Brittany Reynoso <[email protected]>
committer: encukou <[email protected]>
date: 2026-09-29T11:30:57+02:00
summary:

gh-153888: Resolve lazy imports before using them as a mock spec (GH-157768)

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:

- 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.

files:
A Misc/NEWS.d/next/Library/2026-09-17-21-05-42.gh-issue-153888.Ht7Yqb.rst
M Lib/test/test_unittest/testmock/testpatch.py
M Lib/unittest/mock.py

diff --git a/Lib/test/test_unittest/testmock/testpatch.py 
b/Lib/test/test_unittest/testmock/testpatch.py
index bd85fdcfc472a61..bd2a7fb6d6731d7 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 1effc70b5323139..d3d47ff81e85969 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 000000000000000..278c9560cec216e
--- /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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to