diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index c56e2a237c..0aaff5822a 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -2,6 +2,7 @@ import codecs import concurrent.futures import glob import os +import tempfile from pathlib import Path from django.core.management.base import BaseCommand, CommandError @@ -16,12 +17,10 @@ def has_bom(fn): ) -def is_writable(path): - # Known side effect: updating file access/modified time to current time if - # it is writable. +def is_dir_writable(path): try: - with open(path, "a"): - os.utime(path, None) + with tempfile.NamedTemporaryFile(dir=path): + pass except OSError: return False return True @@ -172,7 +171,7 @@ class Command(BaseCommand): continue # Check writability on first location - if i == 0 and not is_writable(mo_path): + if i == 0 and not is_dir_writable(mo_path.parent): self.stderr.write( "The po files under %s are in a seemingly not writable " "location. mo files will not be updated/created." % dirpath diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py index 7b02776dbe..4b0bb9f6bb 100644 --- a/tests/i18n/test_compilation.py +++ b/tests/i18n/test_compilation.py @@ -43,9 +43,9 @@ class PoFileTests(MessageCompilationTests): def test_no_write_access(self): mo_file_en = Path(self.MO_FILE_EN) err_buffer = StringIO() - # Put file in read-only mode. - old_mode = mo_file_en.stat().st_mode - mo_file_en.chmod(stat.S_IREAD) + # Put parent directory in read-only mode. + old_mode = mo_file_en.parent.stat().st_mode + mo_file_en.parent.chmod(stat.S_IRUSR | stat.S_IXUSR) # Ensure .po file is more recent than .mo file. mo_file_en.with_suffix(".po").touch() try: @@ -57,7 +57,7 @@ class PoFileTests(MessageCompilationTests): ) self.assertIn("not writable location", err_buffer.getvalue()) finally: - mo_file_en.chmod(old_mode) + mo_file_en.parent.chmod(old_mode) def test_no_compile_when_unneeded(self): mo_file_en = Path(self.MO_FILE_EN) @@ -258,6 +258,9 @@ class CompilationErrorHandling(MessageCompilationTests): # po file contains wrong po formatting. with self.assertRaises(CommandError): call_command("compilemessages", locale=["ja"], verbosity=0) + # It should still fail a second time. + with self.assertRaises(CommandError): + call_command("compilemessages", locale=["ja"], verbosity=0) def test_msgfmt_error_including_non_ascii(self): # po file contains invalid msgstr content (triggers non-ascii error content).