|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- import datetime
- import importlib
- import os
- import sys
-
- from django.apps import apps
- from django.core.management.base import OutputWrapper
- from django.db.models import NOT_PROVIDED
- from django.utils import timezone
- from django.utils.version import get_docs_version
-
- from .loader import MigrationLoader
-
-
- class MigrationQuestioner:
- """
- Give the autodetector responses to questions it might have.
- This base class has a built-in noninteractive mode, but the
- interactive subclass is what the command-line arguments will use.
- """
-
- def __init__(self, defaults=None, specified_apps=None, dry_run=None):
- self.defaults = defaults or {}
- self.specified_apps = specified_apps or set()
- self.dry_run = dry_run
-
- def ask_initial(self, app_label):
- """Should we create an initial migration for the app?"""
- # If it was specified on the command line, definitely true
- if app_label in self.specified_apps:
- return True
- # Otherwise, we look to see if it has a migrations module
- # without any Python files in it, apart from __init__.py.
- # Apps from the new app template will have these; the Python
- # file check will ensure we skip South ones.
- try:
- app_config = apps.get_app_config(app_label)
- except LookupError: # It's a fake app.
- return self.defaults.get("ask_initial", False)
- migrations_import_path, _ = MigrationLoader.migrations_module(app_config.label)
- if migrations_import_path is None:
- # It's an application with migrations disabled.
- return self.defaults.get("ask_initial", False)
- try:
- migrations_module = importlib.import_module(migrations_import_path)
- except ImportError:
- return self.defaults.get("ask_initial", False)
- else:
- if getattr(migrations_module, "__file__", None):
- filenames = os.listdir(os.path.dirname(migrations_module.__file__))
- elif hasattr(migrations_module, "__path__"):
- if len(migrations_module.__path__) > 1:
- return False
- filenames = os.listdir(list(migrations_module.__path__)[0])
- return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
-
- def ask_not_null_addition(self, field_name, model_name):
- """Adding a NOT NULL field to a model."""
- # None means quit
- return None
-
- def ask_not_null_alteration(self, field_name, model_name):
- """Changing a NULL field to NOT NULL."""
- # None means quit
- return None
-
- def ask_rename(self, model_name, old_name, new_name, field_instance):
- """Was this field really renamed?"""
- return self.defaults.get("ask_rename", False)
-
- def ask_rename_model(self, old_model_state, new_model_state):
- """Was this model really renamed?"""
- return self.defaults.get("ask_rename_model", False)
-
- def ask_merge(self, app_label):
- """Should these migrations really be merged?"""
- return self.defaults.get("ask_merge", False)
-
- def ask_auto_now_add_addition(self, field_name, model_name):
- """Adding an auto_now_add field to a model."""
- # None means quit
- return None
-
- def ask_unique_callable_default_addition(self, field_name, model_name):
- """Adding a unique field with a callable default."""
- # None means continue.
- return None
-
-
- class InteractiveMigrationQuestioner(MigrationQuestioner):
- def __init__(
- self, defaults=None, specified_apps=None, dry_run=None, prompt_output=None
- ):
- super().__init__(
- defaults=defaults, specified_apps=specified_apps, dry_run=dry_run
- )
- self.prompt_output = prompt_output or OutputWrapper(sys.stdout)
-
- def _boolean_input(self, question, default=None):
- self.prompt_output.write(f"{question} ", ending="")
- result = input()
- if not result and default is not None:
- return default
- while not result or result[0].lower() not in "yn":
- self.prompt_output.write("Please answer yes or no: ", ending="")
- result = input()
- return result[0].lower() == "y"
-
- def _choice_input(self, question, choices):
- self.prompt_output.write(f"{question}")
- for i, choice in enumerate(choices):
- self.prompt_output.write(" %s) %s" % (i + 1, choice))
- self.prompt_output.write("Select an option: ", ending="")
- result = input()
- while True:
- try:
- value = int(result)
- except ValueError:
- pass
- else:
- if 0 < value <= len(choices):
- return value
- self.prompt_output.write("Please select a valid option: ", ending="")
- result = input()
-
- def _ask_default(self, default=""):
- """
- Prompt for a default value.
-
- The ``default`` argument allows providing a custom default value (as a
- string) which will be shown to the user and used as the return value
- if the user doesn't provide any other input.
- """
- self.prompt_output.write("Please enter the default value as valid Python.")
- if default:
- self.prompt_output.write(
- f"Accept the default '{default}' by pressing 'Enter' or "
- f"provide another value."
- )
- self.prompt_output.write(
- "The datetime and django.utils.timezone modules are available, so "
- "it is possible to provide e.g. timezone.now as a value."
- )
- self.prompt_output.write("Type 'exit' to exit this prompt")
- while True:
- if default:
- prompt = "[default: {}] >>> ".format(default)
- else:
- prompt = ">>> "
- self.prompt_output.write(prompt, ending="")
- code = input()
- if not code and default:
- code = default
- if not code:
- self.prompt_output.write(
- "Please enter some code, or 'exit' (without quotes) to exit."
- )
- elif code == "exit":
- sys.exit(1)
- else:
- try:
- return eval(code, {}, {"datetime": datetime, "timezone": timezone})
- except (SyntaxError, NameError) as e:
- self.prompt_output.write("Invalid input: %s" % e)
-
- def ask_not_null_addition(self, field_name, model_name):
- """Adding a NOT NULL field to a model."""
- if not self.dry_run:
- choice = self._choice_input(
- f"It is impossible to add a non-nullable field '{field_name}' "
- f"to {model_name} without specifying a default. This is "
- f"because the database needs something to populate existing "
- f"rows.\n"
- f"Please select a fix:",
- [
- (
- "Provide a one-off default now (will be set on all existing "
- "rows with a null value for this column)"
- ),
- "Quit and manually define a default value in models.py.",
- ],
- )
- if choice == 2:
- sys.exit(3)
- else:
- return self._ask_default()
- return None
-
- def ask_not_null_alteration(self, field_name, model_name):
- """Changing a NULL field to NOT NULL."""
- if not self.dry_run:
- choice = self._choice_input(
- f"It is impossible to change a nullable field '{field_name}' "
- f"on {model_name} to non-nullable without providing a "
- f"default. This is because the database needs something to "
- f"populate existing rows.\n"
- f"Please select a fix:",
- [
- (
- "Provide a one-off default now (will be set on all existing "
- "rows with a null value for this column)"
- ),
- "Ignore for now. Existing rows that contain NULL values "
- "will have to be handled manually, for example with a "
- "RunPython or RunSQL operation.",
- "Quit and manually define a default value in models.py.",
- ],
- )
- if choice == 2:
- return NOT_PROVIDED
- elif choice == 3:
- sys.exit(3)
- else:
- return self._ask_default()
- return None
-
- def ask_rename(self, model_name, old_name, new_name, field_instance):
- """Was this field really renamed?"""
- msg = "Was %s.%s renamed to %s.%s (a %s)? [y/N]"
- return self._boolean_input(
- msg
- % (
- model_name,
- old_name,
- model_name,
- new_name,
- field_instance.__class__.__name__,
- ),
- False,
- )
-
- def ask_rename_model(self, old_model_state, new_model_state):
- """Was this model really renamed?"""
- msg = "Was the model %s.%s renamed to %s? [y/N]"
- return self._boolean_input(
- msg
- % (old_model_state.app_label, old_model_state.name, new_model_state.name),
- False,
- )
-
- def ask_merge(self, app_label):
- return self._boolean_input(
- "\nMerging will only work if the operations printed above do not conflict\n"
- + "with each other (working on different fields or models)\n"
- + "Should these migration branches be merged? [y/N]",
- False,
- )
-
- def ask_auto_now_add_addition(self, field_name, model_name):
- """Adding an auto_now_add field to a model."""
- if not self.dry_run:
- choice = self._choice_input(
- f"It is impossible to add the field '{field_name}' with "
- f"'auto_now_add=True' to {model_name} without providing a "
- f"default. This is because the database needs something to "
- f"populate existing rows.\n",
- [
- "Provide a one-off default now which will be set on all "
- "existing rows",
- "Quit and manually define a default value in models.py.",
- ],
- )
- if choice == 2:
- sys.exit(3)
- else:
- return self._ask_default(default="timezone.now")
- return None
-
- def ask_unique_callable_default_addition(self, field_name, model_name):
- """Adding a unique field with a callable default."""
- if not self.dry_run:
- version = get_docs_version()
- choice = self._choice_input(
- f"Callable default on unique field {model_name}.{field_name} "
- f"will not generate unique values upon migrating.\n"
- f"Please choose how to proceed:\n",
- [
- f"Continue making this migration as the first step in "
- f"writing a manual migration to generate unique values "
- f"described here: "
- f"https://docs.djangoproject.com/en/{version}/howto/"
- f"writing-migrations/#migrations-that-add-unique-fields.",
- "Quit and edit field options in models.py.",
- ],
- )
- if choice == 2:
- sys.exit(3)
- return None
-
-
- class NonInteractiveMigrationQuestioner(MigrationQuestioner):
- def __init__(
- self,
- defaults=None,
- specified_apps=None,
- dry_run=None,
- verbosity=1,
- log=None,
- ):
- self.verbosity = verbosity
- self.log = log
- super().__init__(
- defaults=defaults,
- specified_apps=specified_apps,
- dry_run=dry_run,
- )
-
- def log_lack_of_migration(self, field_name, model_name, reason):
- if self.verbosity > 0:
- self.log(
- f"Field '{field_name}' on model '{model_name}' not migrated: "
- f"{reason}."
- )
-
- def ask_not_null_addition(self, field_name, model_name):
- # We can't ask the user, so act like the user aborted.
- self.log_lack_of_migration(
- field_name,
- model_name,
- "it is impossible to add a non-nullable field without specifying "
- "a default",
- )
- sys.exit(3)
-
- def ask_not_null_alteration(self, field_name, model_name):
- # We can't ask the user, so set as not provided.
- self.log(
- f"Field '{field_name}' on model '{model_name}' given a default of "
- f"NOT PROVIDED and must be corrected."
- )
- return NOT_PROVIDED
-
- def ask_auto_now_add_addition(self, field_name, model_name):
- # We can't ask the user, so act like the user aborted.
- self.log_lack_of_migration(
- field_name,
- model_name,
- "it is impossible to add a field with 'auto_now_add=True' without "
- "specifying a default",
- )
- sys.exit(3)
|