Skip to content

Commit

Permalink
Make questioner prompt responsive to --scriptable
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobtylerwalls committed Sep 4, 2021
1 parent 8260129 commit b93f083
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 31 deletions.
4 changes: 3 additions & 1 deletion django/core/management/commands/makemigrations.py
Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions django/db/migrations/questioner.py
Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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."""
Expand Down
24 changes: 10 additions & 14 deletions tests/migrations/test_commands.py
Expand Up @@ -2,7 +2,6 @@
import importlib
import io
import os
import sys
from unittest import mock

from django.apps import apps
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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):
Expand Down
8 changes: 4 additions & 4 deletions tests/migrations/test_questioner.py
Expand Up @@ -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)

0 comments on commit b93f083

Please sign in to comment.