Loading django/db/migrations/autodetector.py +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 Loading Loading @@ -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): Loading docs/releases/1.9.6.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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`). tests/migrations/test_autodetector.py +54 −0 Original line number Diff line number Diff line # -*- coding: utf-8 -*- import functools import re from django.apps import apps Loading Loading @@ -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): Loading Loading
django/db/migrations/autodetector.py +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 Loading Loading @@ -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): Loading
docs/releases/1.9.6.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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`).
tests/migrations/test_autodetector.py +54 −0 Original line number Diff line number Diff line # -*- coding: utf-8 -*- import functools import re from django.apps import apps Loading Loading @@ -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): Loading