/bangkokhotel/lib/python2.5/site-packages/south/migration/base.py
Python | 439 lines | 332 code | 40 blank | 67 comment | 46 complexity | 62c69efc9694a55e0e6ae27a1840eecc MD5 | raw file
1from collections import deque
2import datetime
3import os
4import re
5import sys
6
7from django.core.exceptions import ImproperlyConfigured
8from django.db import models
9from django.conf import settings
10from django.utils import importlib
11
12from south import exceptions
13from south.migration.utils import depends, dfs, flatten, get_app_label
14from south.orm import FakeORM
15from south.utils import memoize, ask_for_it_by_name, datetime_utils
16from south.migration.utils import app_label_to_app_module
17
18
19def 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
35def 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
44class 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
70class 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
254class 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