Skip to content

Commit 74a9c27

Browse files
adamchainzjacobtylerwalls
authored andcommitted
Refs #28586 -- Split descriptor from GenericForeignKey.
This makes GenericForeignKey more similar to other fields which act as descriptors, preparing it to add “fetcher protocol” support in a clear and consistent way.
1 parent 762d3be commit 74a9c27

5 files changed

Lines changed: 60 additions & 38 deletions

File tree

django/contrib/contenttypes/checks.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def check_generic_foreign_keys(app_configs, **kwargs):
8-
from .fields import GenericForeignKey
8+
from .fields import GenericForeignKeyDescriptor
99

1010
if app_configs is None:
1111
models = apps.get_models()
@@ -14,14 +14,14 @@ def check_generic_foreign_keys(app_configs, **kwargs):
1414
app_config.get_models() for app_config in app_configs
1515
)
1616
errors = []
17-
fields = (
17+
descriptors = (
1818
obj
1919
for model in models
2020
for obj in vars(model).values()
21-
if isinstance(obj, GenericForeignKey)
21+
if isinstance(obj, GenericForeignKeyDescriptor)
2222
)
23-
for field in fields:
24-
errors.extend(field.check())
23+
for descriptor in descriptors:
24+
errors.extend(descriptor.field.check())
2525
return errors
2626

2727

django/contrib/contenttypes/fields.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ def __init__(
4848

4949
def contribute_to_class(self, cls, name, **kwargs):
5050
super().contribute_to_class(cls, name, private_only=True, **kwargs)
51-
# GenericForeignKey is its own descriptor.
52-
setattr(cls, self.attname, self)
51+
setattr(cls, self.attname, GenericForeignKeyDescriptor(self))
5352

5453
def get_attname_column(self):
5554
attname, column = super().get_attname_column()
@@ -161,11 +160,19 @@ def get_content_type(self, obj=None, id=None, using=None, model=None):
161160
# This should never happen. I love comments like this, don't you?
162161
raise Exception("Impossible arguments to GFK.get_content_type!")
163162

163+
164+
class GenericForeignKeyDescriptor:
165+
def __init__(self, field):
166+
self.field = field
167+
168+
def is_cached(self, instance):
169+
return self.field.is_cached(instance)
170+
164171
def get_prefetch_querysets(self, instances, querysets=None):
165172
custom_queryset_dict = {}
166173
if querysets is not None:
167174
for queryset in querysets:
168-
ct_id = self.get_content_type(
175+
ct_id = self.field.get_content_type(
169176
model=queryset.query.model, using=queryset.db
170177
).pk
171178
if ct_id in custom_queryset_dict:
@@ -179,12 +186,12 @@ def get_prefetch_querysets(self, instances, querysets=None):
179186
fk_dict = defaultdict(set)
180187
# We need one instance for each group in order to get the right db:
181188
instance_dict = {}
182-
ct_attname = self.model._meta.get_field(self.ct_field).attname
189+
ct_attname = self.field.model._meta.get_field(self.field.ct_field).attname
183190
for instance in instances:
184191
# We avoid looking for values if either ct_id or fkey value is None
185192
ct_id = getattr(instance, ct_attname)
186193
if ct_id is not None:
187-
fk_val = getattr(instance, self.fk_field)
194+
fk_val = getattr(instance, self.field.fk_field)
188195
if fk_val is not None:
189196
fk_dict[ct_id].add(fk_val)
190197
instance_dict[ct_id] = instance
@@ -196,7 +203,7 @@ def get_prefetch_querysets(self, instances, querysets=None):
196203
ret_val.extend(custom_queryset_dict[ct_id].filter(pk__in=fkeys))
197204
else:
198205
instance = instance_dict[ct_id]
199-
ct = self.get_content_type(id=ct_id, using=instance._state.db)
206+
ct = self.field.get_content_type(id=ct_id, using=instance._state.db)
200207
ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
201208

202209
# For doing the join in Python, we have to match both the FK val and
@@ -207,17 +214,17 @@ def gfk_key(obj):
207214
if ct_id is None:
208215
return None
209216
else:
210-
model = self.get_content_type(
217+
model = self.field.get_content_type(
211218
id=ct_id, using=obj._state.db
212219
).model_class()
213-
return str(getattr(obj, self.fk_field)), model
220+
return str(getattr(obj, self.field.fk_field)), model
214221

215222
return (
216223
ret_val,
217224
lambda obj: (obj._meta.pk.value_to_string(obj), obj.__class__),
218225
gfk_key,
219226
True,
220-
self.name,
227+
self.field.name,
221228
False,
222229
)
223230

@@ -229,43 +236,44 @@ def __get__(self, instance, cls=None):
229236
# reload the same ContentType over and over (#5570). Instead, get the
230237
# content type ID here, and later when the actual instance is needed,
231238
# use ContentType.objects.get_for_id(), which has a global cache.
232-
f = self.model._meta.get_field(self.ct_field)
239+
f = self.field.model._meta.get_field(self.field.ct_field)
233240
ct_id = getattr(instance, f.attname, None)
234-
pk_val = getattr(instance, self.fk_field)
241+
pk_val = getattr(instance, self.field.fk_field)
235242

236-
rel_obj = self.get_cached_value(instance, default=None)
237-
if rel_obj is None and self.is_cached(instance):
243+
rel_obj = self.field.get_cached_value(instance, default=None)
244+
if rel_obj is None and self.field.is_cached(instance):
238245
return rel_obj
239246
if rel_obj is not None:
240247
ct_match = (
241-
ct_id == self.get_content_type(obj=rel_obj, using=instance._state.db).id
248+
ct_id
249+
== self.field.get_content_type(obj=rel_obj, using=instance._state.db).id
242250
)
243251
pk_match = ct_match and rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk
244252
if pk_match:
245253
return rel_obj
246254
else:
247255
rel_obj = None
248256
if ct_id is not None:
249-
ct = self.get_content_type(id=ct_id, using=instance._state.db)
257+
ct = self.field.get_content_type(id=ct_id, using=instance._state.db)
250258
try:
251259
rel_obj = ct.get_object_for_this_type(
252260
using=instance._state.db, pk=pk_val
253261
)
254262
except ObjectDoesNotExist:
255263
pass
256-
self.set_cached_value(instance, rel_obj)
264+
self.field.set_cached_value(instance, rel_obj)
257265
return rel_obj
258266

259267
def __set__(self, instance, value):
260268
ct = None
261269
fk = None
262270
if value is not None:
263-
ct = self.get_content_type(obj=value)
271+
ct = self.field.get_content_type(obj=value)
264272
fk = value.pk
265273

266-
setattr(instance, self.ct_field, ct)
267-
setattr(instance, self.fk_field, fk)
268-
self.set_cached_value(instance, value)
274+
setattr(instance, self.field.ct_field, ct)
275+
setattr(instance, self.field.fk_field, fk)
276+
self.field.set_cached_value(instance, value)
269277

270278

271279
class GenericRel(ForeignObjectRel):

docs/releases/6.1.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ backends.
246246
Miscellaneous
247247
-------------
248248

249-
* ...
249+
* :class:`~django.contrib.contenttypes.fields.GenericForeignKey` now uses a
250+
separate descriptor class: the private ``GenericForeignKeyDescriptor``.
250251

251252
.. _deprecated-features-6.1:
252253

tests/contenttypes_tests/test_checks.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,36 @@ class TaggedItem(models.Model):
1919
object_id = models.PositiveIntegerField()
2020
content_object = GenericForeignKey()
2121

22+
field = TaggedItem._meta.get_field("content_object")
23+
2224
expected = [
2325
checks.Error(
2426
"The GenericForeignKey content type references the nonexistent "
2527
"field 'TaggedItem.content_type'.",
26-
obj=TaggedItem.content_object,
28+
obj=field,
2729
id="contenttypes.E002",
2830
)
2931
]
30-
self.assertEqual(TaggedItem.content_object.check(), expected)
32+
self.assertEqual(field.check(), expected)
3133

3234
def test_invalid_content_type_field(self):
3335
class Model(models.Model):
3436
content_type = models.IntegerField() # should be ForeignKey
3537
object_id = models.PositiveIntegerField()
3638
content_object = GenericForeignKey("content_type", "object_id")
3739

40+
field = Model._meta.get_field("content_object")
41+
3842
self.assertEqual(
39-
Model.content_object.check(),
43+
field.check(),
4044
[
4145
checks.Error(
4246
"'Model.content_type' is not a ForeignKey.",
4347
hint=(
4448
"GenericForeignKeys must use a ForeignKey to "
4549
"'contenttypes.ContentType' as the 'content_type' field."
4650
),
47-
obj=Model.content_object,
51+
obj=field,
4852
id="contenttypes.E003",
4953
)
5054
],
@@ -58,8 +62,10 @@ class Model(models.Model):
5862
object_id = models.PositiveIntegerField()
5963
content_object = GenericForeignKey("content_type", "object_id")
6064

65+
field = Model._meta.get_field("content_object")
66+
6167
self.assertEqual(
62-
Model.content_object.check(),
68+
field.check(),
6369
[
6470
checks.Error(
6571
"'Model.content_type' is not a ForeignKey to "
@@ -68,7 +74,7 @@ class Model(models.Model):
6874
"GenericForeignKeys must use a ForeignKey to "
6975
"'contenttypes.ContentType' as the 'content_type' field."
7076
),
71-
obj=Model.content_object,
77+
obj=field,
7278
id="contenttypes.E004",
7379
)
7480
],
@@ -80,13 +86,15 @@ class TaggedItem(models.Model):
8086
# missing object_id field
8187
content_object = GenericForeignKey()
8288

89+
field = TaggedItem._meta.get_field("content_object")
90+
8391
self.assertEqual(
84-
TaggedItem.content_object.check(),
92+
field.check(),
8593
[
8694
checks.Error(
8795
"The GenericForeignKey object ID references the nonexistent "
8896
"field 'object_id'.",
89-
obj=TaggedItem.content_object,
97+
obj=field,
9098
id="contenttypes.E001",
9199
)
92100
],
@@ -98,12 +106,14 @@ class Model(models.Model):
98106
object_id = models.PositiveIntegerField()
99107
content_object_ = GenericForeignKey("content_type", "object_id")
100108

109+
field = Model._meta.get_field("content_object_")
110+
101111
self.assertEqual(
102-
Model.content_object_.check(),
112+
field.check(),
103113
[
104114
checks.Error(
105115
"Field names must not end with an underscore.",
106-
obj=Model.content_object_,
116+
obj=field,
107117
id="fields.E001",
108118
)
109119
],

tests/contenttypes_tests/test_fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ def test_str(self):
1515
class Model(models.Model):
1616
field = GenericForeignKey()
1717

18-
self.assertEqual(str(Model.field), "contenttypes_tests.Model.field")
18+
field = Model._meta.get_field("field")
19+
20+
self.assertEqual(str(field), "contenttypes_tests.Model.field")
1921

2022
def test_get_content_type_no_arguments(self):
23+
field = Answer._meta.get_field("question")
2124
with self.assertRaisesMessage(
2225
Exception, "Impossible arguments to GFK.get_content_type!"
2326
):
24-
Answer.question.get_content_type()
27+
field.get_content_type()
2528

2629
def test_get_object_cache_respects_deleted_objects(self):
2730
question = Question.objects.create(text="Who?")

0 commit comments

Comments
 (0)