'python: clear / reset `@lru_cache` functool caching with every pytest testcase for undisturbed mocking

I combine mocking and caching in my code. The mocking is (kind of) random for each pytest as I do not know exactly, what will be returned in the real case. Hence I want to mock the very same function (in my example fct_child) with different values in different testcases. The caching makes some problems however, as the return values (in my example of fct_parent are cached and hence the mocked function is only reached in the first testcase and then always skipped due to the caching of the parent function. I need to find a way to clear/reset the cache between pytests.

In the following code the tests test_1 and test_2 can be executed independently of each over ($ pytest test_main.py::test_1 and $ pytest test_main.py::test_2), successfully. If pytest runs over the full module ($ pytest test_main.py), however, the second test crashes. Also the main part works ($ python test_main.py), where I ensure, that caching works as expected.

So how can I fix the code, such that pytest also passes when all testcases are executed (the $ pytest test_main.py scenario)?

test_main.py

# test_main.py

from my_lib import fct_parent, get_n_calls_fct_child

class ChildMock:

    def __init__(self, val_child):
        self.n_calls_mock = 0
        self.val_child = val_child

    def fct(self):
        self.n_calls_mock += 1
        return self.val_child

def test_1(monkeypatch):
    """This test interacts with test_2:
    Exectuing each test independently with pytest works, executing both in one run, fails.
    This is due to the lru_cache being not cleaned.
    """
    val_child = "mocked test 1"
    child_mock = ChildMock(val_child)

    with monkeypatch.context() as mpc:
        mpc.setattr("my_lib.fct_child", child_mock.fct)  # mocks fct_child to return ret_val
        assert fct_parent() == val_child
        assert fct_parent() == val_child
        assert child_mock.n_calls_mock == 1

def test_2(monkeypatch):
    """This test interacts with test_1:
    Exectuing each test independently with pytest works, executing both in one run, fails.
    This is due to the lru_cache being not cleaned.
    """
    val_child = "mocked test 2"
    child_mock = ChildMock(val_child)

    with monkeypatch.context() as mpc:
        mpc.setattr("my_lib.fct_child", child_mock.fct)  # mocks fct_child to return ret_val
        assert fct_parent() == val_child
        assert fct_parent() == val_child
        assert child_mock.n_calls_mock == 1

if __name__ == "__main__":
    assert fct_parent() == "unmocked"
    assert fct_parent() == "unmocked"
    n_calls_fct_child = get_n_calls_fct_child()
    assert n_calls_fct_child == 1, f"{n_calls_fct_child=} should be == 1"
    print("good: fct_child was only computed once")

my_lib.py

# my_lib.py

from functools import lru_cache

_n_child_calls = 0

@lru_cache(256)
def fct_parent():
    return fct_child()

def fct_child():
    global _n_child_calls
    _n_child_calls += 1
    return "unmocked"

def get_n_calls_fct_child():
    return _n_child_calls


Solution 1:[1]

The following approach defines an @decorator, which clears the cache for the decorated function as soon as arriving in a new testcase.

my_lib_fixed.py

import os
from functools import lru_cache, wraps

_pytest_cache_func = {}  # Dict {'func.__name__: name_of_pytest_with_last_caching}

_n_child_calls = 0

def lru_cache_pytest_save(*lru_cache_args, **lru_cache_kwargs):
    """like @lru_cache, but additionally clears lru_cache of this function in between pytest testcases"""

    # if you want to switch _pytest_save off:
    # def decorator(func):
    #     return lru_cache(func)
    # return decorator

    def decorator(func):
        func_cached = lru_cache(func)

        @wraps(func)
        def wrapper(*args, **kwargs):
            pytest_current = os.environ.get("PYTEST_CURRENT_TEST")
            if _pytest_cache_func.get(func_cached.__name__) != pytest_current:
                func_cached.cache_clear()
                _pytest_cache_func[func_cached.__name__] = pytest_current

            return func_cached(*args, **kwargs)

        return wrapper

    return decorator


@lru_cache_pytest_save(256)
def fct_parent():
    return fct_child()

def fct_child():
    global _n_child_calls
    _n_child_calls += 1
    return "unmocked"

def get_n_calls_fct_child():
    return _n_child_calls

def reset_n_calls_fct_child():
    global _n_child_calls
    _n_child_calls = 0

as the modulename is slightly different, you need minor modifications in

test_main_fixed.py

# test_main_fixed.py

from my_lib_fixed import fct_parent, get_n_calls_fct_child

class ChildMock:

    def __init__(self, val_child):
        self.n_calls_mock = 0
        self.val_child = val_child

    def fct(self):
        self.n_calls_mock += 1
        return self.val_child

def test_1(monkeypatch):
    """This test interacts with test_2:
    Exectuing each test independently with pytest works, executing both in one run, fails.
    This is due to the lru_cache being not cleaned.
    """
    val_child = "mocked test 1"
    child_mock = ChildMock(val_child)

    with monkeypatch.context() as mpc:
        mpc.setattr("my_lib_fixed.fct_child", child_mock.fct)  # mocks fct_child to return ret_val
        assert fct_parent() == val_child
        assert fct_parent() == val_child
        assert child_mock.n_calls_mock == 1

def test_2(monkeypatch):
    """This test interacts with test_1:
    Exectuing each test independently with pytest works, executing both in one run, fails.
    This is due to the lru_cache being not cleaned.
    """
    val_child = "mocked test 2"
    child_mock = ChildMock(val_child)

    with monkeypatch.context() as mpc:
        mpc.setattr("my_lib_fixed.fct_child", child_mock.fct)  # mocks fct_child to return ret_val
        assert fct_parent() == val_child
        assert fct_parent() == val_child
        assert child_mock.n_calls_mock == 1

if __name__ == "__main__":
    assert fct_parent() == "unmocked"
    assert fct_parent() == "unmocked"
    n_calls_fct_child = get_n_calls_fct_child()
    assert n_calls_fct_child == 1, f"{n_calls_fct_child=} should be == 1"
    print("good: fct_child was only computed once")

now all of the 4 commands work:

$ python test_main.py
$ pytest test_main_fixed.py::test_1
$ pytest test_main_fixed.py::test_2
$ pytest test_main_fixed.py

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1