From 083c6b4a849a2686bb55d1c431ea53facb1b653c Mon Sep 17 00:00:00 2001 From: agu2347 Date: Tue, 28 Jul 2026 16:13:50 +0000 Subject: [PATCH] Decode msgctxt using the catalog's charset in read_mo() read_mo() extracted the message context (msgctxt) as raw bytes and used it directly, while msgid/msgstr a few lines later are properly decoded via catalog.charset. write_mo() already expects (and calls .encode(catalog.charset) on) a plain str context when serializing, so this asymmetry broke round-tripping a catalog through write_mo() then read_mo(): a catalog written with a str context came back from read_mo() with a bytes context instead, and made it impossible to use Catalog.get()/Catalog.__getitem__ with a plain str context on the result without first manually re-encoding it to match. Decode ctxt via catalog.charset immediately after splitting it off, matching how msg/tmsg are decoded a few lines below and matching what write_mo() already expects on the way out. Verified with a real write_mo() -> read_mo() round-trip: confirmed the context comes back as a plain str matching the original value, and that Catalog.get() finds the message using that same plain str context without needing any manual re-encoding -- the reporter's exact use case. Also confirmed messages without a context are unaffected. Added a regression test performing this exact round-trip. Confirmed the test fails with the original code (context comes back as bytes) and passes with the fix. Ran the full existing test_mofile.py suite (5 passed: 4 baseline + 1 new) and the broader messages test suite (test_catalog.py, test_pofile.py: 82 passed total), no regressions. Fixes #1147 --- babel/messages/mofile.py | 1 + tests/messages/test_mofile.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 1a6fedfcb..512435ff7 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -84,6 +84,7 @@ def read_mo(fileobj: SupportsRead[bytes]) -> Catalog: if b'\x04' in msg: # context ctxt, msg = msg.split(b'\x04') + ctxt = ctxt.decode(catalog.charset) else: ctxt = None diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index 85f4e9f34..cc1949cf7 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -31,6 +31,41 @@ def test_basics(): assert catalog['foobar'].string == ['Fuhstange', 'Fuhstangen'] +def test_read_mo_decodes_msgctxt(): + """ + Regression test for + https://github.com/python-babel/babel/issues/1147 + + read_mo() must decode msgctxt using the catalog's charset, the + same way it already decodes msgid/msgstr, rather than leaving it + as raw bytes. write_mo() already expects (and .encode()s) a plain + str context, so leaving it undecoded on read broke round-tripping + a catalog through write_mo() then read_mo(), and made it + impossible to use a plain str context with Catalog.get()/ + Catalog.__getitem__ on the result of read_mo() without manually + re-encoding it first. + """ + catalog = Catalog() + catalog.add('foo', 'bar', context='fooctxt') + + buf = BytesIO() + mofile.write_mo(buf, catalog) + buf.seek(0) + + read_catalog = mofile.read_mo(buf) + message_id, context = next(iter(read_catalog._messages)) + assert message_id == 'foo' + assert context == 'fooctxt' + assert isinstance(context, str) + + # The actual reported use case: Catalog.get() with a plain str + # context must find the message without needing to manually + # encode the context to bytes first. + message = read_catalog.get('foo', context='fooctxt') + assert message is not None + assert message.string == 'bar' + + def test_sorting(): # Ensure the header is sorted to the first entry so that its charset