Opened 3 weeks ago

Closed 3 weeks ago

Last modified 3 weeks ago

#37447 closed Bug (fixed)

makemessages fails in UTF-8 mode when processing non-UTF-8 filenames

Reported by: Jacob Walls Owned by: Natalia Bidart
Component: Internationalization Version: 6.1
Severity: Release blocker Keywords: makemessages, locale, utf-8
Cc: Sarah Boyce Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Surfaced on CI just now after bumping Windows jobs to Python 3.15, now that UTF-8 mode is the ​default.

ERROR: test_unicode_file_name (i18n.test_extraction.BasicExtractorTests.test_unicode_file_name)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\hostedtoolcache\windows\Python\3.15.1\x64\Lib\unittest\case.py", line 58, in testPartExecutor
    yield
  File "C:\hostedtoolcache\windows\Python\3.15.1\x64\Lib\unittest\case.py", line 691, in run
    self._callTestMethod(testMethod)
    
  File "C:\hostedtoolcache\windows\Python\3.15.1\x64\Lib\unittest\case.py", line 637, in _callTestMethod
    result = method()
    ^^^^^^^^^^^^^^^
  File "D:\a\django\django\tests\i18n\test_extraction.py", line 372, in test_unicode_file_name
    management.call_command("makemessages", locale=[LOCALE], verbosity=0)
    ^^^^^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\__init__.py", line 195, in call_command
    return command.execute(*args, **defaults)
      ^^^^^^^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\base.py", line 471, in execute
    output = self.handle(*args, **options)
    ^^^^^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\commands\makemessages.py", line 423, in handle
    potfiles = self.build_potfiles()
    ^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\commands\makemessages.py", line 504, in build_potfiles
    self.process_files(file_list)
    ^^^^^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\commands\makemessages.py", line 596, in process_files
    self.process_locale_dir(locale_dir, files)
    ^^^^^^^^^^^
  File "D:\a\django\django\django\core\management\commands\makemessages.py", line 677, in process_locale_dir
    raise CommandError(
    ^^^^^^^
django.core.management.base.CommandError: errors happened while running xgettext on .\__init__.py
.\ignore_dir\ignored.html.py
.\media_root\media_ignored.html.py
.\static\static_ignored.html.py
.\templates\empty.html.py
.\templates\subdir\ignored.html.py
.\templates\test.html.py
.\templates\xxx_ignored.html.py
.\vidéo.txt.py
xgettext: error while opening ".\vidéo.txt.py" for reading: No such file or directory

I believe the fix is to write using the system locale, which we should have been doing all along (​linter rule on this topic)

  • django/core/management/commands/makemessages.py

    diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
    index 5a4bd55bb8..9db5a6ef2a 100644
    a b class Command(BaseCommand):  
    663663            return
    664664
    665665        input_files = [bf.work_path for bf in build_files]
    666         with NamedTemporaryFile(mode="w+") as input_files_list:
    667             input_files_list.write("\n".join(input_files))
     666        with NamedTemporaryFile(mode="w+b") as input_files_list:
     667            input_files_list.write(
     668                "\n".join(input_files).encode(DEFAULT_LOCALE_ENCODING)
     669            )
    668670            input_files_list.flush()
    669671            args.extend(["--files-from", input_files_list.name])
    670672            args.extend(self.xgettext_options)

If a triager confirms the approach, I can open the PR. I propose backporting to 6.1 as part of 3.15 support, but with a release note, since it affects released behavior and would have been visible on prior Python versions when running in UTF-8 mode.

Change History (6)

comment:1 by Natalia Bidart, 3 weeks ago

Triage Stage: Unreviewed → Accepted

I was 15 minutes into debugging my PR's failing CI and about to file a ticket when it occurred to me to check for duplicates, and here I am :-) Accepting, of course!

Two notes:

  • The failure surfaced after #37396 put gettext on PATH in tests.yml. Until then the test was skipped on Windows (and schedule_tests.yml doesn't install gettext at all), which is why the Windows/Python 3.15 runs stayed green after the bump. I think we should also install gettext in the scheduled tests.
  • DEFAULT_LOCALE_ENCODING falls back to "ascii" when locale.getlocale() reports no encoding (for example, the "C" locale, which is (None, None)). In that case .encode() would raise UnicodeEncodeError for non-ASCII names, while today on POSIX it works (the list is written as UTF-8 and xgettext opens the raw bytes). Would it be safer to use os.fsencode() on POSIX, and the ANSI code page (locale.getencoding(), which ignores UTF-8 mode) only on Windows?

References:

Proposed patch (commit should also Refs #36664):

  • .github/workflows/schedule_tests.yml

    diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml
    index d8133764b5..3dfc900927 100644
    a b jobs:  
    3434          python-version: ${{ matrix.python-version }}
    3535          cache: 'pip'
    3636          cache-dependency-path: 'tests/requirements/py3.txt'
     37      - name: Install gettext
     38        run: |
     39          choco install gettext --no-progress -y
     40          "C:\Program Files\gettext-iconv\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
    3741      - name: Install and upgrade packaging tools
    3842        run: python -m pip install --upgrade pip wheel
    3943      - run: python -m pip install -r tests/requirements/py3.txt -e .
  • django/core/management/commands/makemessages.py

    diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
    index 5a4bd55bb8..86fd34876d 100644
    a b  
    11import glob
     2import locale
    23import os
    34import re
    45import sys
    … … class Command(BaseCommand):  
    663664            return
    664665
    665666        input_files = [bf.work_path for bf in build_files]
    666         with NamedTemporaryFile(mode="w+") as input_files_list:
    667             input_files_list.write("\n".join(input_files))
     667        # xgettext reads the file list as bytes, decoded with the ANSI code
     668        # page on Windows, so it must not depend on Python's UTF-8 mode.
     669        if os.name == "nt":
     670            files_list = "\n".join(input_files).encode(locale.getencoding())
     671        else:
     672            files_list = b"\n".join(os.fsencode(path) for path in input_files)
     673        with NamedTemporaryFile(mode="w+b") as input_files_list:
     674            input_files_list.write(files_list)
    668675            input_files_list.flush()
    669676            args.extend(["--files-from", input_files_list.name])
    670677            args.extend(self.xgettext_options)

comment:2 by Natalia Bidart, 3 weeks ago

Owner: changed from Jacob Walls to Natalia Bidart

I agreed with Jacob I will put up a PR for this.

comment:3 by Natalia Bidart, 3 weeks ago

Has patch: set
Patch needs improvement: set

Setting as PNI until I see the windows run results.

comment:4 by Jacob Walls, 3 weeks ago

Patch needs improvement: unset
Triage Stage: Accepted → Ready for checkin

comment:5 by nessita <124304+nessita@…>, 3 weeks ago

Resolution: → fixed
Status: assigned → closed

In fa214ea7:

Fixed #37447 -- Fixed makemessages with non-ASCII file names on Windows.

Python 3.15 enables UTF-8 mode by default, so the temporary file
listing the files to process was written as UTF-8. xgettext opens
these paths as raw bytes, which Windows interprets in the ANSI code
page, so non-ASCII file names could not be found.

Refs #36664.

comment:6 by Natalia <124304+nessita@…>, 3 weeks ago

In be876ef4:

[6.1.x] Fixed #37447 -- Fixed makemessages with non-ASCII file names on Windows.

Python 3.15 enables UTF-8 mode by default, so the temporary file
listing the files to process was written as UTF-8. xgettext opens
these paths as raw bytes, which Windows interprets in the ANSI code
page, so non-ASCII file names could not be found.

Refs #36664.

Backport of fa214ea7b217a37484507b6222cca4915c70b4cc from main.

Note: See TracTickets for help on using tickets.
Back to Top