Skip to content

Commit 2ce9cd9

Browse files
committed
ENH: Added support for multiple functions+description in a See Also block.
1 parent de21add commit 2ce9cd9

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
@@ -229,6 +229,24 @@ def _parse_param_list(self, content):
229229

230230
return params
231231

232+
_role = r":(?P<role>\w+):"
233+
_funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`"
234+
_funcplain = r"(?P<name2>[a-zA-Z0-9_.-]+)"
235+
_funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
236+
_funcnamenext = _funcname.replace('role', 'rolenext').replace('name', 'namenext')
237+
_description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
238+
_func_rgx = re.compile(r"^\s*" + _funcname + r"\s*", re.X)
239+
# _funcs_rgx = re.compile(r"^\s*" + _funcname + r"(?P<morefuncs>([,\s]\s*" + _funcnamenext + r")*)" + r"\s*", re.X)
240+
_line_rgx = re.compile(r"^\s*"
241+
+ r"(?P<allfuncs>" # group for all function names
242+
+ _funcname
243+
+ r"(?P<morefuncs>([,]\s+"
244+
+ _funcnamenext + r")*)"
245+
+ r")" # end of "allfuncs"
246+
+ r"(\s*,)?" # Some function lists have a trailing comma
247+
+ _description,
248+
re.X)
249+
232250
_name_rgx = re.compile(r"^\s*(:(?P<role>\w+):"
233251
r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`|"
234252
r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X)
@@ -245,48 +263,62 @@ def _parse_see_also(self, content):
245263

246264
def parse_item_name(text):
247265
"""Match ':role:`name`' or 'name'"""
248-
m = self._name_rgx.match(text)
249-
if m:
250-
g = m.groups()
251-
if g[1] is None:
252-
return g[3], None
253-
else:
254-
return g[2], g[1]
255-
raise ParseError("%s is not a item name" % text)
266+
m = self._func_rgx.match(text)
267+
if not m:
268+
raise ParseError("%s is not a item name" % text)
269+
role = m.groupdict().get('role')
270+
if role:
271+
name = m.group('name')
272+
else:
273+
name = m.group('name2')
274+
return name, role, m
256275

257276
def push_item(name, rest):
258277
if not name:
259278
return
260-
name, role = parse_item_name(name)
279+
name, role, m2 = parse_item_name(name)
261280
items.append((name, list(rest), role))
262281
del rest[:]
263282

264-
current_func = None
265283
rest = []
266284

267285
for line in content:
268286
if not line.strip():
269287
continue
270288

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

292324
def _parse_index(self, section, content):
@@ -432,25 +464,35 @@ def _str_see_also(self, func_role):
432464
return []
433465
out = []
434466
out += self._str_header("See Also")
467+
out += ['']
435468
last_had_desc = True
436-
for func, desc, role in self['See Also']:
437-
if role:
438-
link = ':%s:`%s`' % (role, func)
439-
elif func_role:
440-
link = ':%s:`%s`' % (func_role, func)
441-
else:
442-
link = "`%s`_" % func
443-
if desc or last_had_desc:
444-
out += ['']
445-
out += [link]
446-
else:
447-
out[-1] += ", %s" % link
469+
for funcs, desc in self['See Also']:
470+
assert isinstance(funcs, (list, tuple))
471+
links = []
472+
for func, role in funcs:
473+
if role:
474+
link = ':%s:`%s`' % (role, func)
475+
elif func_role:
476+
link = ':%s:`%s`' % (func_role, func)
477+
else:
478+
link = "`%s`_" % func
479+
links.append(link)
480+
link = ', '.join(links)
481+
out += [link]
448482
if desc:
449483
out += self._str_indent([' '.join(desc)])
450484
last_had_desc = True
451485
else:
452486
last_had_desc = False
487+
out += ['']
488+
if last_had_desc:
489+
out += ['']
453490
out += ['']
491+
# if 1:
492+
# print()
493+
# for l in out:
494+
# print(repr(l))
495+
# # print(out)
454496
return out
455497

456498
def _str_index(self):

numpydoc/tests/test_docscrape.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -694,36 +694,51 @@ def test_see_also():
694694
multiple lines
695695
func_f, func_g, :meth:`func_h`, func_j,
696696
func_k
697+
func_f1, func_g1, :meth:`func_h1`, func_j1
698+
func_f2, func_g2, :meth:`func_h2`, func_j2 : description of multiple
697699
:obj:`baz.obj_q`
698700
:obj:`~baz.obj_r`
699701
:class:`class_j`: fubar
700702
foobar
701703
""")
702704

703-
assert len(doc6['See Also']) == 13
704-
for func, desc, role in doc6['See Also']:
705-
if func in ('func_a', 'func_b', 'func_c', 'func_f',
706-
'func_g', 'func_h', 'func_j', 'func_k', 'baz.obj_q',
707-
'~baz.obj_r'):
708-
assert(not desc)
709-
else:
710-
assert(desc)
711-
712-
if func == 'func_h':
713-
assert role == 'meth'
714-
elif func == 'baz.obj_q' or func == '~baz.obj_r':
715-
assert role == 'obj'
716-
elif func == 'class_j':
717-
assert role == 'class'
718-
else:
719-
assert role is None
720-
721-
if func == 'func_d':
722-
assert desc == ['some equivalent func']
723-
elif func == 'foo.func_e':
724-
assert desc == ['some other func over', 'multiple lines']
725-
elif func == 'class_j':
726-
assert desc == ['fubar', 'foobar']
705+
assert len(doc6['See Also']) == 10, str([len(doc6['See Also'])])
706+
for funcs, desc in doc6['See Also']:
707+
print(funcs, desc)
708+
for func, role in funcs:
709+
if func in ('func_a', 'func_b', 'func_c', 'func_f',
710+
'func_g', 'func_h', 'func_j', 'func_k', 'baz.obj_q',
711+
'func_f1', 'func_g1', 'func_h1', 'func_j1',
712+
'~baz.obj_r'):
713+
assert (not desc), str([func, desc])
714+
elif func in ('func_f2', 'func_g2', 'func_h2', 'func_j2'):
715+
assert (desc), str([func, desc])
716+
else:
717+
assert(desc), str([func, desc])
718+
719+
if func == 'func_h':
720+
assert role == 'meth'
721+
elif func == 'baz.obj_q' or func == '~baz.obj_r':
722+
assert role == 'obj'
723+
elif func == 'class_j':
724+
assert role == 'class'
725+
elif func in ['func_h1', 'func_h2']:
726+
assert role == 'meth'
727+
else:
728+
assert role is None, str([func, role])
729+
730+
if func == 'func_d':
731+
assert desc == ['some equivalent func']
732+
elif func == 'foo.func_e':
733+
assert desc == ['some other func over', 'multiple lines']
734+
elif func == 'class_j':
735+
assert desc == ['fubar', 'foobar']
736+
elif func == 'func_j2':
737+
assert desc == ['description of multiple'], str([desc, ['description of multiple']])
738+
739+
# s = str(doc6)
740+
# print(repr(s))
741+
# assert 1 == 0
727742

728743

729744
def test_see_also_parse_error():

0 commit comments

Comments
 (0)