Skip to content

Commit eade1e7

Browse files
committed
Closes #1904: [Napoleon] parses restructuredtext references in fields/params BEFORE splitting on colon
1 parent d966317 commit eade1e7

File tree

2 files changed

+81
-27
lines changed

2 files changed

+81
-27
lines changed

sphinx/ext/napoleon/docstring.py

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424

2525
_directive_regex = re.compile(r'\.\. \S+::')
26-
_google_untyped_arg_regex = re.compile(r'(.+)\s*(?<!:):(?!:)\s*(.*)')
27-
_google_typed_arg_regex = re.compile(r'(.+)\((.+)\)\s*(?<!:):(?!:)\s*(.*)')
26+
_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.+?)\s*\)')
27+
_xref_regex = re.compile(r'(:\w+:\S+:`.+?`|:\S+:`.+?`|`.+?`)')
2828

2929

3030
class GoogleDocstring(UnicodeMixin):
@@ -202,20 +202,14 @@ def _consume_empty(self):
202202
def _consume_field(self, parse_type=True, prefer_type=False):
203203
line = next(self._line_iter)
204204

205-
match = None
206-
_name, _type, _desc = line.strip(), '', ''
207-
if parse_type:
208-
match = _google_typed_arg_regex.match(line)
209-
if match:
210-
_name = match.group(1).strip()
211-
_type = match.group(2).strip()
212-
_desc = match.group(3).strip()
205+
before, colon, after = self._partition_field_on_colon(line)
206+
_name, _type, _desc = before, '', after
213207

214-
if not match:
215-
match = _google_untyped_arg_regex.match(line)
208+
if parse_type:
209+
match = _google_typed_arg_regex.match(before)
216210
if match:
217-
_name = match.group(1).strip()
218-
_desc = match.group(2).strip()
211+
_name = match.group(1)
212+
_type = match.group(2)
219213

220214
if _name[:2] == '**':
221215
_name = r'\*\*'+_name[2:]
@@ -241,20 +235,21 @@ def _consume_fields(self, parse_type=True, prefer_type=False):
241235
def _consume_returns_section(self):
242236
lines = self._dedent(self._consume_to_next_section())
243237
if lines:
238+
before, colon, after = self._partition_field_on_colon(lines[0])
244239
_name, _type, _desc = '', '', lines
245-
match = _google_typed_arg_regex.match(lines[0])
246-
if match:
247-
_name = match.group(1).strip()
248-
_type = match.group(2).strip()
249-
_desc = match.group(3).strip()
250-
else:
251-
match = _google_untyped_arg_regex.match(lines[0])
240+
241+
if colon:
242+
if after:
243+
_desc = [after] + lines[1:]
244+
else:
245+
_desc = lines[1:]
246+
247+
match = _google_typed_arg_regex.match(before)
252248
if match:
253-
_type = match.group(1).strip()
254-
_desc = match.group(2).strip()
255-
if match:
256-
lines[0] = _desc
257-
_desc = lines
249+
_name = match.group(1)
250+
_type = match.group(2)
251+
else:
252+
_type = before
258253

259254
_desc = self.__class__(_desc, self._config).lines()
260255
return [(_name, _type, _desc,)]
@@ -593,6 +588,27 @@ def _parse_yields_section(self, section):
593588
fields = self._consume_returns_section()
594589
return self._format_fields('Yields', fields)
595590

591+
def _partition_field_on_colon(self, line):
592+
before_colon = []
593+
after_colon = []
594+
colon = ''
595+
found_colon = False
596+
for i, source in enumerate(_xref_regex.split(line)):
597+
if found_colon:
598+
after_colon.append(source)
599+
else:
600+
if (i % 2) == 0 and ":" in source:
601+
found_colon = True
602+
before, colon, after = source.partition(":")
603+
before_colon.append(before)
604+
after_colon.append(after)
605+
else:
606+
before_colon.append(source)
607+
608+
return ("".join(before_colon).strip(),
609+
colon,
610+
"".join(after_colon).strip())
611+
596612
def _strip_empty(self, lines):
597613
if lines:
598614
start = -1
@@ -719,7 +735,7 @@ def __init__(self, docstring, config=None, app=None, what='', name='',
719735
def _consume_field(self, parse_type=True, prefer_type=False):
720736
line = next(self._line_iter)
721737
if parse_type:
722-
_name, _, _type = line.partition(':')
738+
_name, _, _type = self._partition_field_on_colon(line)
723739
if not _name:
724740
_type = line
725741
else:

tests/test_ext_napoleon_docstring.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,22 @@ def test_colon_in_return_type(self):
356356
:returns: an example instance
357357
if available, None if not available.
358358
:rtype: :py:class:`~.module.submodule.SomeClass`
359+
"""
360+
actual = str(GoogleDocstring(docstring))
361+
self.assertEqual(expected, actual)
362+
363+
def test_xrefs_in_return_type(self):
364+
docstring = """Example Function
365+
366+
Returns:
367+
:class:`numpy.ndarray`: A :math:`n \\times 2` array containing
368+
a bunch of math items
369+
"""
370+
expected = """Example Function
371+
372+
:returns: A :math:`n \\times 2` array containing
373+
a bunch of math items
374+
:rtype: :class:`numpy.ndarray`
359375
"""
360376
actual = str(GoogleDocstring(docstring))
361377
self.assertEqual(expected, actual)
@@ -696,3 +712,25 @@ def test_underscore_in_attribute(self):
696712
actual = str(NumpyDocstring(docstring, config, app, "class"))
697713

698714
self.assertEqual(expected, actual)
715+
716+
def test_xrefs_in_return_type(self):
717+
docstring = """
718+
Example Function
719+
720+
Returns
721+
-------
722+
:class:`numpy.ndarray`
723+
A :math:`n \\times 2` array containing
724+
a bunch of math items
725+
"""
726+
expected = """
727+
Example Function
728+
729+
:returns: A :math:`n \\times 2` array containing
730+
a bunch of math items
731+
:rtype: :class:`numpy.ndarray`
732+
"""
733+
config = Config()
734+
app = mock.Mock()
735+
actual = str(NumpyDocstring(docstring, config, app, "method"))
736+
self.assertEqual(expected, actual)

0 commit comments

Comments
 (0)