From 7abe5112f4cb50c15b79a2afd4c1b68f0767b243 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Mon, 29 Apr 2024 16:06:39 +0100 Subject: [PATCH] Fixed #35407 -- Cached model's Options.swapped. --- django/db/models/options.py | 10 +++++++++- tests/model_meta/models.py | 5 +++++ tests/model_meta/tests.py | 28 +++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index ed7be7dd7a..68a7228cbe 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -5,6 +5,7 @@ from collections import defaultdict from django.apps import apps from django.conf import settings from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured +from django.core.signals import setting_changed from django.db import connections from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint from django.db.models.query_utils import PathInfo @@ -230,6 +231,9 @@ class Options: self.db_table, connection.ops.max_name_length() ) + if self.swappable: + setting_changed.connect(self.setting_changed) + def _format_names(self, objs): """App label/class name interpolation for object names.""" names = {"app_label": self.app_label.lower(), "class": self.model_name} @@ -399,7 +403,7 @@ class Options: with override(None): return str(self.verbose_name) - @property + @cached_property def swapped(self): """ Has this model been swapped out for another? If so, return the model @@ -427,6 +431,10 @@ class Options: return swapped_for return None + def setting_changed(self, *, setting, **kwargs): + if setting == self.swappable and "swapped" in self.__dict__: + del self.swapped + @cached_property def managers(self): managers = [] diff --git a/tests/model_meta/models.py b/tests/model_meta/models.py index bc69d61a59..20a75baf4f 100644 --- a/tests/model_meta/models.py +++ b/tests/model_meta/models.py @@ -166,6 +166,11 @@ class Relating(models.Model): people_hidden = models.ManyToManyField(Person, related_name="+") +class Swappable(models.Model): + class Meta: + swappable = "MODEL_META_TESTS_SWAPPED" + + # ParentListTests models class CommonAncestor(models.Model): pass diff --git a/tests/model_meta/tests.py b/tests/model_meta/tests.py index 0aa04d760d..93883b5cf1 100644 --- a/tests/model_meta/tests.py +++ b/tests/model_meta/tests.py @@ -3,7 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio from django.core.exceptions import FieldDoesNotExist from django.db.models import CharField, Field, ForeignObjectRel, ManyToManyField from django.db.models.options import EMPTY_RELATION_TREE, IMMUTABLE_WARNING -from django.test import SimpleTestCase +from django.test import SimpleTestCase, override_settings from .models import ( AbstractPerson, @@ -16,6 +16,7 @@ from .models import ( Relating, Relation, SecondParent, + Swappable, ) from .results import TEST_RESULTS @@ -233,6 +234,31 @@ class VerboseNameRawTests(SimpleTestCase): self.assertEqual(Person._meta.verbose_name_raw, "Person") +class SwappedTests(SimpleTestCase): + def test_plain_model_none(self): + self.assertIsNone(Relation._meta.swapped) + + def test_unset(self): + self.assertIsNone(Swappable._meta.swapped) + + def test_set_and_unset(self): + with override_settings(MODEL_META_TESTS_SWAPPED="model_meta.Relation"): + self.assertEqual(Swappable._meta.swapped, "model_meta.Relation") + self.assertIsNone(Swappable._meta.swapped) + + def test_setting_none(self): + with override_settings(MODEL_META_TESTS_SWAPPED=None): + self.assertIsNone(Swappable._meta.swapped) + + def test_setting_non_label(self): + with override_settings(MODEL_META_TESTS_SWAPPED="not-a-label"): + self.assertEqual(Swappable._meta.swapped, "not-a-label") + + def test_setting_self(self): + with override_settings(MODEL_META_TESTS_SWAPPED="model_meta.swappable"): + self.assertIsNone(Swappable._meta.swapped) + + class RelationTreeTests(SimpleTestCase): all_models = (Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating)