diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index a65500d76a..55b8faf38b 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -201,7 +201,7 @@ class Command(BaseCommand): pre_migrate_state = executor._create_project_state(with_applied_migrations=True) pre_migrate_apps = pre_migrate_state.apps emit_pre_migrate_signal( - self.verbosity, self.interactive, connection.alias, apps=pre_migrate_apps, plan=plan, + self.verbosity, self.interactive, connection.alias, stdout=self.stdout, apps=pre_migrate_apps, plan=plan, ) # Run the syncdb phase. @@ -266,7 +266,7 @@ class Command(BaseCommand): # Send the post_migrate signal, so individual apps can do whatever they need # to do at this point. emit_post_migrate_signal( - self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan, + self.verbosity, self.interactive, connection.alias, stdout=self.stdout, apps=post_migrate_apps, plan=plan, ) def migration_progress_callback(self, action, migration=None, fake=False): diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 1e55a24802..a7e122a15f 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -1,3 +1,5 @@ +import sys + from django.apps import apps from django.db import models @@ -21,7 +23,8 @@ def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs): if app_config.models_module is None: continue if verbosity >= 2: - print("Running pre-migrate handlers for application %s" % app_config.label) + stdout = kwargs.get('stdout', sys.stdout) + stdout.write('Running pre-migrate handlers for application %s' % app_config.label) models.signals.pre_migrate.send( sender=app_config, app_config=app_config, @@ -38,7 +41,8 @@ def emit_post_migrate_signal(verbosity, interactive, db, **kwargs): if app_config.models_module is None: continue if verbosity >= 2: - print("Running post-migrate handlers for application %s" % app_config.label) + stdout = kwargs.get('stdout', sys.stdout) + stdout.write('Running post-migrate handlers for application %s' % app_config.label) models.signals.post_migrate.send( sender=app_config, app_config=app_config, diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index a4ed248769..4eb55c906a 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -424,6 +424,11 @@ Arguments sent with this signal: For example, the :mod:`django.contrib.auth` app only prompts to create a superuser when ``interactive`` is ``True``. +``stdout`` + .. versionadded:: 4.0 + + A stream-like object where verbose output should be redirected. + ``using`` The alias of database on which a command will operate. @@ -478,6 +483,11 @@ Arguments sent with this signal: For example, the :mod:`django.contrib.auth` app only prompts to create a superuser when ``interactive`` is ``True``. +``stdout`` + .. versionadded:: 4.0 + + A stream-like object where verbose output should be redirected. + ``using`` The database alias used for synchronization. Defaults to the ``default`` database. diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 3d94e8a8a9..94782e891f 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -191,7 +191,11 @@ Serialization Signals ~~~~~~~ -* ... +* The new ``stdout`` argument for :func:`~django.db.models.signals.pre_migrate` + and :func:`~django.db.models.signals.post_migrate` signals allows redirecting + output to a stream-like object. It should be preferred over + :py:data:`sys.stdout` and :py:func:`print` when emitting verbose output in + order to allow proper capture when testing. Templates ~~~~~~~~~ diff --git a/tests/migrate_signals/tests.py b/tests/migrate_signals/tests.py index e3d00ee9e3..e084f26ed8 100644 --- a/tests/migrate_signals/tests.py +++ b/tests/migrate_signals/tests.py @@ -1,3 +1,5 @@ +from io import StringIO + from django.apps import apps from django.core import management from django.db import migrations @@ -5,7 +7,7 @@ from django.db.models import signals from django.test import TransactionTestCase, override_settings APP_CONFIG = apps.get_app_config('migrate_signals') -SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using', 'plan', 'apps'] +SIGNAL_ARGS = ['app_config', 'verbosity', 'interactive', 'using', 'stdout', 'plan', 'apps'] MIGRATE_DATABASE = 'default' MIGRATE_VERBOSITY = 0 MIGRATE_INTERACTIVE = False @@ -69,7 +71,7 @@ class MigrateSignalTests(TransactionTestCase): post_migrate_receiver = Receiver(signals.post_migrate) management.call_command( 'migrate', database=MIGRATE_DATABASE, verbosity=MIGRATE_VERBOSITY, - interactive=MIGRATE_INTERACTIVE, + interactive=MIGRATE_INTERACTIVE, stdout=StringIO('test_args'), ) for receiver in [pre_migrate_receiver, post_migrate_receiver]: @@ -81,6 +83,7 @@ class MigrateSignalTests(TransactionTestCase): self.assertEqual(args['verbosity'], MIGRATE_VERBOSITY) self.assertEqual(args['interactive'], MIGRATE_INTERACTIVE) self.assertEqual(args['using'], 'default') + self.assertIn('test_args', args['stdout'].getvalue()) self.assertEqual(args['plan'], []) self.assertIsInstance(args['apps'], migrations.state.StateApps) diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index da9a571e8a..2477872423 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -39,10 +39,12 @@ class MigrateTests(MigrationTestBase): self.assertTableNotExists("migrations_book") # Run the migrations to 0001 only stdout = io.StringIO() - call_command('migrate', 'migrations', '0001', verbosity=1, stdout=stdout, no_color=True) + call_command('migrate', 'migrations', '0001', verbosity=2, stdout=stdout, no_color=True) stdout = stdout.getvalue() self.assertIn('Target specific migration: 0001_initial, from migrations', stdout) self.assertIn('Applying migrations.0001_initial... OK', stdout) + self.assertIn('Running pre-migrate handlers for application migrations', stdout) + self.assertIn('Running post-migrate handlers for application migrations', stdout) # The correct tables exist self.assertTableExists("migrations_author") self.assertTableExists("migrations_tribble") @@ -55,10 +57,12 @@ class MigrateTests(MigrationTestBase): self.assertTableExists("migrations_book") # Unmigrate everything stdout = io.StringIO() - call_command('migrate', 'migrations', 'zero', verbosity=1, stdout=stdout, no_color=True) + call_command('migrate', 'migrations', 'zero', verbosity=2, stdout=stdout, no_color=True) stdout = stdout.getvalue() self.assertIn('Unapply all migrations: migrations', stdout) self.assertIn('Unapplying migrations.0002_second... OK', stdout) + self.assertIn('Running pre-migrate handlers for application migrations', stdout) + self.assertIn('Running post-migrate handlers for application migrations', stdout) # Tables are gone self.assertTableNotExists("migrations_author") self.assertTableNotExists("migrations_tribble")