Repository: python-babel/babel (python)

Issue:
Extract Python messages in nested gettext calls
When Babel extracts messages from Python code, it ignores `_(...)` in another `_(...)`.

My `_` is slightly customized to accept `**kwargs` for formatting.  I guess it is one of common patterns.  So I can write like this code:

```python
#: TRANSLATOR: Users can have one or more fruits.
text = _('You have {fruit}!', fruit=_('an apple'))
```

Babel extracts only `You have {fruit}!` in the above code.  There's no `an apple`.  But at the same case in C#, xgettext can extract both of the messages.  So I think this is a bug in Babel.

Pull request description:
Extract Python messages in nested gettext calls
This patch fixes #487.

There're only one small change and complex test cases.

Patch:
diff --git a/babel/messages/extract.py b/babel/messages/extract.py
index efddbf5..ffcfac8 100644
--- a/babel/messages/extract.py
+++ b/babel/messages/extract.py
@@ -445,7 +445,8 @@ def extract_python(fileobj, keywords, comment_tags, options):
                     translator_comments.append((lineno, value))
                     break
         elif funcname and call_stack == 0:
-            if tok == OP and value == ')':
+            nested = (tok == NAME and value in keywords)
+            if (tok == OP and value == ')') or nested:
                 if buf:
                     messages.append(''.join(buf))
                     del buf[:]
@@ -470,6 +471,8 @@ def extract_python(fileobj, keywords, comment_tags, options):
                 messages = []
                 translator_comments = []
                 in_translator_comments = False
+                if nested:
+                    funcname = value
             elif tok == STRING:
                 # Unwrap quotes in a safe manner, maintaining the string's
                 # encoding


Test patch:
diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py
index 22ea1cd..873439d 100644
--- a/tests/messages/test_extract.py
+++ b/tests/messages/test_extract.py
@@ -424,6 +424,37 @@ _('Babatschi')""")
         self.assertEqual([u'This is a multiline comment with',
                           u'a prefix too'], messages[1][2])
 
+    def test_nested_messages(self):
+        buf = BytesIO(b"""
+# NOTE: First
+_(u'Hello, {name}!', name=_(u'Foo Bar'))
+
+# NOTE: Second
+_(u'Hello, {name1} and {name2}!', name1=_(u'Heungsub'),
+  name2=_(u'Armin'))
+
+# NOTE: Third
+_(u'Hello, {0} and {1}!', _(u'Heungsub'),
+  _(u'Armin'))
+""")
+        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+        self.assertEqual((u'Hello, {name}!', None), messages[0][2])
+        self.assertEqual([u'NOTE: First'], messages[0][3])
+        self.assertEqual(u'Foo Bar', messages[1][2])
+        self.assertEqual([], messages[1][3])
+        self.assertEqual((u'Hello, {name1} and {name2}!', None), messages[2][2])
+        self.assertEqual([u'NOTE: Second'], messages[2][3])
+        self.assertEqual(u'Heungsub', messages[3][2])
+        self.assertEqual([], messages[3][3])
+        self.assertEqual(u'Armin', messages[4][2])
+        self.assertEqual([], messages[4][3])
+        self.assertEqual((u'Hello, {0} and {1}!', None), messages[5][2])
+        self.assertEqual([u'NOTE: Third'], messages[5][3])
+        self.assertEqual(u'Heungsub', messages[6][2])
+        self.assertEqual([], messages[6][3])
+        self.assertEqual(u'Armin', messages[7][2])
+        self.assertEqual([], messages[7][3])
+
 
 class ExtractTestCase(unittest.TestCase):