Skip to content

Commit b2f199a

Browse files
committed
Fix #80 -- Address a crash on unrestricted select_related() usage.
1 parent a2d3a48 commit b2f199a

File tree

4 files changed

+56
-9
lines changed

4 files changed

+56
-9
lines changed

CHANGELOG.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
1.6.3
2+
=====
3+
:release-date: unreleased
4+
5+
- Address a crash on unrestricted ``select_related()`` usage (#81)
6+
17
1.6.2
28
=====
39
:release-date: 2024-08-12

seal/query.py

+34-6
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,36 @@
22
from operator import attrgetter
33

44
from django.db import models
5+
from django.db.models.query_utils import select_related_descend
56

67
cached_value_getter = attrgetter("get_cached_value")
78

89

9-
def get_select_related_getters(lookups, opts):
10+
def get_restricted_select_related_getters(lookups, opts):
1011
"""Turn a select_related dict structure into a tree of attribute getters"""
1112
for lookup, nested_lookups in lookups.items():
1213
field = opts.get_field(lookup)
1314
lookup_opts = field.related_model._meta
1415
yield (
1516
cached_value_getter(field),
16-
tuple(get_select_related_getters(nested_lookups, lookup_opts)),
17+
tuple(get_restricted_select_related_getters(nested_lookups, lookup_opts)),
18+
)
19+
20+
21+
def get_unrestricted_select_related_getters(opts, max_depth, cur_depth=1):
22+
if cur_depth > max_depth:
23+
return
24+
for field in opts.fields:
25+
if not select_related_descend(field, False, None, {}):
26+
continue
27+
related_model_meta = field.related_model._meta
28+
yield (
29+
cached_value_getter(field),
30+
tuple(
31+
get_unrestricted_select_related_getters(
32+
related_model_meta, max_depth=max_depth, cur_depth=cur_depth + 1
33+
)
34+
),
1735
)
1836

1937

@@ -44,12 +62,22 @@ def _sealed_related_iterator(self, related_walker):
4462
yield obj
4563

4664
def __iter__(self):
47-
select_related = self.queryset.query.select_related
65+
query = self.queryset.query
66+
select_related = query.select_related
4867
if select_related:
4968
opts = self.queryset.model._meta
50-
select_related_getters = tuple(
51-
get_select_related_getters(self.queryset.query.select_related, opts)
52-
)
69+
if isinstance(select_related, dict):
70+
select_related_getters = tuple(
71+
get_restricted_select_related_getters(
72+
self.queryset.query.select_related, opts
73+
)
74+
)
75+
else:
76+
select_related_getters = tuple(
77+
get_unrestricted_select_related_getters(
78+
opts, max_depth=query.max_depth
79+
)
80+
)
5381
related_walker = partial(
5482
walk_select_relateds, getters=select_related_getters
5583
)

tests/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Location(SealableModel):
2626
related_locations = models.ManyToManyField("self")
2727

2828

29-
class Island(models.Model):
29+
class Island(SealableModel):
3030
# Explicitly avoid setting a related_name.
3131
location = models.ForeignKey(Location, on_delete=models.CASCADE)
3232

tests/test_query.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def setUpTestData(cls):
4545
cls.nickname = Nickname.objects.create(
4646
name="Jonathan Livingston", content_object=cls.gull
4747
)
48+
cls.island = Island.objects.create(location=cls.location)
4849
tests_models = tuple(apps.get_app_config("tests").get_models())
4950
ContentType.objects.get_for_models(*tests_models, for_concrete_models=True)
5051

@@ -177,6 +178,19 @@ def test_sealed_select_related_reverse_one_to_one(self):
177178
with self.assertRaises(SeaLion.gull.RelatedObjectDoesNotExist):
178179
instance.gull
179180

181+
def test_sealed_select_related_unrestricted(self):
182+
instance = Island.objects.select_related().seal().get()
183+
self.assertEqual(instance.location, self.location)
184+
instance = SeaLion.objects.select_related().seal().get()
185+
message = (
186+
'Attempt to fetch related field "location" on sealed <SeaLion instance>'
187+
)
188+
with self.assertWarnsMessage(UnsealedAttributeAccess, message) as ctx:
189+
# null=True relationships are not followed when using
190+
# an unrestricted select_related()
191+
instance.location
192+
self.assertEqual(ctx.filename, __file__)
193+
180194
def test_sealed_prefetch_related_reverse_one_to_one(self):
181195
instance = SeaLion.objects.prefetch_related("gull").seal().get()
182196
self.assertEqual(instance.gull, self.gull)
@@ -434,9 +448,8 @@ def test_sealed_prefetched_select_related_many_to_many(self):
434448
self.assertSequenceEqual(instance.location.climates.all(), [self.climate])
435449

436450
def test_prefetch_without_related_name(self):
437-
island = Island.objects.create(location=self.location)
438451
location = Location.objects.prefetch_related("island_set").seal().get()
439-
self.assertSequenceEqual(location.island_set.all(), [island])
452+
self.assertSequenceEqual(location.island_set.all(), [self.island])
440453

441454
def test_prefetch_combine(self):
442455
with self.assertNumQueries(6):

0 commit comments

Comments
 (0)