classification
Title: AsyncMock restores stopped patch if same object stopped multiple times
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: lisroach Nosy List: lisroach, xtreak
Priority: normal Keywords:

Created on 2020-10-26 21:05 by lisroach, last changed 2020-10-27 06:59 by xtreak.

Messages (2)
msg379687 - (view) Author: Lisa Roach (lisroach) * (Python committer) Date: 2020-10-26 21:05
Hard to explain in the title, easier to see via a test case:

     async def async_func():
         raise Exception

    def test_simultaneous_mocks(self):
        class Test(IsolatedAsyncioTestCase):
            async def test_test(self):
                patcher1 = patch(f"{__name__}.async_func")
                patcher2 = patch(f"{__name__}.async_func")
                patcher1.start()
                await async_func()
                patcher2.start()
                await async_func()
                patcher1.stop()
                with self.assertRaises(Exception):
                    await async_func()
                patcher2.stop()
                with self.assertRaises(Exception): # Fails, mock is restored!
                    await async_func()

        test = Test("test_test")
        output = test.run()
        self.assertTrue(output.wasSuccessful()) # Fail


Calling stop() on the second patch actually restores the mock and causes the test to fail.
msg379737 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python committer) Date: 2020-10-27 06:59
This is not a problem with AsyncMock. The patching is not done when the patch object is created to store reference to the original unpatched function. Hence patcher1, patcher2 that patch the same function don't store a reference to the original sync_func. The lookup is done during start(). patcher1.start() makes a lookup and stores the function. When patcher2.start() makes a lookup the function is already patched with a mock and thus it resorts to the original as the mock. 

When stop is called on patcher1 it resets back to the original function. Meanwhile for patcher2 the original function set during start itself is a mock and it resets back to that. The lookup is done at https://github.com/python/cpython/blob/a6879d9445f98833c4e300e187956e2a218a2315/Lib/unittest/mock.py#L1360 . Here target will print the function for patcher1.start() but the mock for patcher2.start().

import asyncio
import unittest
from unittest import TestCase
from unittest.mock import *

def sync_func():
    raise Exception

class Test(TestCase):

    def test_simultaneous_mocks_sync(self):
        patcher1 = patch(f"{__name__}.sync_func")
        patcher2 = patch(f"{__name__}.sync_func")

        patcher1.start()
        print(sync_func())

        patcher2.start()
        print(sync_func())

        breakpoint()
        patcher1.stop()
        with self.assertRaises(Exception):
            sync_func()

        breakpoint()
        patcher2.stop()

        with self.assertRaises(Exception): # Fails, mock is restored!
            sync_func()

if __name__ == "__main__":
    unittest.main()
History
Date User Action Args
2020-10-27 06:59:03xtreaksettype: behavior
messages: + msg379737
components: + Library (Lib)
2020-10-27 06:12:00xtreaksetnosy: + xtreak
2020-10-26 21:05:24lisroachcreate