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 ``self.validate(app)`` from ``handle()``, where ``app`` is the
application's Python module. 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. # Metadata about this command.
option_list = ( option_list = (
@ -163,6 +179,7 @@ class BaseCommand(object):
can_import_settings = True can_import_settings = True
requires_model_validation = True requires_model_validation = True
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
def __init__(self): def __init__(self):
self.style = color_style() self.style = color_style()
@ -235,18 +252,28 @@ class BaseCommand(object):
needed (as controlled by the attribute needed (as controlled by the attribute
``self.requires_model_validation``, except if force-skipped). ``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.stdout = OutputWrapper(options.get('stdout', sys.stdout))
self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR)
if self.can_import_settings: 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 from django.utils import translation
saved_lang = translation.get_language() saved_locale = translation.get_language()
translation.activate('en-us') translation.activate('en-us')
try: try:
@ -265,8 +292,8 @@ class BaseCommand(object):
if self.output_transaction: if self.output_transaction:
self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;")) self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;"))
finally: finally:
if saved_lang is not None: if saved_locale is not None:
translation.activate(saved_lang) translation.activate(saved_locale)
def validate(self, app=None, display_num_errors=False): 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.' help = 'Compiles .po files to .mo files for use with builtin gettext support.'
requires_model_validation = False requires_model_validation = False
can_import_settings = False leave_locale_alone = True
def handle(self, **options): def handle(self, **options):
locale = options.get('locale') locale = options.get('locale')

View File

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

View File

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

View File

@ -112,54 +112,61 @@ In addition to being able to add custom command line options, all
:doc:`management commands</ref/django-admin>` can accept some :doc:`management commands</ref/django-admin>` can accept some
default options such as :djadminopt:`--verbosity` and :djadminopt:`--traceback`. 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 Management commands and locales
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``).
If your custom management command uses another locale, you should manually By default, the :meth:`BaseCommand.execute` method sets the hardcoded 'en-us'
activate and deactivate it in your :meth:`~BaseCommand.handle` or locale because most of the commands shipped with Django perform several tasks
:meth:`~NoArgsCommand.handle_noargs` method using the functions provided by (for example, user-facing content rendering and database population) that
the I18N support code: 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:
from django.core.management.base import BaseCommand, CommandError .. code-block:: python
from django.utils import translation
class Command(BaseCommand): from django.core.management.base import BaseCommand, CommandError
... from django.utils import translation
can_import_settings = True
def handle(self, *args, **options): class Command(BaseCommand):
...
can_import_settings = True
# Activate a fixed locale, e.g. Russian def handle(self, *args, **options):
translation.activate('ru')
# Or you can activate the LANGUAGE_CODE # Activate a fixed locale, e.g. Russian
# chosen in the settings: translation.activate('ru')
#
#from django.conf import settings
#translation.activate(settings.LANGUAGE_CODE)
# Your command logic here # Or you can activate the LANGUAGE_CODE # chosen in the settings:
# ... #
#from django.conf import settings
#translation.activate(settings.LANGUAGE_CODE)
translation.deactivate() # Your command logic here
# ...
Take into account though, that system management commands typically have to translation.deactivate()
be very careful about running in non-uniform locales, so:
* Make sure the :setting:`USE_I18N` setting is always ``True`` when running Another need might be that your command simply should use the locale set in
the command (this is one good example of the potential problems stemming settings and Django should be kept from forcing it to 'en-us'. You can achieve
from a dynamic runtime environment that Django commands avoid offhand by it by using the :data:`BaseCommand.leave_locale_alone` option.
always using a fixed locale).
* Review the code of your command and the code it calls for behavioral When working on the scenarios described above though, take into account that
differences when locales are changed and evaluate its impact on system management commands typically have to be very careful about running in
predictable behavior of your command. 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
differences when locales are changed and evaluate its impact on
predictable behavior of your command.
Command objects Command objects
=============== ===============
@ -222,6 +229,29 @@ All attributes can be set in your derived class and can be used in
rather than all applications' models, call rather than all applications' models, call
:meth:`~BaseCommand.validate` from :meth:`~BaseCommand.handle`. :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 Methods
------- -------

View File

@ -39,6 +39,14 @@ Minor features
<lazy-plural-translations>` can be provided at translation time rather than <lazy-plural-translations>` can be provided at translation time rather than
at definition time. 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 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: finally:
sys.stderr = old_stderr sys.stderr = old_stderr
self.assertIn("CommandError", err.getvalue()) 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('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) 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): def test_extraction_error(self):
os.chdir(self.test_dir) os.chdir(self.test_dir)
self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0) self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0)