Skip to content

Commit 5ca74fe

Browse files
committed
ENH: Added support for multiple functions+description in a See Also block.
1 parent 40b3733 commit 5ca74fe

File tree

2 files changed

+121
-64
lines changed

2 files changed

+121
-64
lines changed

numpydoc/docscrape.py

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,24 @@ def _parse_param_list(self, content):
236236

237237
return params
238238

239+
_role = r":(?P<role>\w+):"
240+
_funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`"
241+
_funcplain = r"(?P<name2>[a-zA-Z0-9_.-]+)"
242+
_funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
243+
_funcnamenext = _funcname.replace('role', 'rolenext').replace('name', 'namenext')
244+
_description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
245+
_func_rgx = re.compile(r"^\s*" + _funcname + r"\s*", re.X)
246+
# _funcs_rgx = re.compile(r"^\s*" + _funcname + r"(?P<morefuncs>([,\s]\s*" + _funcnamenext + r")*)" + r"\s*", re.X)
247+
_line_rgx = re.compile(r"^\s*"
248+
+ r"(?P<allfuncs>" # group for all function names
249+
+ _funcname
250+
+ r"(?P<morefuncs>([,]\s+"
251+
+ _funcnamenext + r")*)"
252+
+ r")" # end of "allfuncs"
253+
+ r"(\s*,)?" # Some function lists have a trailing comma
254+
+ _description,
255+
re.X)
256+
239257
_name_rgx = re.compile(r"^\s*(:(?P<role>\w+):"
240258
r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`|"
241259
r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
@@ -252,48 +270,62 @@ def _parse_see_also(self, content):
252270

253271
def parse_item_name(text):
254272
"""Match ':role:`name`' or 'name'"""
255-
m = self._name_rgx.match(text)
256-
if m:
257-
g = m.groups()
258-
if g[1] is None:
259-
return g[3], None
260-
else:
261-
return g[2], g[1]
262-
raise ParseError("%s is not a item name" % text)
273+
m = self._func_rgx.match(text)
274+
if not m:
275+
raise ParseError("%s is not a item name" % text)
276+
role = m.groupdict().get('role')
277+
if role:
278+
name = m.group('name')
279+
else:
280+
name = m.group('name2')
281+
return name, role, m
263282

264283
def push_item(name, rest):
265284
if not name:
266285
return
267-
name, role = parse_item_name(name)
286+
name, role, m2 = parse_item_name(name)
268287
items.append((name, list(rest), role))
269288
del rest[:]
270289

271-
current_func = None
272290
rest = []
273291

274292
for line in content:
275293
if not line.strip():
276294
continue
277295

278-
m = self._name_rgx.match(line)
279-
if m and line[m.end():].strip().startswith(':'):
280-
push_item(current_func, rest)
281-
current_func, line = line[:m.end()], line[m.end():]
282-
rest = [line.split(':', 1)[1].strip()]
283-
if not rest[0]:
284-
rest = []
285-
elif not line.startswith(' '):
286-
push_item(current_func, rest)
287-
current_func = None
288-
if ',' in line:
289-
for func in line.split(','):
290-
if func.strip():
291-
push_item(func, [])
292-
elif line.strip():
293-
current_func = line
294-
elif current_func is not None:
296+
ml = self._line_rgx.match(line)
297+
description = None
298+
if ml:
299+
if 'description' in ml.groupdict():
300+
description = ml.groupdict().get('desc')
301+
if not description and line.startswith(' '):
295302
rest.append(line.strip())
296-
push_item(current_func, rest)
303+
elif ml:
304+
funcs = []
305+
text = ml.group('allfuncs')
306+
while True:
307+
if not text.strip():
308+
break
309+
name, role, m2 = parse_item_name(text)
310+
# m2 = self._func_rgx.match(text)
311+
# if not m2:
312+
# raise ParseError("%s is not a item name" % line)
313+
# role = m2.groupdict().get('role')
314+
# if role:
315+
# name = m2.group('name')
316+
# else:
317+
# name = m2.group('name2')
318+
funcs.append((name, role))
319+
text = text[m2.end():].strip()
320+
if text and text[0] == ',':
321+
text = text[1:].strip()
322+
if description:
323+
rest = [description]
324+
else:
325+
rest = []
326+
items.append((funcs, rest))
327+
else:
328+
raise ParseError("%s is not a item name" % line)
297329
return items
298330

299331
def _parse_index(self, section, content):
@@ -440,25 +472,35 @@ def _str_see_also(self, func_role):
440472
return []
441473
out = []
442474
out += self._str_header("See Also")
475+
out += ['']
443476
last_had_desc = True
444-
for func, desc, role in self['See Also']:
445-
if role:
446-
link = ':%s:`%s`' % (role, func)
447-
elif func_role:
448-
link = ':%s:`%s`' % (func_role, func)
449-
else:
450-
link = "`%s`_" % func
451-
if desc or last_had_desc:
452-
out += ['']
453-
out += [link]
454-
else:
455-
out[-1] += ", %s" % link
477+
for funcs, desc in self['See Also']:
478+
assert isinstance(funcs, (list, tuple))
479+
links = []
480+
for func, role in funcs:
481+
if role:
482+
link = ':%s:`%s`' % (role, func)
483+
elif func_role:
484+
link = ':%s:`%s`' % (func_role, func)
485+
else:
486+
link = "`%s`_" % func
487+
links.append(link)
488+
link = ', '.join(links)
489+
out += [link]
456490
if desc:
457491
out += self._str_indent([' '.join(desc)])
458492
last_had_desc = True
459493
else:
460494
last_had_desc = False
495+
out += ['']
496+
if last_had_desc:
497+
out += ['']
461498
out += ['']
499+
# if 1:
500+
# print()
501+
# for l in out:
502+
# print(repr(l))
503+
# # print(out)
462504
return out
463505

464506
def _str_index(self):

numpydoc/tests/test_docscrape.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -709,36 +709,51 @@ def test_see_also():
709709
multiple lines
710710
func_f, func_g, :meth:`func_h`, func_j,
711711
func_k
712+
func_f1, func_g1, :meth:`func_h1`, func_j1
713+
func_f2, func_g2, :meth:`func_h2`, func_j2 : description of multiple
712714
:obj:`baz.obj_q`
713715
:obj:`~baz.obj_r`
714716
:class:`class_j`: fubar
715717
foobar
716718
""")
717719

718-
assert len(doc6['See Also']) == 13
719-
for func, desc, role in doc6['See Also']:
720-
if func in ('func_a', 'func_b', 'func_c', 'func_f',
721-
'func_g', 'func_h', 'func_j', 'func_k', 'baz.obj_q',
722-
'~baz.obj_r'):
723-
assert(not desc)
724-
else:
725-
assert(desc)
726-
727-
if func == 'func_h':
728-
assert role == 'meth'
729-
elif func == 'baz.obj_q' or func == '~baz.obj_r':
730-
assert role == 'obj'
731-
elif func == 'class_j':
732-
assert role == 'class'
733-
else:
734-
assert role is None
735-
736-
if func == 'func_d':
737-
assert desc == ['some equivalent func']
738-
elif func == 'foo.func_e':
739-
assert desc == ['some other func over', 'multiple lines']
740-
elif func == 'class_j':
741-
assert desc == ['fubar', 'foobar']
720+
assert len(doc6['See Also']) == 10, str([len(doc6['See Also'])])
721+
for funcs, desc in doc6['See Also']:
722+
print(funcs, desc)
723+
for func, role in funcs:
724+
if func in ('func_a', 'func_b', 'func_c', 'func_f',
725+
'func_g', 'func_h', 'func_j', 'func_k', 'baz.obj_q',
726+
'func_f1', 'func_g1', 'func_h1', 'func_j1',
727+
'~baz.obj_r'):
728+
assert (not desc), str([func, desc])
729+
elif func in ('func_f2', 'func_g2', 'func_h2', 'func_j2'):
730+
assert (desc), str([func, desc])
731+
else:
732+
assert(desc), str([func, desc])
733+
734+
if func == 'func_h':
735+
assert role == 'meth'
736+
elif func == 'baz.obj_q' or func == '~baz.obj_r':
737+
assert role == 'obj'
738+
elif func == 'class_j':
739+
assert role == 'class'
740+
elif func in ['func_h1', 'func_h2']:
741+
assert role == 'meth'
742+
else:
743+
assert role is None, str([func, role])
744+
745+
if func == 'func_d':
746+
assert desc == ['some equivalent func']
747+
elif func == 'foo.func_e':
748+
assert desc == ['some other func over', 'multiple lines']
749+
elif func == 'class_j':
750+
assert desc == ['fubar', 'foobar']
751+
elif func == 'func_j2':
752+
assert desc == ['description of multiple'], str([desc, ['description of multiple']])
753+
754+
# s = str(doc6)
755+
# print(repr(s))
756+
# assert 1 == 0
742757

743758

744759
def test_see_also_parse_error():

0 commit comments

Comments
 (0)