Skip to content

Commit 21f24ea

Browse files
authored
[3.8] bpo-38163: Child mocks detect their type as sync or async (GH-16471) (GH-16484)
1 parent 36e7e4a commit 21f24ea

File tree

4 files changed

+76
-28
lines changed

4 files changed

+76
-28
lines changed

Doc/library/unittest.mock.rst

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ object::
865865
True
866866

867867
The result of ``mock()`` is an async function which will have the outcome
868-
of ``side_effect`` or ``return_value``:
868+
of ``side_effect`` or ``return_value`` after it has been awaited:
869869

870870
- if ``side_effect`` is a function, the async function will return the
871871
result of that function,
@@ -890,6 +890,32 @@ object::
890890
>>> mock() # doctest: +SKIP
891891
<coroutine object AsyncMockMixin._mock_call at ...>
892892

893+
894+
Setting the *spec* of a :class:`Mock`, :class:`MagicMock`, or :class:`AsyncMock`
895+
to a class with asynchronous and synchronous functions will automatically
896+
detect the synchronous functions and set them as :class:`MagicMock` (if the
897+
parent mock is :class:`AsyncMock` or :class:`MagicMock`) or :class:`Mock` (if
898+
the parent mock is :class:`Mock`). All asynchronous functions will be
899+
:class:`AsyncMock`.
900+
901+
>>> class ExampleClass:
902+
... def sync_foo():
903+
... pass
904+
... async def async_foo():
905+
... pass
906+
...
907+
>>> a_mock = AsyncMock(ExampleClass)
908+
>>> a_mock.sync_foo
909+
<MagicMock name='mock.sync_foo' id='...'>
910+
>>> a_mock.async_foo
911+
<AsyncMock name='mock.async_foo' id='...'>
912+
>>> mock = Mock(ExampleClass)
913+
>>> mock.sync_foo
914+
<Mock name='mock.sync_foo' id='...'>
915+
>>> mock.async_foo
916+
<AsyncMock name='mock.async_foo' id='...'>
917+
918+
893919
.. method:: assert_awaited()
894920

895921
Assert that the mock was awaited at least once. Note that this is separate

Lib/unittest/mock.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,8 +997,9 @@ def _get_child_mock(self, /, **kw):
997997
# Any asynchronous magic becomes an AsyncMock
998998
klass = AsyncMock
999999
elif issubclass(_type, AsyncMockMixin):
1000-
if _new_name in _all_sync_magics:
1001-
# Any synchronous magic becomes a MagicMock
1000+
if (_new_name in _all_sync_magics or
1001+
self._mock_methods and _new_name in self._mock_methods):
1002+
# Any synchronous method on AsyncMock becomes a MagicMock
10021003
klass = MagicMock
10031004
else:
10041005
klass = AsyncMock

Lib/unittest/test/testmock/testasync.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
import unittest
55

6-
from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock,
6+
from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock, Mock,
77
create_autospec, sentinel, _CallList)
88

99

@@ -228,33 +228,50 @@ async def test_async():
228228

229229

230230
class AsyncSpecTest(unittest.TestCase):
231-
def test_spec_as_async_positional_magicmock(self):
232-
mock = MagicMock(async_func)
233-
self.assertIsInstance(mock, MagicMock)
234-
m = mock()
235-
self.assertTrue(inspect.isawaitable(m))
236-
asyncio.run(m)
231+
def test_spec_normal_methods_on_class(self):
232+
def inner_test(mock_type):
233+
mock = mock_type(AsyncClass)
234+
self.assertIsInstance(mock.async_method, AsyncMock)
235+
self.assertIsInstance(mock.normal_method, MagicMock)
237236

238-
def test_spec_as_async_kw_magicmock(self):
239-
mock = MagicMock(spec=async_func)
240-
self.assertIsInstance(mock, MagicMock)
241-
m = mock()
242-
self.assertTrue(inspect.isawaitable(m))
243-
asyncio.run(m)
237+
for mock_type in [AsyncMock, MagicMock]:
238+
with self.subTest(f"test method types with {mock_type}"):
239+
inner_test(mock_type)
244240

245-
def test_spec_as_async_kw_AsyncMock(self):
246-
mock = AsyncMock(spec=async_func)
247-
self.assertIsInstance(mock, AsyncMock)
248-
m = mock()
249-
self.assertTrue(inspect.isawaitable(m))
250-
asyncio.run(m)
241+
def test_spec_normal_methods_on_class_with_mock(self):
242+
mock = Mock(AsyncClass)
243+
self.assertIsInstance(mock.async_method, AsyncMock)
244+
self.assertIsInstance(mock.normal_method, Mock)
251245

252-
def test_spec_as_async_positional_AsyncMock(self):
253-
mock = AsyncMock(async_func)
254-
self.assertIsInstance(mock, AsyncMock)
255-
m = mock()
256-
self.assertTrue(inspect.isawaitable(m))
257-
asyncio.run(m)
246+
def test_spec_mock_type_kw(self):
247+
def inner_test(mock_type):
248+
async_mock = mock_type(spec=async_func)
249+
self.assertIsInstance(async_mock, mock_type)
250+
with self.assertWarns(RuntimeWarning):
251+
# Will raise a warning because never awaited
252+
self.assertTrue(inspect.isawaitable(async_mock()))
253+
254+
sync_mock = mock_type(spec=normal_func)
255+
self.assertIsInstance(sync_mock, mock_type)
256+
257+
for mock_type in [AsyncMock, MagicMock, Mock]:
258+
with self.subTest(f"test spec kwarg with {mock_type}"):
259+
inner_test(mock_type)
260+
261+
def test_spec_mock_type_positional(self):
262+
def inner_test(mock_type):
263+
async_mock = mock_type(async_func)
264+
self.assertIsInstance(async_mock, mock_type)
265+
with self.assertWarns(RuntimeWarning):
266+
# Will raise a warning because never awaited
267+
self.assertTrue(inspect.isawaitable(async_mock()))
268+
269+
sync_mock = mock_type(normal_func)
270+
self.assertIsInstance(sync_mock, mock_type)
271+
272+
for mock_type in [AsyncMock, MagicMock, Mock]:
273+
with self.subTest(f"test spec positional with {mock_type}"):
274+
inner_test(mock_type)
258275

259276
def test_spec_as_normal_kw_AsyncMock(self):
260277
mock = AsyncMock(spec=normal_func)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Child mocks will now detect their type as either synchronous or
2+
asynchronous, asynchronous child mocks will be AsyncMocks and synchronous
3+
child mocks will be either MagicMock or Mock (depending on their parent
4+
type).

0 commit comments

Comments
 (0)