﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
37064	SimpleTestCase._remove_databases_failures() crashes on un-wrapped connection methods	Rio Weber		"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:
{{{
#!python
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:
{{{
#!python
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:

{{{
#!python
@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
 * [https://github.com/django/django/pull/20758 PR #20758] by Michele
   Fiori uses a hasattr guard; happy to rebase onto isinstance + tests
   or open a fresh PR crediting the original author
"	Bug	new	Testing framework	6.0	Normal		testcase, databases, multi-db, regression	Rio Weber	Unreviewed	1	0	0	0	0	0
