def write_migration_files(self, changes): """ Take a changes dict and write them out as migration files. """ directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.log( self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label)) for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration, self.include_header) if self.verbosity >= 1: # Display a relative path if it's below the current working # directory, or an absolute path otherwise. try: migration_string = os.path.relpath(writer.path) except ValueError: migration_string = writer.path if migration_string.startswith(".."): migration_string = writer.path self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string)) for operation in migration.operations: self.log(" - %s" % operation.describe()) if self.scriptable: self.stdout.write(migration_string) if not self.dry_run: # Write the migrations file to the disk. migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label): os.makedirs(migrations_directory, exist_ok=True) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "w", encoding="utf-8") as fh: fh.write(migration_string) self.written_files.append(writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will log the migrations rather than saving the file to # the disk. self.log( self.style.MIGRATE_HEADING( "Full migrations file '%s':" % writer.filename)) self.log(writer.as_string()) run_formatters(self.written_files)
class TemplateCommand(BaseCommand): """ Copy either a Django application layout template or a Django project layout template into the specified directory. :param style: A color style object (see django.core.management.color). :param app_or_project: The string 'app' or 'project'. :param name: The name of the application or project. :param directory: The directory to which the template should be copied. :param options: The additional variables passed to project or app templates """ requires_system_checks = [] # The supported URL schemes url_schemes = ["http", "https", "ftp"] # Rewrite the following suffixes when determining the target filename. rewrite_template_suffixes = ( # Allow shipping invalid .py files without byte-compilation. (".py-tpl", ".py"), ) def add_arguments(self, parser): parser.add_argument("name", help="Name of the application or project.") parser.add_argument( "directory", nargs="?", help="Optional destination directory" ) parser.add_argument( "--template", help="The path or URL to load the template from." ) parser.add_argument( "--extension", "-e", dest="extensions", action="append", default=["py"], help='The file extension(s) to render (default: "py"). ' "Separate multiple extensions with commas, or use " "-e multiple times.", ) parser.add_argument( "--name", "-n", dest="files", action="append", default=[], help="The file name(s) to render. Separate multiple file names " "with commas, or use -n multiple times.", ) parser.add_argument( "--exclude", "-x", action="append", default=argparse.SUPPRESS, nargs="?", const="", help=( "The directory name(s) to exclude, in addition to .git and " "__pycache__. Can be used multiple times." ), ) def handle(self, app_or_project, name, target=None, **options): self.written_files = [] self.app_or_project = app_or_project self.a_or_an = "an" if app_or_project == "app" else "a" self.paths_to_remove = [] self.verbosity = options["verbosity"] self.validate_name(name) # if some directory is given, make sure it's nicely expanded if target is None: top_dir = os.path.join(os.getcwd(), name) try: os.makedirs(top_dir) except FileExistsError: raise CommandError("'%s' already exists" % top_dir) except OSError as e: raise CommandError(e) else: top_dir = os.path.abspath(os.path.expanduser(target)) if app_or_project == "app": self.validate_name(os.path.basename(top_dir), "directory") if not os.path.exists(top_dir): raise CommandError( "Destination directory '%s' does not " "exist, please create it first." % top_dir ) extensions = tuple(handle_extensions(options["extensions"])) extra_files = [] excluded_directories = [".git", "__pycache__"] for file in options["files"]: extra_files.extend(map(lambda x: x.strip(), file.split(","))) if exclude := options.get("exclude"): for directory in exclude: excluded_directories.append(directory.strip()) if self.verbosity >= 2: self.stdout.write( "Rendering %s template files with extensions: %s" % (app_or_project, ", ".join(extensions)) ) self.stdout.write( "Rendering %s template files with filenames: %s" % (app_or_project, ", ".join(extra_files)) ) base_name = "%s_name" % app_or_project base_subdir = "%s_template" % app_or_project base_directory = "%s_directory" % app_or_project camel_case_name = "camel_case_%s_name" % app_or_project camel_case_value = "".join(x for x in name.title() if x != "_") context = Context( { **options, base_name: name, base_directory: top_dir, camel_case_name: camel_case_value, "docs_version": get_docs_version(), "django_version": django.__version__, }, autoescape=False, ) # Setup a stub settings environment for template rendering if not settings.configured: settings.configure() django.setup() template_dir = self.handle_template(options["template"], base_subdir) prefix_length = len(template_dir) + 1 for root, dirs, files in os.walk(template_dir): path_rest = root[prefix_length:] relative_dir = path_rest.replace(base_name, name) if relative_dir: target_dir = os.path.join(top_dir, relative_dir) os.makedirs(target_dir, exist_ok=True) for dirname in dirs[:]: if "exclude" not in options: if dirname.startswith(".") or dirname == "__pycache__": dirs.remove(dirname) elif dirname in excluded_directories: dirs.remove(dirname) for filename in files: if filename.endswith((".pyo", ".pyc", ".py.class")): # Ignore some files as they cause various breakages. continue old_path = os.path.join(root, filename) new_path = os.path.join( top_dir, relative_dir, filename.replace(base_name, name) ) for old_suffix, new_suffix in self.rewrite_template_suffixes: if new_path.endswith(old_suffix): new_path = new_path[: -len(old_suffix)] + new_suffix break # Only rewrite once if os.path.exists(new_path): raise CommandError( "%s already exists. Overlaying %s %s into an existing " "directory won't replace conflicting files." % ( new_path, self.a_or_an, app_or_project, ) ) # Only render the Python files, as we don't want to # accidentally render Django templates files if new_path.endswith(extensions) or filename in extra_files: with open(old_path, encoding="utf-8") as template_file: content = template_file.read() template = Engine().from_string(content) content = template.render(context) with open(new_path, "w", encoding="utf-8") as new_file: new_file.write(content) else: shutil.copyfile(old_path, new_path) self.written_files.append(new_path) if self.verbosity >= 2: self.stdout.write("Creating %s" % new_path) try: self.apply_umask(old_path, new_path) self.make_writeable(new_path) except OSError: self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " "problem." % new_path, self.style.NOTICE, ) if self.paths_to_remove: if self.verbosity >= 2: self.stdout.write("Cleaning up temporary files.") for path_to_remove in self.paths_to_remove: if os.path.isfile(path_to_remove): os.remove(path_to_remove) else: shutil.rmtree(path_to_remove) run_formatters(self.written_files)
def handle_merge(self, loader, conflicts): """ Handles merging together conflicted migrations interactively, if it's safe; otherwise, advises on how to fix it. """ if self.interactive: questioner = InteractiveMigrationQuestioner(prompt_output=self.log_output) else: questioner = MigrationQuestioner(defaults={"ask_merge": True}) for app_label, migration_names in conflicts.items(): # Grab out the migrations in question, and work out their # common ancestor. merge_migrations = [] for migration_name in migration_names: migration = loader.get_migration(app_label, migration_name) migration.ancestry = [ mig for mig in loader.graph.forwards_plan((app_label, migration_name)) if mig[0] == migration.app_label ] merge_migrations.append(migration) def all_items_equal(seq): return all(item == seq[0] for item in seq[1:]) merge_migrations_generations = zip(*(m.ancestry for m in merge_migrations)) common_ancestor_count = sum( 1 for common_ancestor_generation in takewhile( all_items_equal, merge_migrations_generations ) ) if not common_ancestor_count: raise ValueError( "Could not find common ancestor of %s" % migration_names ) # Now work out the operations along each divergent branch for migration in merge_migrations: migration.branch = migration.ancestry[common_ancestor_count:] migrations_ops = ( loader.get_migration(node_app, node_name).operations for node_app, node_name in migration.branch ) migration.merged_operations = sum(migrations_ops, []) # In future, this could use some of the Optimizer code # (can_optimize_through) to automatically see if they're # mergeable. For now, we always just prompt the user. if self.verbosity > 0: self.log(self.style.MIGRATE_HEADING("Merging %s" % app_label)) for migration in merge_migrations: self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) for operation in migration.merged_operations: self.log(" - %s" % operation.describe()) if questioner.ask_merge(app_label): # If they still want to merge it, then write out an empty # file depending on the migrations needing merging. numbers = [ MigrationAutodetector.parse_number(migration.name) for migration in merge_migrations ] try: biggest_number = max(x for x in numbers if x is not None) except ValueError: biggest_number = 1 subclass = type( "Migration", (Migration,), { "dependencies": [ (app_label, migration.name) for migration in merge_migrations ], }, ) parts = ["%04i" % (biggest_number + 1)] if self.migration_name: parts.append(self.migration_name) else: parts.append("merge") leaf_names = "_".join( sorted(migration.name for migration in merge_migrations) ) if len(leaf_names) > 47: parts.append(get_migration_name_timestamp()) else: parts.append(leaf_names) migration_name = "_".join(parts) new_migration = subclass(migration_name, app_label) writer = MigrationWriter(new_migration, self.include_header) if not self.dry_run: # Write the merge migrations file to the disk with open(writer.path, "w", encoding="utf-8") as fh: fh.write(writer.as_string()) run_formatters([writer.path]) if self.verbosity > 0: self.log("\nCreated new merge migration %s" % writer.path) if self.scriptable: self.stdout.write(writer.path) elif self.verbosity == 3: # Alternatively, makemigrations --merge --dry-run --verbosity 3 # will log the merge migrations rather than saving the file # to the disk. self.log( self.style.MIGRATE_HEADING( "Full merge migrations file '%s':" % writer.filename ) ) self.log(writer.as_string())
def write_migration_files(self, changes, update_previous_migration_paths=None): """ Take a changes dict and write them out as migration files. """ directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.log(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label)) for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration, self.include_header) if self.verbosity >= 1: # Display a relative path if it's below the current working # directory, or an absolute path otherwise. migration_string = self.get_relative_path(writer.path) self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string)) for operation in migration.operations: self.log(" - %s" % operation.describe()) if self.scriptable: self.stdout.write(migration_string) if not self.dry_run: # Write the migrations file to the disk. migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label): os.makedirs(migrations_directory, exist_ok=True) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "w", encoding="utf-8") as fh: fh.write(migration_string) self.written_files.append(writer.path) if update_previous_migration_paths: prev_path = update_previous_migration_paths[app_label] rel_prev_path = self.get_relative_path(prev_path) if writer.needs_manual_porting: migration_path = self.get_relative_path(writer.path) self.log( self.style.WARNING( f"Updated migration {migration_path} requires " f"manual porting.\n" f"Previous migration {rel_prev_path} was kept and " f"must be deleted after porting functions manually." ) ) else: os.remove(prev_path) self.log(f"Deleted {rel_prev_path}") elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will log the migrations rather than saving the file to # the disk. self.log( self.style.MIGRATE_HEADING( "Full migrations file '%s':" % writer.filename ) ) self.log(writer.as_string()) run_formatters(self.written_files)
def handle(self, **options): self.verbosity = options["verbosity"] self.interactive = options["interactive"] app_label = options["app_label"] start_migration_name = options["start_migration_name"] migration_name = options["migration_name"] no_optimize = options["no_optimize"] squashed_name = options["squashed_name"] include_header = options["include_header"] # Validate app_label. try: apps.get_app_config(app_label) except LookupError as err: raise CommandError(str(err)) # Load the current graph state, check the app and migration they asked # for exists. loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) if app_label not in loader.migrated_apps: raise CommandError( "App '%s' does not have migrations (so squashmigrations on " "it makes no sense)" % app_label) migration = self.find_migration(loader, app_label, migration_name) # Work out the list of predecessor migrations migrations_to_squash = [ loader.get_migration(al, mn) for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name)) if al == migration.app_label ] if start_migration_name: start_migration = self.find_migration(loader, app_label, start_migration_name) start = loader.get_migration(start_migration.app_label, start_migration.name) try: start_index = migrations_to_squash.index(start) migrations_to_squash = migrations_to_squash[start_index:] except ValueError: raise CommandError( "The migration '%s' cannot be found. Maybe it comes after " "the migration '%s'?\n" "Have a look at:\n" " python manage.py showmigrations %s\n" "to debug this issue." % (start_migration, migration, app_label)) # Tell them what we're doing and optionally ask if we should proceed if self.verbosity > 0 or self.interactive: self.stdout.write( self.style.MIGRATE_HEADING( "Will squash the following migrations:")) for migration in migrations_to_squash: self.stdout.write(" - %s" % migration.name) if self.interactive: answer = None while not answer or answer not in "yn": answer = input("Do you wish to proceed? [yN] ") if not answer: answer = "n" break else: answer = answer[0].lower() if answer != "y": return # Load the operations from all those migrations and concat together, # along with collecting external dependencies and detecting # double-squashing operations = [] dependencies = set() # We need to take all dependencies from the first migration in the list # as it may be 0002 depending on 0001 first_migration = True for smigration in migrations_to_squash: if smigration.replaces: raise CommandError( "You cannot squash squashed migrations! Please transition it to a " "normal migration first: https://docs.djangoproject.com/en/%s/" "topics/migrations/#squashing-migrations" % get_docs_version()) operations.extend(smigration.operations) for dependency in smigration.dependencies: if isinstance(dependency, SwappableTuple): if settings.AUTH_USER_MODEL == dependency.setting: dependencies.add(("__setting__", "AUTH_USER_MODEL")) else: dependencies.add(dependency) elif dependency[0] != smigration.app_label or first_migration: dependencies.add(dependency) first_migration = False if no_optimize: if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("(Skipping optimization.)")) new_operations = operations else: if self.verbosity > 0: self.stdout.write(self.style.MIGRATE_HEADING("Optimizing...")) optimizer = MigrationOptimizer() new_operations = optimizer.optimize(operations, migration.app_label) if self.verbosity > 0: if len(new_operations) == len(operations): self.stdout.write(" No optimizations possible.") else: self.stdout.write( " Optimized from %s operations to %s operations." % (len(operations), len(new_operations))) # Work out the value of replaces (any squashed ones we're re-squashing) # need to feed their replaces into ours replaces = [] for migration in migrations_to_squash: if migration.replaces: replaces.extend(migration.replaces) else: replaces.append((migration.app_label, migration.name)) # Make a new migration with those operations subclass = type( "Migration", (migrations.Migration, ), { "dependencies": dependencies, "operations": new_operations, "replaces": replaces, }, ) if start_migration_name: if squashed_name: # Use the name from --squashed-name. prefix, _ = start_migration.name.split("_", 1) name = "%s_%s" % (prefix, squashed_name) else: # Generate a name. name = "%s_squashed_%s" % (start_migration.name, migration.name) new_migration = subclass(name, app_label) else: name = "0001_%s" % (squashed_name or "squashed_%s" % migration.name) new_migration = subclass(name, app_label) new_migration.initial = True # Write out the new migration file writer = MigrationWriter(new_migration, include_header) if os.path.exists(writer.path): raise CommandError( f"Migration {new_migration.name} already exists. Use a different name." ) with open(writer.path, "w", encoding="utf-8") as fh: fh.write(writer.as_string()) run_formatters([writer.path]) if self.verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING( "Created new squashed migration %s" % writer.path) + "\n" " You should commit this migration but leave the old ones in place;\n" " the new migration will be used for new installs. Once you are sure\n" " all instances of the codebase have applied the migrations you " "squashed,\n" " you can delete them.") if writer.needs_manual_porting: self.stdout.write( self.style.MIGRATE_HEADING("Manual porting required") + "\n" " Your migrations contained functions that must be manually " "copied over,\n" " as we could not safely copy their implementation.\n" " See the comment at the top of the squashed migration for " "details.") if shutil.which("black"): self.stdout.write( self.style.WARNING( "Squashed migration couldn't be formatted using the " '"black" command. You can call it manually.'))
def handle(self, *args, **options): verbosity = options["verbosity"] app_label = options["app_label"] migration_name = options["migration_name"] check = options["check"] # Validate app_label. try: apps.get_app_config(app_label) except LookupError as err: raise CommandError(str(err)) # Load the current graph state. loader = MigrationLoader(None) if app_label not in loader.migrated_apps: raise CommandError(f"App '{app_label}' does not have migrations.") # Find a migration. try: migration = loader.get_migration_by_prefix(app_label, migration_name) except AmbiguityError: raise CommandError( f"More than one migration matches '{migration_name}' in app " f"'{app_label}'. Please be more specific." ) except KeyError: raise CommandError( f"Cannot find a migration matching '{migration_name}' from app " f"'{app_label}'." ) # Optimize the migration. optimizer = MigrationOptimizer() new_operations = optimizer.optimize(migration.operations, migration.app_label) if len(migration.operations) == len(new_operations): if verbosity > 0: self.stdout.write("No optimizations possible.") return else: if verbosity > 0: self.stdout.write( "Optimizing from %d operations to %d operations." % (len(migration.operations), len(new_operations)) ) if check: sys.exit(1) # Set the new migration optimizations. migration.operations = new_operations # Write out the optimized migration file. writer = MigrationWriter(migration) migration_file_string = writer.as_string() if writer.needs_manual_porting: if migration.replaces: raise CommandError( "Migration will require manual porting but is already a squashed " "migration.\nTransition to a normal migration first: " "https://docs.djangoproject.com/en/%s/topics/migrations/" "#squashing-migrations" % get_docs_version() ) # Make a new migration with those operations. subclass = type( "Migration", (migrations.Migration,), { "dependencies": migration.dependencies, "operations": new_operations, "replaces": [(migration.app_label, migration.name)], }, ) optimized_migration_name = "%s_optimized" % migration.name optimized_migration = subclass(optimized_migration_name, app_label) writer = MigrationWriter(optimized_migration) migration_file_string = writer.as_string() if verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING("Manual porting required") + "\n" " Your migrations contained functions that must be manually " "copied over,\n" " as we could not safely copy their implementation.\n" " See the comment at the top of the optimized migration for " "details." ) if shutil.which("black"): self.stdout.write( self.style.WARNING( "Optimized migration couldn't be formatted using the " '"black" command. You can call it manually.' ) ) with open(writer.path, "w", encoding="utf-8") as fh: fh.write(migration_file_string) run_formatters([writer.path]) if verbosity > 0: self.stdout.write( self.style.MIGRATE_HEADING(f"Optimized migration {writer.path}") )