def get_mutations(app, evolution_labels, database=DEFAULT_DB_ALIAS): """Return the mutations provided by the given evolution names. Args: app (module): The app the evolutions belong to. evolution_labels (unicode): The labels of the evolutions to return mutations for. database (unicode, optional): The name of the database the evolutions cover. Returns: list of django_evolution.mutations.BaseMutation: The list of mutations provided by the evolutions. Raises: django_evolution.errors.EvolutionException: One or more evolutions are missing. """ # For each item in the evolution sequence. Check each item to see if it is # a python file or an sql file. try: app_name = get_app_name(app) if app_name in BUILTIN_SEQUENCES: module_name = 'django_evolution.builtin_evolutions' else: module_name = '%s.evolutions' % app_name evolution_module = import_module(module_name) except ImportError: return [] mutations = [] for label in evolution_labels: directory_name = os.path.dirname(evolution_module.__file__) # The first element is used for compatibility purposes. filenames = [ os.path.join(directory_name, label + '.sql'), os.path.join(directory_name, "%s_%s.sql" % (database, label)), ] found = False for filename in filenames: if os.path.exists(filename): sql_file = open(filename, 'r') sql = sql_file.readlines() sql_file.close() mutations.append(SQLMutation(label, sql)) found = True break if not found: try: module_name = [evolution_module.__name__, label] module = __import__('.'.join(module_name), {}, {}, [module_name]) mutations.extend(module.MUTATIONS) except ImportError: raise EvolutionException( 'Error: Failed to find an SQL or Python evolution named %s' % label) latest_version = Version.objects.current_version(using=database) app_id = get_app_label(app) old_project_sig = latest_version.signature project_sig = ProjectSignature.from_database(database) old_app_sig = old_project_sig.get_app_sig(app_id) app_sig = project_sig.get_app_sig(app_id) if old_app_sig is not None and app_sig is not None: # We want to go through now and make sure we're only applying # evolutions for models where the signature is different between # what's stored and what's current. # # The reason for this is that we may have just installed a baseline, # which would have the up-to-date signature, and we might be trying # to apply evolutions on top of that (which would already be applied). # These would generate errors. So, try hard to prevent that. # # First, Find the list of models in the latest signature of this app # that aren't in the old signature. changed_models = set( model_sig.model_name for model_sig in app_sig.model_sigs if old_app_sig.get_model_sig(model_sig.model_name) != model_sig ) # Now do the same for models in the old signature, in case the # model has been deleted. changed_models.update( old_model_sig.model_name for old_model_sig in old_app_sig.model_sigs if app_sig.get_model_sig(old_model_sig.model_name) is None ) # We should now have a full list of which models changed. Filter # the list of mutations appropriately. # # Changes affecting a model that was newly-introduced are removed, # unless the mutation is a RenameModel, in which case we'll need it # during the optimization step (and will remove it if necessary then). mutations = [ mutation for mutation in mutations if (not hasattr(mutation, 'model_name') or mutation.model_name in changed_models or isinstance(mutation, RenameModel)) ] return mutations
def __init__(self, hinted=False, verbosity=0, interactive=False, database_name=DEFAULT_DB_ALIAS): """Initialize the evolver. Args: hinted (bool, optional): Whether to operate against hinted evolutions. This may result in changes to the database without there being any accompanying evolution files backing those changes. verbosity (int, optional): The verbosity level for any output. This is passed along to signal emissions. interactive (bool, optional): Whether the evolution operations are being performed in a way that allows interactivity on the command line. This is passed along to signal emissions. database_name (unicode, optional): The name of the database to evolve. Raises: django_evolution.errors.EvolutionBaselineMissingError: An initial baseline for the project was not yet installed. This is due to ``syncdb``/``migrate`` not having been run. """ self.database_name = database_name self.hinted = hinted self.verbosity = verbosity self.interactive = interactive self.evolved = False self.initial_diff = None self.project_sig = None self.version = None self.installed_new_database = False self.connection = connections[database_name] if hasattr(self.connection, 'prepare_database'): # Django >= 1.8 self.connection.prepare_database() self.database_state = DatabaseState(self.database_name) self.target_project_sig = \ ProjectSignature.from_database(database_name) self._tasks_by_class = OrderedDict() self._tasks_by_id = OrderedDict() self._tasks_prepared = False latest_version = None if self.database_state.has_model(Version): try: latest_version = \ Version.objects.current_version(using=database_name) except Version.DoesNotExist: # We'll populate this next. pass if latest_version is None: # Either the models aren't yet synced to the database, or we # don't have a saved project signature, so let's set these up. self.installed_new_database = True self.project_sig = ProjectSignature() app = get_app('django_evolution') task = EvolveAppTask(evolver=self, app=app) task.prepare(hinted=False) with self.sql_executor() as sql_executor: task.execute(sql_executor=sql_executor, create_models_now=True) self.database_state.rescan_tables() app_sig = AppSignature.from_app(app=app, database=database_name) self.project_sig.add_app_sig(app_sig) # Let's make completely sure that we've only found the models # we expect. This is mostly for the benefit of unit tests. model_names = set(model_sig.model_name for model_sig in app_sig.model_sigs) expected_model_names = set(['Evolution', 'Version']) assert model_names == expected_model_names, ( 'Unexpected models found for django_evolution app: %s' % ', '.join(model_names - expected_model_names)) self._save_project_sig(new_evolutions=task.new_evolutions) latest_version = self.version self.project_sig = latest_version.signature self.initial_diff = Diff(self.project_sig, self.target_project_sig)
def _on_app_models_updated(app, verbosity=1, using=DEFAULT_DB_ALIAS, **kwargs): """Handler for when an app's models were updated. This is called in response to a syncdb or migrate operation for an app. It will install baselines for any new models, record the changes in the evolution history, and notify the user if any of the changes require an evolution. Args: app (module): The app models module that was updated. verbosity (int, optional): The verbosity used to control output. This will have been provided by the syncdb or migrate command. using (str, optional): The database being updated. **kwargs (dict): Additional keyword arguments provided by the signal handler for the syncdb or migrate operation. """ project_sig = ProjectSignature.from_database(using) try: latest_version = Version.objects.current_version(using=using) except Version.DoesNotExist: # We need to create a baseline version. if verbosity > 0: print("Installing baseline version") latest_version = Version(signature=project_sig) latest_version.save(using=using) for a in get_apps(): _install_baseline(app=a, latest_version=latest_version, using=using, verbosity=verbosity) unapplied = get_unapplied_evolutions(app, using) if unapplied: print( style.NOTICE('There are unapplied evolutions for %s.' % get_app_label(app))) # Evolutions are checked over the entire project, so we only need to check # once. We do this check when Django Evolutions itself is synchronized. if app is get_app('django_evolution'): old_project_sig = latest_version.signature # If any models or apps have been added, a baseline must be set # for those new models changed = False new_apps = [] for new_app_sig in project_sig.app_sigs: app_id = new_app_sig.app_id old_app_sig = old_project_sig.get_app_sig(app_id) if old_app_sig is None: # App has been added old_project_sig.add_app_sig(new_app_sig.clone()) new_apps.append(app_id) changed = True else: for new_model_sig in new_app_sig.model_sigs: model_name = new_model_sig.model_name old_model_sig = old_app_sig.get_model_sig(model_name) if old_model_sig is None: # Model has been added old_app_sig.add_model_sig( project_sig.get_app_sig(app_id).get_model_sig( model_name).clone()) changed = True if changed: if verbosity > 0: print("Adding baseline version for new models") latest_version = Version(signature=old_project_sig) latest_version.save(using=using) for app_name in new_apps: app = get_app(app_name, True) if app: _install_baseline(app=app, latest_version=latest_version, using=using, verbosity=verbosity) # TODO: Model introspection step goes here. # # If the current database state doesn't match the last # # saved signature (as reported by latest_version), # # then we need to update the Evolution table. # actual_sig = introspect_project_sig() # acutal = pickle.dumps(actual_sig) # if actual != latest_version.signature: # nudge = Version(signature=actual) # nudge.save() # latest_version = nudge diff = Diff(old_project_sig, project_sig) if not diff.is_empty(): print( style.NOTICE( 'Project signature has changed - an evolution is required') ) if verbosity > 1: print(diff)
def get_app_pending_mutations(app, evolution_labels=[], mutations=None, old_project_sig=None, project_sig=None, database=DEFAULT_DB_ALIAS): """Return an app's pending mutations provided by the given evolution names. This is similar to :py:meth:`get_app_mutations`, but filters the list of mutations down to remove any that are unnecessary (ones that do not operate on changed parts of the project signature). Args: app (module): The app the evolutions belong to. evolution_labels (list of unicode, optional): The labels of the evolutions to return mutations for. If ``None``, this will factor in all evolution labels for the app. mutations (list of django_evolution.mutations.BaseMutation, optional): An explicit list of mutations to use. If provided, ``evolution_labels`` will be ignored. old_project_sig (django_evolution.signature.ProjectSignature, optional): A pre-fetched old project signature. If provided, this will be used instead of the latest one in the database. project_sig (django_evolution.signature.ProjectSignature, optional): A project signature representing the current state of the database. If provided, this will be used instead of generating a new one from the current database state. database (unicode, optional): The name of the database the evolutions cover. Returns: list of django_evolution.mutations.BaseMutation: The list of mutations provided by the evolutions. Raises: django_evolution.errors.EvolutionException: One or more evolutions are missing. """ # Avoids a nasty circular import. Util modules should always be # importable, so we compensate here. from django_evolution.models import Version from django_evolution.mutations import RenameModel from django_evolution.signature import ProjectSignature if mutations is None: mutations = get_app_mutations(app=app, evolution_labels=evolution_labels, database=database) if old_project_sig is None: latest_version = Version.objects.current_version(using=database) old_project_sig = latest_version.signature if project_sig is None: project_sig = ProjectSignature.from_database(database) app_id = get_app_label(app) old_app_sig = old_project_sig.get_app_sig(app_id) app_sig = project_sig.get_app_sig(app_id) if old_app_sig is not None and app_sig is not None: # We want to go through now and make sure we're only applying # evolutions for models where the signature is different between # what's stored and what's current. # # The reason for this is that we may have just installed a baseline, # which would have the up-to-date signature, and we might be trying # to apply evolutions on top of that (which would already be applied). # Or we may be working with models that weren't present in the old # signature and will soon be added. In either case, these would # generate errors. So, try hard to prevent that. # # First, Find the list of models in the latest signature of this app # that aren't in the old signature. If a model signature isn't found # in the old app signature, it's a new model, and we don't want to # try to apply evolutions to it. changed_models = set( model_sig.model_name for model_sig in app_sig.model_sigs if old_app_sig.get_model_sig(model_sig.model_name) not in ( None, model_sig)) # Now do the same for models in the old signature, in case the # model has been deleted. changed_models.update( old_model_sig.model_name for old_model_sig in old_app_sig.model_sigs if app_sig.get_model_sig(old_model_sig.model_name) is None) # We should now have a full list of which models changed. Filter # the list of mutations appropriately. # # Changes affecting a model that was newly-introduced are removed, # unless the mutation is a RenameModel, in which case we'll need it # during the optimization step (and will remove it if necessary then). mutations = [ mutation for mutation in mutations if (not hasattr(mutation, 'model_name') or mutation.model_name in changed_models or isinstance(mutation, RenameModel)) ] return mutations
def _on_app_models_updated(app, verbosity=1, using=DEFAULT_DB_ALIAS, **kwargs): """Handler for when an app's models were updated. This is called in response to a syncdb or migrate operation for an app. It will install baselines for any new models, record the changes in the evolution history, and notify the user if any of the changes require an evolution. Args: app (module): The app models module that was updated. verbosity (int, optional): The verbosity used to control output. This will have been provided by the syncdb or migrate command. using (str, optional): The database being updated. **kwargs (dict): Additional keyword arguments provided by the signal handler for the syncdb or migrate operation. """ project_sig = ProjectSignature.from_database(using) try: latest_version = Version.objects.current_version(using=using) except Version.DoesNotExist: # We need to create a baseline version. if verbosity > 0: print("Installing baseline version") latest_version = Version(signature=project_sig) latest_version.save(using=using) for a in get_apps(): _install_baseline(app=a, latest_version=latest_version, using=using, verbosity=verbosity) unapplied = get_unapplied_evolutions(app, using) if unapplied: print(style.NOTICE('There are unapplied evolutions for %s.' % get_app_label(app))) # Evolutions are checked over the entire project, so we only need to check # once. We do this check when Django Evolutions itself is synchronized. if app is get_app('django_evolution'): old_project_sig = latest_version.signature # If any models or apps have been added, a baseline must be set # for those new models changed = False new_apps = [] for new_app_sig in project_sig.app_sigs: app_id = new_app_sig.app_id old_app_sig = old_project_sig.get_app_sig(app_id) if old_app_sig is None: # App has been added old_project_sig.add_app_sig(new_app_sig.clone()) new_apps.append(app_id) changed = True else: for new_model_sig in new_app_sig.model_sigs: model_name = new_model_sig.model_name old_model_sig = old_app_sig.get_model_sig(model_name) if old_model_sig is None: # Model has been added old_app_sig.add_model_sig( project_sig .get_app_sig(app_id) .get_model_sig(model_name) .clone()) changed = True if changed: if verbosity > 0: print("Adding baseline version for new models") latest_version = Version(signature=old_project_sig) latest_version.save(using=using) for app_name in new_apps: app = get_app(app_name, True) if app: _install_baseline(app=app, latest_version=latest_version, using=using, verbosity=verbosity) # TODO: Model introspection step goes here. # # If the current database state doesn't match the last # # saved signature (as reported by latest_version), # # then we need to update the Evolution table. # actual_sig = introspect_project_sig() # acutal = pickle.dumps(actual_sig) # if actual != latest_version.signature: # nudge = Version(signature=actual) # nudge.save() # latest_version = nudge diff = Diff(old_project_sig, project_sig) if not diff.is_empty(): print(style.NOTICE( 'Project signature has changed - an evolution is required')) if verbosity > 1: print(diff)