Skip to content

Commit 3667e1e

Browse files
authored
bpo-38163: Child mocks detect their type as sync or async (pythonGH-16471)
1 parent 5bcc6d8 commit 3667e1e

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
@@ -992,8 +992,9 @@ def _get_child_mock(self, /, **kw):
992992
# Any asynchronous magic becomes an AsyncMock
993993
klass = AsyncMock
994994
elif issubclass(_type, AsyncMockMixin):
995-
if _new_name in _all_sync_magics:
996-
# Any synchronous magic becomes a MagicMock
995+
if (_new_name in _all_sync_magics or
996+
self._mock_methods and _new_name in self._mock_methods):
997+
# Any synchronous method on AsyncMock becomes a MagicMock
997998
klass = MagicMock
998999
else:
9991000
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

@@ -232,33 +232,50 @@ async def test_async():
232232

233233

234234
class AsyncSpecTest(unittest.TestCase):
235-
def test_spec_as_async_positional_magicmock(self):
236-
mock = MagicMock(async_func)
237-
self.assertIsInstance(mock, MagicMock)
238-
m = mock()
239-
self.assertTrue(inspect.isawaitable(m))
240-
asyncio.run(m)
235+
def test_spec_normal_methods_on_class(self):
236+
def inner_test(mock_type):
237+
mock = mock_type(AsyncClass)
238+
self.assertIsInstance(mock.async_method, AsyncMock)
239+
self.assertIsInstance(mock.normal_method, MagicMock)
241240

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

249-
def test_spec_as_async_kw_AsyncMock(self):
250-
mock = AsyncMock(spec=async_func)
251-
self.assertIsInstance(mock, AsyncMock)
252-
m = mock()
253-
self.assertTrue(inspect.isawaitable(m))
254-
asyncio.run(m)
245+
def test_spec_normal_methods_on_class_with_mock(self):
246+
mock = Mock(AsyncClass)
247+
self.assertIsInstance(mock.async_method, AsyncMock)
248+
self.assertIsInstance(mock.normal_method, Mock)
255249

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

263280
def test_spec_as_normal_kw_AsyncMock(self):
264281
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)