summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2021-08-06 09:59:53 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-01-10 18:49:57 +0100
commit6f78cb6b13fca2512f80dcd2bce7838bf33b70f0 (patch)
treecb7599bf8851b490f3423a862867d93b0a7684fc
parent274771df9133542df048cc104c19e7756f9d3715 (diff)
Fixed #29026 -- Added --scriptable option to makemigrations.
-rw-r--r--django/core/management/commands/makemigrations.py25
-rw-r--r--docs/ref/django-admin.txt7
-rw-r--r--docs/releases/4.1.txt4
-rw-r--r--tests/migrations/test_commands.py41
4 files changed, 74 insertions, 3 deletions
diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py
index 4d26bfe278..4349f33a61 100644
--- a/django/core/management/commands/makemigrations.py
+++ b/django/core/management/commands/makemigrations.py
@@ -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
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 6188324d52..78770fbf72 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -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``
-----------
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index 219201fd00..8cdec777f9 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -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
~~~~~~~~~~
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index ef5db241d6..f274c8486b 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -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