summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2021-12-31 10:30:48 -0500
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-01-03 12:30:51 +0100
commit0ab58c120939093fea90822f376e1866fc714d1f (patch)
tree83c0412efaf60c2715d5c3ffe8cd9e67dccb1fea
parent03a648811615cb623affc2d79dccd4b05919319e (diff)
Refs #29026 -- Allowed customizing InteractiveMigrationQuestioner's prompt destination.
Previously, the questioner did not obey the value of stdout provided to the command.
-rw-r--r--django/core/management/commands/makemigrations.py8
-rw-r--r--django/db/migrations/questioner.py35
-rw-r--r--tests/migrations/test_commands.py49
-rw-r--r--tests/migrations/test_questioner.py39
4 files changed, 74 insertions, 57 deletions
diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py
index 9077660467..4d26bfe278 100644
--- a/django/core/management/commands/makemigrations.py
+++ b/django/core/management/commands/makemigrations.py
@@ -144,7 +144,11 @@ class Command(BaseCommand):
return self.handle_merge(loader, conflicts)
if self.interactive:
- questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run)
+ questioner = InteractiveMigrationQuestioner(
+ specified_apps=app_labels,
+ dry_run=self.dry_run,
+ prompt_output=self.stdout,
+ )
else:
questioner = NonInteractiveMigrationQuestioner(
specified_apps=app_labels,
@@ -250,7 +254,7 @@ class Command(BaseCommand):
if it's safe; otherwise, advises on how to fix it.
"""
if self.interactive:
- questioner = InteractiveMigrationQuestioner()
+ questioner = InteractiveMigrationQuestioner(prompt_output=self.stdout)
else:
questioner = MigrationQuestioner(defaults={'ask_merge': True})
diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py
index 17883ca76c..3460e2b3ab 100644
--- a/django/db/migrations/questioner.py
+++ b/django/db/migrations/questioner.py
@@ -4,6 +4,7 @@ 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
@@ -87,20 +88,26 @@ class MigrationQuestioner:
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):
- result = input("%s " % question)
+ 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":
- result = input("Please answer yes or no: ")
+ self.prompt_output.write('Please answer yes or no: ', ending='')
+ result = input()
return result[0].lower() == "y"
def _choice_input(self, question, choices):
- print(question)
+ self.prompt_output.write(f'{question}')
for i, choice in enumerate(choices):
- print(" %s) %s" % (i + 1, choice))
- result = input("Select an option: ")
+ 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)
@@ -109,7 +116,8 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
else:
if 0 < value <= len(choices):
return value
- result = input("Please select a valid option: ")
+ self.prompt_output.write('Please select a valid option: ', ending='')
+ result = input()
def _ask_default(self, default=''):
"""
@@ -119,34 +127,35 @@ class InteractiveMigrationQuestioner(MigrationQuestioner):
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_output.write('Please enter the default value as valid Python.')
if default:
- print(
+ self.prompt_output.write(
f"Accept the default '{default}' by pressing 'Enter' or "
f"provide another value."
)
- print(
+ 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.'
)
- print("Type 'exit' to exit this prompt")
+ self.prompt_output.write("Type 'exit' to exit this prompt")
while True:
if default:
prompt = "[default: {}] >>> ".format(default)
else:
prompt = ">>> "
- code = input(prompt)
+ self.prompt_output.write(prompt, ending='')
+ code = input()
if not code and default:
code = default
if not code:
- print("Please enter some code, or 'exit' (without quotes) to exit.")
+ 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:
- print("Invalid input: %s" % 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."""
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index bf78b60a85..ef5db241d6 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -1304,7 +1304,15 @@ class MakeMigrationsTests(MigrationTestBase):
# Monkeypatch interactive questioner to auto reject
with mock.patch('builtins.input', mock.Mock(return_value='N')):
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
- call_command("makemigrations", "migrations", name="merge", merge=True, interactive=True, verbosity=0)
+ with captured_stdout():
+ call_command(
+ 'makemigrations',
+ 'migrations',
+ name='merge',
+ merge=True,
+ interactive=True,
+ verbosity=0,
+ )
merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertFalse(os.path.exists(merge_file))
@@ -1766,6 +1774,10 @@ class MakeMigrationsTests(MigrationTestBase):
' - remove field silly_field from author\n'
' - add field rating to author\n'
' - create model book\n'
+ '\n'
+ 'merging 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] '
)
def test_makemigrations_with_custom_name(self):
@@ -1886,30 +1898,25 @@ class MakeMigrationsTests(MigrationTestBase):
"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"
+ "rows.\n"
" 1) Provide a one-off default now which will be set on all "
"existing rows\n"
" 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()
- prompt_output = prompt_stdout.getvalue()
- self.assertIn(input_msg, prompt_output)
- self.assertIn(
- 'Please enter the default value as valid Python.',
- prompt_output,
- )
- self.assertIn(
- "Accept the default 'timezone.now' by pressing 'Enter' or "
- "provide another value.",
- prompt_output,
- )
- self.assertIn("Type 'exit' to exit this prompt", prompt_output)
- self.assertIn("Add field creation_date to entry", output)
+ prompt_stdout = io.StringIO()
+ with self.temporary_migration_module(module='migrations.test_auto_now_add'):
+ call_command('makemigrations', 'migrations', interactive=True, stdout=prompt_stdout)
+ prompt_output = prompt_stdout.getvalue()
+ self.assertIn(input_msg, prompt_output)
+ self.assertIn('Please enter the default value as valid Python.', prompt_output)
+ self.assertIn(
+ "Accept the default 'timezone.now' by pressing 'Enter' or provide "
+ "another value.",
+ prompt_output,
+ )
+ self.assertIn("Type 'exit' to exit this prompt", prompt_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):
@@ -1960,7 +1967,7 @@ class MakeMigrationsTests(MigrationTestBase):
input_msg = (
f'Callable default on unique field book.created will not generate '
f'unique values upon migrating.\n'
- f'Please choose how to proceed:\n\n'
+ f'Please choose how to proceed:\n'
f' 1) Continue making this migration as the first step in writing '
f'a manual migration to generate unique values described here: '
f'https://docs.djangoproject.com/en/{version}/howto/'
diff --git a/tests/migrations/test_questioner.py b/tests/migrations/test_questioner.py
index c9690d0b99..472a3f248f 100644
--- a/tests/migrations/test_questioner.py
+++ b/tests/migrations/test_questioner.py
@@ -1,12 +1,14 @@
import datetime
+from io import StringIO
from unittest import mock
+from django.core.management.base import OutputWrapper
from django.db.migrations.questioner import (
InteractiveMigrationQuestioner, MigrationQuestioner,
)
from django.db.models import NOT_PROVIDED
from django.test import SimpleTestCase
-from django.test.utils import captured_stdout, override_settings
+from django.test.utils import override_settings
class QuestionerTests(SimpleTestCase):
@@ -24,65 +26,60 @@ class QuestionerTests(SimpleTestCase):
@mock.patch('builtins.input', return_value='2')
def test_ask_not_null_alteration_not_provided(self, mock):
- questioner = InteractiveMigrationQuestioner()
- with captured_stdout():
- question = questioner.ask_not_null_alteration('field_name', 'model_name')
+ questioner = InteractiveMigrationQuestioner(prompt_output=OutputWrapper(StringIO()))
+ question = questioner.ask_not_null_alteration('field_name', 'model_name')
self.assertEqual(question, NOT_PROVIDED)
class QuestionerHelperMethodsTests(SimpleTestCase):
- questioner = InteractiveMigrationQuestioner()
+ def setUp(self):
+ self.prompt = OutputWrapper(StringIO())
+ self.questioner = InteractiveMigrationQuestioner(prompt_output=self.prompt)
@mock.patch('builtins.input', return_value='datetime.timedelta(days=1)')
def test_questioner_default_timedelta(self, mock_input):
- questioner = InteractiveMigrationQuestioner()
- with captured_stdout():
- value = questioner._ask_default()
+ value = self.questioner._ask_default()
self.assertEqual(value, datetime.timedelta(days=1))
@mock.patch('builtins.input', return_value='')
def test_questioner_default_no_user_entry(self, mock_input):
- with captured_stdout():
- value = self.questioner._ask_default(default='datetime.timedelta(days=1)')
+ value = self.questioner._ask_default(default='datetime.timedelta(days=1)')
self.assertEqual(value, datetime.timedelta(days=1))
@mock.patch('builtins.input', side_effect=['', 'exit'])
def test_questioner_no_default_no_user_entry(self, mock_input):
- with captured_stdout() as stdout, self.assertRaises(SystemExit):
+ with self.assertRaises(SystemExit):
self.questioner._ask_default()
self.assertIn(
"Please enter some code, or 'exit' (without quotes) to exit.",
- stdout.getvalue(),
+ self.prompt.getvalue(),
)
@mock.patch('builtins.input', side_effect=['bad code', 'exit'])
def test_questioner_no_default_bad_user_entry_code(self, mock_input):
- with captured_stdout() as stdout, self.assertRaises(SystemExit):
+ with self.assertRaises(SystemExit):
self.questioner._ask_default()
- self.assertIn('Invalid input: ', stdout.getvalue())
+ self.assertIn('Invalid input: ', self.prompt.getvalue())
@mock.patch('builtins.input', side_effect=['', 'n'])
def test_questioner_no_default_no_user_entry_boolean(self, mock_input):
- with captured_stdout():
- value = self.questioner._boolean_input('Proceed?')
+ value = self.questioner._boolean_input('Proceed?')
self.assertIs(value, False)
@mock.patch('builtins.input', return_value='')
def test_questioner_default_no_user_entry_boolean(self, mock_input):
- with captured_stdout():
- value = self.questioner._boolean_input('Proceed?', default=True)
+ value = self.questioner._boolean_input('Proceed?', default=True)
self.assertIs(value, True)
@mock.patch('builtins.input', side_effect=[10, 'garbage', 1])
def test_questioner_bad_user_choice(self, mock_input):
question = 'Make a choice:'
- with captured_stdout() as stdout:
- value = self.questioner._choice_input(question, choices='abc')
+ value = self.questioner._choice_input(question, choices='abc')
expected_msg = (
f'{question}\n'
f' 1) a\n'
f' 2) b\n'
f' 3) c\n'
)
- self.assertIn(expected_msg, stdout.getvalue())
+ self.assertIn(expected_msg, self.prompt.getvalue())
self.assertEqual(value, 1)