root / env / lib / python2.7 / site-packages / south / migration / base.py @ d1a4905f
History | View | Annotate | Download (15.7 KB)
1 |
from collections import deque |
---|---|
2 |
import datetime |
3 |
import os |
4 |
import re |
5 |
import sys |
6 |
|
7 |
from django.core.exceptions import ImproperlyConfigured |
8 |
from django.db import models |
9 |
from django.conf import settings |
10 |
from django.utils import importlib |
11 |
|
12 |
from south import exceptions |
13 |
from south.migration.utils import depends, dfs, flatten, get_app_label |
14 |
from south.orm import FakeORM |
15 |
from south.utils import memoize, ask_for_it_by_name, datetime_utils |
16 |
from south.migration.utils import app_label_to_app_module |
17 |
|
18 |
|
19 |
def all_migrations(applications=None): |
20 |
"""
|
21 |
Returns all Migrations for all `applications` that are migrated.
|
22 |
"""
|
23 |
if applications is None: |
24 |
applications = models.get_apps() |
25 |
for model_module in applications: |
26 |
# The app they've passed is the models module - go up one level
|
27 |
app_path = ".".join(model_module.__name__.split(".")[:-1]) |
28 |
app = ask_for_it_by_name(app_path) |
29 |
try:
|
30 |
yield Migrations(app)
|
31 |
except exceptions.NoMigrations:
|
32 |
pass
|
33 |
|
34 |
|
35 |
def application_to_app_label(application): |
36 |
"Works out the app label from either the app label, the app name, or the module"
|
37 |
if isinstance(application, basestring): |
38 |
app_label = application.split('.')[-1] |
39 |
else:
|
40 |
app_label = application.__name__.split('.')[-1] |
41 |
return app_label
|
42 |
|
43 |
|
44 |
class MigrationsMetaclass(type): |
45 |
|
46 |
"""
|
47 |
Metaclass which ensures there is only one instance of a Migrations for
|
48 |
any given app.
|
49 |
"""
|
50 |
|
51 |
def __init__(self, name, bases, dict): |
52 |
super(MigrationsMetaclass, self).__init__(name, bases, dict) |
53 |
self.instances = {}
|
54 |
|
55 |
def __call__(self, application, **kwds): |
56 |
|
57 |
app_label = application_to_app_label(application) |
58 |
|
59 |
# If we don't already have an instance, make one
|
60 |
if app_label not in self.instances: |
61 |
self.instances[app_label] = super(MigrationsMetaclass, self).__call__(app_label_to_app_module(app_label), **kwds) |
62 |
|
63 |
return self.instances[app_label] |
64 |
|
65 |
def _clear_cache(self): |
66 |
"Clears the cache of Migration objects."
|
67 |
self.instances = {}
|
68 |
|
69 |
|
70 |
class Migrations(list): |
71 |
"""
|
72 |
Holds a list of Migration objects for a particular app.
|
73 |
"""
|
74 |
|
75 |
__metaclass__ = MigrationsMetaclass |
76 |
|
77 |
if getattr(settings, "SOUTH_USE_PYC", False): |
78 |
MIGRATION_FILENAME = re.compile(r'(?!__init__)' # Don't match __init__.py |
79 |
r'[0-9a-zA-Z_]*' # Don't match dotfiles, or names with dots/invalid chars in them |
80 |
r'(\.pyc?)?$') # Match .py or .pyc files, or module dirs |
81 |
else:
|
82 |
MIGRATION_FILENAME = re.compile(r'(?!__init__)' # Don't match __init__.py |
83 |
r'[0-9a-zA-Z_]*' # Don't match dotfiles, or names with dots/invalid chars in them |
84 |
r'(\.py)?$') # Match only .py files, or module dirs |
85 |
|
86 |
def __init__(self, application, force_creation=False, verbose_creation=True): |
87 |
"Constructor. Takes the module of the app, NOT its models (like get_app returns)"
|
88 |
self._cache = {}
|
89 |
self.set_application(application, force_creation, verbose_creation)
|
90 |
|
91 |
def create_migrations_directory(self, verbose=True): |
92 |
"Given an application, ensures that the migrations directory is ready."
|
93 |
migrations_dir = self.migrations_dir()
|
94 |
# Make the directory if it's not already there
|
95 |
if not os.path.isdir(migrations_dir): |
96 |
if verbose:
|
97 |
print "Creating migrations directory at '%s'..." % migrations_dir |
98 |
os.mkdir(migrations_dir) |
99 |
# Same for __init__.py
|
100 |
init_path = os.path.join(migrations_dir, "__init__.py")
|
101 |
if not os.path.isfile(init_path): |
102 |
# Touch the init py file
|
103 |
if verbose:
|
104 |
print "Creating __init__.py in '%s'..." % migrations_dir |
105 |
open(init_path, "w").close() |
106 |
|
107 |
def migrations_dir(self): |
108 |
"""
|
109 |
Returns the full path of the migrations directory.
|
110 |
If it doesn't exist yet, returns where it would exist, based on the
|
111 |
app's migrations module (defaults to app.migrations)
|
112 |
"""
|
113 |
module_path = self.migrations_module()
|
114 |
try:
|
115 |
module = importlib.import_module(module_path) |
116 |
except ImportError: |
117 |
# There's no migrations module made yet; guess!
|
118 |
try:
|
119 |
parent = importlib.import_module(".".join(module_path.split(".")[:-1])) |
120 |
except ImportError: |
121 |
# The parent doesn't even exist, that's an issue.
|
122 |
raise exceptions.InvalidMigrationModule(
|
123 |
application = self.application.__name__,
|
124 |
module = module_path, |
125 |
) |
126 |
else:
|
127 |
# Good guess.
|
128 |
return os.path.join(os.path.dirname(parent.__file__), module_path.split(".")[-1]) |
129 |
else:
|
130 |
# Get directory directly
|
131 |
return os.path.dirname(module.__file__)
|
132 |
|
133 |
def migrations_module(self): |
134 |
"Returns the module name of the migrations module for this"
|
135 |
app_label = application_to_app_label(self.application)
|
136 |
if hasattr(settings, "SOUTH_MIGRATION_MODULES"): |
137 |
if app_label in settings.SOUTH_MIGRATION_MODULES: |
138 |
# There's an override.
|
139 |
return settings.SOUTH_MIGRATION_MODULES[app_label]
|
140 |
return self._application.__name__ + '.migrations' |
141 |
|
142 |
def get_application(self): |
143 |
return self._application |
144 |
|
145 |
def set_application(self, application, force_creation=False, verbose_creation=True): |
146 |
"""
|
147 |
Called when the application for this Migrations is set.
|
148 |
Imports the migrations module object, and throws a paddy if it can't.
|
149 |
"""
|
150 |
self._application = application
|
151 |
if not hasattr(application, 'migrations'): |
152 |
try:
|
153 |
module = importlib.import_module(self.migrations_module())
|
154 |
self._migrations = application.migrations = module
|
155 |
except ImportError: |
156 |
if force_creation:
|
157 |
self.create_migrations_directory(verbose_creation)
|
158 |
module = importlib.import_module(self.migrations_module())
|
159 |
self._migrations = application.migrations = module
|
160 |
else:
|
161 |
raise exceptions.NoMigrations(application)
|
162 |
self._load_migrations_module(application.migrations)
|
163 |
|
164 |
application = property(get_application, set_application)
|
165 |
|
166 |
def _load_migrations_module(self, module): |
167 |
self._migrations = module
|
168 |
filenames = [] |
169 |
dirname = self.migrations_dir()
|
170 |
for f in os.listdir(dirname): |
171 |
if self.MIGRATION_FILENAME.match(os.path.basename(f)): |
172 |
full_path = os.path.join(dirname, f) |
173 |
# If it's a .pyc file, only append if the .py isn't already around
|
174 |
if f.endswith(".pyc") and (os.path.isfile(full_path[:-1])): |
175 |
continue
|
176 |
# If it's a module directory, only append if it contains __init__.py[c].
|
177 |
if os.path.isdir(full_path):
|
178 |
if not (os.path.isfile(os.path.join(full_path, "__init__.py")) or \ |
179 |
(getattr(settings, "SOUTH_USE_PYC", False) and \ |
180 |
os.path.isfile(os.path.join(full_path, "__init__.pyc")))):
|
181 |
continue
|
182 |
filenames.append(f) |
183 |
filenames.sort() |
184 |
self.extend(self.migration(f) for f in filenames) |
185 |
|
186 |
def migration(self, filename): |
187 |
name = Migration.strip_filename(filename) |
188 |
if name not in self._cache: |
189 |
self._cache[name] = Migration(self, name) |
190 |
return self._cache[name] |
191 |
|
192 |
def __getitem__(self, value): |
193 |
if isinstance(value, basestring): |
194 |
return self.migration(value) |
195 |
return super(Migrations, self).__getitem__(value) |
196 |
|
197 |
def _guess_migration(self, prefix): |
198 |
prefix = Migration.strip_filename(prefix) |
199 |
matches = [m for m in self if m.name().startswith(prefix)] |
200 |
if len(matches) == 1: |
201 |
return matches[0] |
202 |
elif len(matches) > 1: |
203 |
raise exceptions.MultiplePrefixMatches(prefix, matches)
|
204 |
else:
|
205 |
raise exceptions.UnknownMigration(prefix, None) |
206 |
|
207 |
def guess_migration(self, target_name): |
208 |
if target_name == 'zero' or not self: |
209 |
return
|
210 |
elif target_name is None: |
211 |
return self[-1] |
212 |
else:
|
213 |
return self._guess_migration(prefix=target_name) |
214 |
|
215 |
def app_label(self): |
216 |
return self._application.__name__.split('.')[-1] |
217 |
|
218 |
def full_name(self): |
219 |
return self._migrations.__name__ |
220 |
|
221 |
@classmethod
|
222 |
def calculate_dependencies(cls, force=False): |
223 |
"Goes through all the migrations, and works out the dependencies."
|
224 |
if getattr(cls, "_dependencies_done", False) and not force: |
225 |
return
|
226 |
for migrations in all_migrations(): |
227 |
for migration in migrations: |
228 |
migration.calculate_dependencies() |
229 |
cls._dependencies_done = True
|
230 |
|
231 |
@staticmethod
|
232 |
def invalidate_all_modules(): |
233 |
"Goes through all the migrations, and invalidates all cached modules."
|
234 |
for migrations in all_migrations(): |
235 |
for migration in migrations: |
236 |
migration.invalidate_module() |
237 |
|
238 |
def next_filename(self, name): |
239 |
"Returns the fully-formatted filename of what a new migration 'name' would be"
|
240 |
highest_number = 0
|
241 |
for migration in self: |
242 |
try:
|
243 |
number = int(migration.name().split("_")[0]) |
244 |
highest_number = max(highest_number, number)
|
245 |
except ValueError: |
246 |
pass
|
247 |
# Work out the new filename
|
248 |
return "%04i_%s.py" % ( |
249 |
highest_number + 1,
|
250 |
name, |
251 |
) |
252 |
|
253 |
|
254 |
class Migration(object): |
255 |
|
256 |
"""
|
257 |
Class which represents a particular migration file on-disk.
|
258 |
"""
|
259 |
|
260 |
def __init__(self, migrations, filename): |
261 |
"""
|
262 |
Returns the migration class implied by 'filename'.
|
263 |
"""
|
264 |
self.migrations = migrations
|
265 |
self.filename = filename
|
266 |
self.dependencies = set() |
267 |
self.dependents = set() |
268 |
|
269 |
def __str__(self): |
270 |
return self.app_label() + ':' + self.name() |
271 |
|
272 |
def __repr__(self): |
273 |
return u'<Migration: %s>' % unicode(self) |
274 |
|
275 |
def __eq__(self, other): |
276 |
return self.app_label() == other.app_label() and self.name() == other.name() |
277 |
|
278 |
def __hash__(self): |
279 |
return hash(str(self)) |
280 |
|
281 |
def app_label(self): |
282 |
return self.migrations.app_label() |
283 |
|
284 |
@staticmethod
|
285 |
def strip_filename(filename): |
286 |
return os.path.splitext(os.path.basename(filename))[0] |
287 |
|
288 |
def name(self): |
289 |
return self.strip_filename(os.path.basename(self.filename)) |
290 |
|
291 |
def full_name(self): |
292 |
return self.migrations.full_name() + '.' + self.name() |
293 |
|
294 |
def migration(self): |
295 |
"Tries to load the actual migration module"
|
296 |
full_name = self.full_name()
|
297 |
try:
|
298 |
migration = sys.modules[full_name] |
299 |
except KeyError: |
300 |
try:
|
301 |
migration = __import__(full_name, {}, {}, ['Migration']) |
302 |
except ImportError, e: |
303 |
raise exceptions.UnknownMigration(self, sys.exc_info()) |
304 |
except Exception, e: |
305 |
raise exceptions.BrokenMigration(self, sys.exc_info()) |
306 |
# Override some imports
|
307 |
migration._ = lambda x: x # Fake i18n |
308 |
migration.datetime = datetime_utils |
309 |
return migration
|
310 |
migration = memoize(migration) |
311 |
|
312 |
def migration_class(self): |
313 |
"Returns the Migration class from the module"
|
314 |
return self.migration().Migration |
315 |
|
316 |
def migration_instance(self): |
317 |
"Instantiates the migration_class"
|
318 |
return self.migration_class()() |
319 |
migration_instance = memoize(migration_instance) |
320 |
|
321 |
def previous(self): |
322 |
"Returns the migration that comes before this one in the sequence."
|
323 |
index = self.migrations.index(self) - 1 |
324 |
if index < 0: |
325 |
return None |
326 |
return self.migrations[index] |
327 |
previous = memoize(previous) |
328 |
|
329 |
def next(self): |
330 |
"Returns the migration that comes after this one in the sequence."
|
331 |
index = self.migrations.index(self) + 1 |
332 |
if index >= len(self.migrations): |
333 |
return None |
334 |
return self.migrations[index] |
335 |
next = memoize(next)
|
336 |
|
337 |
def _get_dependency_objects(self, attrname): |
338 |
"""
|
339 |
Given the name of an attribute (depends_on or needed_by), either yields
|
340 |
a list of migration objects representing it, or errors out.
|
341 |
"""
|
342 |
for app, name in getattr(self.migration_class(), attrname, []): |
343 |
try:
|
344 |
migrations = Migrations(app) |
345 |
except ImproperlyConfigured:
|
346 |
raise exceptions.DependsOnUnmigratedApplication(self, app) |
347 |
migration = migrations.migration(name) |
348 |
try:
|
349 |
migration.migration() |
350 |
except exceptions.UnknownMigration:
|
351 |
raise exceptions.DependsOnUnknownMigration(self, migration) |
352 |
if migration.is_before(self) == False: |
353 |
raise exceptions.DependsOnHigherMigration(self, migration) |
354 |
yield migration
|
355 |
|
356 |
def calculate_dependencies(self): |
357 |
"""
|
358 |
Loads dependency info for this migration, and stores it in itself
|
359 |
and any other relevant migrations.
|
360 |
"""
|
361 |
# Normal deps first
|
362 |
for migration in self._get_dependency_objects("depends_on"): |
363 |
self.dependencies.add(migration)
|
364 |
migration.dependents.add(self)
|
365 |
# And reverse deps
|
366 |
for migration in self._get_dependency_objects("needed_by"): |
367 |
self.dependents.add(migration)
|
368 |
migration.dependencies.add(self)
|
369 |
# And implicit ordering deps
|
370 |
previous = self.previous()
|
371 |
if previous:
|
372 |
self.dependencies.add(previous)
|
373 |
previous.dependents.add(self)
|
374 |
|
375 |
def invalidate_module(self): |
376 |
"""
|
377 |
Removes the cached version of this migration's module import, so we
|
378 |
have to re-import it. Used when south.db.db changes.
|
379 |
"""
|
380 |
reload(self.migration()) |
381 |
self.migration._invalidate()
|
382 |
|
383 |
def forwards(self): |
384 |
return self.migration_instance().forwards |
385 |
|
386 |
def backwards(self): |
387 |
return self.migration_instance().backwards |
388 |
|
389 |
def forwards_plan(self): |
390 |
"""
|
391 |
Returns a list of Migration objects to be applied, in order.
|
392 |
|
393 |
This list includes `self`, which will be applied last.
|
394 |
"""
|
395 |
return depends(self, lambda x: x.dependencies) |
396 |
|
397 |
def _backwards_plan(self): |
398 |
return depends(self, lambda x: x.dependents) |
399 |
|
400 |
def backwards_plan(self): |
401 |
"""
|
402 |
Returns a list of Migration objects to be unapplied, in order.
|
403 |
|
404 |
This list includes `self`, which will be unapplied last.
|
405 |
"""
|
406 |
return list(self._backwards_plan()) |
407 |
|
408 |
def is_before(self, other): |
409 |
if self.migrations == other.migrations: |
410 |
if self.filename < other.filename: |
411 |
return True |
412 |
return False |
413 |
|
414 |
def is_after(self, other): |
415 |
if self.migrations == other.migrations: |
416 |
if self.filename > other.filename: |
417 |
return True |
418 |
return False |
419 |
|
420 |
def prev_orm(self): |
421 |
if getattr(self.migration_class(), 'symmetrical', False): |
422 |
return self.orm() |
423 |
previous = self.previous()
|
424 |
if previous is None: |
425 |
# First migration? The 'previous ORM' is empty.
|
426 |
return FakeORM(None, self.app_label()) |
427 |
return previous.orm()
|
428 |
prev_orm = memoize(prev_orm) |
429 |
|
430 |
def orm(self): |
431 |
return FakeORM(self.migration_class(), self.app_label()) |
432 |
orm = memoize(orm) |
433 |
|
434 |
def no_dry_run(self): |
435 |
migration_class = self.migration_class()
|
436 |
try:
|
437 |
return migration_class.no_dry_run
|
438 |
except AttributeError: |
439 |
return False |