Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions Lib/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,13 @@ def npgettext(self, context, msgid1, msgid2, n):
tmsg = msgid2
return tmsg

# 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):
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):
Expand Down Expand Up @@ -513,8 +520,14 @@ 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 _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)
else:
Expand All @@ -541,10 +554,14 @@ def translation(domain, localedir=None, languages=None,
# once.
result = None
for mofile in mofiles:
key = (class_, 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 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
Expand Down
98 changes: 96 additions & 2 deletions Lib/test/test_gettext.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from collections.abc import Iterator
from importlib.resources.abc import Traversable
import io
import os
import base64
import gettext
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the gettext functions accept a localedir of type Traversable.
Loading