Commit 799e81ef authored by Matthew Schinckel's avatar Matthew Schinckel Committed by Tim Graham
Browse files

[1.9.x] Fixed #26475 -- Added functools.partial() support to migrations autodetector.

Backport of 5402f3ab from master
parent f5b8e9b2
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
from __future__ import unicode_literals

import datetime
import functools
import re
from itertools import chain

@@ -63,6 +64,8 @@ class MigrationAutodetector(object):
                key: self.deep_deconstruct(value)
                for key, value in obj.items()
            }
        elif isinstance(obj, functools.partial):
            return (obj.func, self.deep_deconstruct(obj.args), self.deep_deconstruct(obj.keywords))
        elif isinstance(obj, COMPILED_REGEX_TYPE):
            return RegexObject(obj)
        elif isinstance(obj, type):
+3 −0
Original line number Diff line number Diff line
@@ -15,3 +15,6 @@ Bugfixes

* Fixed ``TimeField`` microseconds round-tripping on MySQL and SQLite
  (:ticket:`26498`).

* Prevented ``makemigrations`` from generating infinite migrations for a model
  field that references a ``functools.partial`` (:ticket:`26475`).
+54 −0
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-
import functools
import re

from django.apps import apps
@@ -657,6 +658,59 @@ class AutodetectorTests(TestCase):
        self.assertOperationTypes(changes, 'testapp', 0, ["AlterField"])
        self.assertOperationAttributes(changes, "testapp", 0, 0, name="name", preserve_default=True)

    def test_supports_functools_partial(self):
        def _content_file_name(instance, filename, key, **kwargs):
            return '{}/{}'.format(instance, filename)

        def content_file_name(key, **kwargs):
            return functools.partial(_content_file_name, key, **kwargs)

        # An unchanged partial reference.
        before = self.make_project_state([ModelState("testapp", "Author", [
            ("id", models.AutoField(primary_key=True)),
            ("file", models.FileField(max_length=200, upload_to=content_file_name('file'))),
        ])])
        after = self.make_project_state([ModelState("testapp", "Author", [
            ("id", models.AutoField(primary_key=True)),
            ("file", models.FileField(max_length=200, upload_to=content_file_name('file'))),
        ])])
        autodetector = MigrationAutodetector(before, after)
        changes = autodetector._detect_changes()
        self.assertNumberMigrations(changes, 'testapp', 0)

        # A changed partial reference.
        args_changed = self.make_project_state([ModelState("testapp", "Author", [
            ("id", models.AutoField(primary_key=True)),
            ("file", models.FileField(max_length=200, upload_to=content_file_name('other-file'))),
        ])])
        autodetector = MigrationAutodetector(before, args_changed)
        changes = autodetector._detect_changes()
        self.assertNumberMigrations(changes, 'testapp', 1)
        self.assertOperationTypes(changes, 'testapp', 0, ['AlterField'])
        # Can't use assertOperationFieldAttributes because we need the
        # deconstructed version, i.e., the exploded func/args/keywords rather
        # than the partial: we don't care if it's not the same instance of the
        # partial, only if it's the same source function, args, and keywords.
        value = changes['testapp'][0].operations[0].field.upload_to
        self.assertEqual(
            (_content_file_name, ('other-file',), {}),
            (value.func, value.args, value.keywords)
        )

        kwargs_changed = self.make_project_state([ModelState("testapp", "Author", [
            ("id", models.AutoField(primary_key=True)),
            ("file", models.FileField(max_length=200, upload_to=content_file_name('file', spam='eggs'))),
        ])])
        autodetector = MigrationAutodetector(before, kwargs_changed)
        changes = autodetector._detect_changes()
        self.assertNumberMigrations(changes, 'testapp', 1)
        self.assertOperationTypes(changes, 'testapp', 0, ['AlterField'])
        value = changes['testapp'][0].operations[0].field.upload_to
        self.assertEqual(
            (_content_file_name, ('file',), {'spam': 'eggs'}),
            (value.func, value.args, value.keywords)
        )

    @mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_not_null_alteration',
                side_effect=AssertionError("Should not have prompted for not null addition"))
    def test_alter_field_to_not_null_with_default(self, mocked_ask_method):