Fixed #19730 -- Don't validate importability of settings by using i18n in management commands.

They are handled independently now and the latter can be influenced by
the new BaseCommand.leave_locale_alone internal option.

Thanks chrischambers for the report, Claude, lpiatek, neaf and gabooo for
their work on a patch, originally on refs. #17379.
This commit is contained in:
Ramiro Morales 2013-02-03 20:53:48 -03:00
parent 2c173ff3b4
commit 869c9ba306
10 changed files with 153 additions and 46 deletions

View File

@ -143,6 +143,22 @@ class BaseCommand(object):
``self.validate(app)`` from ``handle()``, where ``app`` is the
application's Python module.
``leave_locale_alone``
A boolean indicating whether the locale set in settings should be
preserved during the execution of the command instead of being
forcibly set to 'en-us'.
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 because many of them create
database content that is locale-sensitive (like permissions) and that
content shouldn't contain any translations so making the locale differ
from the de facto default 'en-us' can cause unintended effects.
This option can't be False when the can_import_settings option is set
to False too because attempting to set the locale needs access to
settings. This condition will generate a CommandError.
"""
# Metadata about this command.
option_list = (
@ -163,6 +179,7 @@ class BaseCommand(object):
can_import_settings = True
requires_model_validation = True
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
leave_locale_alone = False
def __init__(self):
self.style = color_style()
@ -235,18 +252,28 @@ class BaseCommand(object):
needed (as controlled by the attribute
``self.requires_model_validation``, except if force-skipped).
"""
# Switch to English, because django-admin.py creates database content
# like permissions, and those shouldn't contain any translations.
# But only do this if we can assume we have a working settings file,
# because django.utils.translation requires settings.
saved_lang = None
self.stdout = OutputWrapper(options.get('stdout', sys.stdout))
self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR)
if self.can_import_settings:
from django.conf import settings
saved_locale = None
if not self.leave_locale_alone:
# Only mess with locales if we can assume we have a working
# settings file, because django.utils.translation requires settings
# (The final saying about whether the i18n machinery is active will be
# found in the value of the USE_I18N setting)
if not self.can_import_settings:
raise CommandError("Incompatible values of 'leave_locale_alone' "
"(%s) and 'can_import_settings' (%s) command "
"options." % (self.leave_locale_alone,
self.can_import_settings))
# Switch to US English, because django-admin.py creates database
# content like permissions, and those shouldn't contain any
# translations.
from django.utils import translation
saved_lang = translation.get_language()
saved_locale = translation.get_language()
translation.activate('en-us')
try:
@ -265,8 +292,8 @@ class BaseCommand(object):
if self.output_transaction:
self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;"))
finally:
if saved_lang is not None:
translation.activate(saved_lang)
if saved_locale is not None:
translation.activate(saved_locale)
def validate(self, app=None, display_num_errors=False):
"""

View File

@ -63,7 +63,7 @@ class Command(BaseCommand):
help = 'Compiles .po files to .mo files for use with builtin gettext support.'
requires_model_validation = False
can_import_settings = False
leave_locale_alone = True
def handle(self, **options):
locale = options.get('locale')

View File

@ -189,7 +189,7 @@ class Command(NoArgsCommand):
"--locale or --all options.")
requires_model_validation = False
can_import_settings = False
leave_locale_alone = True
def handle_noargs(self, *args, **options):
locale = options.get('locale')

View File

@ -61,6 +61,9 @@ class TemplateCommand(BaseCommand):
can_import_settings = False
# The supported URL schemes
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
def handle(self, app_or_project, name, target=None, **options):
self.app_or_project = app_or_project

View File

@ -112,19 +112,22 @@ In addition to being able to add custom command line options, all
:doc:`management commands</ref/django-admin>` can accept some
default options such as :djadminopt:`--verbosity` and :djadminopt:`--traceback`.
.. admonition:: Management commands and locales
.. _management-commands-and-locales:
The :meth:`BaseCommand.execute` method sets the hardcoded ``en-us`` locale
because the commands shipped with Django perform several tasks
(for example, user-facing content rendering and database population) that
require a system-neutral string language (for which we use ``en-us``).
Management commands and locales
===============================
If your custom management command uses another locale, you should manually
activate and deactivate it in your :meth:`~BaseCommand.handle` or
:meth:`~NoArgsCommand.handle_noargs` method using the functions provided by
the I18N support code:
By default, the :meth:`BaseCommand.execute` method sets the hardcoded 'en-us'
locale because most of the commands shipped with Django perform several tasks
(for example, user-facing content rendering and database population) that
require a system-neutral string language (for which we use 'en-us').
.. code-block:: python
If, for some reason, your custom management command needs to use a fixed locale
different from 'en-us', you should manually activate and deactivate it in your
:meth:`~BaseCommand.handle` or :meth:`~NoArgsCommand.handle_noargs` method using
the functions provided by the I18N support code:
.. code-block:: python
from django.core.management.base import BaseCommand, CommandError
from django.utils import translation
@ -138,8 +141,7 @@ default options such as :djadminopt:`--verbosity` and :djadminopt:`--traceback`.
# Activate a fixed locale, e.g. Russian
translation.activate('ru')
# Or you can activate the LANGUAGE_CODE
# chosen in the settings:
# Or you can activate the LANGUAGE_CODE # chosen in the settings:
#
#from django.conf import settings
#translation.activate(settings.LANGUAGE_CODE)
@ -149,15 +151,20 @@ default options such as :djadminopt:`--verbosity` and :djadminopt:`--traceback`.
translation.deactivate()
Take into account though, that system management commands typically have to
be very careful about running in non-uniform locales, so:
Another need might be that your command simply should use the locale set in
settings and Django should be kept from forcing it to 'en-us'. You can achieve
it by using the :data:`BaseCommand.leave_locale_alone` option.
* Make sure the :setting:`USE_I18N` setting is always ``True`` when running
the command (this is one good example of the potential problems stemming
When working on the scenarios described above though, take into account that
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 command (this is a good example of the potential problems stemming
from a dynamic runtime environment that Django commands avoid offhand by
always using a fixed locale).
* Review the code of your command and the code it calls for behavioral
* 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.
@ -222,6 +229,29 @@ All attributes can be set in your derived class and can be used in
rather than all applications' models, call
:meth:`~BaseCommand.validate` from :meth:`~BaseCommand.handle`.
.. 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 being forcibly set to 'en-us'.
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 because many of them create database
content that is locale-sensitive (like permissions) and that content
shouldn't contain any translations so making the locale differ from the de
facto default 'en-us' can cause unintended effects. See the `Management
commands and locales`_ section above for further details.
This option can't be ``False`` when the
:data:`~BaseCommand.can_import_settings` option is set to ``False`` too
because attempting to set the locale needs access to settings. This condition
will generate a :class:`CommandError`.
.. versionadded:: 1.6
The ``leave_locale_alone`` option was added in Django 1.6.
Methods
-------

View File

@ -39,6 +39,14 @@ Minor features
<lazy-plural-translations>` can be provided at translation time rather than
at definition time.
* For custom managemente commands: Validation of the presence of valid settings
in managements commands that ask for it by using the
:attr:`~django.core.management.BaseCommand.can_import_settings` internal
option is now performed independently from handling of the locale that should
active during the execution of the command. The latter can now be influenced
by the new :attr:`~django.core.management.BaseCommand.leave_locale_alone`
internal option. See :ref:`management-commands-and-locales` for more details.
Backwards incompatible changes in 1.6
=====================================

View File

@ -0,0 +1,10 @@
from django.core.management.base import BaseCommand
from django.utils import translation
class Command(BaseCommand):
can_import_settings = True
leave_locale_alone = False
def handle(self, *args, **options):
return translation.get_language()

View File

@ -0,0 +1,10 @@
from django.core.management.base import BaseCommand
from django.utils import translation
class Command(BaseCommand):
can_import_settings = True
leave_locale_alone = True
def handle(self, *args, **options):
return translation.get_language()

View File

@ -44,3 +44,17 @@ class CommandTests(TestCase):
finally:
sys.stderr = old_stderr
self.assertIn("CommandError", err.getvalue())
def test_default_en_us_locale_set(self):
# Forces en_us when set to true
out = StringIO()
with translation.override('pl'):
management.call_command('leave_locale_alone_false', stdout=out)
self.assertEqual(out.getvalue(), "en-us\n")
def test_configured_locale_preserved(self):
# Leaves locale from settings when set to false
out = StringIO()
with translation.override('pl'):
management.call_command('leave_locale_alone_true', stdout=out)
self.assertEqual(out.getvalue(), "pl\n")

View File

@ -110,6 +110,11 @@ class BasicExtractorTests(ExtractorTests):
self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents)
self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents)
def test_force_en_us_locale(self):
"""Value of locale-munging option used by the command is the right one"""
from django.core.management.commands.makemessages import Command
self.assertTrue(Command.leave_locale_alone)
def test_extraction_error(self):
os.chdir(self.test_dir)
self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0)