@@ -238,9 +238,41 @@ def _parse_param_list(self, content):
238
238
239
239
return params
240
240
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 = '..'
244
276
245
277
def _parse_see_also (self , content ):
246
278
"""
@@ -250,52 +282,49 @@ def _parse_see_also(self, content):
250
282
func_name1, func_name2, :meth:`func_name`, func_name3
251
283
252
284
"""
285
+
253
286
items = []
254
287
255
288
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 ()
272
296
273
- current_func = None
274
297
rest = []
275
-
276
298
for line in content :
277
299
if not line .strip ():
278
300
continue
279
301
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 (' ' ):
297
312
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 )
299
328
return items
300
329
301
330
def _parse_index (self , section , content ):
@@ -445,24 +474,30 @@ def _str_see_also(self, func_role):
445
474
return []
446
475
out = []
447
476
out += self ._str_header ("See Also" )
477
+ out += ['' ]
448
478
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 ]
461
492
if desc :
462
493
out += self ._str_indent ([' ' .join (desc )])
463
494
last_had_desc = True
464
495
else :
465
496
last_had_desc = False
497
+ out += self ._str_indent ([self .empty_description ])
498
+
499
+ if last_had_desc :
500
+ out += ['' ]
466
501
out += ['' ]
467
502
return out
468
503
0 commit comments