From d65b0f72de8d35617fe0554ddabc950c7f323eef Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 4 Feb 2013 14:35:52 +0100 Subject: [PATCH] Fixed #17379 -- Removed management commands deactivation of the locale. --- django/apps/registry.py | 5 ++ django/core/management/base.py | 70 ++++++++----------- .../management/commands/compilemessages.py | 1 - .../core/management/commands/makemessages.py | 1 - .../management/commands/makemigrations.py | 5 +- django/core/management/commands/migrate.py | 5 +- django/core/management/commands/runserver.py | 1 - django/core/management/templates.py | 3 - docs/howto/custom-management-commands.txt | 63 ++++------------- docs/releases/1.6.txt | 2 +- docs/releases/1.8.txt | 2 +- docs/releases/2.1.txt | 5 ++ tests/i18n/test_extraction.py | 4 -- .../commands/leave_locale_alone_true.py | 10 --- ...cale_alone_false.py => no_translations.py} | 5 +- tests/user_commands/tests.py | 17 +++-- 16 files changed, 72 insertions(+), 127 deletions(-) delete mode 100644 tests/user_commands/management/commands/leave_locale_alone_true.py rename tests/user_commands/management/commands/{leave_locale_alone_false.py => no_translations.py} (62%) diff --git a/django/apps/registry.py b/django/apps/registry.py index f522550d60..e01352b1de 100644 --- a/django/apps/registry.py +++ b/django/apps/registry.py @@ -124,6 +124,11 @@ class Apps: def check_apps_ready(self): """Raise an exception if all apps haven't been imported yet.""" 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.") def check_models_ready(self): diff --git a/django/core/management/base.py b/django/core/management/base.py index 45e97c7398..d6b4e27b78 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -73,6 +73,21 @@ def handle_default_options(options): 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): """ Wrapper around stdout/stderr @@ -171,19 +186,6 @@ class BaseCommand: is the list of application's configuration provided by the 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`` A tuple of any options the command uses which aren't defined by the argument parser. @@ -194,7 +196,6 @@ class BaseCommand: # Configuration shortcuts that alter various logic. _called_from_command_line = False output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" - leave_locale_alone = False requires_migrations_checks = False requires_system_checks = True # Arguments, common to all commands, which aren't defined by the argument @@ -323,33 +324,20 @@ class BaseCommand: if options.get('stderr'): self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func) - saved_locale = None - if not self.leave_locale_alone: - # Deactivate translations, because django-admin creates database - # content like permissions, and those shouldn't contain any - # translations. - from django.utils import translation - saved_locale = translation.get_language() - translation.deactivate_all() - - try: - if self.requires_system_checks and not options.get('skip_checks'): - self.check() - if self.requires_migrations_checks: - self.check_migrations() - 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) + if self.requires_system_checks and not options.get('skip_checks'): + self.check() + if self.requires_migrations_checks: + self.check_migrations() + 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) return output def _run_checks(self, **kwargs): diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index fcbe24b1f3..bf704a4e8d 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -27,7 +27,6 @@ class Command(BaseCommand): help = 'Compiles .po files to .mo files for use with builtin gettext support.' requires_system_checks = False - leave_locale_alone = True program = 'msgfmt' program_options = ['--check-format'] diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 4e47dd0687..a1e72c73ad 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -207,7 +207,6 @@ class Command(BaseCommand): build_file_class = BuildFile requires_system_checks = False - leave_locale_alone = True msgmerge_options = ['-q', '--previous'] msguniq_options = ['--to-code=utf-8'] diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index 96b9b2adbc..14461418c1 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -4,7 +4,9 @@ from itertools import takewhile from django.apps import apps 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.migrations import Migration from django.db.migrations.autodetector import MigrationAutodetector @@ -51,6 +53,7 @@ class Command(BaseCommand): help='Exit with a non-zero status if model changes are missing migrations.', ) + @no_translations def handle(self, *app_labels, **options): self.verbosity = options['verbosity'] self.interactive = options['interactive'] diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 7ca728fc97..0e27eaa19f 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -4,7 +4,9 @@ from importlib import import_module from django.apps import apps 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 ( emit_post_migrate_signal, emit_pre_migrate_signal, ) @@ -58,6 +60,7 @@ class Command(BaseCommand): issues.extend(super()._run_checks(**kwargs)) return issues + @no_translations def handle(self, *args, **options): self.verbosity = options['verbosity'] diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index d38491d0a9..0e0fd1ca2c 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -25,7 +25,6 @@ class Command(BaseCommand): # Validation is called explicitly each time the server is reloaded. requires_system_checks = False - leave_locale_alone = True stealth_options = ('shutdown_message',) default_addr = '127.0.0.1' diff --git a/django/core/management/templates.py b/django/core/management/templates.py index c55118260d..19da55f25b 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -32,9 +32,6 @@ class TemplateCommand(BaseCommand): requires_system_checks = 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 # Rewrite the following suffixes when determining the target filename. rewrite_template_suffixes = ( # Allow shipping invalid .py files without byte-compilation. diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index d9c929d255..dadbaf9742 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -126,52 +126,30 @@ such as :option:`--verbosity` and :option:`--traceback`. Management commands and locales =============================== -By default, the :meth:`BaseCommand.execute` method deactivates translations -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. +By default, management commands are executed with the current active locale. -If, for some reason, your custom management command needs to use a fixed locale, -you should manually activate and deactivate it in your -:meth:`~BaseCommand.handle` method using the functions provided by the I18N -support code:: +If, for some reason, your custom management command must run without an active +locale (for example, to prevent translated content from being inserted into +the database), deactivate translations using the ``@no_translations`` +decorator on your :meth:`~BaseCommand.handle` method:: - from django.core.management.base import BaseCommand, CommandError - from django.utils import translation + from django.core.management.base import BaseCommand, no_translations class Command(BaseCommand): ... + @no_translations 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. +.. versionchanged:: 2.1 -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 - 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. + The ``@no_translations`` decorator is new. In older versions, translations + are deactivated before running a command unless the command's + ``leave_locale_alone`` attribute (now removed) is set to ``True``. Testing ======= @@ -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 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 An instance attribute that helps create colored output when writing to diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index f878c29d48..00d099b025 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -193,7 +193,7 @@ Minor features 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 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. * The :attr:`~django.views.generic.edit.DeletionMixin.success_url` of diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index a122b52666..73d147ad35 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -1166,7 +1166,7 @@ Miscellaneous that Django includes) will no longer convert null values back to an empty 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 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, diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index 5f23d6d241..efa2b468c7 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -418,6 +418,11 @@ Miscellaneous * The database router :meth:`allow_relation` method is called in more cases. 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 `. + .. _deprecated-features-2.1: Features deprecated in 2.1 diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index b4c2ce4c9d..d9ce3b43c7 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -194,10 +194,6 @@ class BasicExtractorTests(ExtractorTests): self.assertMsgId("Get my line number", po_contents) 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): msg = ( 'Translation blocks must not include other block tags: blocktrans ' diff --git a/tests/user_commands/management/commands/leave_locale_alone_true.py b/tests/user_commands/management/commands/leave_locale_alone_true.py deleted file mode 100644 index 3100a2901b..0000000000 --- a/tests/user_commands/management/commands/leave_locale_alone_true.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.core.management.base import BaseCommand -from django.utils import translation - - -class Command(BaseCommand): - - leave_locale_alone = True - - def handle(self, *args, **options): - return translation.get_language() diff --git a/tests/user_commands/management/commands/leave_locale_alone_false.py b/tests/user_commands/management/commands/no_translations.py similarity index 62% rename from tests/user_commands/management/commands/leave_locale_alone_false.py rename to tests/user_commands/management/commands/no_translations.py index aa3e20664a..2a8af6605b 100644 --- a/tests/user_commands/management/commands/leave_locale_alone_false.py +++ b/tests/user_commands/management/commands/no_translations.py @@ -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 class Command(BaseCommand): - leave_locale_alone = False - + @no_translations def handle(self, *args, **options): return translation.get_language() diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 61e649c75a..b5d29721b4 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -63,17 +63,16 @@ class CommandTests(SimpleTestCase): dance.Command.requires_system_checks = True self.assertIn("CommandError", stderr.getvalue()) - def test_deactivate_locale_set(self): - # Deactivate translation when set to true + def test_no_translations_deactivate_translations(self): + """ + 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'): - result = management.call_command('leave_locale_alone_false', stdout=StringIO()) + result = management.call_command('no_translations', stdout=StringIO()) self.assertIsNone(result) - - 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") + self.assertEqual(translation.get_language(), current_locale) def test_find_command_without_PATH(self): """