From 3681e1fa161e5557145568535a4078267e85179e Mon Sep 17 00:00:00 2001 From: Jesse205 <2055675594@qq.com> Date: Wed, 13 May 2026 12:09:50 +0800 Subject: [PATCH 1/5] Make gettext supports Traversables --- Lib/gettext.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/gettext.py b/Lib/gettext.py index 2f77f0e849e9ae..91f3173852235c 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -487,6 +487,8 @@ def npgettext(self, context, msgid1, msgid2, n): # Locate a .mo file using the gettext strategy def find(domain, localedir=None, languages=None, all=False): + from importlib.resources.abc import Traversable + # Get some reasonable defaults for arguments that were not supplied if localedir is None: localedir = _default_localedir @@ -513,8 +515,13 @@ def find(domain, localedir=None, languages=None, all=False): for lang in nelangs: if lang == 'C': break - mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain) - if os.path.exists(mofile): + if isinstance(localedir, Traversable): + mofile = localedir.joinpath(lang, 'LC_MESSAGES', '%s.mo' % domain) + is_exists= mofile.is_file() + else: + mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain) + is_exists = os.path.exists(mofile) + if is_exists: if all: result.append(mofile) else: @@ -528,6 +535,8 @@ def find(domain, localedir=None, languages=None, all=False): def translation(domain, localedir=None, languages=None, class_=None, fallback=False): + from importlib.resources.abc import Traversable + if class_ is None: class_ = GNUTranslations mofiles = find(domain, localedir, languages, all=True) @@ -541,10 +550,10 @@ def translation(domain, localedir=None, languages=None, # once. result = None for mofile in mofiles: - key = (class_, os.path.abspath(mofile)) + key = (class_, mofile if isinstance(mofile,Traversable) else os.path.abspath(mofile)) t = _translations.get(key) if t is None: - with open(mofile, 'rb') as fp: + with (mofile.open('rb') if isinstance(mofile,Traversable) else open(mofile, 'rb')) as fp: t = _translations.setdefault(key, class_(fp)) # Copy the translation object to allow setting fallbacks and # output charset. All other instance data is shared with the From af7f4b507853f6d45ea18293062bc12a6f4fe0fc Mon Sep 17 00:00:00 2001 From: Jesse205 <2055675594@qq.com> Date: Thu, 14 May 2026 02:49:52 +0800 Subject: [PATCH 2/5] Fix test_argparse --- Lib/gettext.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Lib/gettext.py b/Lib/gettext.py index 91f3173852235c..a9cda2313aeaf4 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -484,11 +484,16 @@ def npgettext(self, context, msgid1, msgid2, n): tmsg = msgid2 return tmsg +# Path objects also implement Traversable, but they work with legacy APIs (str/PathLike). +# Only return True for non-path Traversable objects that truly need the Traversable API. +def _needs_traversable_api(file): + if not isinstance(file, str | os.PathLike): + from importlib.resources.abc import Traversable + return isinstance(file, Traversable) + return False # Locate a .mo file using the gettext strategy def find(domain, localedir=None, languages=None, all=False): - from importlib.resources.abc import Traversable - # Get some reasonable defaults for arguments that were not supplied if localedir is None: localedir = _default_localedir @@ -515,12 +520,13 @@ def find(domain, localedir=None, languages=None, all=False): for lang in nelangs: if lang == 'C': break - if isinstance(localedir, Traversable): + if _needs_traversable_api(localedir): mofile = localedir.joinpath(lang, 'LC_MESSAGES', '%s.mo' % domain) is_exists= mofile.is_file() else: mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain) is_exists = os.path.exists(mofile) + if is_exists: if all: result.append(mofile) @@ -535,8 +541,6 @@ def find(domain, localedir=None, languages=None, all=False): def translation(domain, localedir=None, languages=None, class_=None, fallback=False): - from importlib.resources.abc import Traversable - if class_ is None: class_ = GNUTranslations mofiles = find(domain, localedir, languages, all=True) @@ -550,10 +554,14 @@ def translation(domain, localedir=None, languages=None, # once. result = None for mofile in mofiles: - key = (class_, mofile if isinstance(mofile,Traversable) else os.path.abspath(mofile)) + use_traversable_api = _needs_traversable_api(mofile) + if not use_traversable_api: + mofile = os.path.abspath(mofile) + + key = (class_, mofile) t = _translations.get(key) if t is None: - with (mofile.open('rb') if isinstance(mofile,Traversable) else open(mofile, 'rb')) as fp: + with (mofile.open('rb') if use_traversable_api else open(mofile, 'rb')) as fp: t = _translations.setdefault(key, class_(fp)) # Copy the translation object to allow setting fallbacks and # output charset. All other instance data is shared with the From a1705de5d7eca77a16fd90ced84d88eacfd3a9a2 Mon Sep 17 00:00:00 2001 From: Jesse205 <2055675594@qq.com> Date: Thu, 14 May 2026 05:48:02 +0800 Subject: [PATCH 3/5] Add tests --- Lib/test/test_gettext.py | 98 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py index 9ad37909a8ec4e..acb631ffef9b9a 100644 --- a/Lib/test/test_gettext.py +++ b/Lib/test/test_gettext.py @@ -1,3 +1,6 @@ +from collections.abc import Iterator +from importlib.resources.abc import Traversable +import io import os import base64 import gettext @@ -208,6 +211,73 @@ def setUp(self): Ri04CgA= ''' + +ROOT_VFS = { + "locale": { + "ga_IE": {"LC_MESSAGES": {"mofile.mo": GNU_MO_DATA}}, + "es_ES": {"LC_MESSAGES": {"mofile.mo": GNU_MO_DATA}}, + }, +} + + +class MockTraversable(Traversable): + def __init__(self, path, vfs): + self.path = path + self.vfs = vfs + + def __str__(self) -> str: + return self.path + + @property + def name(self): + return self.path.split('/')[-1] + + def joinpath(self, *args): + vfs = self.vfs + for name in args: + if vfs: + vfs = vfs.get(name) + return MockTraversable(self.path + "/" + "/".join(args), vfs) + + def is_file(self): + return isinstance(self.vfs, bytes) + + def open(self, mode='r', *args, **kwargs): + if "b" in mode: + return io.BytesIO(base64.decodebytes(self.vfs)) + else: + return io.StringIO(base64.decodebytes(self.vfs).decode(encoding=kwargs.get('encoding', 'utf-8'))) + + def is_dir(self): + return isinstance(self.vfs, dict) + + def iterdir(self) -> Iterator[Traversable]: + for name in self.vfs.keys(): + yield self.joinpath(name) + + def read_bytes(self) -> bytes: + with self.open('rb') as strm: + return strm.read() + + def read_text(self, encoding) -> str: + with self.open(encoding=encoding) as strm: + return strm.read() + + def __eq__(self, value: object) -> bool: + if not isinstance(value, MockTraversable): + return False + return self.path == value.path + + def __lt__(self, other): + return self.path < other.path + + def __gt__(self, other): + return self.path > other.path + + def __hash__(self): + return hash(self.path) + + class GettextTestCase1(GettextBaseTest): def setUp(self): GettextBaseTest.setUp(self) @@ -926,6 +996,14 @@ def test_find_deduplication(self): languages=['ga_IE', 'ga_IE'], all=True) self.assertEqual(result, mo_file) + def test_find_with_traversable_directory(self): + traversable_dir = MockTraversable("/", ROOT_VFS) + result = gettext.find('mofile', + localedir=traversable_dir.joinpath("locale"), + languages=["ga_IE", "es_ES"], all=True) + self.assertEqual(sorted(result), sorted((traversable_dir.joinpath("locale", "ga_IE", "LC_MESSAGES", "mofile.mo"), + traversable_dir.joinpath("locale", "es_ES", "LC_MESSAGES", "mofile.mo")))) + class MiscTestCase(unittest.TestCase): def test__all__(self): @@ -934,15 +1012,31 @@ def test__all__(self): @cpython_only def test_lazy_import(self): - ensure_lazy_imports("gettext", {"re", "warnings", "locale"}) + ensure_lazy_imports("gettext", {"re", "warnings", "locale", "importlib.resources.abc"}) -class TranslationFallbackTestCase(unittest.TestCase): +class TranslationTestCase(unittest.TestCase): def test_translation_fallback(self): with os_helper.temp_cwd() as tempdir: t = gettext.translation('gettext', localedir=tempdir, fallback=True) self.assertIsInstance(t, gettext.NullTranslations) + def test_translation_with_traversable_directory(self): + traversable_dir = MockTraversable("/", ROOT_VFS) + trans = gettext.translation('mofile', localedir=traversable_dir.joinpath( + "locale"), languages=["ga_IE", "es_ES"]) + self.assertIsInstance(trans, gettext.GNUTranslations) + + +class NeedsTraversableApiTestCase(unittest.TestCase): + def test_needs_traversable_api_function(self): + from pathlib import Path + from gettext import _needs_traversable_api + + self.assertFalse(_needs_traversable_api("some/path")) + self.assertFalse(_needs_traversable_api(Path("some/path"))) + self.assertFalse(_needs_traversable_api(Path("some/path"))) + if __name__ == '__main__': unittest.main() From cb0420a08a5e16dd35b9622fcde9cac4a0c59f15 Mon Sep 17 00:00:00 2001 From: Jesse205 <2055675594@qq.com> Date: Thu, 14 May 2026 06:30:48 +0800 Subject: [PATCH 4/5] Add news --- .../next/Library/2026-05-14-06-29-49.gh-issue-149809.juDEkc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-14-06-29-49.gh-issue-149809.juDEkc.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-14-06-29-49.gh-issue-149809.juDEkc.rst b/Misc/NEWS.d/next/Library/2026-05-14-06-29-49.gh-issue-149809.juDEkc.rst new file mode 100644 index 00000000000000..e9071908ad5dd2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-14-06-29-49.gh-issue-149809.juDEkc.rst @@ -0,0 +1 @@ +Make the gettext functions accept a localedir of type Traversable. From 6d8b8fe87872b36eba1774c3c55a4c9b9e007123 Mon Sep 17 00:00:00 2001 From: Jesse205 <2055675594@qq.com> Date: Thu, 14 May 2026 07:05:33 +0800 Subject: [PATCH 5/5] Update comment --- Lib/gettext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/gettext.py b/Lib/gettext.py index a9cda2313aeaf4..0cb821db5bc2ee 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -484,7 +484,7 @@ def npgettext(self, context, msgid1, msgid2, n): tmsg = msgid2 return tmsg -# Path objects also implement Traversable, but they work with legacy APIs (str/PathLike). +# Path objects also implement Traversable, but they work with legacy APIs. # Only return True for non-path Traversable objects that truly need the Traversable API. def _needs_traversable_api(file): if not isinstance(file, str | os.PathLike):