Commit 48dd1e63 authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Ported over Field.deconstruct() from my schema alteration branch.

This is to help other ongoing branches which would benefit from
this functionality.
parent 8809da67
Loading
Loading
Loading
Loading
+187 −1
Original line number Diff line number Diff line
@@ -99,7 +99,8 @@ class Field(object):
            db_tablespace=None, auto_created=False, validators=[],
            error_messages=None):
        self.name = name
        self.verbose_name = verbose_name
        self.verbose_name = verbose_name  # May be set by set_attributes_from_name
        self._verbose_name = verbose_name  # Store original for deconstruction
        self.primary_key = primary_key
        self.max_length, self._unique = max_length, unique
        self.blank, self.null = blank, null
@@ -128,14 +129,99 @@ class Field(object):
            self.creation_counter = Field.creation_counter
            Field.creation_counter += 1

        self._validators = validators  # Store for deconstruction later
        self.validators = self.default_validators + validators

        messages = {}
        for c in reversed(self.__class__.__mro__):
            messages.update(getattr(c, 'default_error_messages', {}))
        messages.update(error_messages or {})
        self._error_messages = error_messages  # Store for deconstruction later
        self.error_messages = messages

    def deconstruct(self):
        """
        Returns enough information to recreate the field as a 4-tuple:

         * The name of the field on the model, if contribute_to_class has been run
         * The import path of the field, including the class: django.db.models.IntegerField
           This should be the most portable version, so less specific may be better.
         * A list of positional arguments
         * A dict of keyword arguments

        Note that the positional or keyword arguments must contain values of the
        following types (including inner values of collection types):

         * None, bool, str, unicode, int, long, float, complex, set, frozenset, list, tuple, dict
         * UUID
         * datetime.datetime (naive), datetime.date
         * top-level classes, top-level functions - will be referenced by their full import path
         * Storage instances - these have their own deconstruct() method

        This is because the values here must be serialised into a text format
        (possibly new Python code, possibly JSON) and these are the only types
        with encoding handlers defined.

        There's no need to return the exact way the field was instantiated this time,
        just ensure that the resulting field is the same - prefer keyword arguments
        over positional ones, and omit parameters with their default values.
        """
        # Short-form way of fetching all the default parameters
        keywords = {}
        possibles = {
            "verbose_name": None,
            "primary_key": False,
            "max_length": None,
            "unique": False,
            "blank": False,
            "null": False,
            "db_index": False,
            "default": NOT_PROVIDED,
            "editable": True,
            "serialize": True,
            "unique_for_date": None,
            "unique_for_month": None,
            "unique_for_year": None,
            "choices": [],
            "help_text": '',
            "db_column": None,
            "db_tablespace": settings.DEFAULT_INDEX_TABLESPACE,
            "auto_created": False,
            "validators": [],
            "error_messages": None,
        }
        attr_overrides = {
            "unique": "_unique",
            "choices": "_choices",
            "error_messages": "_error_messages",
            "validators": "_validators",
            "verbose_name": "_verbose_name",
        }
        equals_comparison = set(["choices", "validators", "db_tablespace"])
        for name, default in possibles.items():
            value = getattr(self, attr_overrides.get(name, name))
            if name in equals_comparison:
                if value != default:
                    keywords[name] = value
            else:
                if value is not default:
                    keywords[name] = value
        # Work out path - we shorten it for known Django core fields
        path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
        if path.startswith("django.db.models.fields.related"):
            path = path.replace("django.db.models.fields.related", "django.db.models")
        if path.startswith("django.db.models.fields.files"):
            path = path.replace("django.db.models.fields.files", "django.db.models")
        if path.startswith("django.db.models.fields"):
            path = path.replace("django.db.models.fields", "django.db.models")
        # Return basic info - other fields should override this.
        return (
            self.name,
            path,
            [],
            keywords,
        )

    def __eq__(self, other):
        # Needed for @total_ordering
        if isinstance(other, Field):
@@ -566,6 +652,7 @@ class Field(object):
            return '<%s: %s>' % (path, name)
        return '<%s>' % path


class AutoField(Field):
    description = _("Integer")

@@ -580,6 +667,12 @@ class AutoField(Field):
        kwargs['blank'] = True
        Field.__init__(self, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(AutoField, self).deconstruct()
        del kwargs['blank']
        kwargs['primary_key'] = True
        return name, path, args, kwargs

    def get_internal_type(self):
        return "AutoField"

@@ -630,6 +723,11 @@ class BooleanField(Field):
        kwargs['blank'] = True
        Field.__init__(self, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(BooleanField, self).deconstruct()
        del kwargs['blank']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "BooleanField"

@@ -733,6 +831,18 @@ class DateField(Field):
            kwargs['blank'] = True
        Field.__init__(self, verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(DateField, self).deconstruct()
        if self.auto_now:
            kwargs['auto_now'] = True
            del kwargs['editable']
            del kwargs['blank']
        if self.auto_now_add:
            kwargs['auto_now_add'] = True
            del kwargs['editable']
            del kwargs['blank']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "DateField"

@@ -927,6 +1037,14 @@ class DecimalField(Field):
        self.max_digits, self.decimal_places = max_digits, decimal_places
        Field.__init__(self, verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(DecimalField, self).deconstruct()
        if self.max_digits:
            kwargs['max_digits'] = self.max_digits
        if self.decimal_places:
            kwargs['decimal_places'] = self.decimal_places
        return name, path, args, kwargs

    def get_internal_type(self):
        return "DecimalField"

@@ -989,6 +1107,12 @@ class EmailField(CharField):
        kwargs['max_length'] = kwargs.get('max_length', 75)
        CharField.__init__(self, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(EmailField, self).deconstruct()
        # We do not exclude max_length if it matches default as we want to change
        # the default in future.
        return name, path, args, kwargs

    def formfield(self, **kwargs):
        # As with CharField, this will cause email validation to be performed
        # twice.
@@ -1008,6 +1132,22 @@ class FilePathField(Field):
        kwargs['max_length'] = kwargs.get('max_length', 100)
        Field.__init__(self, verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(FilePathField, self).deconstruct()
        if self.path != '':
            kwargs['path'] = self.path
        if self.match is not None:
            kwargs['match'] = self.match
        if self.recursive is not False:
            kwargs['recursive'] = self.recursive
        if self.allow_files is not True:
            kwargs['allow_files'] = self.allow_files
        if self.allow_folders is not False:
            kwargs['allow_folders'] = self.allow_folders
        if kwargs.get("max_length", None) == 100:
            del kwargs["max_length"]
        return name, path, args, kwargs

    def formfield(self, **kwargs):
        defaults = {
            'path': self.path,
@@ -1115,6 +1255,11 @@ class IPAddressField(Field):
        kwargs['max_length'] = 15
        Field.__init__(self, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(IPAddressField, self).deconstruct()
        del kwargs['max_length']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "IPAddressField"

@@ -1131,12 +1276,23 @@ class GenericIPAddressField(Field):
    def __init__(self, verbose_name=None, name=None, protocol='both',
                 unpack_ipv4=False, *args, **kwargs):
        self.unpack_ipv4 = unpack_ipv4
        self.protocol = protocol
        self.default_validators, invalid_error_message = \
            validators.ip_address_validators(protocol, unpack_ipv4)
        self.default_error_messages['invalid'] = invalid_error_message
        kwargs['max_length'] = 39
        Field.__init__(self, verbose_name, name, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(GenericIPAddressField, self).deconstruct()
        if self.unpack_ipv4 is not False:
            kwargs['unpack_ipv4'] = self.unpack_ipv4
        if self.protocol != "both":
            kwargs['protocol'] = self.protocol
        if kwargs.get("max_length", None) == 39:
            del kwargs['max_length']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "GenericIPAddressField"

@@ -1177,6 +1333,12 @@ class NullBooleanField(Field):
        kwargs['blank'] = True
        Field.__init__(self, *args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(NullBooleanField, self).deconstruct()
        del kwargs['null']
        del kwargs['blank']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "NullBooleanField"

@@ -1254,6 +1416,16 @@ class SlugField(CharField):
            kwargs['db_index'] = True
        super(SlugField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(SlugField, self).deconstruct()
        if kwargs.get("max_length", None) == 50:
            del kwargs['max_length']
        if self.db_index is False:
            kwargs['db_index'] = False
        else:
            del kwargs['db_index']
        return name, path, args, kwargs

    def get_internal_type(self):
        return "SlugField"

@@ -1302,6 +1474,14 @@ class TimeField(Field):
            kwargs['blank'] = True
        Field.__init__(self, verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(TimeField, self).deconstruct()
        if self.auto_now is not False:
            kwargs["auto_now"] = self.auto_now
        if self.auto_now_add is not False:
            kwargs["auto_now_add"] = self.auto_now_add
        return name, path, args, kwargs

    def get_internal_type(self):
        return "TimeField"

@@ -1367,6 +1547,12 @@ class URLField(CharField):
        kwargs['max_length'] = kwargs.get('max_length', 200)
        CharField.__init__(self, verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(URLField, self).deconstruct()
        if kwargs.get("max_length", None) == 200:
            del kwargs['max_length']
        return name, path, args, kwargs

    def formfield(self, **kwargs):
        # As with CharField, this will cause URL validation to be performed
        # twice.
+19 −0
Original line number Diff line number Diff line
@@ -227,6 +227,17 @@ class FileField(Field):
        kwargs['max_length'] = kwargs.get('max_length', 100)
        super(FileField, self).__init__(verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(FileField, self).deconstruct()
        if kwargs.get("max_length", None) != 100:
            kwargs["max_length"] = 100
        else:
            del kwargs["max_length"]
        kwargs['upload_to'] = self.upload_to
        if self.storage is not default_storage:
            kwargs['storage'] = self.storage
        return name, path, args, kwargs

    def get_internal_type(self):
        return "FileField"

@@ -326,6 +337,14 @@ class ImageField(FileField):
        self.width_field, self.height_field = width_field, height_field
        super(ImageField, self).__init__(verbose_name, name, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(ImageField, self).deconstruct()
        if self.width_field:
            kwargs['width_field'] = self.width_field
        if self.height_field:
            kwargs['height_field'] = self.height_field
        return name, path, args, kwargs

    def contribute_to_class(self, cls, name):
        super(ImageField, self).contribute_to_class(cls, name)
        # Attach update_dimension_fields so that dimension fields declared
+42 −0
Original line number Diff line number Diff line
@@ -1151,6 +1151,27 @@ class ForeignKey(ForeignObject):
        )
        super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(ForeignKey, self).deconstruct()
        # Handle the simpler arguments
        if self.db_index:
            del kwargs['db_index']
        else:
            kwargs['db_index'] = False
        if self.db_constraint is not True:
            kwargs['db_constraint'] = self.db_constraint
        if self.rel.on_delete is not CASCADE:
            kwargs['on_delete'] = self.rel.on_delete
        # Rel needs more work.
        rel = self.rel
        if self.rel.field_name:
            kwargs['to_field'] = self.rel.field_name
        if isinstance(self.rel.to, basestring):
            kwargs['to'] = self.rel.to
        else:
            kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
        return name, path, args, kwargs

    @property
    def related_field(self):
        return self.foreign_related_fields[0]
@@ -1268,6 +1289,12 @@ class OneToOneField(ForeignKey):
        kwargs['unique'] = True
        super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(OneToOneField, self).deconstruct()
        if "unique" in kwargs:
            del kwargs['unique']
        return name, path, args, kwargs

    def contribute_to_related_class(self, cls, related):
        setattr(cls, related.get_accessor_name(),
                SingleRelatedObjectDescriptor(related))
@@ -1357,6 +1384,21 @@ class ManyToManyField(RelatedField):

        super(ManyToManyField, self).__init__(**kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(ManyToManyField, self).deconstruct()
        # Handle the simpler arguments
        if self.rel.db_constraint is not True:
            kwargs['db_constraint'] = self.db_constraint
        if "help_text" in kwargs:
            del kwargs['help_text']
        # Rel needs more work.
        rel = self.rel
        if isinstance(self.rel.to, basestring):
            kwargs['to'] = self.rel.to
        else:
            kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
        return name, path, args, kwargs

    def _get_path_info(self, direct=False):
        """
        Called by both direct an indirect m2m traversal.
+0 −0

Empty file added.

+0 −0

Empty file added.

Loading