From 4a3480e092d4b5b71943ebac21534ef1a6b79a6b Mon Sep 17 00:00:00 2001 From: abhijeet117 Date: Tue, 25 Aug 2026 15:42:20 +0530 Subject: [PATCH] Fix Catalog.is_identical() for messages with msgctxt is_identical() iterated the internal _messages keys, which for context-specific messages are (msgid, msgctxt) tuples, but resolved them through get(), whose _key_for() collapses a tuple id to just the msgid. The lookup therefore always missed context-specific messages and the method returned False, making pybabel update --check report any catalog containing a msgctxt as out of date on every run. Look up the internal keys directly instead of round-tripping through get(). --- babel/messages/catalog.py | 7 +++++-- tests/messages/test_catalog.py | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5e6c28255..5f1e6c2fe 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -1084,8 +1084,11 @@ def is_identical(self, other: Catalog) -> bool: """ assert isinstance(other, Catalog) for key in self._messages.keys() | other._messages.keys(): - message_1 = self.get(key) - message_2 = other.get(key) + # The keys are internal keys already (possibly ``(msgid, + # msgctxt)`` tuples), so they must not be passed through ``get()``, + # which would strip the context and miss the message entirely. + message_1 = self._messages.get(key) + message_2 = other._messages.get(key) if message_1 is None or message_2 is None or not message_1.is_identical(message_2): return False return dict(self.mime_headers) == dict(other.mime_headers) diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 7c730d325..0556516ac 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -510,6 +510,44 @@ def test_catalog_add(): assert cat['foo'] is foo +def test_catalog_is_identical_with_context(): + cat = catalog.Catalog() + cat.add('Guide', 'guide', context='navigation') + + # A catalog is identical to itself even when it contains + # context-specific messages (issue #1307). + assert cat.is_identical(cat) + + other = catalog.Catalog() + other.add('Guide', 'guide', context='navigation') + assert cat.is_identical(other) + + # Same msgid, different context. + other = catalog.Catalog() + other.add('Guide', 'guide', context='menu') + assert not cat.is_identical(other) + + # Same context, different string. + other = catalog.Catalog() + other.add('Guide', 'handbook', context='navigation') + assert not cat.is_identical(other) + + # Context-specific vs context-free message with the same msgid. + other = catalog.Catalog() + other.add('Guide', 'guide') + assert not cat.is_identical(other) + + +def test_catalog_is_identical_without_context(): + cat = catalog.Catalog() + cat.add('foo', 'bar') + other = catalog.Catalog() + other.add('foo', 'bar') + assert cat.is_identical(other) + other.add('baz', 'qux') + assert not cat.is_identical(other) + + def test_catalog_update(): template = catalog.Catalog(header_comment="# A Custom Header") template.add('green', locations=[('main.py', 99)])