]> granicus.if.org Git - python/commitdiff
bpo-37047: Refactor AsyncMock setup logic for autospeccing (GH-13574)
authorXtreak <tir.karthi@gmail.com>
Mon, 27 May 2019 12:56:23 +0000 (18:26 +0530)
committerYury Selivanov <yury@magic.io>
Mon, 27 May 2019 12:56:23 +0000 (14:56 +0200)
Handle late binding and attribute access in unittest.mock.AsyncMock
setup for autospeccing.

Doc/library/unittest.mock.rst
Lib/unittest/mock.py
Lib/unittest/test/testmock/testasync.py
Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst [new file with mode: 0644]

index 163da9aecdbbc5ce12829aad2299cb3ee9c7cd25..36ac24afa2bc0738fe73e2effcc2353e3cd4aa5b 100644 (file)
@@ -1945,7 +1945,7 @@ The full list of supported magic methods is:
 * Container methods: ``__getitem__``, ``__setitem__``, ``__delitem__``,
   ``__contains__``, ``__len__``, ``__iter__``, ``__reversed__``
   and ``__missing__``
-* Context manager: ``__enter__`` and ``__exit__``
+* Context manager: ``__enter__``, ``__exit__``, ``__aenter`` and ``__aexit__``
 * Unary numeric methods: ``__neg__``, ``__pos__`` and ``__invert__``
 * The numeric methods (including right hand and in-place variants):
   ``__add__``, ``__sub__``, ``__mul__``, ``__matmul__``, ``__div__``, ``__truediv__``,
@@ -1957,10 +1957,14 @@ The full list of supported magic methods is:
 * Pickling: ``__reduce__``, ``__reduce_ex__``, ``__getinitargs__``,
   ``__getnewargs__``, ``__getstate__`` and ``__setstate__``
 * File system path representation: ``__fspath__``
+* Asynchronous iteration methods: ``__aiter__`` and ``__anext__``
 
 .. versionchanged:: 3.8
    Added support for :func:`os.PathLike.__fspath__`.
 
+.. versionchanged:: 3.8
+   Added support for ``__aenter__``, ``__aexit__``, ``__aiter__`` and ``__anext__``.
+
 
 The following methods exist but are *not* supported as they are either in use
 by mock, can't be set dynamically, or can cause problems:
index b14bf01b28fdbdb4281b9352979d82d9e325e6bc..b91afd88dd132e16c444b92b1bed3375b311af52 100644 (file)
@@ -51,6 +51,13 @@ def _is_async_obj(obj):
         return False
 
 
+def _is_async_func(func):
+    if getattr(func, '__code__', None):
+        return asyncio.iscoroutinefunction(func)
+    else:
+        return False
+
+
 def _is_instance_mock(obj):
     # can't use isinstance on Mock objects because they override __class__
     # The base class for all mocks is NonCallableMock
@@ -225,6 +232,34 @@ def _setup_func(funcopy, mock, sig):
     mock._mock_delegate = funcopy
 
 
+def _setup_async_mock(mock):
+    mock._is_coroutine = asyncio.coroutines._is_coroutine
+    mock.await_count = 0
+    mock.await_args = None
+    mock.await_args_list = _CallList()
+    mock.awaited = _AwaitEvent(mock)
+
+    # Mock is not configured yet so the attributes are set
+    # to a function and then the corresponding mock helper function
+    # is called when the helper is accessed similar to _setup_func.
+    def wrapper(attr, *args, **kwargs):
+        return getattr(mock.mock, attr)(*args, **kwargs)
+
+    for attribute in ('assert_awaited',
+                      'assert_awaited_once',
+                      'assert_awaited_with',
+                      'assert_awaited_once_with',
+                      'assert_any_await',
+                      'assert_has_awaits',
+                      'assert_not_awaited'):
+
+        # setattr(mock, attribute, wrapper) causes late binding
+        # hence attribute will always be the last value in the loop
+        # Use partial(wrapper, attribute) to ensure the attribute is bound
+        # correctly.
+        setattr(mock, attribute, partial(wrapper, attribute))
+
+
 def _is_magic(name):
     return '__%s__' % name[2:-2] == name
 
@@ -2151,7 +2186,7 @@ class AsyncMockMixin(Base):
         """
         self = _mock_self
         if self.await_count != 0:
-            msg = (f"Expected {self._mock_name or 'mock'} to have been awaited once."
+            msg = (f"Expected {self._mock_name or 'mock'} to not have been awaited."
                    f" Awaited {self.await_count} times.")
             raise AssertionError(msg)
 
@@ -2457,10 +2492,7 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
         spec = type(spec)
 
     is_type = isinstance(spec, type)
-    if getattr(spec, '__code__', None):
-        is_async_func = asyncio.iscoroutinefunction(spec)
-    else:
-        is_async_func = False
+    is_async_func = _is_async_func(spec)
     _kwargs = {'spec': spec}
     if spec_set:
         _kwargs = {'spec_set': spec}
@@ -2498,26 +2530,11 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
                  name=_name, **_kwargs)
 
     if isinstance(spec, FunctionTypes):
-        wrapped_mock = mock
         # should only happen at the top level because we don't
         # recurse for functions
         mock = _set_signature(mock, spec)
         if is_async_func:
-            mock._is_coroutine = asyncio.coroutines._is_coroutine
-            mock.await_count = 0
-            mock.await_args = None
-            mock.await_args_list = _CallList()
-
-            for a in ('assert_awaited',
-                      'assert_awaited_once',
-                      'assert_awaited_with',
-                      'assert_awaited_once_with',
-                      'assert_any_await',
-                      'assert_has_awaits',
-                      'assert_not_awaited'):
-                def f(*args, **kwargs):
-                    return getattr(wrapped_mock, a)(*args, **kwargs)
-                setattr(mock, a, f)
+            _setup_async_mock(mock)
     else:
         _check_signature(spec, mock, is_type, instance)
 
index a9aa1434b963f1313fe4715ae9f52e2590673c28..0519d59696f6c6500854cfb5a890d7c3654a94b1 100644 (file)
@@ -2,7 +2,8 @@ import asyncio
 import inspect
 import unittest
 
-from unittest.mock import call, AsyncMock, patch, MagicMock, create_autospec
+from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec,
+                           _AwaitEvent)
 
 
 def tearDownModule():
@@ -20,6 +21,9 @@ class AsyncClass:
 async def async_func():
     pass
 
+async def async_func_args(a, b, *, c):
+    pass
+
 def normal_func():
     pass
 
@@ -141,8 +145,63 @@ class AsyncAutospecTest(unittest.TestCase):
             create_autospec(async_func, instance=True)
 
     def test_create_autospec(self):
-        spec = create_autospec(async_func)
+        spec = create_autospec(async_func_args)
+        awaitable = spec(1, 2, c=3)
+        async def main():
+            await awaitable
+
+        self.assertEqual(spec.await_count, 0)
+        self.assertIsNone(spec.await_args)
+        self.assertEqual(spec.await_args_list, [])
+        self.assertIsInstance(spec.awaited, _AwaitEvent)
+        spec.assert_not_awaited()
+
+        asyncio.run(main())
+
         self.assertTrue(asyncio.iscoroutinefunction(spec))
+        self.assertTrue(asyncio.iscoroutine(awaitable))
+        self.assertEqual(spec.await_count, 1)
+        self.assertEqual(spec.await_args, call(1, 2, c=3))
+        self.assertEqual(spec.await_args_list, [call(1, 2, c=3)])
+        spec.assert_awaited_once()
+        spec.assert_awaited_once_with(1, 2, c=3)
+        spec.assert_awaited_with(1, 2, c=3)
+        spec.assert_awaited()
+
+    def test_patch_with_autospec(self):
+
+        async def test_async():
+            with patch(f"{__name__}.async_func_args", autospec=True) as mock_method:
+                awaitable = mock_method(1, 2, c=3)
+                self.assertIsInstance(mock_method.mock, AsyncMock)
+
+                self.assertTrue(asyncio.iscoroutinefunction(mock_method))
+                self.assertTrue(asyncio.iscoroutine(awaitable))
+                self.assertTrue(inspect.isawaitable(awaitable))
+
+                # Verify the default values during mock setup
+                self.assertEqual(mock_method.await_count, 0)
+                self.assertEqual(mock_method.await_args_list, [])
+                self.assertIsNone(mock_method.await_args)
+                self.assertIsInstance(mock_method.awaited, _AwaitEvent)
+                mock_method.assert_not_awaited()
+
+                await awaitable
+
+            self.assertEqual(mock_method.await_count, 1)
+            self.assertEqual(mock_method.await_args, call(1, 2, c=3))
+            self.assertEqual(mock_method.await_args_list, [call(1, 2, c=3)])
+            mock_method.assert_awaited_once()
+            mock_method.assert_awaited_once_with(1, 2, c=3)
+            mock_method.assert_awaited_with(1, 2, c=3)
+            mock_method.assert_awaited()
+
+            mock_method.reset_mock()
+            self.assertEqual(mock_method.await_count, 0)
+            self.assertIsNone(mock_method.await_args)
+            self.assertEqual(mock_method.await_args_list, [])
+
+        asyncio.run(test_async())
 
 
 class AsyncSpecTest(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst b/Misc/NEWS.d/next/Library/2019-05-26-01-20-06.bpo-37047.K9epi8.rst
new file mode 100644 (file)
index 0000000..ace5a3a
--- /dev/null
@@ -0,0 +1,3 @@
+Handle late binding and attribute access in :class:`unittest.mock.AsyncMock`
+setup for autospeccing. Document newly implemented async methods in
+:class:`unittest.mock.MagicMock`.