Refs #22608 -- Made app_label required when optimizing migrations.

This paved the way for the removal of lot of logic when app_label was
not specified.
This commit is contained in:
Simon Charette 2020-04-03 22:57:42 -04:00 committed by Mariusz Felisiak
parent 911545da1d
commit 25bf15c0da
6 changed files with 48 additions and 36 deletions

View File

@ -369,7 +369,7 @@ class MigrationAutodetector:
# Optimize migrations
for app_label, migrations in self.migrations.items():
for migration in migrations:
migration.operations = MigrationOptimizer().optimize(migration.operations, app_label=app_label)
migration.operations = MigrationOptimizer().optimize(migration.operations, app_label)
def check_dependency(self, operation, dependency):
"""

View File

@ -113,7 +113,7 @@ class Operation:
return router.allow_migrate_model(connection_alias, model)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
"""
Return either a list of operations the actual operation should be
replaced with or a boolean that indicates whether or not the specified

View File

@ -60,9 +60,9 @@ class FieldOperation(Operation):
return True
return False
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
return (
super().reduce(operation, app_label=app_label) or
super().reduce(operation, app_label) or
not operation.references_field(self.model_name, self.name, app_label)
)
@ -122,7 +122,7 @@ class AddField(FieldOperation):
def describe(self):
return "Add field %s to %s" % (self.name, self.model_name)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if isinstance(operation, FieldOperation) and self.is_same_field_operation(operation):
if isinstance(operation, AlterField):
return [
@ -142,7 +142,7 @@ class AddField(FieldOperation):
field=self.field,
),
]
return super().reduce(operation, app_label=app_label)
return super().reduce(operation, app_label)
class RemoveField(FieldOperation):
@ -186,11 +186,11 @@ class RemoveField(FieldOperation):
def describe(self):
return "Remove field %s from %s" % (self.name, self.model_name)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
from .models import DeleteModel
if isinstance(operation, DeleteModel) and operation.name_lower == self.model_name_lower:
return [operation]
return super().reduce(operation, app_label=app_label)
return super().reduce(operation, app_label)
class AlterField(FieldOperation):
@ -256,7 +256,7 @@ class AlterField(FieldOperation):
def describe(self):
return "Alter field %s on %s" % (self.name, self.model_name)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if isinstance(operation, RemoveField) and self.is_same_field_operation(operation):
return [operation]
elif isinstance(operation, RenameField) and self.is_same_field_operation(operation):
@ -268,7 +268,7 @@ class AlterField(FieldOperation):
field=self.field,
),
]
return super().reduce(operation, app_label=app_label)
return super().reduce(operation, app_label)
class RenameField(FieldOperation):
@ -382,7 +382,7 @@ class RenameField(FieldOperation):
name.lower() == self.new_name_lower
)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if (isinstance(operation, RenameField) and
self.is_same_model_operation(operation) and
self.new_name_lower == operation.old_name_lower):
@ -396,6 +396,6 @@ class RenameField(FieldOperation):
# Skip `FieldOperation.reduce` as we want to run `references_field`
# against self.new_name.
return (
super(FieldOperation, self).reduce(operation, app_label=app_label) or
super(FieldOperation, self).reduce(operation, app_label) or
not operation.references_field(self.model_name, self.new_name, app_label)
)

View File

@ -31,9 +31,9 @@ class ModelOperation(Operation):
def references_model(self, name, app_label=None):
return name.lower() == self.name_lower
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
return (
super().reduce(operation, app_label=app_label) or
super().reduce(operation, app_label) or
not operation.references_model(self.name, app_label)
)
@ -117,7 +117,7 @@ class CreateModel(ModelOperation):
return True
return False
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if (isinstance(operation, DeleteModel) and
self.name_lower == operation.name_lower and
not self.options.get("proxy", False)):
@ -236,7 +236,7 @@ class CreateModel(ModelOperation):
managers=self.managers,
),
]
return super().reduce(operation, app_label=app_label)
return super().reduce(operation, app_label)
class DeleteModel(ModelOperation):
@ -411,7 +411,7 @@ class RenameModel(ModelOperation):
def describe(self):
return "Rename model %s to %s" % (self.old_name, self.new_name)
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if (isinstance(operation, RenameModel) and
self.new_name_lower == operation.old_name_lower):
return [
@ -423,16 +423,16 @@ class RenameModel(ModelOperation):
# Skip `ModelOperation.reduce` as we want to run `references_model`
# against self.new_name.
return (
super(ModelOperation, self).reduce(operation, app_label=app_label) or
super(ModelOperation, self).reduce(operation, app_label) or
not operation.references_model(self.new_name, app_label)
)
class ModelOptionOperation(ModelOperation):
def reduce(self, operation, app_label=None):
def reduce(self, operation, app_label):
if isinstance(operation, (self.__class__, DeleteModel)) and self.name_lower == operation.name_lower:
return [operation]
return super().reduce(operation, app_label=app_label)
return super().reduce(operation, app_label)
class AlterModelTable(ModelOptionOperation):

View File

@ -9,7 +9,7 @@ class MigrationOptimizer:
nothing.
"""
def optimize(self, operations, app_label=None):
def optimize(self, operations, app_label):
"""
Main optimization entry point. Pass in a list of Operation instances,
get out a new list of Operation instances.
@ -25,11 +25,10 @@ class MigrationOptimizer:
The inner loop is run until the starting list is the same as the result
list, and then the result is returned. This means that operation
optimization must be stable and always return an equal or shorter list.
The app_label argument is optional, but if you pass it you'll get more
efficient optimization.
"""
# Internal tracking variable for test assertions about # of loops
if app_label is None:
raise TypeError('app_label must be a str.')
self._iterations = 0
while True:
result = self.optimize_inner(operations, app_label)
@ -38,7 +37,7 @@ class MigrationOptimizer:
return result
operations = result
def optimize_inner(self, operations, app_label=None):
def optimize_inner(self, operations, app_label):
"""Inner optimization loop."""
new_operations = []
for i, operation in enumerate(operations):

View File

@ -23,7 +23,7 @@ class OptimizerTests(SimpleTestCase):
return serializer_factory(value).serialize()[0]
def assertOptimizesTo(self, operations, expected, exact=None, less_than=None, app_label=None):
result, iterations = self.optimize(operations, app_label)
result, iterations = self.optimize(operations, app_label or 'migrations')
result = [self.serialize(f) for f in result]
expected = [self.serialize(f) for f in expected]
self.assertEqual(expected, result)
@ -39,6 +39,11 @@ class OptimizerTests(SimpleTestCase):
def assertDoesNotOptimize(self, operations, **kwargs):
self.assertOptimizesTo(operations, operations, **kwargs)
def test_none_app_label(self):
optimizer = MigrationOptimizer()
with self.assertRaisesMessage(TypeError, 'app_label must be a str'):
optimizer.optimize([], None)
def test_single(self):
"""
The optimizer does nothing on a single operation,
@ -212,16 +217,8 @@ class OptimizerTests(SimpleTestCase):
],
[],
)
# This should not work - FK should block it
self.assertDoesNotOptimize(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]),
migrations.DeleteModel("Foo"),
],
)
# The same operations should be optimized if app_label is specified and
# a FK references a model from the other app.
# Operations should be optimized if the FK references a model from the
# other app.
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
@ -235,6 +232,13 @@ class OptimizerTests(SimpleTestCase):
)
# But it shouldn't work if a FK references a model with the same
# app_label.
self.assertDoesNotOptimize(
[
migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]),
migrations.CreateModel('Bar', [('other', models.ForeignKey('Foo', models.CASCADE))]),
migrations.DeleteModel('Foo'),
],
)
self.assertDoesNotOptimize(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
@ -244,12 +248,20 @@ class OptimizerTests(SimpleTestCase):
app_label="testapp",
)
# This should not work - bases should block it
self.assertDoesNotOptimize(
[
migrations.CreateModel('Foo', [('name', models.CharField(max_length=255))]),
migrations.CreateModel('Bar', [('size', models.IntegerField())], bases=('Foo',)),
migrations.DeleteModel('Foo'),
],
)
self.assertDoesNotOptimize(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)),
migrations.DeleteModel("Foo"),
],
app_label='testapp',
)
# The same operations should be optimized if app_label and none of
# bases belong to that app.
@ -293,6 +305,7 @@ class OptimizerTests(SimpleTestCase):
('reviewer', models.ForeignKey('test_app.Reviewer', models.CASCADE)),
]),
],
app_label='test_app',
)
def test_create_model_add_field(self):