diff --git a/django/core/management/base.py b/django/core/management/base.py index 7b9001ed14..9dc321a061 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -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): """ diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index 684ef3514c..8f2c1ff771 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -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') diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 4550605af2..daa4c5f023 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -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') diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 927dbc13ac..5ce50b4dfd 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -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 diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 6a7f644218..eba187bb37 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -112,54 +112,61 @@ In addition to being able to add custom command line options, all :doc:`management commands` 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: - from django.core.management.base import BaseCommand, CommandError - from django.utils import translation +.. code-block:: python - class Command(BaseCommand): - ... - can_import_settings = True + from django.core.management.base import BaseCommand, CommandError + from django.utils import translation - def handle(self, *args, **options): + class Command(BaseCommand): + ... + can_import_settings = True - # Activate a fixed locale, e.g. Russian - translation.activate('ru') + def handle(self, *args, **options): - # Or you can activate the LANGUAGE_CODE - # chosen in the settings: - # - #from django.conf import settings - #translation.activate(settings.LANGUAGE_CODE) + # Activate a fixed locale, e.g. Russian + translation.activate('ru') - # 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 - be very careful about running in non-uniform locales, so: + translation.deactivate() - * Make sure the :setting:`USE_I18N` setting is always ``True`` when running - the command (this is one good example of the potential problems stemming - from a dynamic runtime environment that Django commands avoid offhand by - always using a fixed locale). +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. - * 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. +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 + differences when locales are changed and evaluate its impact on + predictable behavior of your command. 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 :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 ------- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5e1d959f60..acc273cef4 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -39,6 +39,14 @@ Minor features ` 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 ===================================== diff --git a/tests/modeltests/user_commands/management/commands/leave_locale_alone_false.py b/tests/modeltests/user_commands/management/commands/leave_locale_alone_false.py new file mode 100644 index 0000000000..8ebb607d5a --- /dev/null +++ b/tests/modeltests/user_commands/management/commands/leave_locale_alone_false.py @@ -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() diff --git a/tests/modeltests/user_commands/management/commands/leave_locale_alone_true.py b/tests/modeltests/user_commands/management/commands/leave_locale_alone_true.py new file mode 100644 index 0000000000..e0f923591e --- /dev/null +++ b/tests/modeltests/user_commands/management/commands/leave_locale_alone_true.py @@ -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() diff --git a/tests/modeltests/user_commands/tests.py b/tests/modeltests/user_commands/tests.py index d25911c09f..c8740577a5 100644 --- a/tests/modeltests/user_commands/tests.py +++ b/tests/modeltests/user_commands/tests.py @@ -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") diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 0367d23ec6..ca89f30de8 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -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)