Skip to content

Commit

Permalink
Fixed #17379 -- Removed management commands deactivation of the locale.
Browse files Browse the repository at this point in the history
  • Loading branch information
claudep committed May 13, 2018
1 parent 1e0cbc7 commit d65b0f7
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 127 deletions.
5 changes: 5 additions & 0 deletions django/apps/registry.py
Expand Up @@ -124,6 +124,11 @@ def populate(self, installed_apps=None):
def check_apps_ready(self): def check_apps_ready(self):
"""Raise an exception if all apps haven't been imported yet.""" """Raise an exception if all apps haven't been imported yet."""
if not self.apps_ready: if not self.apps_ready:
from django.conf import settings
# If "not ready" is due to unconfigured settings, accessing
# INSTALLED_APPS raises a more helpful ImproperlyConfigured
# exception.
settings.INSTALLED_APPS
raise AppRegistryNotReady("Apps aren't loaded yet.") raise AppRegistryNotReady("Apps aren't loaded yet.")


def check_models_ready(self): def check_models_ready(self):
Expand Down
70 changes: 29 additions & 41 deletions django/core/management/base.py
Expand Up @@ -73,6 +73,21 @@ def handle_default_options(options):
sys.path.insert(0, options.pythonpath) sys.path.insert(0, options.pythonpath)




def no_translations(handle_func):
"""Decorator that forces a command to run with translations deactivated."""
def wrapped(*args, **kwargs):
from django.utils import translation
saved_locale = translation.get_language()
translation.deactivate_all()
try:
res = handle_func(*args, **kwargs)
finally:
if saved_locale is not None:
translation.activate(saved_locale)
return res
return wrapped


class OutputWrapper(TextIOBase): class OutputWrapper(TextIOBase):
""" """
Wrapper around stdout/stderr Wrapper around stdout/stderr
Expand Down Expand Up @@ -171,19 +186,6 @@ class BaseCommand:
is the list of application's configuration provided by the is the list of application's configuration provided by the
app registry. app registry.
``leave_locale_alone``
A boolean indicating whether the locale set in settings should be
preserved during the execution of the command instead of translations
being deactivated.
Default value is ``False``.
Make sure you know what you are doing if you decide to change the value
of this option in your custom command if it creates database content
that is locale-sensitive and such content shouldn't contain any
translations (like it happens e.g. with django.contrib.auth
permissions) as activating any locale might cause unintended effects.
``stealth_options`` ``stealth_options``
A tuple of any options the command uses which aren't defined by the A tuple of any options the command uses which aren't defined by the
argument parser. argument parser.
Expand All @@ -194,7 +196,6 @@ class BaseCommand:
# Configuration shortcuts that alter various logic. # Configuration shortcuts that alter various logic.
_called_from_command_line = False _called_from_command_line = False
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
leave_locale_alone = False
requires_migrations_checks = False requires_migrations_checks = False
requires_system_checks = True requires_system_checks = True
# Arguments, common to all commands, which aren't defined by the argument # Arguments, common to all commands, which aren't defined by the argument
Expand Down Expand Up @@ -323,33 +324,20 @@ def execute(self, *args, **options):
if options.get('stderr'): if options.get('stderr'):
self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func) self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func)


saved_locale = None if self.requires_system_checks and not options.get('skip_checks'):
if not self.leave_locale_alone: self.check()
# Deactivate translations, because django-admin creates database if self.requires_migrations_checks:
# content like permissions, and those shouldn't contain any self.check_migrations()
# translations. output = self.handle(*args, **options)
from django.utils import translation if output:
saved_locale = translation.get_language() if self.output_transaction:
translation.deactivate_all() connection = connections[options.get('database', DEFAULT_DB_ALIAS)]

output = '%s\n%s\n%s' % (
try: self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
if self.requires_system_checks and not options.get('skip_checks'): output,
self.check() self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
if self.requires_migrations_checks: )
self.check_migrations() self.stdout.write(output)
output = self.handle(*args, **options)
if output:
if self.output_transaction:
connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
output = '%s\n%s\n%s' % (
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
output,
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
)
self.stdout.write(output)
finally:
if saved_locale is not None:
translation.activate(saved_locale)
return output return output


def _run_checks(self, **kwargs): def _run_checks(self, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion django/core/management/commands/compilemessages.py
Expand Up @@ -27,7 +27,6 @@ class Command(BaseCommand):
help = 'Compiles .po files to .mo files for use with builtin gettext support.' help = 'Compiles .po files to .mo files for use with builtin gettext support.'


requires_system_checks = False requires_system_checks = False
leave_locale_alone = True


program = 'msgfmt' program = 'msgfmt'
program_options = ['--check-format'] program_options = ['--check-format']
Expand Down
1 change: 0 additions & 1 deletion django/core/management/commands/makemessages.py
Expand Up @@ -207,7 +207,6 @@ class Command(BaseCommand):
build_file_class = BuildFile build_file_class = BuildFile


requires_system_checks = False requires_system_checks = False
leave_locale_alone = True


msgmerge_options = ['-q', '--previous'] msgmerge_options = ['-q', '--previous']
msguniq_options = ['--to-code=utf-8'] msguniq_options = ['--to-code=utf-8']
Expand Down
5 changes: 4 additions & 1 deletion django/core/management/commands/makemigrations.py
Expand Up @@ -4,7 +4,9 @@


from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import (
BaseCommand, CommandError, no_translations,
)
from django.db import DEFAULT_DB_ALIAS, connections, router from django.db import DEFAULT_DB_ALIAS, connections, router
from django.db.migrations import Migration from django.db.migrations import Migration
from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector
Expand Down Expand Up @@ -51,6 +53,7 @@ def add_arguments(self, parser):
help='Exit with a non-zero status if model changes are missing migrations.', help='Exit with a non-zero status if model changes are missing migrations.',
) )


@no_translations
def handle(self, *app_labels, **options): def handle(self, *app_labels, **options):
self.verbosity = options['verbosity'] self.verbosity = options['verbosity']
self.interactive = options['interactive'] self.interactive = options['interactive']
Expand Down
5 changes: 4 additions & 1 deletion django/core/management/commands/migrate.py
Expand Up @@ -4,7 +4,9 @@


from django.apps import apps from django.apps import apps
from django.core.checks import Tags, run_checks from django.core.checks import Tags, run_checks
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import (
BaseCommand, CommandError, no_translations,
)
from django.core.management.sql import ( from django.core.management.sql import (
emit_post_migrate_signal, emit_pre_migrate_signal, emit_post_migrate_signal, emit_pre_migrate_signal,
) )
Expand Down Expand Up @@ -58,6 +60,7 @@ def _run_checks(self, **kwargs):
issues.extend(super()._run_checks(**kwargs)) issues.extend(super()._run_checks(**kwargs))
return issues return issues


@no_translations
def handle(self, *args, **options): def handle(self, *args, **options):


self.verbosity = options['verbosity'] self.verbosity = options['verbosity']
Expand Down
1 change: 0 additions & 1 deletion django/core/management/commands/runserver.py
Expand Up @@ -25,7 +25,6 @@ class Command(BaseCommand):


# Validation is called explicitly each time the server is reloaded. # Validation is called explicitly each time the server is reloaded.
requires_system_checks = False requires_system_checks = False
leave_locale_alone = True
stealth_options = ('shutdown_message',) stealth_options = ('shutdown_message',)


default_addr = '127.0.0.1' default_addr = '127.0.0.1'
Expand Down
3 changes: 0 additions & 3 deletions django/core/management/templates.py
Expand Up @@ -32,9 +32,6 @@ class TemplateCommand(BaseCommand):
requires_system_checks = False requires_system_checks = False
# The supported URL schemes # The supported URL schemes
url_schemes = ['http', 'https', 'ftp'] url_schemes = ['http', 'https', 'ftp']
# Can't perform any active locale changes during this command, because
# setting might not be available at all.
leave_locale_alone = True
# Rewrite the following suffixes when determining the target filename. # Rewrite the following suffixes when determining the target filename.
rewrite_template_suffixes = ( rewrite_template_suffixes = (
# Allow shipping invalid .py files without byte-compilation. # Allow shipping invalid .py files without byte-compilation.
Expand Down
63 changes: 13 additions & 50 deletions docs/howto/custom-management-commands.txt
Expand Up @@ -126,52 +126,30 @@ such as :option:`--verbosity` and :option:`--traceback`.
Management commands and locales Management commands and locales
=============================== ===============================


By default, the :meth:`BaseCommand.execute` method deactivates translations By default, management commands are executed with the current active locale.
because some commands shipped with Django perform several tasks (for example,
user-facing content rendering and database population) that require a
project-neutral string language.


If, for some reason, your custom management command needs to use a fixed locale, If, for some reason, your custom management command must run without an active
you should manually activate and deactivate it in your locale (for example, to prevent translated content from being inserted into
:meth:`~BaseCommand.handle` method using the functions provided by the I18N the database), deactivate translations using the ``@no_translations``
support code:: decorator on your :meth:`~BaseCommand.handle` method::


from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, no_translations
from django.utils import translation


class Command(BaseCommand): class Command(BaseCommand):
... ...


@no_translations
def handle(self, *args, **options): def handle(self, *args, **options):

# Activate a fixed locale, e.g. Russian
translation.activate('ru')

# Or you can activate the LANGUAGE_CODE # chosen in the settings:
from django.conf import settings
translation.activate(settings.LANGUAGE_CODE)

# Your command logic here
... ...


translation.deactivate() Since translation deactivation requires access to configured settings, the

decorator can't be used for commands that work without configured settings.
Another need might be that your command simply should use the locale set in
settings and Django should be kept from deactivating it. You can achieve
it by using the :data:`BaseCommand.leave_locale_alone` option.


When working on the scenarios described above though, take into account that .. versionchanged:: 2.1
system management commands typically have to be very careful about running in
non-uniform locales, so you might need to:


* Make sure the :setting:`USE_I18N` setting is always ``True`` when running The ``@no_translations`` decorator is new. In older versions, translations
the command (this is a good example of the potential problems stemming are deactivated before running a command unless the command's
from a dynamic runtime environment that Django commands avoid offhand by ``leave_locale_alone`` attribute (now removed) is set to ``True``.
deactivating translations).

* Review the code of your command and the code it calls for behavioral
differences when locales are changed and evaluate its impact on
predictable behavior of your command.


Testing Testing
======= =======
Expand Down Expand Up @@ -247,21 +225,6 @@ All attributes can be set in your derived class and can be used in
A boolean; if ``True``, the entire Django project will be checked for A boolean; if ``True``, the entire Django project will be checked for
potential problems prior to executing the command. Default value is ``True``. potential problems prior to executing the command. Default value is ``True``.


.. attribute:: BaseCommand.leave_locale_alone

A boolean indicating whether the locale set in settings should be preserved
during the execution of the command instead of translations being
deactivated.

Default value is ``False``.

Make sure you know what you are doing if you decide to change the value of
this option in your custom command if it creates database content that
is locale-sensitive and such content shouldn't contain any translations
(like it happens e.g. with :mod:`django.contrib.auth` permissions) as
activating any locale might cause unintended effects. See the `Management
commands and locales`_ section above for further details.

.. attribute:: BaseCommand.style .. attribute:: BaseCommand.style


An instance attribute that helps create colored output when writing to An instance attribute that helps create colored output when writing to
Expand Down
2 changes: 1 addition & 1 deletion docs/releases/1.6.txt
Expand Up @@ -193,7 +193,7 @@ Minor features
option is now performed independently from handling of the locale that option is now performed independently from handling of the locale that
should be active during the execution of the command. The latter can now be should be active during the execution of the command. The latter can now be
influenced by the new influenced by the new
:attr:`~django.core.management.BaseCommand.leave_locale_alone` internal ``BaseCommand.leave_locale_alone`` internal
option. See :ref:`management-commands-and-locales` for more details. option. See :ref:`management-commands-and-locales` for more details.


* The :attr:`~django.views.generic.edit.DeletionMixin.success_url` of * The :attr:`~django.views.generic.edit.DeletionMixin.success_url` of
Expand Down
2 changes: 1 addition & 1 deletion docs/releases/1.8.txt
Expand Up @@ -1166,7 +1166,7 @@ Miscellaneous
that Django includes) will no longer convert null values back to an empty that Django includes) will no longer convert null values back to an empty
string. This is consistent with other backends. string. This is consistent with other backends.


* When the :attr:`~django.core.management.BaseCommand.leave_locale_alone` * When the ``BaseCommand.leave_locale_alone``
attribute is ``False``, translations are now deactivated instead of forcing attribute is ``False``, translations are now deactivated instead of forcing
the "en-us" locale. In the case your models contained non-English strings and the "en-us" locale. In the case your models contained non-English strings and
you counted on English translations to be activated in management commands, you counted on English translations to be activated in management commands,
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/2.1.txt
Expand Up @@ -418,6 +418,11 @@ Miscellaneous
* The database router :meth:`allow_relation` method is called in more cases. * The database router :meth:`allow_relation` method is called in more cases.
Improperly written routers may need to be updated accordingly. Improperly written routers may need to be updated accordingly.


* Translations are no longer deactivated before running management commands.
If your custom command requires translations to be deactivated (for example,
to insert untranslated content into the database), use the new
:ref:`@no_translations decorator <management-commands-and-locales>`.

.. _deprecated-features-2.1: .. _deprecated-features-2.1:


Features deprecated in 2.1 Features deprecated in 2.1
Expand Down
4 changes: 0 additions & 4 deletions tests/i18n/test_extraction.py
Expand Up @@ -194,10 +194,6 @@ def test_blocktrans_trimmed(self):
self.assertMsgId("Get my line number", po_contents) self.assertMsgId("Get my line number", po_contents)
self.assertLocationCommentPresent(self.PO_FILE, 'Get my line number', 'templates', 'test.html') self.assertLocationCommentPresent(self.PO_FILE, 'Get my line number', 'templates', 'test.html')


def test_force_en_us_locale(self):
"""Value of locale-munging option used by the command is the right one"""
self.assertTrue(MakeMessagesCommand.leave_locale_alone)

def test_extraction_error(self): def test_extraction_error(self):
msg = ( msg = (
'Translation blocks must not include other block tags: blocktrans ' 'Translation blocks must not include other block tags: blocktrans '
Expand Down
10 changes: 0 additions & 10 deletions tests/user_commands/management/commands/leave_locale_alone_true.py

This file was deleted.

@@ -1,10 +1,9 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand, no_translations
from django.utils import translation from django.utils import translation




class Command(BaseCommand): class Command(BaseCommand):


leave_locale_alone = False @no_translations

def handle(self, *args, **options): def handle(self, *args, **options):
return translation.get_language() return translation.get_language()
17 changes: 8 additions & 9 deletions tests/user_commands/tests.py
Expand Up @@ -63,17 +63,16 @@ def test_system_exit(self):
dance.Command.requires_system_checks = True dance.Command.requires_system_checks = True
self.assertIn("CommandError", stderr.getvalue()) self.assertIn("CommandError", stderr.getvalue())


def test_deactivate_locale_set(self): def test_no_translations_deactivate_translations(self):
# Deactivate translation when set to true """
When the Command handle method is decorated with @no_translations,
translations are deactivated inside the command.
"""
current_locale = translation.get_language()
with translation.override('pl'): with translation.override('pl'):
result = management.call_command('leave_locale_alone_false', stdout=StringIO()) result = management.call_command('no_translations', stdout=StringIO())
self.assertIsNone(result) self.assertIsNone(result)

self.assertEqual(translation.get_language(), current_locale)
def test_configured_locale_preserved(self):
# Leaves locale from settings when set to false
with translation.override('pl'):
result = management.call_command('leave_locale_alone_true', stdout=StringIO())
self.assertEqual(result, "pl")


def test_find_command_without_PATH(self): def test_find_command_without_PATH(self):
""" """
Expand Down

0 comments on commit d65b0f7

Please sign in to comment.