From 2bbee1c7b955e36e68c8fd32011ee496321b0191 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Fri, 31 Jul 2026 09:17:16 +0900 Subject: [PATCH] Fix Catalog.is_identical for context-specific messages is_identical() iterates the internal _messages keys, but looked each one up through Catalog.get(). For a context-specific message that key is a (msgid, msgctxt) tuple, and get() re-runs _key_for() on it, which takes a tuple for a pluralizable message's id and reduces it to msgid alone -- a key no message is stored under. The lookup returned None, so any catalog containing a msgctxt never compared equal, not even to itself. The visible consequence is that pybabel update --check reports every catalog using pgettext/npgettext as out of date on every run, which makes it unusable as the CI staleness gate it was added for (#831). Index _messages directly, since the keys being iterated are its own. Co-Authored-By: Claude Opus 5 Co-Authored-By: Aarni Koskela --- babel/messages/catalog.py | 4 ++-- tests/messages/test_catalog.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5e6c28255..145954a64 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -1084,8 +1084,8 @@ 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) + 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..bf41c1976 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -601,3 +601,34 @@ def test_catalog_tz_pickleable(): msgstr "" "POT-Creation-Date: 2007-04-01 15:30+0200\n" """)))) + + +def test_catalog_is_identical_with_context(): + """A catalog holding a context-specific message must compare equal to itself.""" + cat = catalog.Catalog(locale='en') + cat.add('Guide', 'Guide', context='navigation') + assert cat.is_identical(cat) + + same = catalog.Catalog(locale='en') + same.add('Guide', 'Guide', context='navigation') + assert cat.is_identical(same) + + translated_differently = catalog.Catalog(locale='en') + translated_differently.add('Guide', 'Manual', context='navigation') + assert not cat.is_identical(translated_differently) + + # The same msgid under a different context is a different message. + other_context = catalog.Catalog(locale='en') + other_context.add('Guide', 'Guide', context='footer') + assert not cat.is_identical(other_context) + + +def test_catalog_is_identical_with_pluralizable_context(): + """The same, for a context-specific *pluralizable* message.""" + cat = catalog.Catalog(locale='en') + cat.add(('%d page', '%d pages'), ('%d page', '%d pages'), context='report') + assert cat.is_identical(cat) + + same = catalog.Catalog(locale='en') + same.add(('%d page', '%d pages'), ('%d page', '%d pages'), context='report') + assert cat.is_identical(same)