[Python-checkins] (no subject)

Stéphane Wirtel webhook-mailer at python.org
Sat Sep 21 02:00:08 EDT 2019




To: python-checkins at python.org
Subject: [3.8] bpo-38093: Correctly returns AsyncMock for async subclasses.
 (GH-15947) (GH-16299)
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
MIME-Version: 1.0

https://github.com/python/cpython/commit/865bb685a67798eb98dcf5f3a852e08c7779=
2998
commit: 865bb685a67798eb98dcf5f3a852e08c77792998
branch: 3.8
author: Lisa Roach <lisaroach14 at gmail.com>
committer: St=C3=A9phane Wirtel <stephane at wirtel.be>
date: 2019-09-21T08:00:04+02:00
summary:

[3.8] bpo-38093: Correctly returns AsyncMock for async subclasses. (GH-15947)=
 (GH-16299)

(cherry picked from commit 8b03f943c37e07fb2394acdcfacd066647f9b1fd)

Co-authored-by: Lisa Roach <lisaroach14 at gmail.com>

files:
A Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst
M Doc/library/unittest.mock-examples.rst
M Lib/unittest/mock.py
M Lib/unittest/test/testmock/testasync.py
M Lib/unittest/test/testmock/testmagicmethods.py

diff --git a/Doc/library/unittest.mock-examples.rst b/Doc/library/unittest.mo=
ck-examples.rst
index cf6b671b5bee..e650bb1e23e0 100644
--- a/Doc/library/unittest.mock-examples.rst
+++ b/Doc/library/unittest.mock-examples.rst
@@ -14,7 +14,7 @@
=20
     import asyncio
     import unittest
-    from unittest.mock import Mock, MagicMock, patch, call, sentinel
+    from unittest.mock import Mock, MagicMock, AsyncMock, patch, call, senti=
nel
=20
     class SomeClass:
         attribute =3D 'this is a doctest'
@@ -280,14 +280,16 @@ function returns is what the call returns:
 Mocking asynchronous iterators
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
=20
-Since Python 3.8, ``MagicMock`` has support to mock :ref:`async-iterators`
-through ``__aiter__``. The :attr:`~Mock.return_value` attribute of ``__aiter=
__``
-can be used to set the return values to be used for iteration.
+Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock
+:ref:`async-iterators` through ``__aiter__``. The :attr:`~Mock.return_value`
+attribute of ``__aiter__`` can be used to set the return values to be used f=
or
+iteration.
=20
-    >>> mock =3D MagicMock()
+    >>> mock =3D MagicMock()  # AsyncMock also works here
     >>> mock.__aiter__.return_value =3D [1, 2, 3]
     >>> async def main():
     ...     return [i async for i in mock]
+    ...
     >>> asyncio.run(main())
     [1, 2, 3]
=20
@@ -295,24 +297,25 @@ can be used to set the return values to be used for ite=
ration.
 Mocking asynchronous context manager
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
=20
-Since Python 3.8, ``MagicMock`` has support to mock
-:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. The
-return value of ``__aenter__`` is an :class:`AsyncMock`.
+Since Python 3.8, ``AsyncMock`` and ``MagicMock`` have support to mock
+:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``.
+By default, ``__aenter__`` and ``__aexit__`` are ``AsyncMock`` instances that
+return an async function.
=20
     >>> class AsyncContextManager:
-    ...
     ...     async def __aenter__(self):
     ...         return self
-    ...
-    ...     async def __aexit__(self):
+    ...     async def __aexit__(self, exc_type, exc, tb):
     ...         pass
-    >>> mock_instance =3D MagicMock(AsyncContextManager())
+    ...
+    >>> mock_instance =3D MagicMock(AsyncContextManager())  # AsyncMock also=
 works here
     >>> async def main():
     ...     async with mock_instance as result:
     ...         pass
+    ...
     >>> asyncio.run(main())
-    >>> mock_instance.__aenter__.assert_called_once()
-    >>> mock_instance.__aexit__.assert_called_once()
+    >>> mock_instance.__aenter__.assert_awaited_once()
+    >>> mock_instance.__aexit__.assert_awaited_once()
=20
=20
 Creating a Mock from an Existing Object
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 9fd5c3ce9d0a..0c7545bd2b7f 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -988,9 +988,13 @@ def _get_child_mock(self, /, **kw):
         _type =3D type(self)
         if issubclass(_type, MagicMock) and _new_name in _async_method_magic=
s:
             klass =3D AsyncMock
-        if issubclass(_type, AsyncMockMixin):
+        elif _new_name  in _sync_async_magics:
+            # Special case these ones b/c users will assume they are async,
+            # but they are actually sync (ie. __aiter__)
             klass =3D MagicMock
-        if not issubclass(_type, CallableMixin):
+        elif issubclass(_type, AsyncMockMixin):
+            klass =3D AsyncMock
+        elif not issubclass(_type, CallableMixin):
             if issubclass(_type, NonCallableMagicMock):
                 klass =3D MagicMock
             elif issubclass(_type, NonCallableMock) :
@@ -1867,7 +1871,7 @@ def _patch_stopall():
     '__reduce__', '__reduce_ex__', '__getinitargs__', '__getnewargs__',
     '__getstate__', '__setstate__', '__getformat__', '__setformat__',
     '__repr__', '__dir__', '__subclasses__', '__format__',
-    '__getnewargs_ex__', '__aenter__', '__aexit__', '__anext__', '__aiter__',
+    '__getnewargs_ex__',
 }
=20
=20
@@ -1886,10 +1890,12 @@ def method(self, /, *args, **kw):
=20
 # Magic methods used for async `with` statements
 _async_method_magics =3D {"__aenter__", "__aexit__", "__anext__"}
-# `__aiter__` is a plain function but used with async calls
-_async_magics =3D _async_method_magics | {"__aiter__"}
+# Magic methods that are only used with async calls but are synchronous func=
tions themselves
+_sync_async_magics =3D {"__aiter__"}
+_async_magics =3D _async_method_magics | _sync_async_magics
=20
-_all_magics =3D _magics | _non_defaults
+_all_sync_magics =3D _magics | _non_defaults
+_all_magics =3D _all_sync_magics | _async_magics
=20
 _unsupported_magics =3D {
     '__getattr__', '__setattr__',
diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/test=
mock/testasync.py
index 4660bea3e123..9b50b6d86bbd 100644
--- a/Lib/unittest/test/testmock/testasync.py
+++ b/Lib/unittest/test/testmock/testasync.py
@@ -377,10 +377,6 @@ def test_add_side_effect_iterable(self):
=20
 class AsyncContextManagerTest(unittest.TestCase):
     class WithAsyncContextManager:
-        def __init__(self):
-            self.entered =3D False
-            self.exited =3D False
-
         async def __aenter__(self, *args, **kwargs):
             self.entered =3D True
             return self
@@ -388,32 +384,81 @@ def __init__(self):
         async def __aexit__(self, *args, **kwargs):
             self.exited =3D True
=20
-    def test_magic_methods_are_async_mocks(self):
-        mock =3D MagicMock(self.WithAsyncContextManager())
-        self.assertIsInstance(mock.__aenter__, AsyncMock)
-        self.assertIsInstance(mock.__aexit__, AsyncMock)
+    class WithSyncContextManager:
+        def __enter__(self, *args, **kwargs):
+            return self
+
+        def __exit__(self, *args, **kwargs):
+            pass
+
+    class ProductionCode:
+        # Example real-world(ish) code
+        def __init__(self):
+            self.session =3D None
+
+        async def main(self):
+            async with self.session.post('https://python.org') as response:
+                val =3D await response.json()
+                return val
+
+    def test_async_magic_methods_are_async_mocks_with_magicmock(self):
+        cm_mock =3D MagicMock(self.WithAsyncContextManager())
+        self.assertIsInstance(cm_mock.__aenter__, AsyncMock)
+        self.assertIsInstance(cm_mock.__aexit__, AsyncMock)
+
+    def test_magicmock_has_async_magic_methods(self):
+        cm =3D MagicMock(name=3D'magic_cm')
+        self.assertTrue(hasattr(cm, "__aenter__"))
+        self.assertTrue(hasattr(cm, "__aexit__"))
+
+    def test_magic_methods_are_async_functions(self):
+        cm =3D MagicMock(name=3D'magic_cm')
+        self.assertIsInstance(cm.__aenter__, AsyncMock)
+        self.assertIsInstance(cm.__aexit__, AsyncMock)
+        # AsyncMocks are also coroutine functions
+        self.assertTrue(asyncio.iscoroutinefunction(cm.__aenter__))
+        self.assertTrue(asyncio.iscoroutinefunction(cm.__aexit__))
+
+    def test_set_return_value_of_aenter(self):
+        def inner_test(mock_type):
+            pc =3D self.ProductionCode()
+            pc.session =3D MagicMock(name=3D'sessionmock')
+            cm =3D mock_type(name=3D'magic_cm')
+            response =3D AsyncMock(name=3D'response')
+            response.json =3D AsyncMock(return_value=3D{'json': 123})
+            cm.__aenter__.return_value =3D response
+            pc.session.post.return_value =3D cm
+            result =3D asyncio.run(pc.main())
+            self.assertEqual(result, {'json': 123})
+
+        for mock_type in [AsyncMock, MagicMock]:
+            with self.subTest(f"test set return value of aenter with {mock_t=
ype}"):
+                inner_test(mock_type)
=20
     def test_mock_supports_async_context_manager(self):
-        called =3D False
-        instance =3D self.WithAsyncContextManager()
-        mock_instance =3D MagicMock(instance)
+        def inner_test(mock_type):
+            called =3D False
+            cm =3D self.WithAsyncContextManager()
+            cm_mock =3D mock_type(cm)
+
+            async def use_context_manager():
+                nonlocal called
+                async with cm_mock as result:
+                    called =3D True
+                return result
=20
-        async def use_context_manager():
-            nonlocal called
-            async with mock_instance as result:
-                called =3D True
-            return result
-
-        result =3D asyncio.run(use_context_manager())
-        self.assertFalse(instance.entered)
-        self.assertFalse(instance.exited)
-        self.assertTrue(called)
-        self.assertTrue(mock_instance.entered)
-        self.assertTrue(mock_instance.exited)
-        self.assertTrue(mock_instance.__aenter__.called)
-        self.assertTrue(mock_instance.__aexit__.called)
-        self.assertIsNot(mock_instance, result)
-        self.assertIsInstance(result, AsyncMock)
+            cm_result =3D asyncio.run(use_context_manager())
+            self.assertTrue(called)
+            self.assertTrue(cm_mock.__aenter__.called)
+            self.assertTrue(cm_mock.__aexit__.called)
+            cm_mock.__aenter__.assert_awaited()
+            cm_mock.__aexit__.assert_awaited()
+            # We mock __aenter__ so it does not return self
+            self.assertIsNot(cm_mock, cm_result)
+
+        for mock_type in [AsyncMock, MagicMock]:
+            with self.subTest(f"test context manager magics with {mock_type}=
"):
+                inner_test(mock_type)
=20
     def test_mock_customize_async_context_manager(self):
         instance =3D self.WithAsyncContextManager()
@@ -481,27 +526,30 @@ def __aiter__(self):
=20
             raise StopAsyncIteration
=20
-    def test_mock_aiter_and_anext(self):
-        instance =3D self.WithAsyncIterator()
-        mock_instance =3D MagicMock(instance)
-
-        self.assertEqual(asyncio.iscoroutine(instance.__aiter__),
-                         asyncio.iscoroutine(mock_instance.__aiter__))
-        self.assertEqual(asyncio.iscoroutine(instance.__anext__),
-                         asyncio.iscoroutine(mock_instance.__anext__))
-
-        iterator =3D instance.__aiter__()
-        if asyncio.iscoroutine(iterator):
-            iterator =3D asyncio.run(iterator)
-
-        mock_iterator =3D mock_instance.__aiter__()
-        if asyncio.iscoroutine(mock_iterator):
-            mock_iterator =3D asyncio.run(mock_iterator)
+    def test_aiter_set_return_value(self):
+        mock_iter =3D AsyncMock(name=3D"tester")
+        mock_iter.__aiter__.return_value =3D [1, 2, 3]
+        async def main():
+            return [i async for i in mock_iter]
+        result =3D asyncio.run(main())
+        self.assertEqual(result, [1, 2, 3])
+
+    def test_mock_aiter_and_anext_asyncmock(self):
+        def inner_test(mock_type):
+            instance =3D self.WithAsyncIterator()
+            mock_instance =3D mock_type(instance)
+            # Check that the mock and the real thing bahave the same
+            # __aiter__ is not actually async, so not a coroutinefunction
+            self.assertFalse(asyncio.iscoroutinefunction(instance.__aiter__))
+            self.assertFalse(asyncio.iscoroutinefunction(mock_instance.__ait=
er__))
+            # __anext__ is async
+            self.assertTrue(asyncio.iscoroutinefunction(instance.__anext__))
+            self.assertTrue(asyncio.iscoroutinefunction(mock_instance.__anex=
t__))
+
+        for mock_type in [AsyncMock, MagicMock]:
+            with self.subTest(f"test aiter and anext corourtine with {mock_t=
ype}"):
+                inner_test(mock_type)
=20
-        self.assertEqual(asyncio.iscoroutine(iterator.__aiter__),
-                         asyncio.iscoroutine(mock_iterator.__aiter__))
-        self.assertEqual(asyncio.iscoroutine(iterator.__anext__),
-                         asyncio.iscoroutine(mock_iterator.__anext__))
=20
     def test_mock_async_for(self):
         async def iterate(iterator):
@@ -512,19 +560,30 @@ def test_mock_async_for(self):
             return accumulator
=20
         expected =3D ["FOO", "BAR", "BAZ"]
-        with self.subTest("iterate through default value"):
-            mock_instance =3D MagicMock(self.WithAsyncIterator())
-            self.assertEqual([], asyncio.run(iterate(mock_instance)))
+        def test_default(mock_type):
+            mock_instance =3D mock_type(self.WithAsyncIterator())
+            self.assertEqual(asyncio.run(iterate(mock_instance)), [])
+
=20
-        with self.subTest("iterate through set return_value"):
-            mock_instance =3D MagicMock(self.WithAsyncIterator())
+        def test_set_return_value(mock_type):
+            mock_instance =3D mock_type(self.WithAsyncIterator())
             mock_instance.__aiter__.return_value =3D expected[:]
-            self.assertEqual(expected, asyncio.run(iterate(mock_instance)))
+            self.assertEqual(asyncio.run(iterate(mock_instance)), expected)
=20
-        with self.subTest("iterate through set return_value iterator"):
-            mock_instance =3D MagicMock(self.WithAsyncIterator())
+        def test_set_return_value_iter(mock_type):
+            mock_instance =3D mock_type(self.WithAsyncIterator())
             mock_instance.__aiter__.return_value =3D iter(expected[:])
-            self.assertEqual(expected, asyncio.run(iterate(mock_instance)))
+            self.assertEqual(asyncio.run(iterate(mock_instance)), expected)
+
+        for mock_type in [AsyncMock, MagicMock]:
+            with self.subTest(f"default value with {mock_type}"):
+                test_default(mock_type)
+
+            with self.subTest(f"set return_value with {mock_type}"):
+                test_set_return_value(mock_type)
+
+            with self.subTest(f"set return_value iterator with {mock_type}"):
+                test_set_return_value_iter(mock_type)
=20
=20
 class AsyncMockAssert(unittest.TestCase):
diff --git a/Lib/unittest/test/testmock/testmagicmethods.py b/Lib/unittest/te=
st/testmock/testmagicmethods.py
index 130a3397ba0d..57f85e951e20 100644
--- a/Lib/unittest/test/testmock/testmagicmethods.py
+++ b/Lib/unittest/test/testmock/testmagicmethods.py
@@ -1,8 +1,9 @@
+import asyncio
 import math
 import unittest
 import os
 import sys
-from unittest.mock import Mock, MagicMock, _magics
+from unittest.mock import AsyncMock, Mock, MagicMock, _magics
=20
=20
=20
@@ -271,6 +272,34 @@ def test_magic_mock_equality(self):
         self.assertEqual(mock !=3D mock, False)
=20
=20
+    # This should be fixed with issue38163
+    @unittest.expectedFailure
+    def test_asyncmock_defaults(self):
+        mock =3D AsyncMock()
+        self.assertEqual(int(mock), 1)
+        self.assertEqual(complex(mock), 1j)
+        self.assertEqual(float(mock), 1.0)
+        self.assertNotIn(object(), mock)
+        self.assertEqual(len(mock), 0)
+        self.assertEqual(list(mock), [])
+        self.assertEqual(hash(mock), object.__hash__(mock))
+        self.assertEqual(str(mock), object.__str__(mock))
+        self.assertTrue(bool(mock))
+        self.assertEqual(round(mock), mock.__round__())
+        self.assertEqual(math.trunc(mock), mock.__trunc__())
+        self.assertEqual(math.floor(mock), mock.__floor__())
+        self.assertEqual(math.ceil(mock), mock.__ceil__())
+        self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__))
+        self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__))
+        self.assertIsInstance(mock.__aenter__, AsyncMock)
+        self.assertIsInstance(mock.__aexit__, AsyncMock)
+
+        # in Python 3 oct and hex use __index__
+        # so these tests are for __index__ in py3k
+        self.assertEqual(oct(mock), '0o1')
+        self.assertEqual(hex(mock), '0x1')
+        # how to test __sizeof__ ?
+
     def test_magicmock_defaults(self):
         mock =3D MagicMock()
         self.assertEqual(int(mock), 1)
@@ -286,6 +315,10 @@ def test_magicmock_defaults(self):
         self.assertEqual(math.trunc(mock), mock.__trunc__())
         self.assertEqual(math.floor(mock), mock.__floor__())
         self.assertEqual(math.ceil(mock), mock.__ceil__())
+        self.assertTrue(asyncio.iscoroutinefunction(mock.__aexit__))
+        self.assertTrue(asyncio.iscoroutinefunction(mock.__aenter__))
+        self.assertIsInstance(mock.__aenter__, AsyncMock)
+        self.assertIsInstance(mock.__aexit__, AsyncMock)
=20
         # in Python 3 oct and hex use __index__
         # so these tests are for __index__ in py3k
diff --git a/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rs=
t b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst
new file mode 100644
index 000000000000..24a53013cec7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-09-11-14-45-30.bpo-38093.yQ6k7y.rst
@@ -0,0 +1,2 @@
+Fixes AsyncMock so it doesn't crash when used with AsyncContextManagers
+or AsyncIterators.



More information about the Python-checkins mailing list