ipwnponies / pytest-antilru

Bust functools.lru_cache when running pytest to avoid test pollution
MIT License
22 stars 3 forks source link

pytest-django loads earlier than pytest-antilru preventing lru_cache from being busted when AppConfig.ready is used #27

Closed joetsoi closed 2 months ago

joetsoi commented 1 year ago

pytest-django uses pytest_load_initial_conftests and runs _setup_django() this occurs very early to parse command line options

pytest-antilru uses pytest_collection( which runs at the beginning of each session and importantly runs after pytest_load_initial_conftests

This means if you have an AppConfig that import code that uses lru_cache, then that function will be imported before python-antilru runs preventing the cache from being busted.

Minimal example exampleapp/models.py

from functools import lru_cache

def expensive_network_call() -> int:
    return 1

@lru_cache()
def cache_me() -> int:
    return expensive_network_call()

exampleapp/tests.py

import sys
from unittest import mock
from . import models
def test_a_run_first() -> None:
    assert models.cache_me() == 1

def test_b_run_second() -> None:
    # We want to mock the network call for this test case
    with mock.patch.object(models, 'expensive_network_call', return_value=2) as mock_network_call:
        assert models.cache_me() == 2
        assert mock_network_call.called

in our app config we import a function that uses lru_cache this is called super early by pytest-django in _setup_django preventing python-antilru from monkey patching lru_cache

from django.apps import AppConfig

class ExampleappConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'exampleapp'

    def ready(self):
        from .models import cache_me  # this import is loaded before `pytest_collection` runs

in our project settings example/settings.py we add our exampleapp

INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'django.contrib.staticfiles',
    'exampleapp',
]

Now we run with python-antilru installed, but without pytest-django settings configured followed by with settings configured.

$ pip install pytest-antilru
Collecting pytest-antilru
  Using cached pytest_antilru-1.1.1-py2.py3-none-any.whl (5.3 kB)
...
Successfully installed pytest-antilru-1.1.1

$ pytest exampleapp/tests.py::test_a_run_first exampleapp/tests.py::test_b_run_second
=============================================================== test session starts ===============================================================
platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
plugins: django-4.5.2, antilru-1.1.1
collected 2 items

exampleapp/tests.py ..                                                                                                                      [100%]

================================================================ 2 passed in 0.02s ================================================================
$ pytest exampleapp/tests.py::test_b_run_second exampleapp/tests.py::test_a_run_first
=============================================================== test session starts ===============================================================
platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
plugins: django-4.5.2, antilru-1.1.1
collected 2 items

exampleapp/tests.py ..                                                                                                                      [100%]

================================================================ 2 passed in 0.02s ================================================================
$ pytest -s --ds=example.settings exampleapp/tests.py
=============================================================== test session starts ===============================================================
platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
django: settings: example.settings (from option)
rootdir: /home/example
plugins: django-4.5.2, antilru-1.1.1
collected 2 items

exampleapp/tests.py .F

==================================================================== FAILURES =====================================================================
________________________________________________________________ test_b_run_second ________________________________________________________________

    def test_b_run_second() -> None:
        # We want to mock the network call for this test case
        with mock.patch.object(models, 'expensive_network_call', return_value=2) as mock_network_call:
>           assert models.cache_me() == 2
E           assert 1 == 2
E            +  where 1 = <functools._lru_cache_wrapper object at 0x7f8ab13ae140>()
E            +    where <functools._lru_cache_wrapper object at 0x7f8ab13ae140> = models.cache_me

exampleapp/tests.py:12: AssertionError
============================================================= short test summary info =============================================================
FAILED exampleapp/tests.py::test_b_run_second - assert 1 == 2
=========================================================== 1 failed, 1 passed in 0.08s ===========================================================

I think a solution to this might be to make pytest-antilru use pytest_load_initial_conftest instead of pytest_collection, but thought I'd ask here first. I'm also aware that it would dependent on the order that plugins would be loaded, which I think (currently) can't be specifed. Is there a better way to solve this? I'm happy to open a pr

ipwnponies commented 1 year ago

Thanks for the detailed report! I appreciate the minimal repo to help illustrate the situation.

pytest_collection was chosen arbitrarily, as a early enough place to monkey patch before application code imported lru_cache. I have no reservations using pytest_load_initial_conftest, to get ahead of pytest-django.

I'd very much appreciate a PR, as I have not used django and verifying correctness would take time.

Is there a better way to solve this?

I don't think there's much more we can do. The bootstrap hooks are super early and without the ability to specify ordering, will be tricky. I think a doc section for gotchas will be all we can do, for pytest-django and other plugins that import early. The open pytest issue doesn't seem to provide much of a solution.

rmartine-ias commented 3 months ago

I think I am encountering this with https://github.com/tophat/syrupy as well. https://github.com/ipwnponies/pytest-antilru/pull/35 fixes it.