Skip to content

Commit de28147

Browse files
authored
Merge pull request #858 from bckohan/manager_caching
Fix default PolymorphicManager for base_manager being overridden by apps registry cache clear.
2 parents 2468a08 + 4195993 commit de28147

7 files changed

Lines changed: 360 additions & 235 deletions

File tree

docs/changelog/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
v4.9.2, v4.10.5 (2026-01-26)
5+
----------------------------
6+
7+
* Fixed `Bad warning messages polymorphic.W001 and polymorphic.W002 when deriving from PolymorphicModel <https://github.com/jazzband/django-polymorphic/issues/857>`_
8+
* Fixed `Unexpected return type of polymorphic data since 4.9.1 <https://github.com/jazzband/django-polymorphic/issues/855>`_
9+
* Fixed `related item missing <https://github.com/jazzband/django-polymorphic/issues/851>`_
10+
411
v4.10.4 (2026-01-19)
512
--------------------
613

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "django-polymorphic"
7-
version = "4.10.4"
7+
version = "4.10.5"
88
description = "Seamless polymorphic inheritance for Django models."
99
readme = "README.md"
1010
license = "BSD-3-Clause"

src/polymorphic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
Seamless Polymorphic Inheritance for Django Models
2020
"""
2121

22-
VERSION = "4.10.4"
22+
VERSION = "4.10.5"
2323

2424
__title__ = "Django Polymorphic"
2525
__version__ = VERSION # version synonym for backwards compatibility

src/polymorphic/base.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from django.db import models
99
from django.db.models.base import ModelBase
10+
from django.db.models.options import Options
1011

1112
from .deletion import PolymorphicGuard
1213
from .managers import PolymorphicManager
@@ -29,8 +30,36 @@ class ManagerInheritanceWarning(RuntimeWarning):
2930
check_dump = hasattr(sys, "_getframe")
3031

3132

32-
###################################################################################
33-
# PolymorphicModel meta class
33+
# We wrap the base_manager property to return a PolymorphicManager
34+
# for polymorphic models when the base manager would otherwise
35+
# be the default auto-created manager. This ensures that
36+
# reverse relations to polymorphic models also use polymorphic
37+
# querysets by default.
38+
# https://github.com/jazzband/django-polymorphic/pull/858
39+
dj_base_manager = Options.base_manager.func
40+
41+
42+
def polymorphic_base_manager(self):
43+
"""
44+
Return a polymorphic base manager for polymorphic models.
45+
"""
46+
from polymorphic.models import PolymorphicModel
47+
48+
mgr = dj_base_manager(self)
49+
if (
50+
issubclass(self.model, PolymorphicModel)
51+
and mgr.__class__ is models.Manager
52+
and mgr.auto_created
53+
):
54+
manager = PolymorphicManager()
55+
manager.name = "_base_manager"
56+
manager.model = self.model
57+
manager.auto_created = True
58+
return manager
59+
return mgr
60+
61+
62+
Options.base_manager.func = polymorphic_base_manager
3463

3564

3665
class PolymorphicModelBase(ModelBase):
@@ -79,21 +108,6 @@ def __new__(cls, model_name, bases, attrs, **kwargs):
79108

80109
new_class = super().__new__(cls, model_name, bases, attrs, **kwargs)
81110

82-
if not any(
83-
parent._meta.base_manager_name
84-
for parent in new_class.mro()
85-
if issubclass(parent, PolymorphicModel)
86-
):
87-
if new_class._meta.default_manager.__class__ is PolymorphicManager:
88-
new_class._meta.__dict__["base_manager"] = new_class._meta.default_manager
89-
else:
90-
manager = PolymorphicManager()
91-
manager.name = "_base_manager"
92-
manager.model = new_class
93-
manager.auto_created = True
94-
# write new manager to property cache
95-
new_class._meta.__dict__["base_manager"] = manager
96-
97111
# wrap on_delete handlers of reverse relations back to this model with the
98112
# polymorphic deletion guard
99113
for fk in new_class._meta.fields:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from django.test import TestCase
2+
3+
from django_test_migrations.contrib.unittest_case import MigratorTestCase
4+
from django_test_migrations.migrator import Migrator
5+
6+
from polymorphic.managers import PolymorphicManager
7+
8+
9+
class TestRelatedManagersInMigrationState(MigratorTestCase):
10+
"""
11+
Test that only non-polymorphic managers are used in migrations.
12+
"""
13+
14+
app = "tests"
15+
migrate_from = (app, None)
16+
migrate_to = (app, "0001_initial")
17+
18+
def test_migration_managers_are_nonpoly(self):
19+
migrator = Migrator(database="default")
20+
21+
# Apply migrations up to the state you care about and get the historical apps registry.
22+
state = migrator.apply_initial_migration(self.migrate_to)
23+
apps = state.apps
24+
25+
ChildModelWithManager = apps.get_model("tests", "ChildModelWithManager")
26+
ParentModelWithManager = apps.get_model("tests", "ParentModelWithManager")
27+
pm = ParentModelWithManager.objects.create()
28+
ChildModelWithManager.objects.create(field1="test", fk=pm)
29+
30+
assert not isinstance(ParentModelWithManager.objects, PolymorphicManager)
31+
assert not isinstance(ChildModelWithManager.objects, PolymorphicManager)
32+
assert not isinstance(ChildModelWithManager._meta.base_manager, PolymorphicManager)
33+
assert not isinstance(ParentModelWithManager._meta.base_manager, PolymorphicManager)
34+
assert not isinstance(pm.childmodel_set, PolymorphicManager)
35+
36+
One2OneRelatingModel = apps.get_model("tests", "One2OneRelatingModel")
37+
One2OneRelatingModelDerived = apps.get_model("tests", "One2OneRelatingModelDerived")
38+
Model2A = apps.get_model("tests", "Model2A")
39+
Model2BFiltered = apps.get_model("tests", "Model2BFiltered")
40+
Model2CFiltered = apps.get_model("tests", "Model2CFiltered")
41+
Model2CNamedManagers = apps.get_model("tests", "Model2CNamedManagers")
42+
Model2CNamedDefault = apps.get_model("tests", "Model2CNamedDefault")
43+
ManagerTest = apps.get_model("tests", "ManagerTest")
44+
ManagerTestChild = apps.get_model("tests", "ManagerTestChild")
45+
RelatingModel = apps.get_model("tests", "RelatingModel")
46+
47+
assert not isinstance(Model2BFiltered._meta.base_manager, PolymorphicManager)
48+
assert not isinstance(Model2CFiltered._meta.base_manager, PolymorphicManager)
49+
assert not isinstance(Model2CNamedManagers._meta.base_manager, PolymorphicManager)
50+
assert not isinstance(Model2CNamedDefault._meta.base_manager, PolymorphicManager)
51+
assert not isinstance(Model2BFiltered._meta.default_manager, PolymorphicManager)
52+
assert not isinstance(Model2CFiltered._meta.default_manager, PolymorphicManager)
53+
assert not isinstance(Model2CNamedManagers._meta.default_manager, PolymorphicManager)
54+
assert not isinstance(Model2CNamedDefault._meta.default_manager, PolymorphicManager)
55+
assert not isinstance(Model2BFiltered.objects, PolymorphicManager)
56+
assert not isinstance(Model2CFiltered.objects, PolymorphicManager)
57+
assert not isinstance(Model2CNamedManagers.objects, PolymorphicManager)
58+
assert not isinstance(Model2CNamedDefault.objects, PolymorphicManager)
59+
60+
assert not isinstance(ManagerTest._meta.base_manager, PolymorphicManager)
61+
assert not isinstance(ManagerTest._meta.default_manager, PolymorphicManager)
62+
assert not isinstance(ManagerTestChild._meta.base_manager, PolymorphicManager)
63+
assert not isinstance(ManagerTestChild._meta.default_manager, PolymorphicManager)
64+
65+
b2 = Model2BFiltered.objects.create(field1="testB1", field2="testB2")
66+
b2_2 = Model2BFiltered.objects.create(field1="testB12", field2="testB22")
67+
o2o1 = One2OneRelatingModel.objects.create(field1="relating1", one2one=b2)
68+
o2o2 = One2OneRelatingModelDerived.objects.create(
69+
field1="relating2", field2="relating2", one2one=b2_2
70+
)
71+
72+
o2o1.refresh_from_db()
73+
o2o2.refresh_from_db()
74+
75+
assert o2o1.one2one.__class__ is Model2A
76+
assert o2o2.one2one.__class__ is Model2A
77+
78+
a2 = Model2A.objects.create(field1="testA1")
79+
80+
rel = RelatingModel.objects.create()
81+
assert not isinstance(rel.many2many, PolymorphicManager)
82+
rel.many2many.add(b2)
83+
rel.many2many.add(b2_2)
84+
rel.many2many.add(a2)
85+
86+
rel.refresh_from_db()
87+
assert set(rel.many2many.all()) == {
88+
Model2A.objects.get(pk=b2.pk),
89+
Model2A.objects.get(pk=b2_2.pk),
90+
a2,
91+
}

src/polymorphic/tests/test_orm.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -532,12 +532,6 @@ def base_manager(model):
532532
One2OneRelatingModelDerived,
533533
)
534534

535-
# unless the user provides a manager the default_manager and base_manager are
536-
# the same
537-
assert Model2A._default_manager is Model2A._base_manager
538-
assert Model2B._default_manager is Model2B._base_manager
539-
assert Model2C._default_manager is Model2C._base_manager
540-
541535
assert type(Model2BFiltered._base_manager) is PolymorphicManager
542536
assert type(Model2CFiltered._base_manager) is PolymorphicManager
543537
assert type(Model2CNamedManagers._base_manager) is CustomBaseManager
@@ -2625,3 +2619,17 @@ def test_disparate_pk_values_in_hierarchy(self):
26252619
)
26262620

26272621
DisparateKeysGrandChild2.objects.all().delete()
2622+
2623+
def test_manager_cache_clear_persistence(self):
2624+
"""
2625+
Test that clearing the model registry cache does not remove overriden
2626+
base managers.
2627+
2628+
https://github.com/jazzband/django-polymorphic/issues/857
2629+
"""
2630+
from django.apps import apps
2631+
2632+
apps.clear_cache()
2633+
2634+
self.test_base_manager()
2635+
self.test_default_manager()

0 commit comments

Comments
 (0)