#37064 new Bug

SimpleTestCase._remove_databases_failures() crashes on un-wrapped connection methods

Reported by: Rio Weber Owned by:
Component: Testing framework Version: 6.0
Severity: Normal Keywords: testcase, databases, multi-db, regression
Cc: Rio Weber Triage Stage: Unreviewed
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Re-filing #36942 - got closed for lack of a repro, I have one

TLDR: _remove_databases_failures blindly does method.wrapped on every
connection method it visits. If the method isn't a _DatabaseFailure at
that point (e.g. SQLite recycled the connection between setUpClass and
tearDownClass, or the class's databases set changed after setup),
teardown dies with AttributeError: 'function' object has no attribute
'wrapped'.

Repro, traceback, one-line fix (isinstance guard), and two regression
tests below. Verified on 6.0.3 and on main @ 526b548c (that commit
touches auth/backends.py, not test/testcases.py).

Note: between 6.0.3 and main, the list of disallowed connection methods
moved from a SimpleTestCase class attribute to
connection.features.disallowed_simple_test_case_connection_methods.
Orthogonal to the bug - both branches still blindly access
method.wrapped - but the patch surface differs slightly per branch.

Minimal repro

settings.py:

DATABASES = {
    "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"},
    "shard_b": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db_shard_b.sqlite3"},
}

tests/test_repro.py:

from django.test import TransactionTestCase

class ReproTests(TransactionTestCase):
    # Implicit databases = {"default"} - shard_b is NOT declared
    def test_anything(self):
        assert True

Run under pytest-django; the test body doesn't need to touch the DB:

pytest tests/test_repro.py

Traceback (abridged)

.../django/test/testcases.py:280
    in _remove_databases_failures
        setattr(connection, name, method.wrapped)
                                  ^^^^^^^^^^^^^^
AttributeError: 'function' object has no attribute 'wrapped'

Root cause

_add_databases_failures installs _DatabaseFailure wrappers;
_remove_databases_failures assumes every method it iterates is still
one. That symmetry breaks most often because the SQLite backend
replaces connection objects between setUpClass and tearDownClass the
new connection has fresh, un-wrapped methods. Also reproducible if a
subclass/fixture mutates cls.databases after setup or if a class
skipped super().setUpClass() but still inherits the addClassCleanup

Proposed fix

Add an isinstance guard so teardown only touches what setup installed.
On main:

@classmethod
def _remove_databases_failures(cls):
    for alias in connections:
        if alias in cls.databases:
            continue
        connection = connections[alias]
        disallowed_methods = (
            connection.features.disallowed_simple_test_case_connection_methods
        )
        for name, _ in disallowed_methods:
            method = getattr(connection, name)
            if isinstance(method, _DatabaseFailure):
                setattr(connection, name, method.wrapped)

Backport for stable/6.0.x is identical except the iteration source
stays cls._disallowed_connection_methods

isinstance is preferable to the hasattr("wrapped") approach in #20758
because it only unwraps Django's own wrapper class and won't chase
third-party objects that happen to expose .wrapped

Regression tests

Two tests both scoped to teardown:

  • test_teardown_noops_when_method_is_not_wrapped - replaces a connection method with a plain function and asserts _remove_databases_failures does not raise.
  • test_teardown_still_unwraps_database_failures - installs a _DatabaseFailure manually and asserts it is restored

Full source attached as a patch

Related

  • #36942 (closed needsinfo) - same bug, no reprodce
  • PR #20758 by Michele Fiori uses a hasattr guard; happy to rebase onto isinstance + tests or open a fresh PR crediting the original author

Attachments (1)

remove-databases-failures-isinstance-guard.diff (3.6 KB ) - added by Rio Weber 87 minutes ago.

Download all attachments as: .zip

Change History (1)

Note: See TracTickets for help on using tickets.
Back to Top