Browse Source

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

Jacob Walls 3 years ago
parent
commit
6f78cb6b13

+ 22 - 3
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

+ 7 - 0
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``
 -----------
 

+ 4 - 0
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
 ~~~~~~~~~~
 

+ 41 - 0
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