Repository: python-babel/babel

Issue:
Obsoletes with same ID from different contexts get removed on update
## Overview Description

When a file contains multiple obsoletes _from different contexts_, and they have the same ID, all but the last one will get stripped from the file.

## Steps to Reproduce

1. Place multiple messages into a PO file with same `msgid` but different `msgctxt` (in arbitrary order, optionally including one without a `msgctxt` as well):
   ```po
   msgctxt "context1"
   msgid "foo"
   msgstr "foo-1"

   msgid "foo"
   msgstr "foo-0"

   msgctxt "context2"
   msgid "foo"
   msgstr "foo-2"
   ```
2. Run `pybabel update` with appropriate params; the messages should be moved to the end of the file and marked obsolete:
   ```po
   #~ msgctxt "context1"
   #~ msgid "foo"
   #~ msgstr "foo-1"

   #~ msgid "foo"
   #~ msgstr "foo-0"

   #~ msgctxt "context2"
   #~ msgid "foo"
   #~ msgstr "foo-2"
   ```
3. Re-run the previous command

## Actual Results
Only the last obsolete entry with that `msgid` remains in the file (based on their order before the last update command, and regardless of whether a message without `msgctxt` was present):
```po
#~ msgctxt "context2"
#~ msgid "foo"
#~ msgstr "foo-2"
```

## Expected Results
The messages were _from different contexts_ so they should all be retained.

## Reproducibility
This issue appears to reproduce whenever the described situation occurs.

## Additional Information
Tested on Babel version 2.16.0


Discussion:
Seems to be an issue with how the obsolete mesages are parsed:

https://github.com/python-babel/babel/blob/f91754b01cb9f32b83aeaa80b74ed10b5dfccb6a/babel/messages/pofile.py#L240-L242

The above looks to only be using the msgid, rather than `(msgid, msgctx)`, so messages with the same msgid but different msgctx will get overwritten. This seems to fix it:

```diff
if self.obsolete:
    if not self.ignore_obsolete:
-        self.catalog.obsolete[msgid] = message
+        self.catalog.obsolete[self.catalog._key_for(msgid, msgctxt)] = message
```




Patch:
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index 5cd65d867..fa9f2a713 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -239,7 +239,7 @@ def _add_message(self) -> None:
                           context=msgctxt)
         if self.obsolete:
             if not self.ignore_obsolete:
-                self.catalog.obsolete[msgid] = message
+                self.catalog.obsolete[self.catalog._key_for(msgid, msgctxt)] = message
         else:
             self.catalog[msgid] = message
         self.counter += 1


Test patch:
diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py
index c0ded1296..f1bfcd0fa 100644
--- a/tests/messages/test_pofile.py
+++ b/tests/messages/test_pofile.py
@@ -299,10 +299,62 @@ def test_obsolete_message_with_context(self):
         catalog = pofile.read_po(buf)
         assert len(catalog) == 2
         assert len(catalog.obsolete) == 1
-        message = catalog.obsolete["foo"]
+        message = catalog.obsolete[("foo", "other")]
         assert message.context == 'other'
         assert message.string == 'Voh'
 
+    def test_obsolete_messages_with_context(self):
+        buf = StringIO('''
+# This is an obsolete message
+#~ msgctxt "apple"
+#~ msgid "foo"
+#~ msgstr "Foo"
+
+# This is an obsolete message with the same id but different context
+#~ msgctxt "orange"
+#~ msgid "foo"
+#~ msgstr "Bar"
+''')
+        catalog = pofile.read_po(buf)
+        assert len(catalog) == 0
+        assert len(catalog.obsolete) == 2
+        assert 'foo' not in catalog.obsolete
+
+        apple_msg = catalog.obsolete[('foo', 'apple')]
+        assert apple_msg.id == 'foo'
+        assert apple_msg.string == 'Foo'
+        assert apple_msg.user_comments == ['This is an obsolete message']
+
+        orange_msg = catalog.obsolete[('foo', 'orange')]
+        assert orange_msg.id == 'foo'
+        assert orange_msg.string == 'Bar'
+        assert orange_msg.user_comments == ['This is an obsolete message with the same id but different context']
+
+    def test_obsolete_messages_roundtrip(self):
+        buf = StringIO('''\
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+
+# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This is an obsolete message
+#~ msgctxt "apple"
+#~ msgid "foo"
+#~ msgstr "Foo"
+
+# This is an obsolete message with the same id but different context
+#~ msgctxt "orange"
+#~ msgid "foo"
+#~ msgstr "Bar"
+
+''')
+        generated_po_file = ''.join(pofile.generate_po(pofile.read_po(buf), omit_header=True))
+        assert buf.getvalue() == generated_po_file
+
     def test_multiline_context(self):
         buf = StringIO('''
 msgctxt "a really long "
@@ -394,7 +446,7 @@ def test_obsolete_plural_with_square_brackets(self):
         assert len(catalog) == 0
         assert len(catalog.obsolete) == 1
         assert catalog.num_plurals == 2
-        message = catalog.obsolete[('foo', 'foos')]
+        message = catalog.obsolete['foo']
         assert len(message.string) == 2
         assert message.string[0] == 'Voh [text]'
         assert message.string[1] == 'Vohs [text]'