From 6f78cb6b13fca2512f80dcd2bce7838bf33b70f0 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Fri, 6 Aug 2021 09:59:53 -0400 Subject: [PATCH] Fixed #29026 -- Added --scriptable option to makemigrations. --- .../management/commands/makemigrations.py | 25 +++++++++-- docs/ref/django-admin.txt | 7 ++++ docs/releases/4.1.txt | 4 ++ tests/migrations/test_commands.py | 41 +++++++++++++++++++ 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