from __future__ import print_function from copy import copy, deepcopy import datetime import inspect import sys import traceback from django.core.management import call_command from django.core.management.commands import loaddata from django.db import models from django import VERSION as DJANGO_VERSION import south.db from south import exceptions from south.db import DEFAULT_DB_ALIAS from south.models import MigrationHistory from south.signals import ran_migration from south.utils.py3 import StringIO, iteritems class Migrator(object): def __init__(self, verbosity=0, interactive=False): self.verbosity = int(verbosity) self.interactive = bool(interactive) @staticmethod def title(target): raise NotImplementedError() def print_title(self, target): if self.verbosity: print(self.title(target)) @staticmethod def status(target): raise NotImplementedError() def print_status(self, migration): status = self.status(migration) if self.verbosity and status: print(status) @staticmethod def orm(migration): raise NotImplementedError() def backwards(self, migration): return self._wrap_direction(migration.backwards(), migration.prev_orm()) def direction(self, migration): raise NotImplementedError() @staticmethod def _wrap_direction(direction, orm): args = inspect.getargspec(direction) if len(args[0]) == 1: # Old migration, no ORM should be passed in return direction return (lambda: direction(orm)) @staticmethod def record(migration, database): raise NotImplementedError() def run_migration_error(self, migration, extra_info=''): return ( ' ! Error found during real run of migration! Aborting.\n' '\n' ' ! Since you have a database that does not support running\n' ' ! schema-altering statements in transactions, we have had \n' ' ! to leave it in an interim state between migrations.\n' '%s\n' ' ! The South developers regret this has happened, and would\n' ' ! like to gently persuade you to consider a slightly\n' ' ! easier-to-deal-with DBMS (one that supports DDL transactions)\n' ' ! NOTE: The error which caused the migration to fail is further up.' ) % extra_info def run_migration(self, migration, database): migration_function = self.direction(migration) south.db.db.start_transaction() try: migration_function() south.db.db.execute_deferred_sql() if not isinstance(getattr(self, '_wrapper', self), DryRunMigrator): # record us as having done this in the same transaction, # since we're not in a dry run self.record(migration, database) except: south.db.db.rollback_transaction() if not south.db.db.has_ddl_transactions: print(self.run_migration_error(migration)) print("Error in migration: %s" % migration) raise else: try: south.db.db.commit_transaction() except: print("Error during commit in migration: %s" % migration) raise def run(self, migration, database): # Get the correct ORM. south.db.db.current_orm = self.orm(migration) # If we're not already in a dry run, and the database doesn't support # running DDL inside a transaction, *cough*MySQL*cough* then do a dry # run first. if not isinstance(getattr(self, '_wrapper', self), DryRunMigrator): if not south.db.db.has_ddl_transactions: dry_run = DryRunMigrator(migrator=self, ignore_fail=False) dry_run.run_migration(migration, database) return self.run_migration(migration, database) def send_ran_migration(self, migration, database): ran_migration.send(None, app=migration.app_label(), migration=migration, method=self.__class__.__name__.lower(), verbosity=self.verbosity, interactive=self.interactive, db=database) def migrate(self, migration, database): """ Runs the specified migration forwards/backwards, in order. """ app = migration.migrations._migrations migration_name = migration.name() self.print_status(migration) result = self.run(migration, database) self.send_ran_migration(migration, database) return result def migrate_many(self, target, migrations, database): raise NotImplementedError() class MigratorWrapper(object): def __init__(self, migrator, *args, **kwargs): self._migrator = copy(migrator) attributes = dict([(k, getattr(self, k)) for k in self.__class__.__dict__ if not k.startswith('__')]) self._migrator.__dict__.update(attributes) self._migrator.__dict__['_wrapper'] = self def __getattr__(self, name): return getattr(self._migrator, name) class DryRunMigrator(MigratorWrapper): def __init__(self, ignore_fail=True, *args, **kwargs): super(DryRunMigrator, self).__init__(*args, **kwargs) self._ignore_fail = ignore_fail def _run_migration(self, migration): if migration.no_dry_run(): if self.verbosity: print(" - Migration '%s' is marked for no-dry-run." % migration) return for name, db in iteritems(south.db.dbs): south.db.dbs[name].dry_run = True # preserve the constraint cache as it can be mutated by the dry run constraint_cache = deepcopy(south.db.db._constraint_cache) if self._ignore_fail: south.db.db.debug, old_debug = False, south.db.db.debug pending_creates = south.db.db.get_pending_creates() south.db.db.start_transaction() migration_function = self.direction(migration) try: try: migration_function() south.db.db.execute_deferred_sql() except: raise exceptions.FailedDryRun(migration, sys.exc_info()) finally: south.db.db.rollback_transactions_dry_run() if self._ignore_fail: south.db.db.debug = old_debug south.db.db.clear_run_data(pending_creates) for name, db in iteritems(south.db.dbs): south.db.dbs[name].dry_run = False # restore the preserved constraint cache from before dry run was # executed south.db.db._constraint_cache = constraint_cache def run_migration(self, migration, database): try: self._run_migration(migration) except exceptions.FailedDryRun: if self._ignore_fail: return False raise def send_ran_migration(self, *args, **kwargs): pass class FakeMigrator(MigratorWrapper): def run(self, migration, database): # Don't actually run, just record as if ran self.record(migration, database) if self.verbosity: print(' (faked)') def send_ran_migration(self, *args, **kwargs): pass class LoadInitialDataMigrator(MigratorWrapper): def load_initial_data(self, target, db='default'): if target is None or target != target.migrations[-1]: return # Load initial data, if we ended up at target if self.verbosity: print(" - Loading initial data for %s." % target.app_label()) if DJANGO_VERSION < (1, 6): self.pre_1_6(target, db) else: self.post_1_6(target, db) def pre_1_6(self, target, db): # Override Django's get_apps call temporarily to only load from the # current app old_get_apps = models.get_apps new_get_apps = lambda: [models.get_app(target.app_label())] models.get_apps = new_get_apps loaddata.get_apps = new_get_apps try: call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=db) finally: models.get_apps = old_get_apps loaddata.get_apps = old_get_apps def post_1_6(self, target, db): import django.db.models.loading ## build a new 'AppCache' object with just the app we care about. old_cache = django.db.models.loading.cache new_cache = django.db.models.loading.AppCache() new_cache.get_apps = lambda: [new_cache.get_app(target.app_label())] ## monkeypatch django.db.models.loading.cache = new_cache try: call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=db) finally: ## unmonkeypatch django.db.models.loading.cache = old_cache def migrate_many(self, target, migrations, database): migrator = self._migrator result = migrator.__class__.migrate_many(migrator, target, migrations, database) if result: self.load_initial_data(target, db=database) return True class Forwards(Migrator): """ Runs the specified migration forwards, in order. """ torun = 'forwards' @staticmethod def title(target): if target is not None: return " - Migrating forwards to %s." % target.name() else: assert False, "You cannot migrate forwards to zero." @staticmethod def status(migration): return ' > %s' % migration @staticmethod def orm(migration): return migration.orm() def forwards(self, migration): return self._wrap_direction(migration.forwards(), migration.orm()) direction = forwards @staticmethod def record(migration, database): # Record us as having done this record = MigrationHistory.for_migration(migration, database) try: from django.utils.timezone import now record.applied = now() except ImportError: record.applied = datetime.datetime.utcnow() if database != DEFAULT_DB_ALIAS: record.save(using=database) else: # Django 1.1 and below always go down this branch. record.save() def format_backwards(self, migration): if migration.no_dry_run(): return " (migration cannot be dry-run; cannot discover commands)" old_debug, old_dry_run = south.db.db.debug, south.db.db.dry_run south.db.db.debug = south.db.db.dry_run = True stdout = sys.stdout sys.stdout = StringIO() try: try: self.backwards(migration)() return sys.stdout.getvalue() except: raise finally: south.db.db.debug, south.db.db.dry_run = old_debug, old_dry_run sys.stdout = stdout def run_migration_error(self, migration, extra_info=''): extra_info = ('\n' '! You *might* be able to recover with:' '%s' '%s' % (self.format_backwards(migration), extra_info)) return super(Forwards, self).run_migration_error(migration, extra_info) def migrate_many(self, target, migrations, database): try: for migration in migrations: result = self.migrate(migration, database) if result is False: # The migrations errored, but nicely. return False finally: # Call any pending post_syncdb signals south.db.db.send_pending_create_signals(verbosity=self.verbosity, interactive=self.interactive) return True class Backwards(Migrator): """ Runs the specified migration backwards, in order. """ torun = 'backwards' @staticmethod def title(target): if target is None: return " - Migrating backwards to zero state." else: return " - Migrating backwards to just after %s." % target.name() @staticmethod def status(migration): return ' < %s' % migration @staticmethod def orm(migration): return migration.prev_orm() direction = Migrator.backwards @staticmethod def record(migration, database): # Record us as having not done this record = MigrationHistory.for_migration(migration, database) if record.id is not None: if database != DEFAULT_DB_ALIAS: record.delete(using=database) else: # Django 1.1 always goes down here record.delete() def migrate_many(self, target, migrations, database): for migration in migrations: self.migrate(migration, database) return True