Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 22 additions & 3 deletions django/core/management/commands/makemigrations.py
Expand Up @@ -57,9 +57,20 @@ def add_arguments(self, parser):
'--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):
Expand All @@ -73,6 +84,10 @@ def handle(self, *app_labels, **options):
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)
Expand Down Expand Up @@ -147,7 +162,7 @@ def handle(self, *app_labels, **options):
questioner = InteractiveMigrationQuestioner(
specified_apps=app_labels,
dry_run=self.dry_run,
prompt_output=self.stdout,
prompt_output=self.log_output,
)
else:
questioner = NonInteractiveMigrationQuestioner(
Expand Down Expand Up @@ -226,6 +241,8 @@ def write_migration_files(self, changes):
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)
Expand Down Expand Up @@ -254,7 +271,7 @@ def handle_merge(self, loader, conflicts):
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})

Expand Down Expand Up @@ -327,6 +344,8 @@ def all_items_equal(seq):
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
Expand Down
7 changes: 7 additions & 0 deletions docs/ref/django-admin.txt
Expand Up @@ -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``
-----------

Expand Down
4 changes: 4 additions & 0 deletions docs/releases/4.1.txt
Expand Up @@ -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
~~~~~~~~~~

Expand Down
41 changes: 41 additions & 0 deletions tests/migrations/test_commands.py
Expand Up @@ -1667,6 +1667,47 @@ class Meta:
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
Expand Down