Search code examples
pythondjangodjango-contenttypes

Setting GenericForeignKey field value in Django


I've been reading lately about the generic relations. I know that GenericForeignKey is to define and manaeg the generic relation using ForeignKey and PositiveIntegerField fields. I dove into the source code in search for the __set__ method of the GenericForeignKey to see how does it work.

Here is the snippet for GenericForeignKey.__set__():

def __set__(self, instance, value):
    ct = None
    fk = None
    if value is not None:
        ct = self.get_content_type(obj=value)
        fk = value._get_pk_val()
    setattr(instance, self.ct_field, ct)
    setattr(instance, self.fk_field, fk)
    setattr(instance, self.cache_attr, value)

and model definition from django docs example:

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

Question:

When I assign value of guido to the content_object then what is the value of each of these paremeters: self, instance and value in the GenericForeignKey.__set__()?

Is self=<GenericForeignKey: 1>, instance='content_object', and value=<User: guido>?

>>> guido = User.objects.get(username='Guido')
>>> t = TaggedItem(content_object=guido, tag='bdfl')
>>> t.save()

Solution

  • The __set__ method is for descriptors.

    The following simple example will show what the arguments passed to __set__ are:

    class MyDescriptor:
        def __set__(self, instance, value):
            print((self, instance, value))
    
    class MyClass:
        attr = MyDescriptor()
    
    inst = MyClass()
    inst.attr = "foo"
    

    You'll get something like:

    <__main__.MyDescriptor object at 0x000002017192AD68>,   # self
    <__main__.MyClass object at 0x000002017192ACF8>,        # instance
    'foo'                                                   # value
    

    Specifically:

    • self is the instance of the MyDescriptor descriptor (MyClass.attr),
    • instance is the instance of the MyClass class (inst), and
    • value is what you're setting the attribute to ("foo").

    See a more thorough example here.

    So, without similarly diving into the Django code, it would seem that:

    • self is the instance of the GenericForeignKey descriptor (TaggedItem.content_object),
    • instance is the instance of the TaggedItem class, and
    • value is what you're setting the attribute to.

    But note that, with this line:

    t = TaggedItem(content_object=guido, tag='bdfl')
    

    It looks like you're creating a TaggedItem, which creates a descriptor with this line

    content_object = GenericForeignKey('content_type', 'object_id')
    

    So, at least from the code you posted, the __set__ method won't be called. Instead GenericForeignKey's __init__ method would be called.

    To call GenericForeignKey's __set__ method, you'd need to do have an instance of a class (call it inst) that had a GenericForeignKey descriptor as an attribute (call it attr), then write something like:

    inst.attr = "not guido"
    

    Then, the __set__ method of the GenericForeignKey descriptor would be called.