Pytest/Mock keeping around extra object references in case of caught exceptions

28 Views Asked by At

I am running into a strange issue using pytest and mock: I am trying to create a call to __del__ by deleting an object using del .... According to the documentation, del only reduces the reference counter on the object that is being "deleted" and only actually deletes the object if nobody else is still holding a reference to it. It seems like if Mock throws an exception, that somehow leads to someone grabbing and keeping an extra reference to the object.

I put together a quick demo test to show the issue: The only difference between test_del_passes (which completes just fine) and test_del_fails (which fails with the last assertion, i.e. del del_test does not cause a call to del_test.__del__()) is that in the first one, test_fn returns a value, whereas in the second one, test_fn throws a TimeoutError. I tried deleting the test_fn object, or assigning TimeoutError to a variable instead and deleting that, but I simply can't find a way to get the second test to pass. So somewhere in the testing infra, someone is keeping an extra reference to del_test and I don't know who or why or how to get rid of it.

import unittest.mock as mock

class DelTest:
    def __init__(self, flags, test_fn):
        self.flags = flags
        self.test_fn = test_fn

    def __del__(self):
        self.flags[0] = 1

    def run(self):
        try:
            self.test_fn()
        except TimeoutError:
            pass


def test_del_passes():
    flags = [0]
    test_fn = mock.Mock(side_effect=[True])
    del_test = DelTest(flags, test_fn)
    del_test.run()
    assert flags[0] == 0
    del del_test
    assert flags[0] == 1


def test_del_fails():
    flags = [0]
    test_fn = mock.Mock(side_effect=[TimeoutError()])
    del_test = DelTest(flags, test_fn)
    del_test.run()
    assert flags[0] == 0
    del del_test
    assert flags[0] == 1
1

There are 1 best solutions below

1
Diego Torres Milano On

The frame keeps a reference to the object, which you can see invoking gc.get_referrers(del_test):

[<frame at 0x101b15080, file 'xxx.py', line 17, code run>]