pytest-dev / pytest-dev/pytest-django

RecursionError when mixing use of override_settings and settings fixture

Open
#809 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.5k
Forks
367
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the minimal pytest examples in the issue, especially test_settings_fixture_teardown_called_after_override_settings_teardown and test_show_recursion_error. Trace the settings fixture teardown alongside override_settings teardown and verify that repeated parametrized runs do not increase the override depth or eventually raise RecursionError.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.