From b93f083510ed38f4420b73bc486efd37ef8bb2ba Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 23 Aug 2021 22:44:31 -0400 Subject: [PATCH] Make questioner prompt responsive to --scriptable --- .../management/commands/makemigrations.py | 4 ++- django/db/migrations/questioner.py | 28 +++++++++++-------- tests/migrations/test_commands.py | 24 +++++++--------- tests/migrations/test_questioner.py | 8 +++--- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 21a139ab00f37..885339df16fcb 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -152,7 +152,9 @@ def handle(self, *app_labels, **options): return self.handle_merge(loader, conflicts) if self.interactive: - questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) + prompt_destination = self.stderr if self.scriptable else self.stdout + questioner = InteractiveMigrationQuestioner( + specified_apps=app_labels, dry_run=self.dry_run, prompt_destination=prompt_destination) else: questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run) # Set up autodetector diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py index 3891616ce0157..a303c4aa051dc 100644 --- a/django/db/migrations/questioner.py +++ b/django/db/migrations/questioner.py @@ -81,6 +81,9 @@ def ask_auto_now_add_addition(self, field_name, model_name): class InteractiveMigrationQuestioner(MigrationQuestioner): + def __init__(self, defaults=None, specified_apps=None, dry_run=None, prompt_destination=sys.stdout): + super().__init__(defaults=defaults, specified_apps=specified_apps, dry_run=dry_run) + self.prompt_destination = prompt_destination def _boolean_input(self, question, default=None): result = input("%s " % question) @@ -91,9 +94,9 @@ def _boolean_input(self, question, default=None): return result[0].lower() == "y" def _choice_input(self, question, choices): - print(question) + self.prompt_destination.write(question + os.linesep) for i, choice in enumerate(choices): - print(" %s) %s" % (i + 1, choice)) + self.prompt_destination.write(" %s) %s" % (i + 1, choice) + os.linesep) result = input("Select an option: ") while True: try: @@ -113,17 +116,17 @@ def _ask_default(self, default=''): string) which will be shown to the user and used as the return value if the user doesn't provide any other input. """ - print('Please enter the default value as valid Python.') + self.prompt_destination.write('Please enter the default value as valid Python.' + os.linesep) if default: - print( - f"Accept the default '{default}' by pressing 'Enter' or " - f"provide another value." + self.prompt_destination.write( + f"Accept the default '{default}' by pressing 'Enter' or " + + 'provide another value.' + os.linesep ) - print( + self.prompt_destination.write( 'The datetime and django.utils.timezone modules are available, so ' - 'it is possible to provide e.g. timezone.now as a value.' - ) - print("Type 'exit' to exit this prompt") + 'it is possible to provide e.g. timezone.now as a value.' + + os.linesep) + self.prompt_destination.write("Type 'exit' to exit this prompt" + os.linesep) while True: if default: prompt = "[default: {}] >>> ".format(default) @@ -133,14 +136,15 @@ def _ask_default(self, default=''): if not code and default: code = default if not code: - print("Please enter some code, or 'exit' (without quotes) to exit.") + self.prompt_destination.write( + "Please enter some code, or 'exit' (with no quotes) to exit." + os.linesep) elif code == "exit": sys.exit(1) else: try: return eval(code, {}, {'datetime': datetime, 'timezone': timezone}) except (SyntaxError, NameError) as e: - print("Invalid input: %s" % e) + self.prompt_destination.write("Invalid input: %s" % e + os.linesep) def ask_not_null_addition(self, field_name, model_name): """Adding a NOT NULL field to a model.""" diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index d13e68c80275e..512639acbd064 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -2,7 +2,6 @@ import importlib import io import os -import sys from unittest import mock from django.apps import apps @@ -1376,9 +1375,9 @@ class Meta: "It is impossible to add a non-nullable field 'silly_field' to " "author without specifying a default. This is because the " "database needs something to populate existing rows.\n" - "Please select a fix:\n" + f"Please select a fix:{os.linesep}" " 1) Provide a one-off default now (will be set on all existing " - "rows with a null value for this column)\n" + f"rows with a null value for this column){os.linesep}" " 2) Quit and manually define a default value in models.py." ) with self.temporary_migration_module(module='migrations.test_migrations'): @@ -1435,13 +1434,13 @@ class Meta: "It is impossible to change a nullable field 'slug' on author to " "non-nullable without providing a default. This is because the " "database needs something to populate existing rows.\n" - "Please select a fix:\n" + f"Please select a fix:{os.linesep}" " 1) Provide a one-off default now (will be set on all existing " - "rows with a null value for this column)\n" + f"rows with a null value for this column){os.linesep}" " 2) Ignore for now. Existing rows that contain NULL values will " "have to be handled manually, for example with a RunPython or " - "RunSQL operation.\n" - " 3) Quit and manually define a default value in models.py." + f"RunSQL operation.{os.linesep}" + f" 3) Quit and manually define a default value in models.py.{os.linesep}" ) with self.temporary_migration_module(module='migrations.test_migrations'): # 3 - quit. @@ -1835,7 +1834,6 @@ def test_makemigrations_inconsistent_history_db_failure(self): self.assertEqual(str(cm.warning), msg) @mock.patch('builtins.input', return_value='1') - @mock.patch('django.db.migrations.questioner.sys.stdin', mock.MagicMock(encoding=sys.getdefaultencoding())) def test_makemigrations_auto_now_add_interactive(self, *args): """ makemigrations prompts the user when adding auto_now_add to an existing @@ -1852,17 +1850,15 @@ class Meta: "It is impossible to add the field 'creation_date' with " "'auto_now_add=True' to entry without providing a default. This " "is because the database needs something to populate existing " - "rows.\n\n" + f"rows.\n{os.linesep}" " 1) Provide a one-off default now which will be set on all " - "existing rows\n" + f"existing rows{os.linesep}" " 2) Quit and manually define a default value in models.py." ) # Monkeypatch interactive questioner to auto accept with mock.patch('django.db.migrations.questioner.sys.stdout', new_callable=io.StringIO) as prompt_stdout: - out = io.StringIO() with self.temporary_migration_module(module='migrations.test_auto_now_add'): - call_command('makemigrations', 'migrations', interactive=True, stdout=out) - output = out.getvalue() + call_command('makemigrations', 'migrations', interactive=True, stdout=prompt_stdout) prompt_output = prompt_stdout.getvalue() self.assertIn(input_msg, prompt_output) self.assertIn( @@ -1875,7 +1871,7 @@ class Meta: prompt_output, ) self.assertIn("Type 'exit' to exit this prompt", prompt_output) - self.assertIn("Add field creation_date to entry", output) + self.assertIn("Add field creation_date to entry", prompt_output) @mock.patch('builtins.input', return_value='2') def test_makemigrations_auto_now_add_interactive_quit(self, mock_input): diff --git a/tests/migrations/test_questioner.py b/tests/migrations/test_questioner.py index 9ab7756ab5749..b3aa002613684 100644 --- a/tests/migrations/test_questioner.py +++ b/tests/migrations/test_questioner.py @@ -20,14 +20,14 @@ def test_ask_initial_with_disabled_migrations(self): @mock.patch('builtins.input', return_value='datetime.timedelta(days=1)') def test_timedelta_default(self, mock): - questioner = InteractiveMigrationQuestioner() - with captured_stdout(): + with captured_stdout() as stdout: + questioner = InteractiveMigrationQuestioner(prompt_destination=stdout) value = questioner._ask_default() self.assertEqual(value, datetime.timedelta(days=1)) @mock.patch('builtins.input', return_value='2') def test_ask_not_null_alteration_not_provided(self, mock): - questioner = InteractiveMigrationQuestioner() - with captured_stdout(): + with captured_stdout() as stdout: + questioner = InteractiveMigrationQuestioner(prompt_destination=stdout) question = questioner.ask_not_null_alteration('field_name', 'model_name') self.assertEqual(question, NOT_PROVIDED)