summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2013-06-07 17:56:43 +0100
committerAndrew Godwin <andrew@aeracode.org>2013-06-07 17:56:43 +0100
commit91c470def50c4de420b0c6ee7debddc5bbd53ec8 (patch)
tree61889b1f70bec7e187fdfec4b06a045fc7207e05 /django/db
parentcd809619a270ad48f1a77ee8c32bb0c7d8293f63 (diff)
Auto-naming for migrations and some writer fixes
Diffstat (limited to 'django/db')
-rw-r--r--django/db/migrations/autodetector.py91
-rw-r--r--django/db/migrations/graph.py16
-rw-r--r--django/db/migrations/writer.py12
3 files changed, 97 insertions, 22 deletions
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index ddb14520d3..be3e1c561f 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -1,3 +1,4 @@
+import re
from django.db.migrations import operations
from django.db.migrations.migration import Migration
@@ -11,7 +12,7 @@ class MigrationAutodetector(object):
Note that this naturally operates on entire projects at a time,
as it's likely that changes interact (for example, you can't
add a ForeignKey without having a migration to add the table it
- depends on first). A user interface may offer single-app detection
+ depends on first). A user interface may offer single-app usage
if it wishes, with the caveat that it may not always be possible.
"""
@@ -21,8 +22,12 @@ class MigrationAutodetector(object):
def changes(self):
"""
- Returns a set of migration plans which will achieve the
- change from from_state to to_state.
+ Returns a dict of migration plans which will achieve the
+ change from from_state to to_state. The dict has app labels
+ as kays and a list of migrations as values.
+
+ The resulting migrations aren't specially named, but the names
+ do matter for dependencies inside the set.
"""
# We'll store migrations as lists by app names for now
self.migrations = {}
@@ -53,17 +58,77 @@ class MigrationAutodetector(object):
for app_label, migrations in self.migrations.items():
for m1, m2 in zip(migrations, migrations[1:]):
m2.dependencies.append((app_label, m1.name))
- # Flatten and return
- result = set()
- for app_label, migrations in self.migrations.items():
- for migration in migrations:
- subclass = type("Migration", (Migration,), migration)
- instance = subclass(migration['name'], app_label)
- result.add(instance)
- return result
+ return self.migrations
def add_to_migration(self, app_label, operation):
migrations = self.migrations.setdefault(app_label, [])
if not migrations:
- migrations.append({"name": "auto_%i" % (len(migrations) + 1), "operations": [], "dependencies": []})
- migrations[-1]['operations'].append(operation)
+ subclass = type("Migration", (Migration,), {"operations": [], "dependencies": []})
+ instance = subclass("auto_%i" % (len(migrations) + 1), app_label)
+ migrations.append(instance)
+ migrations[-1].operations.append(operation)
+
+ @classmethod
+ def suggest_name(cls, ops):
+ """
+ Given a set of operations, suggests a name for the migration
+ they might represent. Names not guaranteed to be unique; they
+ must be prefixed by a number or date.
+ """
+ if len(ops) == 1:
+ if isinstance(ops[0], operations.CreateModel):
+ return ops[0].name.lower()
+ elif isinstance(ops[0], operations.DeleteModel):
+ return "delete_%s" % ops[0].name.lower()
+ elif all(isinstance(o, operations.CreateModel) for o in ops):
+ return "_".join(sorted(o.name.lower() for o in ops))
+ return "auto"
+
+ @classmethod
+ def parse_number(cls, name):
+ """
+ Given a migration name, tries to extract a number from the
+ beginning of it. If no number found, returns None.
+ """
+ if re.match(r"^\d+_", name):
+ return int(name.split("_")[0])
+ return None
+
+ @classmethod
+ def arrange_for_graph(cls, changes, graph):
+ """
+ Takes in a result from changes() and a MigrationGraph,
+ and fixes the names and dependencies of the changes so they
+ extend the graph from the leaf nodes for each app.
+ """
+ leaves = graph.leaf_nodes()
+ name_map = {}
+ for app_label, migrations in changes.items():
+ if not migrations:
+ continue
+ # Find the app label's current leaf node
+ app_leaf = None
+ for leaf in leaves:
+ if leaf[0] == app_label:
+ app_leaf = leaf
+ break
+ # Work out the next number in the sequence
+ if app_leaf is None:
+ next_number = 1
+ else:
+ next_number = (cls.parse_number(app_leaf[1]) or 0) + 1
+ # Name each migration
+ for i, migration in enumerate(migrations):
+ if i == 0 and app_leaf:
+ migration.dependencies.append(app_leaf)
+ if i == 0 and not app_leaf:
+ new_name = "0001_initial"
+ else:
+ new_name = "%04i_%s" % (next_number, cls.suggest_name(migration.operations))
+ name_map[(app_label, migration.name)] = (app_label, new_name)
+ migration.name = new_name
+ # Now fix dependencies
+ for app_label, migrations in changes.items():
+ for migration in migrations:
+ migration.dependencies = [name_map.get(d, d) for d in migration.dependencies]
+ return changes
diff --git a/django/db/migrations/graph.py b/django/db/migrations/graph.py
index 620534bc22..c1c3ba75bb 100644
--- a/django/db/migrations/graph.py
+++ b/django/db/migrations/graph.py
@@ -120,14 +120,20 @@ class MigrationGraph(object):
def __str__(self):
return "Graph: %s nodes, %s edges" % (len(self.nodes), sum(len(x) for x in self.dependencies.values()))
- def project_state(self, node, at_end=True):
+ def project_state(self, nodes, at_end=True):
"""
- Given a migration node, returns a complete ProjectState for it.
+ Given a migration node or nodes, returns a complete ProjectState for it.
If at_end is False, returns the state before the migration has run.
"""
- plan = self.forwards_plan(node)
- if not at_end:
- plan = plan[:-1]
+ if not isinstance(nodes[0], tuple):
+ nodes = [nodes]
+ plan = []
+ for node in nodes:
+ for migration in self.forwards_plan(node):
+ if migration not in plan:
+ if not at_end and migration in nodes:
+ continue
+ plan.append(migration)
project_state = ProjectState()
for node in plan:
project_state = self.nodes[node].mutate_state(project_state)
diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py
index b21c6c9648..f386cd847c 100644
--- a/django/db/migrations/writer.py
+++ b/django/db/migrations/writer.py
@@ -1,5 +1,7 @@
+from __future__ import unicode_literals
import datetime
import types
+from django.utils import six
from django.db import models
@@ -36,11 +38,12 @@ class MigrationWriter(object):
operation_strings.append("migrations.%s(%s\n )" % (name, "".join("\n %s," % arg for arg in arg_strings)))
items["operations"] = "[%s\n ]" % "".join("\n %s," % s for s in operation_strings)
# Format imports nicely
+ imports.discard("from django.db import models")
if not imports:
items["imports"] = ""
else:
items["imports"] = "\n".join(imports) + "\n"
- return MIGRATION_TEMPLATE % items
+ return (MIGRATION_TEMPLATE % items).encode("utf8")
@property
def filename(self):
@@ -84,16 +87,17 @@ class MigrationWriter(object):
elif isinstance(value, (datetime.datetime, datetime.date)):
return repr(value), set(["import datetime"])
# Simple types
- elif isinstance(value, (int, long, float, str, unicode, bool, types.NoneType)):
+ elif isinstance(value, (int, long, float, six.binary_type, six.text_type, bool, types.NoneType)):
return repr(value), set()
# Django fields
elif isinstance(value, models.Field):
attr_name, path, args, kwargs = value.deconstruct()
module, name = path.rsplit(".", 1)
if module == "django.db.models":
- imports = set()
+ imports = set(["from django.db import models"])
+ name = "models.%s" % name
else:
- imports = set("import %s" % module)
+ imports = set(["import %s" % module])
name = path
arg_strings = []
for arg in args: