Skip to content

AttributeError: '_patch' object has no attribute '_exit_stack' #152

@aikikode

Description

@aikikode

Setup

I use Python 3.7.8 and the following setup:

# requirements.txt
asynctest==0.13.0
pytest==5.4.3
pytest-asyncio==0.14.0

and the test file

# test.py
import pytest
from asynctest import mock


async def hello():
    return 'HELLO'


@pytest.mark.asyncio
@mock.patch('test.hello')
class TestFetch:
    async def test_will_pass(self, mock_hello):
        mock_hello.return_value = 'OK'
        assert await hello() == 'OK'

    @pytest.mark.parametrize('x',[1, 2])
    async def test_will_fail_on_second_and_further_params(self, mock_hello, x):
        mock_hello.return_value = 'OK'
        assert await hello() == 'OK'

The issue

After I run the tests:
py.test test.py
I get the following stacktrace:

____________________________________________________________________ TestFetch.test_will_fail_on_second_and_further_params[2] _____________________________________________________________________

self = <[AttributeError("'_PatchedGenerator' object has no attribute 'generator'") raised in repr()] _PatchedGenerator object at 0x10cb4a290>

    def __next__(self):
        try:
            with self._limited_patchings_stack():
>               return self.gen.send(None)
E               StopIteration

venv/lib/python3.7/site-packages/asynctest/mock.py:1041: StopIteration

During handling of the above exception, another exception occurred:

kwargs = {'x': 2}, coro = <coroutine object TestFetch.test_will_fail_on_second_and_further_params at 0x10caff170>
task = <Task finished coro=<TestFetch.test_will_fail_on_second_and_further_params() done, defined at /Users/aikikode/tmp/test...hon3.7/site-packages/asynctest/mock.py:113> exception=AttributeError("'_patch' object has no attribute '_exit_stack'")>

    @functools.wraps(func)
    def inner(**kwargs):
        coro = func(**kwargs)
        if coro is not None:
            task = asyncio.ensure_future(coro, loop=_loop)
            try:
>               _loop.run_until_complete(task)

venv/lib/python3.7/site-packages/pytest_asyncio/plugin.py:179:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.pyenv/versions/3.7.8/lib/python3.7/asyncio/base_events.py:587: in run_until_complete
    return future.result()
venv/lib/python3.7/site-packages/asynctest/mock.py:115: in wrapper
    return await coroutine(*args, **kwargs)
venv/lib/python3.7/site-packages/asynctest/mock.py:1044: in __next__
    self._stop_global_patchings()
venv/lib/python3.7/site-packages/asynctest/mock.py:1033: in _stop_global_patchings
    patching.stop()
../../.pyenv/versions/3.7.8/lib/python3.7/unittest/mock.py:1455: in stop
    return self.__exit__(None, None, None)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <asynctest.mock._patch object at 0x10caf5e10>, exc_info = (None, None, None)

    def __exit__(self, *exc_info):
        """Undo the patch."""
        if not _is_started(self):
            raise RuntimeError('stop called on unstarted patcher')

        if self.is_local and self.temp_original is not DEFAULT:
            setattr(self.target, self.attribute, self.temp_original)
        else:
            delattr(self.target, self.attribute)
            if not self.create and (not hasattr(self.target, self.attribute) or
                        self.attribute in ('__doc__', '__module__',
                                           '__defaults__', '__annotations__',
                                           '__kwdefaults__')):
                # needed for proxy objects like django settings
                setattr(self.target, self.attribute, self.temp_original)

        del self.temp_original
        del self.is_local
        del self.target
>       exit_stack = self._exit_stack
E       AttributeError: '_patch' object has no attribute '_exit_stack'

../../.pyenv/versions/3.7.8/lib/python3.7/unittest/mock.py:1435: AttributeError

Investigation

I did some investigation for the possible cause of this issue:

  • _exit_stack was added in Python 3.7.8: python/cpython@4057e8f#diff-ff75b1b83c21770847ade91fa5bb2525L1291 It's set upon patch __enter()__ and deleted in __exit()__.
  • asynctest uses mock_to_reuse to reuse the same mock object for a coroutine: https://github.com/Martiusweb/asynctest/blob/v0.13.0/asynctest/mock.py#L1088 - if we reuse the coroutine, then patch __enter()__ method is not called and _exit_stack attribute is not set.
  • when used in combination with pytest.mark.parametrize it leads to mock __enter()__ being called only once per test case (regardless of number of "subcases" created with parametrisation), but the patch __exit()__ is called after each of the parametrisation "subtests", so only the first "subtest" passes and all other fail.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions