Skip to content
/ cpython Public
  • Rate limit · GitHub

    Access has been restricted

    You have triggered a rate limit.

    Please wait a few minutes before you try again;
    in some cases this may take up to an hour.

  • Notifications You must be signed in to change notification settings
  • Fork 31.2k
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-38093: Correctly returns AsyncMock for async subclasses. #15947

Merged
merged 15 commits into from
Sep 20, 2019
Rate limit · GitHub

Access has been restricted

You have triggered a rate limit.

Please wait a few minutes before you try again;
in some cases this may take up to an hour.

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fixes aiter, warnings, and example docs.
Rate limit · GitHub

Access has been restricted

You have triggered a rate limit.

Please wait a few minutes before you try again;
in some cases this may take up to an hour.

lisroach committed Sep 11, 2019
commit b831e584230f1e0bb75011eb5691f351c1ef795f
40 changes: 40 additions & 0 deletions Doc/library/unittest.mock-examples.rst
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@

.. testsetup::

import asyncio
import unittest
from unittest.mock import Mock, MagicMock, patch, call, sentinel

@@ -276,6 +277,45 @@ function returns is what the call returns:
2


Mocking asynchronous iterators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Since Python 3.8, ``AsyncMock`` 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.

>>> mock = AsyncMock()
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]


Mocking asynchronous context manager
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Since Python 3.8, ``AsyncMock`` has support to mock
:ref:`async-context-managers` through ``__aenter__`` and ``__aexit__``. The
return value of ``__aenter__`` is an async function.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to say something like:

By default, ``__aenter__`` is set to an :class:`AsyncMock` that return an async function.

We want to make the distinction between what the methods are and what they return clear.


>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = AsyncMock(AsyncContextManager())
>>> 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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually we probably want to replace these either with assert_aiwated_once() or by returning a result and checking for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I will update them in this PR if that's okay.



Creating a Mock from an Existing Object
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

13 changes: 9 additions & 4 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
@@ -989,6 +989,9 @@ def _get_child_mock(self, /, **kw):
_type = type(self)
if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
klass = AsyncMock
elif _new_name in _sync_async_magics:
# Special case these ones b/c users will assume they are async, but they are actually sync
klass = MagicMock
elif issubclass(_type, AsyncMockMixin):
klass = AsyncMock
elif not issubclass(_type, CallableMixin):
@@ -1868,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__',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a test for the default values of these functions? There is a behavior change in 3.8b4 and now where __aexit__ for MagicMock returns False and now it returns a coroutine. The docs can also be updated in the "mock and default values" section since these don't have default values : https://docs.python.org/3.8/library/unittest.mock.html?highlight=asyncmock#unittest.mock.NonCallableMagicMock . I guess this also makes updating _return_values dictionary since the default values are not needed now for these and can be removed.

➜  cpython git:(pr_15947) ✗ python3.8
Python 3.8.0b4 (v3.8.0b4:d93605de72, Aug 29 2019, 21:47:47)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from unittest.mock import MagicMock
>>> m = MagicMock()
>>> m.__aexit__()
False
➜  cpython git:(pr_15947) ✗ ./python.exe
Python 3.9.0a0 (heads/pr_15947:f476b5845a, Sep 12 2019, 10:52:24)
[Clang 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from unittest.mock import MagicMock
>>> m = MagicMock()
>>> m.__aexit__()
<coroutine object AsyncMockMixin._mock_call at 0x107005d40>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is a good point about __aexit__, I think it should have a default return_value of False (which is in the _return_values dict, but maybe needs to be added to the _calculate_return_value instead.

This section specifically is the "non-default" values, so I don't think it make sense to update that section of the docs, and there is a unit test for the default values that are set in test_magicmock_defaults. I will update __aexit__ and add it there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah well, this is actually another case of "called" vs "awaited". Once you await __aexit__ it returns False correctly, just calling prints the type of __aexit__ which is a coroutine object.

So I want to leave the __aexit__ in _return_value and I will update the test. Do you think that would resolve this?

Copy link
Member

@tirkarthi tirkarthi Sep 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay so it's a case where once __aexit__ is awaited then it returns the calculated value of False. Sorry, I am not sure of async internals to see if __aexit__ can be used like await __aexit__() or makes sense but I can understand in terms of the PR context with mock that the coroutine needs to be always awaited.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await cm.__aexit__() is a legal call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, this is how async cms work: https://www.python.org/dev/peps/pep-0492/#new-syntax
Both __aenter__ and __aexit__ are awaited.

'__getnewargs_ex__',
}


@@ -1887,10 +1890,12 @@ def method(self, /, *args, **kw):

# Magic methods used for async `with` statements
_async_method_magics = {"__aenter__", "__aexit__", "__anext__"}
# `__aiter__` is a plain function but used with async calls
_async_magics = _async_method_magics | {"__aiter__"}
# Magic methods that are only used with async calls but are synchronous functions themselves
_sync_async_magics = {"__aiter__"}
_async_magics = _async_method_magics | _sync_async_magics

_all_magics = _magics | _non_defaults
_all_sync_magics = _magics | _non_defaults
_all_magics = _all_sync_magics | _async_magics

_unsupported_magics = {
'__getattr__', '__setattr__',