pytest-django icon indicating copy to clipboard operation
pytest-django copied to clipboard

RecursionError when mixing use of override_settings and settings fixture

Open theY4Kman opened this issue 6 years ago • 5 comments

Because the settings fixture undoes its modifications once at fixture teardown, if override_settings is used after settings fixture setup, but before any settings.VAL = 1 setattr statements, the settings fixture will end up restoring django.conf.settings._wrapped to the fake settings installed by override_settings.

After a critical amount of tests where this occurs, Python will raise a RecursionError on any attribute access to django.conf.settings.

Here's a minimal example to show what's going on:

import pytest

from django.conf import settings
from django.test import override_settings


@pytest.fixture
def overrides():
    with override_settings(OVERRIDE=1):
        yield


@pytest.fixture
def add_settings(settings):
    settings.ADDED = 1


def get_settings_override_depth():
    depth = 0
    root = settings._wrapped
    while hasattr(root, 'default_settings'):
        depth += 1
        root = root.default_settings
    return depth


@pytest.mark.parametrize('i', range(2))
def test_settings_fixture_used_first(add_settings, overrides, i):
    # This will pass, as the settings fixture will restore the true Django settings
    assert get_settings_override_depth() == 2


@pytest.mark.parametrize('i', range(2))
def test_settings_fixture_used_after_override_settings(overrides, add_settings, i):
    # This will pass, as the override_settings will teardown last,
    # restoring the true Django settings
    assert get_settings_override_depth() == 2


@pytest.mark.parametrize('i', range(2))
def test_settings_fixture_teardown_called_after_override_settings_teardown(settings, overrides, add_settings, i):
    # This will fail on the second test, because the settings fixture will teardown
    # after override_settings, but the restored Django settings will have come
    # from override_settings
    assert get_settings_override_depth() == 2

And a test to surface the RecursionError:

# Number of frames expected before pytest hands off execution to the test function,
# as well as the number of frames in between getattr(settings, ...) and the eventual
# getattr(settings._wrapped, ...)
BASE_FRAME_DEPTH = 51

# The number of frames an additional override_settings() adds to settings accesses
SETTINGS_DEPTH_MULTIPLIER = 2

# Number of stack frames before Python raises a RecursionError
RECURSION_LIMIT = __import__('sys').getrecursionlimit()

NUM_REPETITIONS_TO_RECURSIONERROR = (
    (RECURSION_LIMIT - BASE_FRAME_DEPTH) // SETTINGS_DEPTH_MULTIPLIER
)


@pytest.mark.parametrize('i', range(NUM_REPETITIONS_TO_RECURSIONERROR))
def test_show_recursion_error(settings, overrides, add_settings, i):
    # This will fail on the last test
    pass

theY4Kman avatar Jan 20 '20 23:01 theY4Kman

In practice, I've worked around this by wrapping settings.finalize() with override_settings(), so the true Django settings is always restored

@pytest.fixture
def settings(settings):
    with override_settings():
        yield settings
        settings.finalize()

theY4Kman avatar Jan 20 '20 23:01 theY4Kman

Out of curiosity, can't you replace override_settings by the settings fixture? Aren't they serving similar purposes?

benjaminrigaud avatar Apr 08 '20 12:04 benjaminrigaud

One valid reason not to use the settings fixture is because it's function scoped. So if you want to override settings in a class/module/session fixture you can't use it. You can use override_settings in that context

craigds avatar Jul 22 '20 09:07 craigds

One valid reason not to use the settings fixture is because it's function scoped. So if you want to override settings in a class/module/session fixture you can't use it. You can use override_settings in that context

https://github.com/pytest-dev/pytest/issues/6888

looks like you could want a settings_session and settings_module and settings_class for each of the scopes

graingert avatar Jul 22 '20 09:07 graingert

One valid reason not to use the settings fixture is because it's function scoped.

This is exactly why. We have a session-scoped fixture running a live test server thread, whose handler must be setup and torn down in a specific order for each participating test.

theY4Kman avatar Aug 24 '20 20:08 theY4Kman