DeadWisdom / Migratory
Migratory (pronounced as a pythoner: 'MY GRAY ER EE') is a migration contrib app for Django. It both generates custom migration files for you and is a system for executing them. What's more, it works great in a vcs environment.
$ hg clone http://bitbucket.org/DeadWisdom/migratory/
| commit 10: | f03bc3704c69 |
| parent 9: | ae147f5f3569 |
| branch: | default |
Changed (Δ5.9 KB):
migratory/management/commands/migrate.py (164 lines added, 111 lines removed)
migratory/manager/base.py (111 lines added, 167 lines removed)
migratory/migrate.py (15 lines added, 57 lines removed)
migratory/models.py (2 lines added, 2 lines removed)
migratory/snapshot.py (4 lines added, 6 lines removed)
test_project/app/models.py (14 lines added, 7 lines removed)
test_project/test (3 lines added, 3 lines removed)
Up to file-list migratory/management/commands/migrate.py:
| … | … | @@ -5,11 +5,12 @@ Automation of the creation of migration |
5 |
5 |
import os, os.path |
6 |
6 |
from datetime import date |
7 |
7 |
|
8 |
from django. |
|
8 |
from django.utils import simplejson |
|
9 |
from django.db.models.loading import get_models, get_app |
|
9 |
10 |
from django.core.management.base import BaseCommand, CommandError |
10 |
11 |
from django.core.management.color import no_style |
11 |
12 |
|
12 |
from django.contrib.migratory |
|
13 |
from django.contrib.migratory.app import AppManager |
|
13 |
14 |
from django.contrib.migratory.matchmaker import matchmaker, compare_strings |
14 |
15 |
|
15 |
16 |
class Command(BaseCommand): |
| … | … | @@ -17,30 +18,28 @@ class Command(BaseCommand): |
17 |
18 |
args = '<app-name migration-name>' |
18 |
19 |
|
19 |
20 |
def handle(self, *args, **options): |
21 |
self.verbosity = options.get('verbosity', 1) |
|
22 |
||
20 |
23 |
try: |
21 |
24 |
app_name, mig_name = args |
22 |
25 |
except: |
23 |
26 |
raise CommandError("Specify the name of an app, and a slug-formatted name for the migration.") |
24 |
27 |
|
25 |
self. |
|
28 |
self.app = get_app(app_name) |
|
26 |
29 |
|
27 |
try: |
|
28 |
if app_name not in self.manager: |
|
29 |
raise CommandError("Unable to find previous snapshot of app %r." % app_name) |
|
30 |
except: |
|
31 |
# No snapshots yet, so we kick out. |
|
32 |
return |
|
30 |
last = AppManager('last') |
|
31 |
old_models = last.get_models(app_name) |
|
33 |
32 |
|
34 |
app = get_new_app(app_name) |
|
35 |
new_models = get_new_models(app) |
|
36 |
|
|
33 |
current = AppManager('current') |
|
34 |
new_models = current.get_models(app_name) |
|
37 |
35 |
|
38 |
36 |
# The order of old_models then new_models is important here, as |
39 |
37 |
# compareModels weights so that all the models in old are also in new. |
40 |
38 |
# If new has a bunch of new ones, it still makes a perfect match. |
41 |
39 |
matches, drop, add = matchmaker(old_models, new_models, compareModels, .4) |
42 |
40 |
|
43 |
|
|
41 |
### Build The Changeset ### |
|
42 |
changeset = Changeset(self.app) |
|
44 |
43 |
|
45 |
44 |
for model in drop: |
46 |
45 |
changeset.drop_model(model) |
| … | … | @@ -70,8 +69,8 @@ class Command(BaseCommand): |
70 |
69 |
changeset.add_field(field) |
71 |
70 |
|
72 |
71 |
for old, new in fields: |
73 |
old_snap = snapshot.field_snap(old, old_model) |
|
74 |
new_snap = snapshot.field_snap(new, new_model) |
|
72 |
old_snap = AppManager.field_snap(old_model, old) |
|
73 |
new_snap = AppManager.field_snap(new_model, new) |
|
75 |
74 |
|
76 |
75 |
# Strip the 'migration-' prefix for the app on the |
77 |
76 |
# related-field. |
| … | … | @@ -95,14 +94,114 @@ class Command(BaseCommand): |
95 |
94 |
filename = "%d-%d-%d-%s.py" % ( |
96 |
95 |
today.year, today.month, today.day, mig_name |
97 |
96 |
) |
97 |
||
98 |
if (self.verbosity > 0): |
|
99 |
print changeset.report() |
|
100 |
||
101 |
self.save(filename, changeset) |
|
102 |
||
103 |
if (self.verbosity > 0 and changeset): |
|
104 |
print "\nPlease look over the changes. When you feel confident they are correct, issue a 'syncdb':\n> python manage.py syncdb" |
|
105 |
||
106 |
def save(self, filename, changeset, comment = ''): |
|
107 |
""" |
|
108 |
Saves the changeset to a new migration file, creates the snapshot |
|
109 |
file, and appends the migration to the manifest (__manifest__.py). |
|
110 |
""" |
|
111 |
path = os.path.join(self.get_migrations_dir(), filename) |
|
112 |
if os.path.exists(path): |
|
113 |
raise CommandError("Migration with that name (%r) already exists." % path) |
|
114 |
||
115 |
if (self.verbosity > 0): |
|
116 |
print "Writing migration to: %s" % path |
|
98 |
117 |
|
99 |
|
|
118 |
o = open(path, 'w') |
|
119 |
if path.endswith('.py'): |
|
120 |
o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % \ |
|
121 |
(filename, comment)) |
|
122 |
o.write(changeset.up()) |
|
123 |
o.write('\n') |
|
124 |
o.write(changeset.down()) |
|
125 |
o.close() |
|
126 |
||
127 |
self.update_manifest(filename, comment) |
|
128 |
||
129 |
if path.endswith('.py'): |
|
130 |
self.save_snapshot(filename) |
|
100 |
131 |
|
101 |
|
|
132 |
def save_snapshot(self, filename): |
|
133 |
manager = AppManager('current') |
|
134 |
json = simplejson.dumps(manager.get_snapshot()) |
|
102 |
135 |
|
103 |
if (changeset): |
|
104 |
print "\nPlease look over the changes. When you feel confident they are correct, issue a 'syncdb':\n> python manage.py syncdb" |
|
136 |
# Convert to '2009-6-16-added-thing.snap' for instance: |
|
137 |
filename = filename.rsplit('.', 1)[0] + '.snap' |
|
138 |
path = os.path.join(self.get_migrations_dir(), filename) |
|
139 |
||
140 |
o = open(path, 'w') |
|
141 |
o.write(json) |
|
142 |
o.close() |
|
143 |
||
144 |
def get_migrations_dir(self): |
|
145 |
""" |
|
146 |
Gets or creates the migration directory on the filesystem. |
|
147 |
""" |
|
148 |
path = os.path.join(os.path.dirname(self.app.__file__), 'migrations') |
|
149 |
if not os.path.isdir(path): |
|
150 |
os.mkdir(path) |
|
151 |
return path |
|
152 |
||
153 |
def update_manifest(self, filename, comment=''): |
|
154 |
path, src = self.get_manifest() |
|
155 |
manifests = eval(src) |
|
156 |
if (filename in manifests): |
|
157 |
raise CommandError("A migration with that name (%r) is already present in the manifest." % filename) |
|
158 |
||
159 |
lines = src.split('\n') |
|
160 |
||
161 |
spaces = ' ' |
|
162 |
if len(lines) > 2: |
|
163 |
# Get the padding of the previous line. |
|
164 |
prev = lines[-2] |
|
165 |
spaces = prev[: len( prev ) - len( prev.lstrip() ) ] |
|
166 |
||
167 |
line = spaces + repr(filename) + ',' |
|
168 |
||
169 |
if (comment): |
|
170 |
count = len(line) |
|
171 |
if '\t' in spaces: |
|
172 |
for c in spaces: |
|
173 |
if c == '\t': count += 3 |
|
174 |
line += (50 - count) * ' ' + '# ' + comment |
|
175 |
||
176 |
lines.insert(-1, line) |
|
177 |
||
178 |
if (self.verbosity > 1): |
|
179 |
print "Adding %r to manifest..." % filename |
|
180 |
||
181 |
o = open(path, 'w') |
|
182 |
o.write( "\n".join(lines) ) |
|
183 |
o.close() |
|
184 |
||
185 |
def get_manifest(self): |
|
186 |
""" |
|
187 |
get_manifest() -> (path, src) |
|
188 |
||
189 |
Returns the manifest (__manifest__.py) path and source. |
|
190 |
""" |
|
191 |
migrations_dir = self.get_migrations_dir() |
|
192 |
path = os.path.join(migrations_dir, '__manifest__.py') |
|
105 |
193 |
|
194 |
if not os.path.exists(path): |
|
195 |
src = '[\n]' |
|
196 |
o = open(path, 'w') |
|
197 |
o.write(src) |
|
198 |
o.close() |
|
199 |
else: |
|
200 |
o = open(path, 'r') |
|
201 |
src = o.read() |
|
202 |
o.close() |
|
203 |
||
204 |
return path, src |
|
106 |
205 |
|
107 |
206 |
### Helper Functions / Classes ### |
108 |
207 |
def compareModels(a, b): |
| … | … | @@ -150,13 +249,15 @@ class Changeset(object): |
150 |
249 |
def drop_model(self, model): |
151 |
250 |
self.changes.append(( |
152 |
251 |
"Drop the model %r." % model._meta.object_name, |
153 |
"database.drop_model(%r)" % model._meta.object_name |
|
252 |
"database.drop_model(%r)" % model._meta.object_name, |
|
253 |
"database.add_model(%r)" % model._meta.object_name |
|
154 |
254 |
)) |
155 |
255 |
|
156 |
256 |
def add_model(self, model): |
157 |
257 |
self.changes.append(( |
158 |
258 |
"Add the new model %r." % model._meta.object_name, |
159 |
"database.add_model(%r)" % model._meta.object_name |
|
259 |
"database.add_model(%r)" % model._meta.object_name, |
|
260 |
"database.drop_model(%r)" % model._meta.object_name |
|
160 |
261 |
)) |
161 |
262 |
|
162 |
263 |
def change_model(self, model): |
| … | … | @@ -177,14 +278,17 @@ class Changeset(object): |
177 |
278 |
if (model_changes): |
178 |
279 |
self.changes.append(( |
179 |
280 |
"Changes to the model %r:" % self.model._meta.object_name, |
180 |
model_changes |
|
281 |
model_changes, |
|
282 |
None |
|
181 |
283 |
)) |
182 |
284 |
|
183 |
285 |
def rename_model(self, new): |
184 |
286 |
self.changes.append(( |
185 |
287 |
"Rename to %r." % new._meta.object_name, |
186 |
288 |
"database.rename_model(%r, %r)" % ( |
187 |
self.model._meta.object_name, new._meta.object_name) |
|
289 |
self.model._meta.object_name, new._meta.object_name), |
|
290 |
"database.rename_model(%r, %r)" % ( |
|
291 |
new._meta.object_name, self.model._meta.object_name), |
|
188 |
292 |
)) |
189 |
293 |
self.model._meta.object_name = new._meta.object_name |
190 |
294 |
|
| … | … | @@ -192,40 +296,48 @@ class Changeset(object): |
192 |
296 |
self.changes.append(( |
193 |
297 |
"Drop the field %r." % field.name, |
194 |
298 |
"database.drop_field(%r, %r)" % ( |
195 |
self.model._meta.object_name, field.name) |
|
299 |
self.model._meta.object_name, field.name), |
|
300 |
"database.add_field(%r, %r)" % ( |
|
301 |
self.model._meta.object_name, field.name), |
|
196 |
302 |
)) |
197 |
303 |
|
198 |
304 |
def add_field(self, field): |
199 |
305 |
self.changes.append(( |
200 |
306 |
"Add the new field %r." % field.name, |
201 |
307 |
"database.add_field(%r, %r)" % ( |
202 |
self.model._meta.object_name, field.name) |
|
308 |
self.model._meta.object_name, field.name), |
|
309 |
"database.drop_field(%r, %r)" % ( |
|
310 |
self.model._meta.object_name, field.name), |
|
203 |
311 |
)) |
204 |
||
312 |
||
205 |
313 |
def change_field(self, old, new=None): |
206 |
314 |
if (new): |
207 |
315 |
self.changes.append(( |
208 |
316 |
"Change to %r, renaming it %r." % (old.name, new.name), |
209 |
317 |
"database.change_field(%r, %r, %r)" % ( |
210 |
self.model._meta.object_name, old.name, new.name) |
|
318 |
self.model._meta.object_name, old.name, new.name), |
|
319 |
"database.change_field(%r, %r, %r)" % ( |
|
320 |
self.model._meta.object_name, new.name, old.name) |
|
211 |
321 |
)) |
212 |
322 |
else: |
213 |
323 |
self.changes.append(( |
214 |
324 |
"Change the field %r." % old.name, |
215 |
325 |
"database.change_field(%r, %r)" % ( |
326 |
self.model._meta.object_name, old.name), |
|
327 |
"database.change_field(%r, %r)" % ( |
|
216 |
328 |
self.model._meta.object_name, old.name) |
217 |
329 |
)) |
218 |
||
330 |
||
219 |
331 |
def report(self): |
220 |
332 |
""" |
221 |
333 |
Returns a natural language report of the changes as a big string. |
222 |
334 |
""" |
223 |
335 |
gather = [] |
224 |
336 |
|
225 |
for report, |
|
337 |
for report, up, _ in self.changes: |
|
226 |
338 |
gather.append(report) |
227 |
if not isinstance(sub, basestring): |
|
228 |
for report, _ in sub: |
|
339 |
if not isinstance(up, basestring): |
|
340 |
for report, _, _ in up: |
|
229 |
341 |
gather.append(' ' + report) |
230 |
342 |
|
231 |
343 |
if not self.changes: |
| … | … | @@ -240,91 +352,32 @@ class Changeset(object): |
240 |
352 |
""" |
241 |
353 |
gather = [] |
242 |
354 |
|
243 |
for _, code in self.changes: |
|
244 |
if isinstance(code, basestring): |
|
245 |
|
|
355 |
for _, up, down in self.changes: |
|
356 |
if isinstance(up, basestring): |
|
357 |
gather.append(up) |
|
246 |
358 |
else: |
247 |
for _, sub in code: |
|
248 |
gather.append(sub) |
|
359 |
for _, up, down in up: |
|
360 |
gather.append(up) |
|
249 |
361 |
|
250 |
362 |
if not self.changes: |
251 |
363 |
gather.append('pass') |
252 |
364 |
|
253 |
return "def up(database):\n %s\n" % '\n '.join(gather) |
|
365 |
return "def up(database):\n %s\n" % '\n '.join(gather) |
|
366 |
||
367 |
def down(self): |
|
368 |
""" |
|
369 |
Produces the python source-code for the down() function. |
|
370 |
""" |
|
371 |
gather = [] |
|
254 |
372 |
|
255 |
def get_migrations_dir(self): |
|
256 |
""" |
|
257 |
Gets or creates the migration directory on the filesystem. |
|
258 |
""" |
|
259 |
path = os.path.join(os.path.dirname(self.app.__file__), 'migrations') |
|
260 |
if not os.path.isdir(path): |
|
261 |
os.mkdir(path) |
|
262 |
return path |
|
373 |
for _, up, down in self.changes: |
|
374 |
if isinstance(up, basestring): |
|
375 |
gather.append(down) |
|
376 |
else: |
|
377 |
for _, up, down in up: |
|
378 |
gather.append(down) |
|
263 |
379 |
|
264 |
def get_manifest(self): |
|
265 |
""" |
|
266 |
|
|
380 |
if not self.changes: |
|
381 |
gather.append('pass') |
|
267 |
382 |
|
268 |
Returns the manifest (__manifest__.py) path and source. |
|
269 |
""" |
|
270 |
migrations_dir = self.get_migrations_dir() |
|
271 |
path = os.path.join(migrations_dir, '__manifest__.py') |
|
272 |
||
273 |
if not os.path.exists(path): |
|
274 |
src = '[\n]' |
|
275 |
o = open(path, 'w') |
|
276 |
o.write(src) |
|
277 |
o.close() |
|
278 |
else: |
|
279 |
o = open(path, 'r') |
|
280 |
src = o.read() |
|
281 |
o.close() |
|
282 |
||
283 |
return path, src |
|
284 |
||
285 |
def save(self, filename, comment = ''): |
|
286 |
""" |
|
287 |
Saves the changeset to a new migration file, and appends to the |
|
288 |
manifest (__manifest__.py). |
|
289 |
""" |
|
290 |
path, src = self.get_manifest() |
|
291 |
manifests = eval(src) |
|
292 |
if (filename in manifests): |
|
293 |
raise CommandError("A migration with that name (%r) is already present in the manifest." % filename) |
|
294 |
||
295 |
lines = src.split('\n') |
|
296 |
||
297 |
spaces = ' ' |
|
298 |
if len(lines) > 2: |
|
299 |
# Get the padding of the previous line. |
|
300 |
prev = lines[-2] |
|
301 |
spaces = prev[: len( prev ) - len( prev.lstrip() ) ] |
|
302 |
||
303 |
line = spaces + repr(filename) + ',' |
|
304 |
||
305 |
if (comment): |
|
306 |
count = len(line) |
|
307 |
if '\t' in spaces: |
|
308 |
for c in spaces: |
|
309 |
if c == '\t': count += 3 |
|
310 |
line += (50 - count) * ' ' + '# ' + comment |
|
311 |
||
312 |
lines.insert(-1, line) |
|
313 |
||
314 |
print "Adding %r to manifest..." % filename |
|
315 |
o = open(path, 'w') |
|
316 |
o.write( "\n".join(lines) ) |
|
317 |
o.close() |
|
318 |
||
319 |
path = os.path.join(os.path.dirname(path), filename) |
|
320 |
if os.path.exists(path): |
|
321 |
print "Migration already exists: %s" % path |
|
322 |
return |
|
323 |
||
324 |
print "Writing migration to: %s" % path |
|
325 |
o = open(path, 'w') |
|
326 |
if path.endswith('.py'): |
|
327 |
o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % \ |
|
328 |
(filename, comment)) |
|
329 |
o.write(self.up()) |
|
330 |
|
|
383 |
return "def down(database):\n %s\n" % '\n '.join(gather) |
Up to file-list migratory/manager/base.py:
| … | … | @@ -6,7 +6,9 @@ specified in settings.DATABASE_ENGINE. |
6 |
6 |
DatabaseManagerBase. |
7 |
7 |
|
8 |
8 |
""" |
9 |
import os |
|
9 |
10 |
|
11 |
from django.core.management.base import CommandError |
|
10 |
12 |
from django.core.management.color import no_style |
11 |
13 |
from django.utils import simplejson |
12 |
14 |
from django.db import models |
| … | … | @@ -14,6 +16,7 @@ from django.db import connection |
14 |
16 |
from django.core.exceptions import ImproperlyConfigured |
15 |
17 |
|
16 |
18 |
from django.contrib.migratory.models import Migration, Snapshot |
19 |
from django.contrib.migratory.app import AppManager |
|
17 |
20 |
|
18 |
21 |
# Are we allowed to do this as a module level? I am for now. |
19 |
22 |
qn = connection.ops.quote_name |
| … | … | @@ -34,6 +37,7 @@ class DatabaseManagerBase(object): |
34 |
37 |
verbosity=0, |
35 |
38 |
mode='execute', |
36 |
39 |
style=None, |
40 |
new_snap=[], |
|
37 |
41 |
created_models=[]): |
38 |
42 |
""" |
39 |
43 |
app_name: |
| … | … | @@ -64,151 +68,77 @@ class DatabaseManagerBase(object): |
64 |
68 |
self.style = style or no_style() |
65 |
69 |
self.created_models = created_models |
66 |
70 |
|
71 |
self.now = None |
|
72 |
self.ideal = None |
|
73 |
||
67 |
74 |
self._apps = {} |
68 |
75 |
self._required = set() |
69 |
76 |
self._rel_fields = [] |
70 |
||
71 |
def parse_sig(self, sig): |
|
72 |
"""Turns 'app.Model' to ('app', 'model').""" |
|
73 |
if '.' not in sig: |
|
74 |
if self.app_name: |
|
75 |
return self.app_name, sig |
|
76 |
else: |
|
77 |
raise KeyError("Must specify the app_name, like: app.model, unless you set it on the DatabaseManager constructor.") |
|
77 |
||
78 |
def run_migration(self, path): |
|
79 |
if not os.path.exists(path): |
|
80 |
raise CommandError( |
|
81 |
"Unable to find migration at the given path: %r" % path) |
|
82 |
||
83 |
if (self.verbosity >= 1 and self.mode != 'sql'): |
|
84 |
print "Executing migration %r..." % name |
|
85 |
||
86 |
### Build Snapshots ### |
|
87 |
snap_path = path.rsplit('.', 1)[0] + '.snap' |
|
88 |
||
89 |
if os.path.exists(snap_path): |
|
90 |
o = open(snap_path) |
|
91 |
src = o.read() |
|
92 |
o.close() |
|
93 |
||
94 |
self.ideal = AppManager(simplejson.loads(src), prefix='ideal-') |
|
78 |
95 |
else: |
79 |
|
|
96 |
if (self.verbosity >= 1 and self.mode != 'sql'): |
|
97 |
print "No snapshot found for migration %r, defaulting to current models..." % name |
|
98 |
self.ideal = AppManager('current', prefix='ideal-') |
|
99 |
||
100 |
self.now = AppManager('last', prefix='now-') |
|
101 |
||
102 |
### Get Migration Up Function ### |
|
103 |
o = open(path) |
|
104 |
src = o.read() |
|
105 |
o.close() |
|
106 |
||
107 |
namespace = {} |
|
108 |
exec src in namespace |
|
109 |
||
110 |
up = namespace.get('up', None) |
|
111 |
if (not up): |
|
112 |
raise CommandError("No up() function found in migration: %s\nWe really need that." % path) |
|
113 |
||
114 |
### Run Migration ### |
|
115 |
try: |
|
116 |
up(self) |
|
117 |
except Exception, e: |
|
118 |
import traceback, sys |
|
80 |
119 |
|
81 |
def __getitem__(self, sig): |
|
82 |
"""Return the model matching the sig from the latest snapshot.""" |
|
83 |
|
|
120 |
lines = ["\nError in migration:\n"] |
|
121 |
lines.extend(traceback.format_tb(sys.exc_traceback)[1:]) |
|
122 |
lines.append("%s: %s" % (e.__class__.__name__, str(e))) |
|
84 |
123 |
|
85 |
def get_model(self, app_name, model_name): |
|
86 |
"""Return the request model from the latest snapshot.""" |
|
87 |
if app_name not in self._apps: |
|
88 |
self.build_app(app_name) |
|
89 |
self.build_rel_fields() |
|
90 |
try: |
|
91 |
return self._apps[app_name][model_name] |
|
92 |
except: |
|
93 |
|
|
124 |
lines.append("\n\nLocals:\n") |
|
125 |
tb = sys.exc_info()[2] |
|
126 |
while tb.tb_next: |
|
127 |
tb = tb.tb_next |
|
128 |
frame = tb.tb_frame |
|
129 |
for key, value in frame.f_locals.items(): |
|
130 |
try: |
|
131 |
value = "%r" % value |
|
132 |
except: |
|
133 |
value = '<not representable>' |
|
134 |
lines.append("\t%s: %s\n" % (key,value)) |
|
135 |
||
136 |
raise CommandError("".join(lines)) |
|
94 |
137 |
|
95 |
def get_models(self, app_name=None): |
|
96 |
"""Return an iterator of models in the latest snapshot.""" |
|
97 |
if app_name not in self._apps: |
|
98 |
self.build_app(app_name) |
|
99 |
self.build_rel_fields() |
|
100 |
if (app_name): |
|
101 |
return iter( self._apps[app_name].values() ) |
|
102 |
else: |
|
103 |
for app, models in self._apps.items(): |
|
104 |
return iter( models.values() ) |
|
105 |
||
106 |
def __contains__(self, app_name): |
|
107 |
"""Does the latest snapshot include this app?""" |
|
108 |
return Snapshot.objects.filter(app=app_name).count() > 0 |
|
109 |
||
110 |
def build_app(self, app_name): |
|
111 |
""" |
|
112 |
Build a dict of models for the given application name from the latest |
|
113 |
snapshot |
|
114 |
""" |
|
115 |
if (app_name in self._apps): |
|
116 |
return |
|
117 |
try: |
|
118 |
snap = Snapshot.objects.get(app=app_name) |
|
119 |
except Snapshot.DoesNotExist: |
|
120 |
try: |
|
121 |
models.get_app(app) |
|
122 |
self._dicts[app_name] = {} # New app, allow it. |
|
123 |
return |
|
124 |
except ImproperlyConfigured: |
|
125 |
raise RuntimeError("Cannot find snapshot of an app named %r." % app_name) |
|
126 |
data = simplejson.loads(snap.json) |
|
127 |
self._apps[app_name] = {} |
|
128 |
for dct in data: |
|
129 |
self.build_model(app_name, dct) |
|
130 |
||
131 |
def build_model(self, app_name, dct): |
|
132 |
""" |
|
133 |
Builds a model with the given app_name and snapshot-dict. |
|
134 |
""" |
|
135 |
attrs = {} |
|
136 |
app_name = app_name.encode() |
|
138 |
def set_app_managers(self, now, ideal): |
|
139 |
self.now = now |
|
140 |
self.ideal = ideal |
|
137 |
141 |
|
138 |
attrs.update( self.build_fields(app_name, dct['name'], dct['meta'], dct['fields']) ) |
|
139 |
attrs['Meta'] = type('Meta', (), dct['meta']) |
|
140 |
attrs['Meta'].app_label = 'migration-' + app_name |
|
141 |
attrs['__module__'] = dct['module'].encode() |
|
142 |
||
143 |
model = type(str(dct['name'].encode()), (models.Model,), attrs) |
|
144 |
self._apps[app_name][model._meta.object_name] = model |
|
145 |
||
146 |
def build_fields(self, app_name, model_name, model_meta, dicts): |
|
147 |
""" |
|
148 |
Builds a field for each dict given. |
|
149 |
||
150 |
This will also populate self._rel_fields with any related fields, and |
|
151 |
self._required with required apps. Only after all the required apps |
|
152 |
and models are built will the relative fields be added with |
|
153 |
self.build_rel_fields() below. |
|
154 |
""" |
|
155 |
gather = {} |
|
156 |
||
157 |
for dct in dicts: |
|
158 |
name = dct['name'].encode() |
|
159 |
cls = getattr(models, dct['__class__']) |
|
160 |
attrs = {} |
|
161 |
for k, v in dct.items(): |
|
162 |
if not k.startswith('_'): attrs[str(k)] = v |
|
163 |
||
164 |
if ('__rel__' in dct): |
|
165 |
to_app_name, to_model_name = dct['__rel__'] |
|
166 |
if (to_app_name not in self._apps): |
|
167 |
self._required.add( to_app_name ) |
|
168 |
if model_meta.get('order_with_respect_to') == name: |
|
169 |
del model_meta['order_with_respect_to'] |
|
170 |
attrs['__order_with_respect_to'] = True |
|
171 |
self._rel_fields.append( (app_name, |
|
172 |
model_name, |
|
173 |
cls, |
|
174 |
name, |
|
175 |
to_app_name, |
|
176 |
to_model_name, |
|
177 |
attrs) ) |
|
178 |
else: |
|
179 |
gather[name] = cls(**attrs) |
|
180 |
||
181 |
return gather |
|
182 |
||
183 |
def build_rel_fields(self): |
|
184 |
""" |
|
185 |
Only once all the required apps and models are built, do we |
|
186 |
actually add the relative fields. |
|
187 |
""" |
|
188 |
while self._required: |
|
189 |
self.build_app( self._required.pop() ) |
|
190 |
||
191 |
for (app_name, |
|
192 |
model_name, |
|
193 |
cls, |
|
194 |
name, |
|
195 |
to_app_name, |
|
196 |
to_model_name, |
|
197 |
attrs) in self._rel_fields: |
|
198 |
||
199 |
order = attrs.pop('__order_with_respect_to', False) |
|
200 |
||
201 |
model = self.get_model(app_name, model_name) |
|
202 |
to_model = self.get_model(to_app_name, to_model_name) |
|
203 |
field = cls(to_model, **attrs) |
|
204 |
model.add_to_class(name, field) |
|
205 |
||
206 |
if order: |
|
207 |
model._meta.order_with_respect_to = field.name |
|
208 |
model._meta._prepare(model) |
|
209 |
||
210 |
self._rel_fields = [] |
|
211 |
||
212 |
142 |
def _field_sql(self, model, field): |
213 |
143 |
""" |
214 |
144 |
Generate the sql needed to create a field. |
| … | … | @@ -448,6 +378,19 @@ class DatabaseManagerBase(object): |
448 |
378 |
|
449 |
379 |
return " ".join(sql) |
450 |
380 |
|
381 |
def __getitem__(self, sig): |
|
382 |
return self.new.get_model(*self.parse_sig(sig)) |
|
383 |
||
384 |
def parse_sig(self, sig): |
|
385 |
"""Turns 'app.Model' to ('app', 'model').""" |
|
386 |
if '.' not in sig: |
|
387 |
if self.app_name: |
|
388 |
return self.app_name, sig |
|
389 |
else: |
|
390 |
raise KeyError("Must specify the app_name, like: app.model.") |
|
391 |
else: |
|
392 |
return sig.split('.') |
|
393 |
||
451 |
394 |
def execute(self, description, *sql_statements): |
452 |
395 |
""" |
453 |
396 |
Executes the sql statements and prints the description to the user, |
| … | … | @@ -496,21 +439,15 @@ class DatabaseManagerBase(object): |
496 |
439 |
def add_field(self, model_sig, name): |
497 |
440 |
""" |
498 |
441 |
Add the new field named 'name' to the model. |
499 |
||
500 |
The field should be defined in the current models.py |
|
501 |
||
502 |
TODO: This won't work, because we might be in a long-ago migration |
|
503 |
refering to models.py that doesn't have the field, or model for that |
|
504 |
matter. Big problem. |
|
505 |
442 |
""" |
506 |
443 |
style = self.style |
507 |
444 |
|
508 |
445 |
app_name, model_name = self.parse_sig(model_sig) |
509 |
model = self.get_model(app_name, model_name) |
|
510 |
real_model = models.get_model(app_name, model_name) |
|
446 |
||
447 |
model = self.now.get_model(app_name, model_name) |
|
511 |
448 |
opts = model._meta |
512 |
449 |
|
513 |
field = |
|
450 |
field = self.ideal.get_field(app_name, model_name, name) |
|
514 |
451 |
|
515 |
452 |
if field.db_type() is None: |
516 |
453 |
self.execute("Adding many-to-many table for field %s.%s" % (model_sig, field.name), self._m2m_sql(model, field)) |
| … | … | @@ -559,15 +496,11 @@ class DatabaseManagerBase(object): |
559 |
496 |
new_field_name = old_field_name |
560 |
497 |
|
561 |
498 |
app_name, model_name = self.parse_sig(model_sig) |
562 |
model = self. |
|
499 |
model = self.now.get_model(app_name, model_name) |
|
563 |
500 |
opts = model._meta |
564 |
501 |
|
565 |
502 |
old = opts.get_field(old_field_name, True) |
566 |
||
567 |
real_model = models.get_model(app_name, model_name) |
|
568 |
real_opts = model._meta |
|
569 |
||
570 |
field = |
|
503 |
field = self.ideal.get_field(app_name, model_name, new_field_name) |
|
571 |
504 |
|
572 |
505 |
if (field.db_type() is None): # Many to Many |
573 |
506 |
opts.local_many_to_many = [f for f in opts.local_many_to_many if f != old] |
| … | … | @@ -645,7 +578,7 @@ class DatabaseManagerBase(object): |
645 |
578 |
|
646 |
579 |
Unless it shows up in self.created_models (see __init__). |
647 |
580 |
""" |
648 |
model = |
|
581 |
model = self.ideal.get_model(*self.parse_sig(model_sig)) |
|
649 |
582 |
|
650 |
583 |
if (model in self.created_models): |
651 |
584 |
return |
| … | … | @@ -655,42 +588,53 @@ class DatabaseManagerBase(object): |
655 |
588 |
for field in model._meta.many_to_many: |
656 |
589 |
self.execute("Adding many-to-many table for field %s.%s" % (model_sig, field.name), self._m2m_sql(model, field)) |
657 |
590 |
|
658 |
def rename_model(self, |
|
591 |
def rename_model(self, old_sig, new_sig): |
|
659 |
592 |
""" |
660 |
593 |
Rename a model. |
661 |
594 |
""" |
662 |
old_model = self[model_sig] |
|
663 |
595 |
|
664 |
app_name, model_name = self.parse_sig(new_sig) |
|
665 |
new_model = models.get_model(app_name, model_name) |
|
666 |
|
|
596 |
old_app_name, old_model_name = self.parse_sig(old_sig) |
|
597 |
model = self.now.get_model(old_app_name, old_model_name) |
|
598 |
||
599 |
new_app_name, new_model_name = self.parse_sig(new_sig) |
|
600 |
ideal = self.ideal.get_model(new_app_name, new_model_name) |
|
601 |
||
602 |
self.now.apps.setdefault(new_app_name, {})[new_model_name] = model |
|
603 |
del self.now.apps[old_app_name][old_model_name] |
|
667 |
604 |
|
668 |
605 |
style = self.style |
669 |
606 |
|
670 |
607 |
# Have to drop the one created by the sync db. |
671 |
608 |
# TODO: Add ManyToMany support. |
672 |
# TODO: Check through self.created_models before doing this. |
|
673 |
sql = [ |
|
674 |
style.SQL_KEYWORD('DROP TABLE'), |
|
675 |
style.SQL_TABLE(qn(new_model._meta.db_table)) |
|
676 |
|
|
609 |
if (ideal in self.created_models): |
|
610 |
sql = [ |
|
611 |
style.SQL_KEYWORD('DROP TABLE'), |
|
612 |
style.SQL_TABLE(qn(ideal._meta.db_table)) |
|
613 |
] |
|
677 |
614 |
|
678 |
|
|
615 |
self.execute('Dropping eroneously created table for %s.' % new_sig, sql) |
|
679 |
616 |
|
680 |
617 |
sql = [ |
681 |
618 |
style.SQL_KEYWORD('ALTER TABLE'), |
682 |
style.SQL_TABLE(qn( |
|
619 |
style.SQL_TABLE(qn(model._meta.db_table)), |
|
683 |
620 |
style.SQL_KEYWORD('RENAME TO'), |
684 |
style.SQL_TABLE(qn( |
|
621 |
style.SQL_TABLE(qn(ideal._meta.db_table)) |
|
685 |
622 |
] |
686 |
623 |
|
624 |
# Execute the renaming. |
|
625 |
model._meta.object_name = ideal._meta.object_name |
|
626 |
model._meta.module_name = ideal._meta.module_name |
|
627 |
model._meta.verbose_name = ideal._meta.verbose_name |
|
628 |
model._meta.verbose_name_plural = ideal._meta.verbose_name_plural |
|
629 |
model._meta.db_table = ideal._meta.db_table |
|
630 |
||
687 |
631 |
self.execute("Renaming model %s to %s." % (model_sig, new_sig), sql) |
688 |
632 |
|
689 |
633 |
def drop_model(self, model_sig): |
690 |
634 |
""" |
691 |
635 |
Drops a model. |
692 |
636 |
""" |
693 |
model = self |
|
637 |
model = self.now.get_model(*self.parse_sig(model_sig)) |
|
694 |
638 |
style = self.style |
695 |
639 |
|
696 |
640 |
drop_sql = [ |
Up to file-list migratory/migrate.py:
1 |
1 |
""" |
2 |
2 |
Performs any new migrations. |
3 |
3 |
|
4 |
This is not to be confused with the ./manage.py command which creates |
|
5 |
migration files rather than actually performing them. |
|
4 |
This is not to be confused with the ./manage.py command "migrate" which |
|
5 |
creates migration files rather than actually performing them. |
|
6 |
6 |
""" |
7 |
7 |
import os |
8 |
8 |
|
| … | … | @@ -29,60 +29,18 @@ def do_migrations(sender=None, |
29 |
29 |
app_name = app.__name__.split('.')[-2] |
30 |
30 |
|
31 |
31 |
migrations = get_migrations(app, verbosity) |
32 |
if (verbosity > 1 and mode != 'sql'): print "%s found." % len(migrations.keys()) |
|
33 |
||
34 |
if migrations: |
|
35 |
database_manager = DatabaseManager(app_name, |
|
36 |
verbosity=verbosity, |
|
37 |
created_models=created_models or [], |
|
38 |
style=style, |
|
39 |
mode=mode) |
|
32 |
if not migrations: |
|
33 |
return |
|
40 |
34 |
|
41 |
for name, path in migrations.items(): |
|
42 |
if (verbosity >= 1 and mode != 'sql'): print "Executing migration %r..." % name |
|
43 |
if run_migration(path, database_manager, verbosity): |
|
44 |
Migration.objects.get_or_create(name=name) |
|
45 |
||
46 |
def run_migration(path, database_manager, verbosity, sql_only=False): |
|
47 |
if not os.path.exists(path): |
|
48 |
raise RuntimeError( |
|
49 |
"Unable to find migration at the given path: %r" % path) |
|
50 |
||
51 |
o = open(path) |
|
52 |
src = o.read() |
|
53 |
o.close() |
|
54 |
||
55 |
namespace = {} |
|
56 |
exec src in namespace |
|
57 |
||
58 |
up = namespace.get('up', None) |
|
59 |
||
60 |
if (up): |
|
61 |
try: |
|
62 |
up(database_manager) |
|
63 |
return True |
|
64 |
except Exception, e: |
|
65 |
import traceback, sys |
|
66 |
||
67 |
lines = ["\nError in migration:\n"] |
|
68 |
lines.extend(traceback.format_tb(sys.exc_traceback)[1:]) |
|
69 |
lines.append("%s: %s" % (e.__class__.__name__, str(e))) |
|
70 |
||
71 |
lines.append("\n\nLocals:\n") |
|
72 |
tb = sys.exc_info()[2] |
|
73 |
while tb.tb_next: |
|
74 |
tb = tb.tb_next |
|
75 |
frame = tb.tb_frame |
|
76 |
for key, value in frame.f_locals.items(): |
|
77 |
try: |
|
78 |
value = "%r" % value |
|
79 |
except: |
|
80 |
value = '<not representable>' |
|
81 |
lines.append("\t%s: %s\n" % (key,value)) |
|
82 |
||
83 |
raise RuntimeError("".join(lines)) |
|
84 |
else: |
|
85 |
|
|
35 |
manager = DatabaseManager(app_name, |
|
36 |
verbosity=verbosity, |
|
37 |
created_models=created_models or [], |
|
38 |
style=style, |
|
39 |
mode=mode) |
|
40 |
||
41 |
for name, path in migrations: |
|
42 |
manager.run_migration(path) |
|
43 |
Migration.objects.get_or_create(name=name) |
|
86 |
44 |
|
87 |
45 |
def get_migrations(app, verbosity): |
88 |
46 |
from django.contrib.migratory.models import Snapshot, Migration |
| … | … | @@ -102,13 +60,13 @@ def get_migrations(app, verbosity): |
102 |
60 |
Migration.objects.get_or_create(name=name) |
103 |
61 |
return {} |
104 |
62 |
|
105 |
gather = |
|
63 |
gather = [] |
|
106 |
64 |
for migration in manifest: |
107 |
65 |
path = os.path.join(folder, migration) |
108 |
66 |
if Migration.objects.filter(name=migration).count() > 0: |
109 |
67 |
continue |
110 |
68 |
if not os.path.exists(path): |
111 |
69 |
raise RuntimeError( "Cannot find a migration present in the __manifest__.py file.") |
112 |
gather |
|
70 |
gather.append((migration, path)) |
|
113 |
71 |
|
114 |
72 |
return gather |
Up to file-list migratory/models.py:
| … | … | @@ -20,7 +20,7 @@ class Migration(models.Model): |
20 |
20 |
|
21 |
21 |
from django.db.models.signals import post_syncdb |
22 |
22 |
from django.contrib.migratory.migrate import do_migrations |
23 |
from django.contrib.migratory. |
|
23 |
from django.contrib.migratory.app import create_snapshot |
|
24 |
24 |
|
25 |
25 |
post_syncdb.connect(do_migrations) |
26 |
post_syncdb.connect(create_snapshot |
|
26 |
post_syncdb.connect(create_snapshot) |
Up to file-list migratory/snapshot.py:
| … | … | @@ -7,7 +7,6 @@ from django.utils import simplejson |
7 |
7 |
from django.db import models |
8 |
8 |
from django.db.models.fields import NOT_PROVIDED |
9 |
9 |
|
10 |
||
11 |
10 |
def create_snapshots(sender=None, |
12 |
11 |
app=None, |
13 |
12 |
created_models=None, |
| … | … | @@ -20,16 +19,15 @@ def create_snapshots(sender=None, |
20 |
19 |
""" |
21 |
20 |
|
22 |
21 |
from django.contrib.migratory.models import Snapshot |
23 |
||
22 |
from django.contrib.migratory.app import AppManager |
|
23 |
||
24 |
24 |
app_name = app.__name__.split('.')[-2] |
25 |
25 |
|
26 |
26 |
if (verbosity > 1): |
27 |
27 |
print "\tCreating snapshot..." |
28 |
||
29 |
model_list = models.get_models(app) |
|
30 |
model_list = [model_snap(model) for model in model_list] |
|
31 |
28 |
|
32 |
|
|
29 |
manager = AppManager(app = app) |
|
30 |
json = simplejson.dumps( manager.get_snapshot()) |
|
33 |
31 |
|
34 |
32 |
if Snapshot.objects.filter(app=app_name).count() > 0: |
35 |
33 |
Snapshot.objects.filter(app=app_name).delete() |
Up to file-list test_project/app/models.py:
1 |
1 |
from django.db import models |
2 |
from django.contrib.auth.models import User |
|
2 |
3 |
|
3 |
class Poll(models.Model): |
|
4 |
question = models.CharField(max_length=255) |
|
5 |
pub_date = models.DateField() |
|
4 |
class Blorg(models.Model): |
|
5 |
name = models.CharField(max_length=255) |
|
6 |
description = models.TextField(blank=True) |
|
7 |
created = models.DateTimeField(auto_now_add=True) |
|
8 |
modified = models.DateTimeField(auto_now=True) |
|
6 |
9 |
|
7 |
class Choice(models.Model): |
|
8 |
poll = models.ForeignKey(Poll) |
|
9 |
choice = models.CharField(max_length=200) |
|
10 |
votes = models.IntegerField(null=True) |
|
10 |
class Post(models.Model): |
|
11 |
blog = models.ForeignKey(Blorg) |
|
12 |
slug = models.SlugField() |
|
13 |
title = models.CharField(max_length=255) |
|
14 |
body = models.TextField(blank=True) |
|
15 |
||
16 |
created = models.DateTimeField(auto_now_add=True) |
|
17 |
modified = models.DateTimeField(auto_now=True) |
Up to file-list test_project/test:
| … | … | @@ -27,18 +27,18 @@ fi |
27 |
27 |
|
28 |
28 |
rm -rf app/migrations/ 2>/dev/null |
29 |
29 |
|
30 |
for i in $( ls app/models-??.py | sed - |
|
30 |
for i in $( ls app/models-??.py | sed -n "s/.*\([0-9][0-9]\).py/\1/p" ); do |
|
31 |
31 |
echo "" |
32 |
32 |
echo --- $i --- |
33 |
33 |
|
34 |
34 |
rm app/models.py app/models.pyc 2>/dev/null |
35 |
35 |
ln app/models-$i.py app/models.py |
36 |
if [ "$i" != |
|
36 |
if [ "$i" != "00" ]; then |
|
37 |
37 |
echo "> python manage.py migrate app test-migration-$i --settings=settings_$1" |
38 |
38 |
python manage.py migrate app test-migration-$i --settings=settings_$1 || exit; |
39 |
echo "" |
|
39 |
40 |
fi |
40 |
41 |
|
41 |
echo "" |
|
42 |
42 |
echo "> python manage.py migratesql app --settings=settings_$1" |
43 |
43 |
python manage.py migratesql app --settings=settings_$1 || exit; |
44 |
44 |
