12.18 Serializing values: serializer_factory and deconstruct
A migration file is Python source that Django writes, so every value in an operation has to become an expression that makes the value again when the file is imported. serializer_factory picks a serializer for a value, the serializer returns the expression as a string with the import lines it needs, and an object with a deconstruct method is written as a call with the arguments the method returns.
A serializer is an instance of a subclass of BaseSerializer, made with the value. The subclass overrides BaseSerializer to return the text and a set of import lines, each a whole statement such as "import datetime"; MigrationWriter only calls serializer_factory and then that method.
A value is written whole, as a literal or its repr; or by name, as the dotted path it is imported by; or as a call that makes it again. One that holds others is written by writing those: its serializer calls serializer_factory on each and adds their imports to its own. Of each recording only the lines a passage is about are quoted: a line is an expression put to a running Django and, after ->, its value, or the word raises and an exception, and written stands for MigrationWriter, giving the text and then the imports. Here the value is a field whose default is next_serial, a function at the top of harbour’s module serials:
written(models.CharField(max_length=8, default=next_serial)) -> models.CharField(default=harbour.serials.next_serial, max_length=8) needs ['from django.db import models', 'import harbour.serials']
A value Django has no expression for cannot be written into a migration file, and that is the limit on what a field’s deconstruct may return. The autodetector compares fields by what it returns (The autodetector: comparing two project states).
serializer_factory: the order of the tests
serializer_factory is a fixed sequence of tests, and the first a value passes decides. It first unwraps: a Promise, which a lazily translated string is, becomes its str then, and a LazyObject the object it wraps.
Then a field gets ModelFieldSerializer, a manager ModelManagerSerializer, an operation OperationSerializer, a class TypeSerializer, and anything else with an attribute deconstruct gets DeconstructibleSerializer. A class whose instances can deconstruct has the attribute too, and is caught by the test before.
What passes none of these reaches the registry, Serializer: a dictionary from a type, or a tuple of types, to a serializer class. Its entries are tried in the dictionary’s order with isinstance, and some depend on that order: models.Choices stands before enum.Enum and datetime before datetime.date, each a subclass of what it precedes; SettingsReference before str; pathlib.PurePath before os.PathLike; and str, bytes, range and the containers before collections, which each of them is.
type(serializer_factory(Rank.MATE)).__name__ -> 'ChoicesSerializer'
type(serializer_factory(uuid.SafeUUID.safe)).__name__ -> 'EnumSerializer'
Rank is harbour’s class of text choices, uuid.SafeUUID an enumeration of Python’s. SET returns a function, which the entry for functions would take, but sets a deconstruct on it, and the attribute is tested first.
deconstruct: a value written as a call
The deconstruct protocol is how an object says how to make it again: the method returns the dotted path of something to call, positional arguments and keyword arguments, and DeconstructibleSerializer writes the call. A path whose module is exactly django.db.models is written models. and the name, with "from django; any other whole, with an import of its module. The positional arguments go through serializer_factory in turn, then the keyword arguments sorted by name; those whose names are not identifiers go into a dictionary written after **. So an object can be written only if everything it deconstructs to can.
A field, a manager and an operation answer in other shapes, so each has its own serializer. Field returns the field’s name first, which ModelFieldSerializer leaves out, and shortens the path of a class under django to django.db.models (Field; A field: the base class). ModelManagerSerializer writes a manager that QuerySet made as a call of that method on the queryset class’s path, and another as a call of its own class (Managers). OperationSerializer hands an operation to OperationWriter (Writing a migration file: MigrationWriter).
Q.deconstruct, Index and BaseConstraint are written by hand, and each shortens its own module to django.db.models (Q.deconstruct; Constraints and indexes). Other classes get the method from the decorator deconstructible. It replaces the class’s __new__ with one that keeps the arguments of the call on the new object, as _constructor_args, and adds a deconstruct that returns them after a path. The path is the module and name of the object’s own class, so a subclass is written under its own name; an object of the decorated class itself gets the path passed as path=, where one was. The method first imports the module, and raises ValueError if the name is not in it, as an inner class’s is not. Tide is a class decorated with path="__main__:
written(Tide(high=4.2)) -> __main__.Tide(high=4.2) needs ['import __main__']
Tide(high=4.2).deconstruct() -> ('__main__.Tide', (), {'high': 4.2})
Expression is decorated, as is every validator class of django/; F, Func, Value and some others are decorated with a path in django.db.models, which is why a file has models.F and not django.
Values written by name
What is written by name is not written out: the file imports whatever has that name when the migration is loaded.
FunctionTypeSerializer takes what FUNCTION_TYPES lists, functions, built-in functions and methods among them. A method whose __self__ is a class is written as the class’s module, its qualified name and the method’s name. Anything else is written as its __module__ and __qualname__: a function at the top of a module, or in the body of a class. A method defined in Python and bound to an instance is written so too, as the function it wraps: the loaded migration has the function and no instance.
TypeSerializer writes a class by module and qualified name too, except models.Model and types.NoneType, written under those names, and a builtin, written bare with no import. EnumSerializer writes a member as its class and, in square brackets, its name, and a combined enum.Flag as its members joined by |. ChoicesSerializer writes a member of a Choices class not by name but as its value alone, so the file does not import the class.
A SettingsReference is a str that also holds a setting’s name, in SettingsReference. A field makes one: where RelatedField names a setting, as it does for a field that points at the user model, which AUTH_USER_MODEL can swap, ForeignObject and ManyToManyField put a SettingsReference in place of the "to" argument. It is written as the setting, so the file follows the setting of the project that loads it; the setting’s value by itself is written as a string:
written(SettingsReference(settings.AUTH_USER_MODEL, 'AUTH_USER_MODEL')) -> settings.AUTH_USER_MODEL needs ['from django.conf import settings']
written(settings.AUTH_USER_MODEL) -> 'auth.User' needs no import
The dependency that goes with it is in From operations to migrations: dependencies, order and names.
Values written whole
Where a value’s repr is an expression that makes it, the repr is the text; BaseSimpleSerializer returns it with no import. The other serializers of this kind mend what repr leaves undone: an import it needs, the module’s name in front of it, or a call of float on a string for a float that is not a number or is infinite. DatetimeDatetimeSerializer first converts a datetime in a time zone other than UTC to UTC. PathSerializer writes a concrete path as the pure path of its flavour, so that, its comment says, a migration made on one platform can be used on another. RegexSerializer writes a pattern’s flags after an exclusive-or with those of an empty str pattern, and none where that leaves none: '^m-' compiled with re.IGNORECASE is written re, without the re.UNICODE that Python added.
For a list, a tuple or a set, BaseSequenceSerializer writes each item and joins the strings, and a subclass gives the brackets through BaseSequenceSerializer: a tuple of one item gets its comma, and an empty set is set(), since {} is a dictionary. BaseUnorderedSequenceSerializer first sorts a set or a frozenset by the repr of its items, and DictionarySerializer a dictionary by its items themselves, so the text does not depend on the order of iteration. IterableSerializer writes any other iterable, a generator too, as a tuple of what it yields.
The registry, in its order
The entries of Serializer stand in the order serializer_factory tries them, so a row takes only what no row above it took.
| serializer | key | writes |
|---|---|---|
Frozenset |
key frozenset |
writes frozenset([1, 2]) |
Sequence |
key list |
writes ['Bergen', 'Leith'] |
SetSerializer |
key set |
writes {'Bergen', 'Leith', 'Lisbon'}, set() |
TupleSerializer |
key tuple |
writes ('Bergen',) |
Dictionary |
key dict |
writes {'name': 'Petrel', 'tonnage': 480} |
Choices |
key models.Choices |
writes 'mate' |
EnumSerializer |
key enum.Enum |
writes uuid |
Datetime |
key datetime |
writes datetime, for 10:00 in Oslo too |
Date |
key datetime.date, datetime, datetime.time |
writes datetime |
Settings |
key Settings |
writes settings |
FloatSerializer |
key float |
writes float("inf") |
Base |
key bool, int, types.NoneType, bytes, str, range |
writes 'North dock', range(1, 4) |
Decimal |
key decimal.Decimal |
writes its repr, with "from decimal import Decimal" |
Functools |
key functools, functools |
writes functools |
Function |
key FUNCTION_TYPES |
writes django, datetime |
Generic |
key types |
writes list[int] |
Iterable |
key collections |
writes ('Bergen',), for an iterator |
RegexSerializer |
key COMPILED_, RegexObject |
writes re |
UUIDSerializer |
key uuid.UUID |
writes uuid |
PathSerializer |
key pathlib.PurePath |
writes pathlib, for a pathlib.Path on Windows |
Path |
key os.PathLike |
writes the repr of what os.fspath returns, a str or bytes, and {} for its imports |
Zone |
key zoneinfo |
writes zoneinfo |
Database |
key DatabaseOnDelete |
writes django |
What cannot be written
A function is written by name, and a lambda has none to import by: FunctionTypeSerializer refuses it, a function with no module, and one for which qualname raises, as it does for a function defined inside another, whose qualified name has "<" in it. What passes no test of serializer_factory gets the general ValueError. datetime.UTC by itself does: the registry has no key for a datetime, and inside a datetime it is only part of a repr. So does object() in these lines, in which nested is a function of the recording’s own that returns one defined inside it:
written(lambda: 1) -> raises ValueError: Cannot serialize function: lambda
written(nested()) -> raises ValueError: Could not find function inner in __main__.\n
written(object()) -> raises ValueError: Cannot serialize: <object object>\nThere are some values Django cannot serialize into migration files.\nFor more, see https://docs.djangoproject.com/en/dev/topics/migrations/#migration-serializing
Registering a serializer
Serializer takes a type, or a tuple of types, and a serializer class; it raises ValueError for a class that is no subclass of BaseSerializer and otherwise stores the pair, and Serializer pops a key; MigrationWriter and MigrationWriter only call the two. A new key goes to the end of the dictionary, so it is reached only by a value no entry before it matches; a key already there keeps its place and gets the new class. And the registry is reached only past the tests before it: a serializer registered for a field class, or for a class with a deconstruct, is never asked. Fathoms is a class with no deconstruct, and this is a serializer for it, which writes the class as if it stood in a module depths that harbour does not have:
class FathomsSerializer(BaseSerializer):
def serialize(self):
return f"harbour.depths.Fathoms({self.value.number})", {"import harbour.depths"}
MigrationWriter.register_serializer(Fathoms, FathomsSerializer)
list(Serializer._registry)[-1] is Fathoms -> True
written(Fathoms(12)) -> harbour.depths.Fathoms(12) needs ['import harbour.depths']