Skip to content

Commit

Permalink
Fixed #29522 -- Refactored the Deserializer functions to classes.
Browse files Browse the repository at this point in the history
Co-authored-by: Emad Mokhtar <[email protected]>
  • Loading branch information
2 people authored and sarahboyce committed Sep 17, 2024
1 parent a060a22 commit ee5147c
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 99 deletions.
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ answer newbie questions, and generally made Django that much better:
Aljaž Košir <[email protected]>
Aljosa Mohorovic <[email protected]>
Alokik Vijay <[email protected]>
Amir Karimi <amk9978@gmail.com>
Amir Karimi <https://github.com/amk9978>
Amit Chakradeo <https://amit.chakradeo.net/>
Amit Ramon <[email protected]>
Amit Upadhyay <http://www.amitu.com/blog/>
Expand Down
32 changes: 20 additions & 12 deletions django/core/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,27 @@ def getvalue(self):
return super(PythonSerializer, self).getvalue()


def Deserializer(stream_or_string, **options):
class Deserializer(PythonDeserializer):
"""Deserialize a stream or string of JSON data."""
if not isinstance(stream_or_string, (bytes, str)):
stream_or_string = stream_or_string.read()
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode()
try:
objects = json.loads(stream_or_string)
yield from PythonDeserializer(objects, **options)
except (GeneratorExit, DeserializationError):
raise
except Exception as exc:
raise DeserializationError() from exc

def __init__(self, stream_or_string, **options):
if not isinstance(stream_or_string, (bytes, str)):
stream_or_string = stream_or_string.read()
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode()
try:
objects = json.loads(stream_or_string)
except Exception as exc:
raise DeserializationError() from exc
super().__init__(objects, **options)

def _handle_object(self, obj):
try:
yield from super()._handle_object(obj)
except (GeneratorExit, DeserializationError):
raise
except Exception as exc:
raise DeserializationError(f"Error deserializing object: {exc}") from exc


class DjangoJSONEncoder(json.JSONEncoder):
Expand Down
33 changes: 22 additions & 11 deletions django/core/serializers/jsonl.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,30 @@ def getvalue(self):
return super(PythonSerializer, self).getvalue()


def Deserializer(stream_or_string, **options):
class Deserializer(PythonDeserializer):
"""Deserialize a stream or string of JSON data."""
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode()
if isinstance(stream_or_string, (bytes, str)):
stream_or_string = stream_or_string.split("\n")

for line in stream_or_string:
if not line.strip():
continue

def __init__(self, stream_or_string, **options):
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode()
if isinstance(stream_or_string, str):
stream_or_string = stream_or_string.splitlines()
super().__init__(Deserializer._get_lines(stream_or_string), **options)

def _handle_object(self, obj):
try:
yield from PythonDeserializer([json.loads(line)], **options)
yield from super()._handle_object(obj)
except (GeneratorExit, DeserializationError):
raise
except Exception as exc:
raise DeserializationError() from exc
raise DeserializationError(f"Error deserializing object: {exc}") from exc

@staticmethod
def _get_lines(stream):
for line in stream:
if not line.strip():
continue
try:
yield json.loads(line)
except Exception as exc:
raise DeserializationError() from exc
119 changes: 71 additions & 48 deletions django/core/serializers/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,45 +96,60 @@ def getvalue(self):
return self.objects


def Deserializer(
object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False, **options
):
class Deserializer(base.Deserializer):
"""
Deserialize simple Python objects back into Django ORM instances.
It's expected that you pass the Python objects themselves (instead of a
stream or a string) to the constructor
"""
handle_forward_references = options.pop("handle_forward_references", False)
field_names_cache = {} # Model: <list of field_names>

for d in object_list:
def __init__(
self, object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False, **options
):
super().__init__(object_list, **options)
self.handle_forward_references = options.pop("handle_forward_references", False)
self.using = using
self.ignorenonexistent = ignorenonexistent
self.field_names_cache = {} # Model: <list of field_names>
self._iterator = None

def __iter__(self):
for obj in self.stream:
yield from self._handle_object(obj)

def __next__(self):
if self._iterator is None:
self._iterator = iter(self)
return next(self._iterator)

def _handle_object(self, obj):
data = {}
m2m_data = {}
deferred_fields = {}

# Look up the model and starting build a dict of data for it.
try:
Model = _get_model(d["model"])
Model = self._get_model_from_node(obj["model"])
except base.DeserializationError:
if ignorenonexistent:
continue
else:
raise
data = {}
if "pk" in d:
if self.ignorenonexistent:
return
raise
if "pk" in obj:
try:
data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk"))
data[Model._meta.pk.attname] = Model._meta.pk.to_python(obj.get("pk"))
except Exception as e:
raise base.DeserializationError.WithData(
e, d["model"], d.get("pk"), None
e, obj["model"], obj.get("pk"), None
)
m2m_data = {}
deferred_fields = {}

if Model not in field_names_cache:
field_names_cache[Model] = {f.name for f in Model._meta.get_fields()}
field_names = field_names_cache[Model]
if Model not in self.field_names_cache:
self.field_names_cache[Model] = {f.name for f in Model._meta.get_fields()}
field_names = self.field_names_cache[Model]

# Handle each field
for field_name, field_value in d["fields"].items():
if ignorenonexistent and field_name not in field_names:
for field_name, field_value in obj["fields"].items():
if self.ignorenonexistent and field_name not in field_names:
# skip fields no longer on model
continue

Expand All @@ -145,51 +160,59 @@ def Deserializer(
field.remote_field, models.ManyToManyRel
):
try:
values = base.deserialize_m2m_values(
field, field_value, using, handle_forward_references
)
values = self._handle_m2m_field_node(field, field_value)
if values == base.DEFER_FIELD:
deferred_fields[field] = field_value
else:
m2m_data[field.name] = values
except base.M2MDeserializationError as e:
raise base.DeserializationError.WithData(
e.original_exc, d["model"], d.get("pk"), e.pk
e.original_exc, obj["model"], obj.get("pk"), e.pk
)
if values == base.DEFER_FIELD:
deferred_fields[field] = field_value
else:
m2m_data[field.name] = values

# Handle FK fields
elif field.remote_field and isinstance(
field.remote_field, models.ManyToOneRel
):
try:
value = base.deserialize_fk_value(
field, field_value, using, handle_forward_references
)
value = self._handle_fk_field_node(field, field_value)
if value == base.DEFER_FIELD:
deferred_fields[field] = field_value
else:
data[field.attname] = value
except Exception as e:
raise base.DeserializationError.WithData(
e, d["model"], d.get("pk"), field_value
e, obj["model"], obj.get("pk"), field_value
)
if value == base.DEFER_FIELD:
deferred_fields[field] = field_value
else:
data[field.attname] = value

# Handle all other fields
else:
try:
data[field.name] = field.to_python(field_value)
except Exception as e:
raise base.DeserializationError.WithData(
e, d["model"], d.get("pk"), field_value
e, obj["model"], obj.get("pk"), field_value
)

obj = base.build_instance(Model, data, using)
yield base.DeserializedObject(obj, m2m_data, deferred_fields)
model_instance = base.build_instance(Model, data, self.using)
yield base.DeserializedObject(model_instance, m2m_data, deferred_fields)

def _handle_m2m_field_node(self, field, field_value):
return base.deserialize_m2m_values(
field, field_value, self.using, self.handle_forward_references
)

def _get_model(model_identifier):
"""Look up a model from an "app_label.model_name" string."""
try:
return apps.get_model(model_identifier)
except (LookupError, TypeError):
raise base.DeserializationError(
"Invalid model identifier: '%s'" % model_identifier
def _handle_fk_field_node(self, field, field_value):
return base.deserialize_fk_value(
field, field_value, self.using, self.handle_forward_references
)

@staticmethod
def _get_model_from_node(model_identifier):
"""Look up a model from an "app_label.model_name" string."""
try:
return apps.get_model(model_identifier)
except (LookupError, TypeError):
raise base.DeserializationError(
f"Invalid model identifier: {model_identifier}"
)
31 changes: 18 additions & 13 deletions django/core/serializers/pyyaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import collections
import decimal
from io import StringIO

import yaml

Expand Down Expand Up @@ -66,17 +65,23 @@ def getvalue(self):
return super(PythonSerializer, self).getvalue()


def Deserializer(stream_or_string, **options):
class Deserializer(PythonDeserializer):
"""Deserialize a stream or string of YAML data."""
if isinstance(stream_or_string, bytes):
stream_or_string = stream_or_string.decode()
if isinstance(stream_or_string, str):
stream = StringIO(stream_or_string)
else:

def __init__(self, stream_or_string, **options):
stream = stream_or_string
try:
yield from PythonDeserializer(yaml.load(stream, Loader=SafeLoader), **options)
except (GeneratorExit, DeserializationError):
raise
except Exception as exc:
raise DeserializationError() from exc
if isinstance(stream_or_string, bytes):
stream = stream_or_string.decode()
try:
objects = yaml.load(stream, Loader=SafeLoader)
except Exception as exc:
raise DeserializationError() from exc
super().__init__(objects, **options)

def _handle_object(self, obj):
try:
yield from super()._handle_object(obj)
except (GeneratorExit, DeserializationError):
raise
except Exception as exc:
raise DeserializationError(f"Error deserializing object: {exc}") from exc
4 changes: 3 additions & 1 deletion docs/releases/5.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ Security
Serialization
~~~~~~~~~~~~~

* ...
* Each serialization format now defines a ``Deserializer`` class, rather than a
function, to improve extensibility when defining a
:ref:`custom serialization format <custom-serialization-formats>`.

Signals
~~~~~~~
Expand Down
Loading

0 comments on commit ee5147c

Please sign in to comment.