From ad41f1c53aa9f2c938df32e4386d8a80138923fc Mon Sep 17 00:00:00 2001 From: Andrew Cordery Date: Tue, 7 Nov 2023 12:32:19 +0100 Subject: [PATCH] Fixed #34952 -- Copied dir list when processing locale folders to avoid missing entries during os.walk traversal. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- .../management/commands/compilemessages.py | 3 +- tests/i18n/test_compilation.py | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index 9ed3ef7c31..eddf31b794 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -91,7 +91,8 @@ class Command(BaseCommand): # Walk entire tree, looking for locale directories for dirpath, dirnames, filenames in os.walk(".", topdown=True): - for dirname in dirnames: + # As we may modify dirnames, iterate through a copy of it instead + for dirname in list(dirnames): if is_ignored_path( os.path.normpath(os.path.join(dirpath, dirname)), ignore_patterns ): diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py index ab74927c40..7da95ba9e9 100644 --- a/tests/i18n/test_compilation.py +++ b/tests/i18n/test_compilation.py @@ -195,6 +195,64 @@ class IgnoreDirectoryCompilationTests(MessageCompilationTests): self.assertNoneExist(self.CACHE_DIR, ["en", "fr", "it"]) self.assertNoneExist(self.NESTED_DIR, ["en", "fr", "it"]) + def test_no_dirs_accidentally_skipped(self): + os_walk_results = [ + # To discover .po filepaths, compilemessages uses with a starting list of + # basedirs to inspect, which in this scenario are: + # ["conf/locale", "locale"] + # Then os.walk is used to discover other locale dirs, ignoring dirs matching + # `ignore_patterns`. Mock the results to place an ignored directory directly + # before and after a directory named "locale". + [("somedir", ["ignore", "locale", "ignore"], [])], + # This will result in three basedirs discovered: + # ["conf/locale", "locale", "somedir/locale"] + # os.walk is called for each locale in each basedir looking for .po files. + # In this scenario, we need to mock os.walk results for "en", "fr", and "it" + # locales for each basedir: + [("exclude/locale/LC_MESSAGES", [], ["en.po"])], + [("exclude/locale/LC_MESSAGES", [], ["fr.po"])], + [("exclude/locale/LC_MESSAGES", [], ["it.po"])], + [("exclude/conf/locale/LC_MESSAGES", [], ["en.po"])], + [("exclude/conf/locale/LC_MESSAGES", [], ["fr.po"])], + [("exclude/conf/locale/LC_MESSAGES", [], ["it.po"])], + [("exclude/somedir/locale/LC_MESSAGES", [], ["en.po"])], + [("exclude/somedir/locale/LC_MESSAGES", [], ["fr.po"])], + [("exclude/somedir/locale/LC_MESSAGES", [], ["it.po"])], + ] + + module_path = "django.core.management.commands.compilemessages" + with mock.patch(f"{module_path}.os.walk", side_effect=os_walk_results): + with mock.patch(f"{module_path}.os.path.isdir", return_value=True): + with mock.patch( + f"{module_path}.Command.compile_messages" + ) as mock_compile_messages: + call_command("compilemessages", ignore=["ignore"], verbosity=4) + + expected = [ + ( + [ + ("exclude/locale/LC_MESSAGES", "en.po"), + ("exclude/locale/LC_MESSAGES", "fr.po"), + ("exclude/locale/LC_MESSAGES", "it.po"), + ], + ), + ( + [ + ("exclude/conf/locale/LC_MESSAGES", "en.po"), + ("exclude/conf/locale/LC_MESSAGES", "fr.po"), + ("exclude/conf/locale/LC_MESSAGES", "it.po"), + ], + ), + ( + [ + ("exclude/somedir/locale/LC_MESSAGES", "en.po"), + ("exclude/somedir/locale/LC_MESSAGES", "fr.po"), + ("exclude/somedir/locale/LC_MESSAGES", "it.po"), + ], + ), + ] + self.assertEqual([c.args for c in mock_compile_messages.mock_calls], expected) + class CompilationErrorHandling(MessageCompilationTests): def test_error_reported_by_msgfmt(self):