classification
Title: call count in not registered in AsyncMock till the coroutine is awaited
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.9, Python 3.8
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: cjw296, lisroach, mariocj89, michael.foord, miss-islington, xtreak
Priority: normal Keywords: patch

Created on 2019-06-24 10:30 by xtreak, last changed 2019-09-10 08:50 by xtreak. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 15761 merged lisroach, 2019-09-09 11:35
PR 15810 merged miss-islington, 2019-09-09 17:10
Messages (10)
msg346364 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-06-24 10:30
I noticed this while working on https://github.com/aio-libs/aiosmtpd/issues/167 where an async function was mocked that now returns an AsyncMock instead of MagicMock. The tests seem to look for call_args, mock_calls etc in the synchronous API without awaiting on the AsyncMock. In AsyncMock __call__ is an async function [0] and hence in the below example mock_calls is not recorded unless the coroutine is awaited. Is this intended since super()._mock_call [1] is inside the async function _mock_call through which the synchronous API is recorded. It's slightly confusing in my opinion while trying to use synchronous helpers before calling await. 

./python.exe -m asyncio
asyncio REPL 3.9.0a0 (heads/master:770847a7db, Jun 24 2019, 10:36:45)
[Clang 7.0.2 (clang-700.1.81)] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from unittest.mock import AsyncMock
>>> mock = AsyncMock()
>>> coro = mock(1, 2)
>>> mock.mock_calls
[]
>>> await coro # Await executes _mock_call now and hence mock_calls are registered
<AsyncMock name='mock()' id='4332060752'>
>>> mock.mock_calls
[call(1, 2)]

[0] https://github.com/python/cpython/blob/47fbc4e45b35b3111e2d947a66490a43ac21d363/Lib/unittest/mock.py#L2081
[1] https://github.com/python/cpython/blob/47fbc4e45b35b3111e2d947a66490a43ac21d363/Lib/unittest/mock.py#L2083
msg351392 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2019-09-09 09:55
I see your point it is confusing the difference between the async and sync API, but I think the current AsyncMock call check is correct. According to the asyncio docs: "...simply calling a coroutine will not schedule it to be executed" (https://docs.python.org/3/library/asyncio-task.html#coroutines) and it goes on to say you either need to call the function with await, asyncio.run(), or asyncio.create_task(). 

So I believe calling an AsyncMock without using await should not log as a call, it is only if one of the three criteria above is met that it should be added to the mock_calls list.
msg351407 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-09-09 10:28
I just checked the behavior with asynctest. The _mock_call implementation where the call is recorded is similar except that in asynctest it's a synchronous function [0] and in AsyncMock it's an async function [1] thus needs to be awaited to register call count. Agreed it's more of a confusion over does call mean a function call or something that should be recorded only once awaited given that there is call_count and await_calls. But would be good to have this documented that call_count is recorded only after await.


Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 16:52:21)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import asynctest
>>> m = asynctest.CoroutineMock()
>>> m(1)
<generator object CoroutineMock._mock_call.<locals>.proxy at 0x1023c8b88>
>>> m.mock_calls
[call(1)]

Python 3.9.0a0 (heads/master:a6563650c8, Sep  9 2019, 14:53:16)
[Clang 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from unittest.mock import AsyncMock
>>> m = AsyncMock()
>>> m.mock_calls
[]


[0] https://github.com/Martiusweb/asynctest/blob/d1d47ecb8220371284230d6d6fe642649ef82ab2/asynctest/mock.py#L584
[1] https://github.com/python/cpython/blob/19052a11314e7be7ba003fd6cdbb5400a5d77d96/Lib/unittest/mock.py#L2120
msg351412 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2019-09-09 10:34
Agreed, I think documentation can be clearer around this. I'll add a PR to try to clarify.
msg351416 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2019-09-09 10:38
I wonder if `await_count` is really necessary, since it is essentially the same as `call_count`. Would it be too late or confusing to remove it now?
msg351423 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-09-09 10:50
> I wonder if `await_count` is really necessary, since it is essentially the same as `call_count`. Would it be too late or confusing to remove it now?

IMO if we are to document that mock_calls is recorded over await then we can have both call_count and await_count because if users start using AsyncMock then they can keep using the call_* functions. Removing it would mean there are also other counterparts as below to make sure we keep or remove all to remove discrepancy. I am more leaned towards keeping them and document it.

>>> m.await
m.await_args      m.await_args_list m.await_count     m.awaited
>>> m.call
m.call_args      m.call_args_list m.call_count     m.called

With respect to removal I think the window is still open till the 3.8.0RC1.
msg351428 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2019-09-09 10:59
I think you are right, I'd prefer to leave it in. It also helps users who are switching from AsyncTest to the std lib AsyncMock, they can keep using `await_count`. I'll update the docs!
msg351533 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2019-09-09 16:54
New changeset b9f65f01fd761da7799f36d29b54518399d3458e by Lisa Roach in branch 'master':
bpo-37383: Updates docs to reflect AsyncMock call_count after await. (#15761)
https://github.com/python/cpython/commit/b9f65f01fd761da7799f36d29b54518399d3458e
msg351584 - (view) Author: miss-islington (miss-islington) Date: 2019-09-10 07:31
New changeset d4391aa5eb4767e19b7b380a836413e7c47f1fb4 by Miss Islington (bot) in branch '3.8':
bpo-37383: Updates docs to reflect AsyncMock call_count after await. (GH-15761)
https://github.com/python/cpython/commit/d4391aa5eb4767e19b7b380a836413e7c47f1fb4
msg351592 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2019-09-10 08:50
Closing since docs are updated. Thank you Lisa.
History
Date User Action Args
2019-09-10 08:50:44xtreaksetstatus: open -> closed
resolution: fixed
messages: + msg351592

stage: patch review -> resolved
2019-09-10 07:31:38miss-islingtonsetnosy: + miss-islington
messages: + msg351584
2019-09-09 17:10:57miss-islingtonsetpull_requests: + pull_request15460
2019-09-09 16:54:16lisroachsetmessages: + msg351533
2019-09-09 11:35:33lisroachsetkeywords: + patch
stage: patch review
pull_requests: + pull_request15414
2019-09-09 10:59:04lisroachsetmessages: + msg351428
2019-09-09 10:50:23xtreaksetmessages: + msg351423
2019-09-09 10:38:15lisroachsetmessages: + msg351416
2019-09-09 10:34:52lisroachsetmessages: + msg351412
2019-09-09 10:28:02xtreaksetmessages: + msg351407
2019-09-09 09:55:22lisroachsetmessages: + msg351392
2019-06-24 10:30:16xtreakcreate