Skip to content

Commit eee72d4

Browse files
Tobotimusserhiy-storchaka
authored andcommitted
bpo-32222: Fix pygettext skipping docstrings for funcs with arg typehints (GH-4745)
1 parent 3a087be commit eee72d4

File tree

3 files changed

+102
-4
lines changed

3 files changed

+102
-4
lines changed

Lib/test/test_tools/test_i18n.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import sys
55
import unittest
6+
import textwrap
67

78
from test.support.script_helper import assert_python_ok
89
from test.test_tools import skip_if_missing, toolsdir
@@ -28,6 +29,41 @@ def get_header(self, data):
2829
headers[key] = val.strip()
2930
return headers
3031

32+
def get_msgids(self, data):
33+
""" utility: return all msgids in .po file as a list of strings """
34+
msgids = []
35+
reading_msgid = False
36+
cur_msgid = []
37+
for line in data.split('\n'):
38+
if reading_msgid:
39+
if line.startswith('"'):
40+
cur_msgid.append(line.strip('"'))
41+
else:
42+
msgids.append('\n'.join(cur_msgid))
43+
cur_msgid = []
44+
reading_msgid = False
45+
continue
46+
if line.startswith('msgid '):
47+
line = line[len('msgid '):]
48+
cur_msgid.append(line.strip('"'))
49+
reading_msgid = True
50+
else:
51+
if reading_msgid:
52+
msgids.append('\n'.join(cur_msgid))
53+
54+
return msgids
55+
56+
def extract_docstrings_from_str(self, module_content):
57+
""" utility: return all msgids extracted from module_content """
58+
filename = 'test_docstrings.py'
59+
with temp_cwd(None) as cwd:
60+
with open(filename, 'w') as fp:
61+
fp.write(module_content)
62+
assert_python_ok(self.script, '-D', filename)
63+
with open('messages.pot') as fp:
64+
data = fp.read()
65+
return self.get_msgids(data)
66+
3167
def test_header(self):
3268
"""Make sure the required fields are in the header, according to:
3369
http://www.gnu.org/software/gettext/manual/gettext.html#Header-Entry
@@ -72,3 +108,55 @@ def test_POT_Creation_Date(self):
72108

73109
# This will raise if the date format does not exactly match.
74110
datetime.strptime(creationDate, '%Y-%m-%d %H:%M%z')
111+
112+
def test_funcdocstring_annotated_args(self):
113+
""" Test docstrings for functions with annotated args """
114+
msgids = self.extract_docstrings_from_str(textwrap.dedent('''\
115+
def foo(bar: str):
116+
"""doc"""
117+
'''))
118+
self.assertIn('doc', msgids)
119+
120+
def test_funcdocstring_annotated_return(self):
121+
""" Test docstrings for functions with annotated return type """
122+
msgids = self.extract_docstrings_from_str(textwrap.dedent('''\
123+
def foo(bar) -> str:
124+
"""doc"""
125+
'''))
126+
self.assertIn('doc', msgids)
127+
128+
def test_funcdocstring_defvalue_args(self):
129+
""" Test docstring for functions with default arg values """
130+
msgids = self.extract_docstrings_from_str(textwrap.dedent('''\
131+
def foo(bar=()):
132+
"""doc"""
133+
'''))
134+
self.assertIn('doc', msgids)
135+
136+
def test_funcdocstring_multiple_funcs(self):
137+
""" Test docstring extraction for multiple functions combining
138+
annotated args, annotated return types and default arg values
139+
"""
140+
msgids = self.extract_docstrings_from_str(textwrap.dedent('''\
141+
def foo1(bar: tuple=()) -> str:
142+
"""doc1"""
143+
144+
def foo2(bar: List[1:2]) -> (lambda x: x):
145+
"""doc2"""
146+
147+
def foo3(bar: 'func'=lambda x: x) -> {1: 2}:
148+
"""doc3"""
149+
'''))
150+
self.assertIn('doc1', msgids)
151+
self.assertIn('doc2', msgids)
152+
self.assertIn('doc3', msgids)
153+
154+
def test_classdocstring_early_colon(self):
155+
""" Test docstring extraction for a class with colons occuring within
156+
the parentheses.
157+
"""
158+
msgids = self.extract_docstrings_from_str(textwrap.dedent('''\
159+
class D(L[1:2], F({1: 2}), metaclass=M(lambda x: x)):
160+
"""doc"""
161+
'''))
162+
self.assertIn('doc', msgids)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix pygettext not extracting docstrings for functions with type annotated
2+
arguments.
3+
Patch by Toby Harradine.

Tools/i18n/pygettext.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ def __init__(self, options):
320320
self.__lineno = -1
321321
self.__freshmodule = 1
322322
self.__curfile = None
323+
self.__enclosurecount = 0
323324

324325
def __call__(self, ttype, tstring, stup, etup, line):
325326
# dispatch
@@ -340,17 +341,23 @@ def __waiting(self, ttype, tstring, lineno):
340341
elif ttype not in (tokenize.COMMENT, tokenize.NL):
341342
self.__freshmodule = 0
342343
return
343-
# class docstring?
344+
# class or func/method docstring?
344345
if ttype == tokenize.NAME and tstring in ('class', 'def'):
345346
self.__state = self.__suiteseen
346347
return
347348
if ttype == tokenize.NAME and tstring in opts.keywords:
348349
self.__state = self.__keywordseen
349350

350351
def __suiteseen(self, ttype, tstring, lineno):
351-
# ignore anything until we see the colon
352-
if ttype == tokenize.OP and tstring == ':':
353-
self.__state = self.__suitedocstring
352+
# skip over any enclosure pairs until we see the colon
353+
if ttype == tokenize.OP:
354+
if tstring == ':' and self.__enclosurecount == 0:
355+
# we see a colon and we're not in an enclosure: end of def
356+
self.__state = self.__suitedocstring
357+
elif tstring in '([{':
358+
self.__enclosurecount += 1
359+
elif tstring in ')]}':
360+
self.__enclosurecount -= 1
354361

355362
def __suitedocstring(self, ttype, tstring, lineno):
356363
# ignore any intervening noise

0 commit comments

Comments
 (0)