@@ -238,9 +238,41 @@ def _parse_param_list(self, content):
238238
239239 return params
240240
241- _name_rgx = re .compile (r"^\s*(:(?P<role>\w+):"
242- r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`|"
243- r" (?P<name2>[a-zA-Z0-9_.-]+))\s*" , re .X )
241+ # See also supports the following formats.
242+ #
243+ # <FUNCNAME>
244+ # <FUNCNAME> SPACE* COLON SPACE+ <DESC> SPACE*
245+ # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)* SPACE*
246+ # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)* SPACE* COLON SPACE+ <DESC> SPACE*
247+
248+ # <FUNCNAME> is one of
249+ # <PLAIN_FUNCNAME>
250+ # COLON <ROLE> COLON BACKTICK <PLAIN_FUNCNAME> BACKTICK
251+ # where
252+ # <PLAIN_FUNCNAME> is a legal function name, and
253+ # <ROLE> is any nonempty sequence of word characters.
254+ # Examples: func_f1 :meth:`func_h1` :obj:`~baz.obj_r` :class:`class_j`
255+ # <DESC> is a string describing the function.
256+
257+ _role = r":(?P<role>\w+):"
258+ _funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_.-]+)`"
259+ _funcplain = r"(?P<name2>[a-zA-Z0-9_.-]+)"
260+ _funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
261+ _funcnamenext = _funcname .replace ('role' , 'rolenext' )
262+ _funcnamenext = _funcnamenext .replace ('name' , 'namenext' )
263+ _description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
264+ _func_rgx = re .compile (r"^\s*" + _funcname + r"\s*" )
265+ _line_rgx = re .compile (
266+ r"^\s*" +
267+ r"(?P<allfuncs>" + # group for all function names
268+ _funcname +
269+ r"(?P<morefuncs>([,]\s+" + _funcnamenext + r")*)" +
270+ r")" + # end of "allfuncs"
271+ r"(?P<trailing>\s*,)?" + # Some function lists have a trailing comma
272+ _description )
273+
274+ # Empty <DESC> elements are replaced with '..'
275+ empty_description = '..'
244276
245277 def _parse_see_also (self , content ):
246278 """
@@ -250,52 +282,49 @@ def _parse_see_also(self, content):
250282 func_name1, func_name2, :meth:`func_name`, func_name3
251283
252284 """
285+
253286 items = []
254287
255288 def parse_item_name (text ):
256- """Match ':role:`name`' or 'name'"""
257- m = self ._name_rgx .match (text )
258- if m :
259- g = m .groups ()
260- if g [1 ] is None :
261- return g [3 ], None
262- else :
263- return g [2 ], g [1 ]
264- raise ParseError ("%s is not a item name" % text )
265-
266- def push_item (name , rest ):
267- if not name :
268- return
269- name , role = parse_item_name (name )
270- items .append ((name , list (rest ), role ))
271- del rest [:]
289+ """Match ':role:`name`' or 'name'."""
290+ m = self ._func_rgx .match (text )
291+ if not m :
292+ raise ParseError ("%s is not a item name" % text )
293+ role = m .group ('role' )
294+ name = m .group ('name' ) if role else m .group ('name2' )
295+ return name , role , m .end ()
272296
273- current_func = None
274297 rest = []
275-
276298 for line in content :
277299 if not line .strip ():
278300 continue
279301
280- m = self ._name_rgx .match (line )
281- if m and line [m .end ():].strip ().startswith (':' ):
282- push_item (current_func , rest )
283- current_func , line = line [:m .end ()], line [m .end ():]
284- rest = [line .split (':' , 1 )[1 ].strip ()]
285- if not rest [0 ]:
286- rest = []
287- elif not line .startswith (' ' ):
288- push_item (current_func , rest )
289- current_func = None
290- if ',' in line :
291- for func in line .split (',' ):
292- if func .strip ():
293- push_item (func , [])
294- elif line .strip ():
295- current_func = line
296- elif current_func is not None :
302+ line_match = self ._line_rgx .match (line )
303+ description = None
304+ if line_match :
305+ description = line_match .group ('desc' )
306+ if line_match .group ('trailing' ):
307+ self ._error_location (
308+ 'Unexpected comma after function list at index %d of '
309+ 'line "%s"' % (line_match .end ('trailing' ), line ),
310+ error = False )
311+ if not description and line .startswith (' ' ):
297312 rest .append (line .strip ())
298- push_item (current_func , rest )
313+ elif line_match :
314+ funcs = []
315+ text = line_match .group ('allfuncs' )
316+ while True :
317+ if not text .strip ():
318+ break
319+ name , role , match_end = parse_item_name (text )
320+ funcs .append ((name , role ))
321+ text = text [match_end :].strip ()
322+ if text and text [0 ] == ',' :
323+ text = text [1 :].strip ()
324+ rest = list (filter (None , [description ]))
325+ items .append ((funcs , rest ))
326+ else :
327+ raise ParseError ("%s is not a item name" % line )
299328 return items
300329
301330 def _parse_index (self , section , content ):
@@ -445,24 +474,30 @@ def _str_see_also(self, func_role):
445474 return []
446475 out = []
447476 out += self ._str_header ("See Also" )
477+ out += ['' ]
448478 last_had_desc = True
449- for func , desc , role in self ['See Also' ]:
450- if role :
451- link = ':%s:`%s`' % (role , func )
452- elif func_role :
453- link = ':%s:`%s`' % (func_role , func )
454- else :
455- link = "`%s`_" % func
456- if desc or last_had_desc :
457- out += ['' ]
458- out += [link ]
459- else :
460- out [- 1 ] += ", %s" % link
479+ for funcs , desc in self ['See Also' ]:
480+ assert isinstance (funcs , list )
481+ links = []
482+ for func , role in funcs :
483+ if role :
484+ link = ':%s:`%s`' % (role , func )
485+ elif func_role :
486+ link = ':%s:`%s`' % (func_role , func )
487+ else :
488+ link = "`%s`_" % func
489+ links .append (link )
490+ link = ', ' .join (links )
491+ out += [link ]
461492 if desc :
462493 out += self ._str_indent ([' ' .join (desc )])
463494 last_had_desc = True
464495 else :
465496 last_had_desc = False
497+ out += self ._str_indent ([self .empty_description ])
498+
499+ if last_had_desc :
500+ out += ['' ]
466501 out += ['' ]
467502 return out
468503
0 commit comments