Skip to content

Commit cac0167

Browse files
authored
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 #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.
1 parent 1407bb9 commit cac0167

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

‎Lib/test/test_unittest/testmock/testpatch.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import sys
77
from collections import OrderedDict
8+
from types import LazyImportType, ModuleType
89

910
import unittest
1011
import test
@@ -2101,5 +2102,39 @@ def test(_):
21012102
test()
21022103

21032104

2105+
class PatchLazyImportTest(unittest.TestCase):
2106+
2107+
def lazy_module(self):
2108+
# `lazy from` binds eagerly when the module it imports from is already
2109+
# imported, so publish the target only once the statement has run.
2110+
self.enterContext(uncache('lazy_patch_user', 'lazy_patch_target'))
2111+
user = ModuleType('lazy_patch_user')
2112+
exec('lazy from lazy_patch_target import function', user.__dict__)
2113+
sys.modules['lazy_patch_user'] = user
2114+
target = ModuleType('lazy_patch_target')
2115+
exec('def function(arg): pass', target.__dict__)
2116+
sys.modules['lazy_patch_target'] = target
2117+
2118+
self.assertIsInstance(user.__dict__['function'], LazyImportType)
2119+
return user
2120+
2121+
def test_autospec(self):
2122+
module = self.lazy_module()
2123+
with patch.object(module, 'function', autospec=True) as mock_function:
2124+
mock_function('arg')
2125+
with self.assertRaises(TypeError):
2126+
mock_function('arg', 'extra')
2127+
2128+
def test_spec(self):
2129+
module = self.lazy_module()
2130+
with patch.object(module, 'function', spec=True) as mock_function:
2131+
mock_function('arg')
2132+
2133+
def test_spec_set(self):
2134+
module = self.lazy_module()
2135+
with patch.object(module, 'function', spec_set=True) as mock_function:
2136+
mock_function('arg')
2137+
2138+
21042139
if __name__ == '__main__':
21052140
unittest.main()

‎Lib/unittest/mock.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import threading
3636
from annotationlib import Format
3737
from dataclasses import fields, is_dataclass
38-
from types import CodeType, ModuleType, MethodType
38+
from types import CodeType, LazyImportType, ModuleType, MethodType
3939
from unittest.util import safe_repr
4040
from functools import wraps, partial
4141
from threading import RLock
@@ -1507,6 +1507,10 @@ def __enter__(self):
15071507

15081508
original, local = self.get_original()
15091509

1510+
if (isinstance(original, LazyImportType)
1511+
and (spec is True or spec_set is True or autospec is True)):
1512+
original = original.resolve()
1513+
15101514
if new is DEFAULT and autospec is None:
15111515
inherit = False
15121516
if spec is True:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`unittest.mock.patch` with ``autospec``, ``spec``, or ``spec_set``
2+
when used with lazy-imported objects.

0 commit comments

Comments
 (0)