Project

General

Profile

Statistics
| Branch: | Revision:

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