diff options
| author | rimi0108 <hyerimc858@gmail.com> | 2025-10-04 13:52:59 +0900 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2025-12-03 15:04:52 -0500 |
| commit | 93540b34d4ef46f68df2c8bfe90447d0f649a852 (patch) | |
| tree | 93967647a5590ec653372f38cfb7123166d819c3 /django/core | |
| parent | ddb7236b0ae9be3163c90f799fb79396e9f61cc8 (diff) | |
Fixed #35729 -- Enabled natural key serialization opt-out for subclasses.
Refactored serialization logic to allow models inheriting a natural_key()
method (e.g. AbstractBaseUser) to explicitly opt out of natural key
serialization by returning an empty tuple from the method.
Thanks Jonas Dittrich for the report.
Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
Diffstat (limited to 'django/core')
| -rw-r--r-- | django/core/serializers/base.py | 28 | ||||
| -rw-r--r-- | django/core/serializers/python.py | 21 | ||||
| -rw-r--r-- | django/core/serializers/xml_serializer.py | 33 |
3 files changed, 54 insertions, 28 deletions
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index efc55981eb..cc9caf9a82 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -209,6 +209,34 @@ class Serializer: if callable(getattr(self.stream, "getvalue", None)): return self.stream.getvalue() + def _resolve_natural_key(self, obj): + """Return a natural key tuple for the given object when available.""" + try: + return obj.natural_key() + except AttributeError: + return None + + def _resolve_fk_natural_key(self, obj, field): + """ + Return the natural key for a ForeignKey's related object, or None if + not supported. + """ + if not self._model_supports_natural_key(field.remote_field.model): + return None + + related = getattr(obj, field.name, None) + try: + return related.natural_key() + except AttributeError: + return None + + def _model_supports_natural_key(self, model): + """Return True if the model defines a natural_key() method.""" + try: + return callable(model.natural_key) + except AttributeError: + return False + class Deserializer: """ diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 2929874b01..53a73e19e5 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -34,7 +34,7 @@ class Serializer(base.Serializer): def get_dump_object(self, obj): data = {"model": str(obj._meta)} - if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): + if not self.use_natural_primary_keys or not self._resolve_natural_key(obj): data["pk"] = self._value_from_field(obj, obj._meta.pk) data["fields"] = self._current return data @@ -52,26 +52,25 @@ class Serializer(base.Serializer): self._current[field.name] = self._value_from_field(obj, field) def handle_fk_field(self, obj, field): - if self.use_natural_foreign_keys and hasattr( - field.remote_field.model, "natural_key" + if self.use_natural_foreign_keys and ( + natural_key_value := self._resolve_fk_natural_key(obj, field) ): - related = getattr(obj, field.name) - if related: - value = related.natural_key() - else: - value = None + value = natural_key_value else: value = self._value_from_field(obj, field) self._current[field.name] = value def handle_m2m_field(self, obj, field): if field.remote_field.through._meta.auto_created: - if self.use_natural_foreign_keys and hasattr( - field.remote_field.model, "natural_key" + if self.use_natural_foreign_keys and self._model_supports_natural_key( + field.remote_field.model ): def m2m_value(value): - return value.natural_key() + if natural := value.natural_key(): + return natural + else: + return self._value_from_field(value, value._meta.pk) def queryset_iterator(obj, field): attr = getattr(obj, field.name) diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index f8ec0865a7..910c489a37 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -73,7 +73,7 @@ class Serializer(base.Serializer): self.indent(1) attrs = {"model": str(obj._meta)} - if not self.use_natural_primary_keys or not hasattr(obj, "natural_key"): + if not self.use_natural_primary_keys or not self._resolve_natural_key(obj): obj_pk = obj.pk if obj_pk is not None: attrs["pk"] = obj._meta.pk.value_to_string(obj) @@ -128,14 +128,11 @@ class Serializer(base.Serializer): self._start_relational_field(field) related_att = getattr(obj, field.attname) if related_att is not None: - if self.use_natural_foreign_keys and hasattr( - field.remote_field.model, "natural_key" + if self.use_natural_foreign_keys and ( + natural_key_value := self._resolve_fk_natural_key(obj, field) ): - related = getattr(obj, field.name) - # If related object has a natural key, use it - related = related.natural_key() # Iterable natural keys are rolled out as subelements - for key_value in related: + for key_value in natural_key_value: self.xml.startElement("natural", {}) self.xml.characters(str(key_value)) self.xml.endElement("natural") @@ -153,19 +150,21 @@ class Serializer(base.Serializer): """ if field.remote_field.through._meta.auto_created: self._start_relational_field(field) - if self.use_natural_foreign_keys and hasattr( - field.remote_field.model, "natural_key" + if self.use_natural_foreign_keys and self._model_supports_natural_key( + field.remote_field.model ): # If the objects in the m2m have a natural key, use it def handle_m2m(value): - natural = value.natural_key() - # Iterable natural keys are rolled out as subelements - self.xml.startElement("object", {}) - for key_value in natural: - self.xml.startElement("natural", {}) - self.xml.characters(str(key_value)) - self.xml.endElement("natural") - self.xml.endElement("object") + if natural := self._resolve_natural_key(value): + # Iterable natural keys are rolled out as subelements + self.xml.startElement("object", {}) + for key_value in natural: + self.xml.startElement("natural", {}) + self.xml.characters(str(key_value)) + self.xml.endElement("natural") + self.xml.endElement("object") + else: + self.xml.addQuickElement("object", attrs={"pk": str(value.pk)}) def queryset_iterator(obj, field): attr = getattr(obj, field.name) |
