[Python-checkins] gh-94924: support `inspect.iscoroutinefunction` in `create_autospec(async_def)` (#94962)

cjw296 webhook-mailer at python.org
Fri Jun 9 09:29:34 EDT 2023


https://github.com/python/cpython/commit/9bf8d825a66ea2a76169b917c12c237a6af2ed75
commit: 9bf8d825a66ea2a76169b917c12c237a6af2ed75
branch: main
author: Thomas Grainger <tagrain at gmail.com>
committer: cjw296 <chris at withers.org>
date: 2023-06-09T13:29:09Z
summary:

gh-94924: support `inspect.iscoroutinefunction` in `create_autospec(async_def)` (#94962)

* support inspect.iscoroutinefunction in create_autospec(async_def)

* test create_autospec with inspect.iscoroutine and inspect.iscoroutinefunction

* test when create_autospec functions check their signature

files:
A Misc/NEWS.d/next/Library/2022-07-18-14-20-56.gh-issue-94924.X0buz2.rst
M Lib/test/test_unittest/testmock/testasync.py
M Lib/unittest/mock.py

diff --git a/Lib/test/test_unittest/testmock/testasync.py b/Lib/test/test_unittest/testmock/testasync.py
index 5f12f9f956674..e9e1f63e84d4e 100644
--- a/Lib/test/test_unittest/testmock/testasync.py
+++ b/Lib/test/test_unittest/testmock/testasync.py
@@ -232,7 +232,9 @@ async def main():
         run(main())
 
         self.assertTrue(iscoroutinefunction(spec))
+        self.assertTrue(inspect.iscoroutinefunction(spec))
         self.assertTrue(asyncio.iscoroutine(awaitable))
+        self.assertTrue(inspect.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)])
@@ -244,6 +246,25 @@ async def main():
         with self.assertRaises(AssertionError):
             spec.assert_any_await(e=1)
 
+    def test_autospec_checks_signature(self):
+        spec = create_autospec(async_func_args)
+        # signature is not checked when called
+        awaitable = spec()
+        self.assertListEqual(spec.mock_calls, [])
+
+        async def main():
+            await awaitable
+
+        # but it is checked when awaited
+        with self.assertRaises(TypeError):
+            run(main())
+
+        # _checksig_ raises before running or awaiting the mock
+        self.assertListEqual(spec.mock_calls, [])
+        self.assertEqual(spec.await_count, 0)
+        self.assertIsNone(spec.await_args)
+        self.assertEqual(spec.await_args_list, [])
+        spec.assert_not_awaited()
 
     def test_patch_with_autospec(self):
 
@@ -253,7 +274,9 @@ async def test_async():
                 self.assertIsInstance(mock_method.mock, AsyncMock)
 
                 self.assertTrue(iscoroutinefunction(mock_method))
+                self.assertTrue(inspect.iscoroutinefunction(mock_method))
                 self.assertTrue(asyncio.iscoroutine(awaitable))
+                self.assertTrue(inspect.iscoroutine(awaitable))
                 self.assertTrue(inspect.isawaitable(awaitable))
 
                 # Verify the default values during mock setup
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 22f81e55b567f..4ca7062961a6b 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -204,6 +204,33 @@ def checksig(*args, **kwargs):
     _setup_func(funcopy, mock, sig)
     return funcopy
 
+def _set_async_signature(mock, original, instance=False, is_async_mock=False):
+    # creates an async function with signature (*args, **kwargs) that delegates to a
+    # mock. It still does signature checking by calling a lambda with the same
+    # signature as the original.
+
+    skipfirst = isinstance(original, type)
+    result = _get_signature_object(original, instance, skipfirst)
+    if result is None:
+        return mock
+    func, sig = result
+    def checksig(*args, **kwargs):
+        sig.bind(*args, **kwargs)
+    _copy_func_details(func, checksig)
+
+    name = original.__name__
+    if not name.isidentifier():
+        name = 'funcopy'
+    context = {'_checksig_': checksig, 'mock': mock}
+    src = """async def %s(*args, **kwargs):
+    _checksig_(*args, **kwargs)
+    return await mock(*args, **kwargs)""" % name
+    exec (src, context)
+    funcopy = context[name]
+    _setup_func(funcopy, mock, sig)
+    _setup_async_mock(funcopy)
+    return funcopy
+
 
 def _setup_func(funcopy, mock, sig):
     funcopy.mock = mock
@@ -2745,9 +2772,10 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
     if isinstance(spec, FunctionTypes):
         # should only happen at the top level because we don't
         # recurse for functions
-        mock = _set_signature(mock, spec)
         if is_async_func:
-            _setup_async_mock(mock)
+            mock = _set_async_signature(mock, spec)
+        else:
+            mock = _set_signature(mock, spec)
     else:
         _check_signature(spec, mock, is_type, instance)
 
diff --git a/Misc/NEWS.d/next/Library/2022-07-18-14-20-56.gh-issue-94924.X0buz2.rst b/Misc/NEWS.d/next/Library/2022-07-18-14-20-56.gh-issue-94924.X0buz2.rst
new file mode 100644
index 0000000000000..7882f224e75ad
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-07-18-14-20-56.gh-issue-94924.X0buz2.rst
@@ -0,0 +1 @@
+:func:`unittest.mock.create_autospec` now properly returns coroutine functions compatible with :func:`inspect.iscoroutinefunction`



More information about the Python-checkins mailing list