Browse Source

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.
Ramiro Morales 12 years ago
parent
commit
869c9ba306

+ 36 - 9
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):
         """

+ 1 - 1
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')

+ 1 - 1
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')

+ 3 - 0
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

+ 65 - 35
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</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:
 
-        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')
+
+            # 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
+            # ...
 
-                # Your command logic here
-                # ...
+            translation.deactivate()
 
-                translation.deactivate()
+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.
 
-    Take into account though, that system management commands typically have to
-    be very careful about running in non-uniform locales, so:
+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 one good example of the potential problems stemming
-      from a dynamic runtime environment that Django commands avoid offhand by
-      always using a fixed locale).
+* 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.
+* 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
 -------
 

+ 8 - 0
docs/releases/1.6.txt

@@ -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
 =====================================
 

+ 10 - 0
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()

+ 10 - 0
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()

+ 14 - 0
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")

+ 5 - 0
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)