tenacity icon indicating copy to clipboard operation
tenacity copied to clipboard

Differences in using `retry` with `AsyncMock` objects on Python 3.9 and Python 3.8 vs. newer Python

Open MatthewFlamm opened this issue 9 months ago • 1 comments

I'm trying to write a test using AsyncMock object with a side_effect error that is then wrapped with tenacity.retry. In Python 3.10+, my tests work, but in Python 3.8 and Python 3.9 the side effect is not swallowed by retry. See below for a MRE.

from unittest.mock import AsyncMock
from tenacity import retry

async def test_mock_retry():

    amock = AsyncMock()
    amock.side_effect = [ValueError, None]

    retry_func = retry(amock)
    await retry_func()
    
    assert amock.call_count == 2

This passes on Python 3.10, Python 3.11, and Python 3.12 but fails on Python 3.8 and Python 3.9 for me.

traceback from Python 3.9:

_______________________________ test_mock_retry ________________________________

    async def test_mock_retry():
    
        amock = AsyncMock()
        amock.side_effect = [ValueError, None]
    
        retry_func = retry(amock)
>       await retry_func()

tests/test_async_mock.py:10: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <AsyncMock id='140142953906240'>, args = (), kwargs = {}, _call = call()
effect = <list_iterator object at 0x7f7593046850>, result = <class 'ValueError'>

    async def _execute_mock_call(self, /, *args, **kwargs):
        # This is nearly just like super(), except for special handling
        # of coroutines
    
        _call = _Call((args, kwargs), two=True)
        self.await_count += 1
        self.await_args = _call
        self.await_args_list.append(_call)
    
        effect = self.side_effect
        if effect is not None:
            if _is_exception(effect):
                raise effect
            elif not _callable(effect):
                try:
                    result = next(effect)
                except StopIteration:
                    # It is impossible to propogate a StopIteration
                    # through coroutines because of PEP 479
                    raise StopAsyncIteration
                if _is_exception(result):
>                   raise result
E                   ValueError

/usr/local/lib/python3.9/unittest/mock.py:2162: ValueError

MatthewFlamm avatar Apr 26 '24 20:04 MatthewFlamm

This is a workaround that works for my use case:

from unittest.mock import AsyncMock
from tenacity import retry

async def test_mock_retry():

    amock = AsyncMock()
    amock.side_effect = [ValueError, None]

    async def wrap_amock(*args, **kwargs):
        return await amock(*args, **kwargs)

    retry_func = retry(wrap_amock)
    await retry_func()
    
    assert amock.call_count == 2

MatthewFlamm avatar Apr 26 '24 23:04 MatthewFlamm