Custom fields using attname, Model __init__ and Deserialization: hard-coded magic for relation fields
|Reported by:||anentropic||Owned by:||nobody|
|Cc:||Triage Stage:||Design decision needed|
|Has patch:||yes||Needs documentation:||no|
|Needs tests:||no||Patch needs improvement:||yes|
I'm struggling with the title for this issue, sorry.
Basically: if you have custom field type where attname is different to name (which can be desirable for same reason it's done in ForeignKey fields) then parts of Django don't work and are not practical to override.
Your field will work okay until you try to do this:
obj = MyModel(my_custom_field_name=val)
At this point Model.__init__ in django/db/models/base.py complains with:
TypeError: 'my_custom_field_name' is an invalid keyword argument for this function
Looking in __init__ it is clear why:
You've got a check for isinstance(field.rel, ManyToOneRel) to decide if is_related_object = True and then:
if is_related_object: setattr(self, field.name, rel_obj) else: setattr(self, field.attname, val)
ie it decides which value to populate on the model. After that we loop through kwargs and check:
if isinstance(getattr(self.__class__, prop), property): setattr(self, prop, kwargs.pop(prop))
Which fails because is_related_object=False for our custom field so attname has been set on the model but name was used in kwargs. After that the TypeError is raised.
Ok you say, you should just always instantiate your model using the attname as a kwarg instead of name.
Unfortunately the Django serialization apparatus has more hard-coded magic that makes ForeignKey and ManyToMany fields work, but ensures your custom field will fail in this regard.
In django/core/serializers/python.py Deserializer class we have checks for if field.rel with special cases for ManyToManyRel and ManyToOneRel, falling through to...
else: data[field.name] = field.to_python(field_value) yield base.DeserializedObject(Model(**data), m2m_data)
...instantiating the model with field.name as the default case.
Ok, so I tried adding a dummy rel=ManyToOneRel attribute on my custom field to trick Model.__init__ ... but then that triggers more magic in the Serializer class, which understandably tries to treat it as a ForeignKey field.
Right now I can't see any way to cleanly fake my way out.
Adding this line to django/core/serializers/python.py Deserializer fixed it for me:
elif field.name != field.attname: data[field.attname] = field.to_python(field_value) # Handle all other fields else: data[field.name] = field.to_python(field_value)
Change History (9)
comment:3 Changed 5 years ago by
|Status:||new → closed|
comment:5 Changed 5 years ago by
|Component:||Uncategorized → Core (Serialization)|
|Triage Stage:||Unreviewed → Accepted|