Skip to content

Commit ff6b2e6

Browse files
tirkarthi1st1
authored andcommitted
bpo-37047: Refactor AsyncMock setup logic for autospeccing (GH-13574)
Handle late binding and attribute access in unittest.mock.AsyncMock setup for autospeccing.
1 parent 431b540 commit ff6b2e6

File tree

4 files changed

+107
-24
lines changed

4 files changed

+107
-24
lines changed

Doc/library/unittest.mock.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1945,7 +1945,7 @@ The full list of supported magic methods is:
19451945
* Container methods: ``__getitem__``, ``__setitem__``, ``__delitem__``,
19461946
``__contains__``, ``__len__``, ``__iter__``, ``__reversed__``
19471947
and ``__missing__``
1948-
* Context manager: ``__enter__`` and ``__exit__``
1948+
* Context manager: ``__enter__``, ``__exit__``, ``__aenter`` and ``__aexit__``
19491949
* Unary numeric methods: ``__neg__``, ``__pos__`` and ``__invert__``
19501950
* The numeric methods (including right hand and in-place variants):
19511951
``__add__``, ``__sub__``, ``__mul__``, ``__matmul__``, ``__div__``, ``__truediv__``,
@@ -1957,10 +1957,14 @@ The full list of supported magic methods is:
19571957
* Pickling: ``__reduce__``, ``__reduce_ex__``, ``__getinitargs__``,
19581958
``__getnewargs__``, ``__getstate__`` and ``__setstate__``
19591959
* File system path representation: ``__fspath__``
1960+
* Asynchronous iteration methods: ``__aiter__`` and ``__anext__``
19601961

19611962
.. versionchanged:: 3.8
19621963
Added support for :func:`os.PathLike.__fspath__`.
19631964

1965+
.. versionchanged:: 3.8
1966+
Added support for ``__aenter__``, ``__aexit__``, ``__aiter__`` and ``__anext__``.
1967+
19641968

19651969
The following methods exist but are *not* supported as they are either in use
19661970
by mock, can't be set dynamically, or can cause problems:

Lib/unittest/mock.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ def _is_async_obj(obj):
5151
return False
5252

5353

54+
def _is_async_func(func):
55+
if getattr(func, '__code__', None):
56+
return asyncio.iscoroutinefunction(func)
57+
else:
58+
return False
59+
60+
5461
def _is_instance_mock(obj):
5562
# can't use isinstance on Mock objects because they override __class__
5663
# The base class for all mocks is NonCallableMock
@@ -225,6 +232,34 @@ def reset_mock():
225232
mock._mock_delegate = funcopy
226233

227234

235+
def _setup_async_mock(mock):
236+
mock._is_coroutine = asyncio.coroutines._is_coroutine
237+
mock.await_count = 0
238+
mock.await_args = None
239+
mock.await_args_list = _CallList()
240+
mock.awaited = _AwaitEvent(mock)
241+
242+
# Mock is not configured yet so the attributes are set
243+
# to a function and then the corresponding mock helper function
244+
# is called when the helper is accessed similar to _setup_func.
245+
def wrapper(attr, *args, **kwargs):
246+
return getattr(mock.mock, attr)(*args, **kwargs)
247+
248+
for attribute in ('assert_awaited',
249+
'assert_awaited_once',
250+
'assert_awaited_with',
251+
'assert_awaited_once_with',
252+
'assert_any_await',
253+
'assert_has_awaits',
254+
'assert_not_awaited'):
255+
256+
# setattr(mock, attribute, wrapper) causes late binding
257+
# hence attribute will always be the last value in the loop
258+
# Use partial(wrapper, attribute) to ensure the attribute is bound
259+
# correctly.
260+
setattr(mock, attribute, partial(wrapper, attribute))
261+
262+
228263
def _is_magic(name):
229264
return '__%s__' % name[2:-2] == name
230265

@@ -2151,7 +2186,7 @@ def assert_not_awaited(_mock_self):
21512186
"""
21522187
self = _mock_self
21532188
if self.await_count != 0:
2154-
msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once."
2189+
msg = (f"Expected {self._mock_name or 'mock'} to not have been awaited."
21552190
f" Awaited {self.await_count} times.")
21562191
raise AssertionError(msg)
21572192

@@ -2457,10 +2492,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
24572492
spec = type(spec)
24582493

24592494
is_type = isinstance(spec, type)
2460-
if getattr(spec, '__code__', None):
2461-
is_async_func = asyncio.iscoroutinefunction(spec)
2462-
else:
2463-
is_async_func = False
2495+
is_async_func = _is_async_func(spec)
24642496
_kwargs = {'spec': spec}
24652497
if spec_set:
24662498
_kwargs = {'spec_set': spec}
@@ -2498,26 +2530,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
24982530
name=_name, **_kwargs)
24992531

25002532
if isinstance(spec, FunctionTypes):
2501-
wrapped_mock = mock
25022533
# should only happen at the top level because we don't
25032534
# recurse for functions
25042535
mock = _set_signature(mock, spec)
25052536
if is_async_func:
2506-
mock._is_coroutine = asyncio.coroutines._is_coroutine
2507-
mock.await_count = 0
2508-
mock.await_args = None
2509-
mock.await_args_list = _CallList()
2510-
2511-
for a in ('assert_awaited',
2512-
'assert_awaited_once',
2513-
'assert_awaited_with',
2514-
'assert_awaited_once_with',
2515-
'assert_any_await',
2516-
'assert_has_awaits',
2517-
'assert_not_awaited'):
2518-
def f(*args, **kwargs):
2519-
return getattr(wrapped_mock, a)(*args, **kwargs)
2520-
setattr(mock, a, f)
2537+
_setup_async_mock(mock)
25212538
else:
25222539
_check_signature(spec, mock, is_type, instance)
25232540

Lib/unittest/test/testmock/testasync.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import inspect
33
import unittest
44

5-
from unittest.mock import call, AsyncMock, patch, MagicMock, create_autospec
5+
from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec,
6+
_AwaitEvent)
67

78

89
def tearDownModule():
@@ -20,6 +21,9 @@ def normal_method(self):
2021
async def async_func():
2122
pass
2223

24+
async def async_func_args(a, b, *, c):
25+
pass
26+
2327
def normal_func():
2428
pass
2529

@@ -141,8 +145,63 @@ def test_create_autospec_instance(self):
141145
create_autospec(async_func, instance=True)
142146

143147
def test_create_autospec(self):
144-
spec = create_autospec(async_func)
148+
spec = create_autospec(async_func_args)
149+
awaitable = spec(1, 2, c=3)
150+
async def main():
151+
await awaitable
152+
153+
self.assertEqual(spec.await_count, 0)
154+
self.assertIsNone(spec.await_args)
155+
self.assertEqual(spec.await_args_list, [])
156+
self.assertIsInstance(spec.awaited, _AwaitEvent)
157+
spec.assert_not_awaited()
158+
159+
asyncio.run(main())
160+
145161
self.assertTrue(asyncio.iscoroutinefunction(spec))
162+
self.assertTrue(asyncio.iscoroutine(awaitable))
163+
self.assertEqual(spec.await_count, 1)
164+
self.assertEqual(spec.await_args, call(1, 2, c=3))
165+
self.assertEqual(spec.await_args_list, [call(1, 2, c=3)])
166+
spec.assert_awaited_once()
167+
spec.assert_awaited_once_with(1, 2, c=3)
168+
spec.assert_awaited_with(1, 2, c=3)
169+
spec.assert_awaited()
170+
171+
def test_patch_with_autospec(self):
172+
173+
async def test_async():
174+
with patch(f"{__name__}.async_func_args", autospec=True) as mock_method:
175+
awaitable = mock_method(1, 2, c=3)
176+
self.assertIsInstance(mock_method.mock, AsyncMock)
177+
178+
self.assertTrue(asyncio.iscoroutinefunction(mock_method))
179+
self.assertTrue(asyncio.iscoroutine(awaitable))
180+
self.assertTrue(inspect.isawaitable(awaitable))
181+
182+
# Verify the default values during mock setup
183+
self.assertEqual(mock_method.await_count, 0)
184+
self.assertEqual(mock_method.await_args_list, [])
185+
self.assertIsNone(mock_method.await_args)
186+
self.assertIsInstance(mock_method.awaited, _AwaitEvent)
187+
mock_method.assert_not_awaited()
188+
189+
await awaitable
190+
191+
self.assertEqual(mock_method.await_count, 1)
192+
self.assertEqual(mock_method.await_args, call(1, 2, c=3))
193+
self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)])
194+
mock_method.assert_awaited_once()
195+
mock_method.assert_awaited_once_with(1, 2, c=3)
196+
mock_method.assert_awaited_with(1, 2, c=3)
197+
mock_method.assert_awaited()
198+
199+
mock_method.reset_mock()
200+
self.assertEqual(mock_method.await_count, 0)
201+
self.assertIsNone(mock_method.await_args)
202+
self.assertEqual(mock_method.await_args_list, [])
203+
204+
asyncio.run(test_async())
146205

147206

148207
class AsyncSpecTest(unittest.TestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Handle late binding and attribute access in :class:`unittest.mock.AsyncMock`
2+
setup for autospeccing. Document newly implemented async methods in
3+
:class:`unittest.mock.MagicMock`.

0 commit comments

Comments
 (0)