From 2c99fbcf95f07eaff0bf9673af7c82d07353df8f Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Thu, 8 May 2025 05:05:24 +0200 Subject: [PATCH] Fixed #36368 -- Prevented duplicate locale paths and write_po_file calls in makemessages. --- AUTHORS | 1 + .../core/management/commands/makemessages.py | 16 ++-- .../commands/app_with_locale/some_file.py | 0 tests/i18n/test_extraction.py | 78 ++++++++++++++++++- 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 tests/i18n/commands/app_with_locale/some_file.py diff --git a/AUTHORS b/AUTHORS index 2b1c185e36..c058559787 100644 --- a/AUTHORS +++ b/AUTHORS @@ -743,6 +743,7 @@ answer newbie questions, and generally made Django that much better: Michal Chruszcz michal@plovarna.cz Michał Modzelewski + Michał Pokusa Mihai Damian Mihai Preda Mikaël Barbero diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 60fe295ac1..bf4ce5fc44 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -382,10 +382,15 @@ class Command(BaseCommand): self.invoked_for_django = True else: if self.settings_available: - self.locale_paths.extend(settings.LOCALE_PATHS) + for path in settings.LOCALE_PATHS: + locale_path = os.path.abspath(path) + if locale_path not in self.locale_paths: + self.locale_paths.append(locale_path) # Allow to run makemessages inside an app dir if os.path.isdir("locale"): - self.locale_paths.append(os.path.abspath("locale")) + locale_path = os.path.abspath("locale") + if locale_path not in self.locale_paths: + self.locale_paths.append(locale_path) if self.locale_paths: self.default_locale_path = self.locale_paths[0] os.makedirs(self.default_locale_path, exist_ok=True) @@ -551,9 +556,10 @@ class Command(BaseCommand): self.stdout.write("ignoring directory %s" % dirname) elif dirname == "locale": dirnames.remove(dirname) - self.locale_paths.insert( - 0, os.path.join(os.path.abspath(dirpath), dirname) - ) + locale_dir = os.path.join(os.path.abspath(dirpath), dirname) + if locale_dir in self.locale_paths: + self.locale_paths.remove(locale_dir) + self.locale_paths.insert(0, locale_dir) for filename in filenames: file_path = os.path.normpath(os.path.join(dirpath, filename)) file_ext = os.path.splitext(filename)[1] diff --git a/tests/i18n/commands/app_with_locale/some_file.py b/tests/i18n/commands/app_with_locale/some_file.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index b02b24ab5b..deac15ef8f 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -14,7 +14,10 @@ from django.core import management from django.core.management import execute_from_command_line from django.core.management.base import CommandError from django.core.management.commands.makemessages import Command as MakeMessagesCommand -from django.core.management.commands.makemessages import write_pot_file +from django.core.management.commands.makemessages import ( + TranslatableFile, + write_pot_file, +) from django.core.management.utils import find_command from django.test import SimpleTestCase, override_settings from django.test.utils import captured_stderr, captured_stdout @@ -600,6 +603,79 @@ class BasicExtractorTests(ExtractorTests): self.assertIn("Content-Type: text/plain; charset=UTF-8", pot_contents) self.assertIn("mañana; charset=CHARSET", pot_contents) + def test_no_duplicate_locale_paths(self): + for locale_paths in [ + [], + [os.path.join(self.test_dir, "locale")], + [Path(self.test_dir, "locale")], + ]: + with self.subTest(locale_paths=locale_paths): + with override_settings(LOCALE_PATHS=locale_paths): + cmd = MakeMessagesCommand() + management.call_command(cmd, locale=["en", "ru"], verbosity=0) + self.assertTrue( + all(isinstance(path, str) for path in cmd.locale_paths) + ) + self.assertEqual(len(cmd.locale_paths), len(set(cmd.locale_paths))) + + def test_no_duplicate_write_po_file_calls(self): + with mock.patch.object( + MakeMessagesCommand, "write_po_file" + ) as mock_write_po_file: + cmd = MakeMessagesCommand() + management.call_command(cmd, locale=["en", "ru"], verbosity=0) + self.assertEqual( + len(mock_write_po_file.call_args_list), + len({call.args for call in mock_write_po_file.call_args_list}), + ) + + def test_correct_translatable_file_locale_dir(self): + + class ReturnTrackingMock(mock.Mock): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.call_return_value_list = [] + + def __call__(self, *args, **kwargs): + value = super().__call__(*args, **kwargs) + self.call_return_value_list.append(value) + return value + + for locale_paths in [ + [], + [ + os.path.join(self.test_dir, "app_with_locale", "locale"), + ], + [ + os.path.join(self.test_dir, "locale"), + os.path.join(self.test_dir, "app_with_locale", "locale"), + ], + ]: + with self.subTest(locale_paths=locale_paths): + with override_settings(LOCALE_PATHS=locale_paths): + cmd = MakeMessagesCommand() + rtm = ReturnTrackingMock(wraps=cmd.find_files) + + with mock.patch.object(cmd, "find_files", new=rtm): + management.call_command(cmd, locale=["en", "ru"], verbosity=0) + self.assertEqual(len(rtm.call_args_list), 1) + self.assertEqual(len(rtm.call_return_value_list), 1) + + for tf in rtm.call_return_value_list[0]: + self.assertIsInstance(tf, TranslatableFile) + abs_file_path = os.path.abspath( + os.path.join(self.test_dir, tf.dirpath, tf.file) + ) + max_common_path = max( + [ + os.path.commonpath([abs_file_path, locale_path]) + for locale_path in cmd.locale_paths + ], + key=len, + ) + correct_locale_dir = os.path.join(max_common_path, "locale") + self.assertEqual(tf.locale_dir, correct_locale_dir) + class JavaScriptExtractorTests(ExtractorTests): PO_FILE = "locale/%s/LC_MESSAGES/djangojs.po" % LOCALE