RecursionError when mixing use of override_settings and settings fixture
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
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()
Out of curiosity, can't you replace override_settings by the settings fixture?
Aren't they serving similar purposes?
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
One valid reason not to use the
settingsfixture 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 useoverride_settingsin 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
One valid reason not to use the
settingsfixture 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.