Fixed #29026 -- Added --scriptable option to makemigrations.

This commit is contained in:
Jacob Walls 2021-08-06 09:59:53 -04:00 committed by Mariusz Felisiak
parent 274771df91
commit 6f78cb6b13
4 changed files with 74 additions and 3 deletions

View File

@ -57,9 +57,20 @@ class Command(BaseCommand):
'--check', action='store_true', dest='check_changes',
help='Exit with a non-zero status if model changes are missing migrations.',
)
parser.add_argument(
'--scriptable', action='store_true', dest='scriptable',
help=(
'Divert log output and input prompts to stderr, writing only '
'paths of generated migration files to stdout.'
),
)
@property
def log_output(self):
return self.stderr if self.scriptable else self.stdout
def log(self, msg):
self.stdout.write(msg)
self.log_output.write(msg)
@no_translations
def handle(self, *app_labels, **options):
@ -73,6 +84,10 @@ class Command(BaseCommand):
raise CommandError('The migration name must be a valid Python identifier.')
self.include_header = options['include_header']
check_changes = options['check_changes']
self.scriptable = options['scriptable']
# If logs and prompts are diverted to stderr, remove the ERROR style.
if self.scriptable:
self.stderr.style_func = None
# Make sure the app they asked for exists
app_labels = set(app_labels)
@ -147,7 +162,7 @@ class Command(BaseCommand):
questioner = InteractiveMigrationQuestioner(
specified_apps=app_labels,
dry_run=self.dry_run,
prompt_output=self.stdout,
prompt_output=self.log_output,
)
else:
questioner = NonInteractiveMigrationQuestioner(
@ -226,6 +241,8 @@ class Command(BaseCommand):
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)
@ -254,7 +271,7 @@ class Command(BaseCommand):
if it's safe; otherwise, advises on how to fix it.
"""
if self.interactive:
questioner = InteractiveMigrationQuestioner(prompt_output=self.stdout)
questioner = InteractiveMigrationQuestioner(prompt_output=self.log_output)
else:
questioner = MigrationQuestioner(defaults={'ask_merge': True})
@ -327,6 +344,8 @@ class Command(BaseCommand):
fh.write(writer.as_string())
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

View File

@ -825,6 +825,13 @@ Generate migration files without Django version and timestamp header.
Makes ``makemigrations`` exit with a non-zero status when model changes without
migrations are detected.
.. django-admin-option:: --scriptable
.. versionadded:: 4.1
Diverts log output and input prompts to ``stderr``, writing only paths of
generated migration files to ``stdout``.
``migrate``
-----------

View File

@ -210,6 +210,10 @@ Management Commands
* :option:`makemigrations --no-input` now logs default answers and reasons why
migrations cannot be created.
* The new :option:`makemigrations --scriptable` options diverts log output and
input prompts to ``stderr``, writing only paths of generated migration files
to ``stdout``.
Migrations
~~~~~~~~~~

View File

@ -1667,6 +1667,47 @@ class MakeMigrationsTests(MigrationTestBase):
self.assertIn("model_name='sillymodel',", out.getvalue())
self.assertIn("name='silly_char',", out.getvalue())
def test_makemigrations_scriptable(self):
"""
With scriptable=True, log output is diverted to stderr, and only the
paths of generated migration files are written to stdout.
"""
out = io.StringIO()
err = io.StringIO()
with self.temporary_migration_module(
module='migrations.migrations.test_migrations',
) as migration_dir:
call_command(
'makemigrations',
'migrations',
scriptable=True,
stdout=out,
stderr=err,
)
initial_file = os.path.join(migration_dir, '0001_initial.py')
self.assertEqual(out.getvalue(), f'{initial_file}\n')
self.assertIn(' - Create model ModelWithCustomBase\n', err.getvalue())
@mock.patch('builtins.input', return_value='Y')
def test_makemigrations_scriptable_merge(self, mock_input):
out = io.StringIO()
err = io.StringIO()
with self.temporary_migration_module(
module='migrations.test_migrations_conflict',
) as migration_dir:
call_command(
'makemigrations',
'migrations',
merge=True,
name='merge',
scriptable=True,
stdout=out,
stderr=err,
)
merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertEqual(out.getvalue(), f'{merge_file}\n')
self.assertIn(f'Created new merge migration {merge_file}', err.getvalue())
def test_makemigrations_migrations_modules_path_not_exist(self):
"""
makemigrations creates migrations when specifying a custom location