Opened 6 years ago

Last modified 3 years ago

#29738 closed Bug

Django can't serialize DateTimeTZRange(lower=None, upper=None, bounds='[)') — at Version 2

Reported by: Graham Mayer Owned by: nobody
Component: Migrations Version: 2.0
Severity: Normal Keywords: rangefield postgresql psycopg2 migrations removed
Cc: jon.dufresne@… Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Simon Charette)

Tried to use DateTimeTZRange(lower=None, upper=None, bounds='[)') as a default for a model field and get the following error when running 'python manage.py makemigrations':

 Traceback (most recent call last):
  File "manage.py", line 12, in <module>
    execute_from_command_line(sys.argv)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/__init__.py", line 371, in execute_from_command_line
    utility.execute()
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/__init__.py", line 365, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/base.py", line 288, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/base.py", line 335, in execute
    output = self.handle(*args, **options)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/commands/makemigrations.py", line 172, in handle
    self.write_migration_files(changes)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/core/management/commands/makemigrations.py", line 210, in write_migration_files
    migration_string = writer.as_string()
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/writer.py", line 151, in as_string
    operation_string, operation_imports = OperationWriter(operation).serialize()
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/writer.py", line 110, in serialize
    _write(arg_name, arg_value)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/writer.py", line 74, in _write
    arg_string, arg_imports = MigrationWriter.serialize(_arg_value)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/writer.py", line 279, in serialize
    return serializer_factory(value).serialize()
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/serializer.py", line 203, in serialize
    return self.serialize_deconstructed(path, args, kwargs)
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/serializer.py", line 90, in serialize_deconstructed
    arg_string, arg_imports = serializer_factory(arg).serialize()
  File "/home/grahammayer/logimeter/logimeter/lib/python3.6/site-packages/django/db/migrations/serializer.py", line 370, in serializer_factory
    "topics/migrations/#migration-serializing" % (value, get_docs_version())
ValueError: Cannot serialize: DateTimeTZRange(None, None, '[)')

Change History (2)

comment:1 by Nick Pope, 6 years ago

Component: Migrationscontrib.postgres
Description: modified (diff)
Easy pickings: set
Keywords: rangefield postgresql psycopg2 migrations added
Needs documentation: set
Needs tests: set
Triage Stage: UnreviewedAccepted

Presumably you are using default=DateTimeTZRange(lower=None, upper=None, bounds='[)')? Could you provide a simple example project?

The problem is that psycopg2.extras.DateTimeTZRange cannot be serialized into a migrations, see documentation.

So there are a few options:

  1. Can you get away with using default=None, null=True?
  2. Does default=(None, None) work instead? (I believe this will be limited to the default bounds: [))
  3. You need to make DateTimeTZRange deconstructible which will allow for other bounds to be defined.

We should be able to get away with making these range objects deconstructible as we only expect them to take simple arguments that are already supported by the serializer.

Here is an example:

from psycopg2.extras import DateTimeTZRange

from django.contrib.postgres.fields import DateTimeRangeField
from django.utils.deconstruct import deconstructible


DateTimeTZRange = deconstructible(DateTimeTZRange, path='psycopg2.extras.DateTimeTZRange')


class RangeTestModel(models.Model):
    a = DateTimeRangeField(
        default=None,
        null=True,
    )
    b = DateTimeRangeField(
        default=(None, None),
        null=False,
    )
    c = DateTimeRangeField(
        default=DateTimeTZRange(None, None, '[]'),
        null=False,
    )

I managed to produce the following migration:

import django.contrib.postgres.fields.ranges
from django.db import migrations, models
import psycopg2.extras


class Migration(migrations.Migration):

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='RangeTestModel',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('a', django.contrib.postgres.fields.ranges.DateTimeRangeField(default=None, null=True)),
                ('b', django.contrib.postgres.fields.ranges.DateTimeRangeField(default=(None, None))),
                ('c', django.contrib.postgres.fields.ranges.DateTimeRangeField(default=psycopg2.extras.DateTimeTZRange(None, None, '[]'))),
            ],
        ),
    ]

(Disclaimer: I have only attempted to produce migrations and not actually called RangeTestModel.objects.create()...)

So in summary, I think that we only need to add some documentation to explain how to handle defaults for range fields and tests to ensure that it continues to work.

comment:2 by Simon Charette, 6 years ago

Description: modified (diff)
Keywords: rangefield postgresql psycopg2 migrations removed

A solution could to make django.db.migration.serializer offer a way to register serializers for types and have django.contrib.postgres.App.ready() be in charge of registering the ones to handle psycopg2 range types.

If the registry uses an OrderedDict we should be able to replace that long series of if by iterating over the registry items.

registry = OrderedDict([
    (models.Field, ModelFieldSerializer),
    ...
])

def serializer_factory(value):
    if isinstance(value, Promise):
        value = str(value)
    elif isinstance(value, LazyObject):
        # The unwrapped value is returned as the first item of the arguments
        # tuple.
    value = value.__reduce__()[1][0]

    for type_, serializer_cls in registry.items():
        if isinstance(value, type_):
            return serializer_cls(value)
    raise ValueError(...)

The hasattr and checks could be performed using abcs.

from abc import ABC
class Deconstructable(ABC):
    @classmethod
    def __subclasshook__(cls, C):
        return hasattr(C, 'deconstruct')
Note: See TracTickets for help on using tickets.
Back to Top