1# Vendored from https://github.com/numpy/numpydoc/,
2# changeset 4ae1e00e72e522c126403c1814f0b99dc5978622
3
4# This file is licensed under the BSD License. See the LICENSE.txt file
5# in the root of the `numpydoc` repository for complete details.
6
7"""Extract reference documentation from the NumPy source tree.
8
9"""
10import inspect
11import textwrap
12import re
13import pydoc
14from warnings import warn
15from collections import namedtuple
16from collections.abc import Callable, Mapping
17import copy
18import sys
19
20
21def strip_blank_lines(l):
22 "Remove leading and trailing blank lines from a list of lines"
23 while l and not l[0].strip():
24 del l[0]
25 while l and not l[-1].strip():
26 del l[-1]
27 return l
28
29
30class Reader:
31 """A line-based string reader.
32
33 """
34
35 def __init__(self, data):
36 """
37 Parameters
38 ----------
39 data : str
40 String with lines separated by '\\n'.
41
42 """
43 if isinstance(data, list):
44 self._str = data
45 else:
46 self._str = data.split('\n') # store string as list of lines
47
48 self.reset()
49
50 def __getitem__(self, n):
51 return self._str[n]
52
53 def reset(self):
54 self._l = 0 # current line nr
55
56 def read(self):
57 if not self.eof():
58 out = self[self._l]
59 self._l += 1
60 return out
61 else:
62 return ''
63
64 def seek_next_non_empty_line(self):
65 for l in self[self._l:]:
66 if l.strip():
67 break
68 else:
69 self._l += 1
70
71 def eof(self):
72 return self._l >= len(self._str)
73
74 def read_to_condition(self, condition_func):
75 start = self._l
76 for line in self[start:]:
77 if condition_func(line):
78 return self[start:self._l]
79 self._l += 1
80 if self.eof():
81 return self[start:self._l+1]
82 return []
83
84 def read_to_next_empty_line(self):
85 self.seek_next_non_empty_line()
86
87 def is_empty(line):
88 return not line.strip()
89
90 return self.read_to_condition(is_empty)
91
92 def read_to_next_unindented_line(self):
93 def is_unindented(line):
94 return (line.strip() and (len(line.lstrip()) == len(line)))
95 return self.read_to_condition(is_unindented)
96
97 def peek(self, n=0):
98 if self._l + n < len(self._str):
99 return self[self._l + n]
100 else:
101 return ''
102
103 def is_empty(self):
104 return not ''.join(self._str).strip()
105
106
107class ParseError(Exception):
108 def __str__(self):
109 message = self.args[0]
110 if hasattr(self, 'docstring'):
111 message = "%s in %r" % (message, self.docstring)
112 return message
113
114
115Parameter = namedtuple('Parameter', ['name', 'type', 'desc'])
116
117
118class NumpyDocString(Mapping):
119 """Parses a numpydoc string to an abstract representation
120
121 Instances define a mapping from section title to structured data.
122
123 """
124
125 sections = {
126 'Signature': '',
127 'Summary': [''],
128 'Extended Summary': [],
129 'Parameters': [],
130 'Returns': [],
131 'Yields': [],
132 'Receives': [],
133 'Raises': [],
134 'Warns': [],
135 'Other Parameters': [],
136 'Attributes': [],
137 'Methods': [],
138 'See Also': [],
139 'Notes': [],
140 'Warnings': [],
141 'References': '',
142 'Examples': '',
143 'index': {}
144 }
145
146 def __init__(self, docstring, config=None):
147 orig_docstring = docstring
148 docstring = textwrap.dedent(docstring).split('\n')
149
150 self._doc = Reader(docstring)
151 self._parsed_data = copy.deepcopy(self.sections)
152
153 try:
154 self._parse()
155 except ParseError as e:
156 e.docstring = orig_docstring
157 raise
158
159 def __getitem__(self, key):
160 return self._parsed_data[key]
161
162 def __setitem__(self, key, val):
163 if key not in self._parsed_data:
164 self._error_location("Unknown section %s" % key, error=False)
165 else:
166 self._parsed_data[key] = val
167
168 def __iter__(self):
169 return iter(self._parsed_data)
170
171 def __len__(self):
172 return len(self._parsed_data)
173
174 def _is_at_section(self):
175 self._doc.seek_next_non_empty_line()
176
177 if self._doc.eof():
178 return False
179
180 l1 = self._doc.peek().strip() # e.g. Parameters
181
182 if l1.startswith('.. index::'):
183 return True
184
185 l2 = self._doc.peek(1).strip() # ---------- or ==========
186 if len(l2) >= 3 and (set(l2) in ({'-'}, {'='})) and len(l2) != len(l1):
187 snip = '\n'.join(self._doc._str[:2])+'...'
188 self._error_location("potentially wrong underline length... \n%s \n%s in \n%s"
189 % (l1, l2, snip), error=False)
190 return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
191
192 def _strip(self, doc):
193 i = 0
194 j = 0
195 for i, line in enumerate(doc):
196 if line.strip():
197 break
198
199 for j, line in enumerate(doc[::-1]):
200 if line.strip():
201 break
202
203 return doc[i:len(doc)-j]
204
205 def _read_to_next_section(self):
206 section = self._doc.read_to_next_empty_line()
207
208 while not self._is_at_section() and not self._doc.eof():
209 if not self._doc.peek(-1).strip(): # previous line was empty
210 section += ['']
211
212 section += self._doc.read_to_next_empty_line()
213
214 return section
215
216 def _read_sections(self):
217 while not self._doc.eof():
218 data = self._read_to_next_section()
219 name = data[0].strip()
220
221 if name.startswith('..'): # index section
222 yield name, data[1:]
223 elif len(data) < 2:
224 yield StopIteration
225 else:
226 yield name, self._strip(data[2:])
227
228 def _parse_param_list(self, content, single_element_is_type=False):
229 content = dedent_lines(content)
230 r = Reader(content)
231 params = []
232 while not r.eof():
233 header = r.read().strip()
234 if ' :' in header:
235 arg_name, arg_type = header.split(' :', maxsplit=1)
236 arg_name, arg_type = arg_name.strip(), arg_type.strip()
237 else:
238 if single_element_is_type:
239 arg_name, arg_type = '', header
240 else:
241 arg_name, arg_type = header, ''
242
243 desc = r.read_to_next_unindented_line()
244 desc = dedent_lines(desc)
245 desc = strip_blank_lines(desc)
246
247 params.append(Parameter(arg_name, arg_type, desc))
248
249 return params
250
251 # See also supports the following formats.
252 #
253 # <FUNCNAME>
254 # <FUNCNAME> SPACE* COLON SPACE+ <DESC> SPACE*
255 # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)+ (COMMA | PERIOD)? SPACE*
256 # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)* SPACE* COLON SPACE+ <DESC> SPACE*
257
258 # <FUNCNAME> is one of
259 # <PLAIN_FUNCNAME>
260 # COLON <ROLE> COLON BACKTICK <PLAIN_FUNCNAME> BACKTICK
261 # where
262 # <PLAIN_FUNCNAME> is a legal function name, and
263 # <ROLE> is any nonempty sequence of word characters.
264 # Examples: func_f1 :meth:`func_h1` :obj:`~baz.obj_r` :class:`class_j`
265 # <DESC> is a string describing the function.
266
267 _role = r":(?P<role>(py:)?\w+):"
268 _funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_\.-]+)`"
269 _funcplain = r"(?P<name2>[a-zA-Z0-9_\.-]+)"
270 _funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
271 _funcnamenext = _funcname.replace('role', 'rolenext')
272 _funcnamenext = _funcnamenext.replace('name', 'namenext')
273 _description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
274 _func_rgx = re.compile(r"^\s*" + _funcname + r"\s*")
275 _line_rgx = re.compile(
276 r"^\s*" +
277 r"(?P<allfuncs>" + # group for all function names
278 _funcname +
279 r"(?P<morefuncs>([,]\s+" + _funcnamenext + r")*)" +
280 r")" + # end of "allfuncs"
281 # Some function lists have a trailing comma (or period) '\s*'
282 r"(?P<trailing>[,\.])?" +
283 _description)
284
285 # Empty <DESC> elements are replaced with '..'
286 empty_description = '..'
287
288 def _parse_see_also(self, content):
289 """
290 func_name : Descriptive text
291 continued text
292 another_func_name : Descriptive text
293 func_name1, func_name2, :meth:`func_name`, func_name3
294
295 """
296
297 content = dedent_lines(content)
298
299 items = []
300
301 def parse_item_name(text):
302 """Match ':role:`name`' or 'name'."""
303 m = self._func_rgx.match(text)
304 if not m:
305 self._error_location(f"Error parsing See Also entry {line!r}")
306 role = m.group('role')
307 name = m.group('name') if role else m.group('name2')
308 return name, role, m.end()
309
310 rest = []
311 for line in content:
312 if not line.strip():
313 continue
314
315 line_match = self._line_rgx.match(line)
316 description = None
317 if line_match:
318 description = line_match.group('desc')
319 if line_match.group('trailing') and description:
320 self._error_location(
321 'Unexpected comma or period after function list at index %d of '
322 'line "%s"' % (line_match.end('trailing'), line),
323 error=False)
324 if not description and line.startswith(' '):
325 rest.append(line.strip())
326 elif line_match:
327 funcs = []
328 text = line_match.group('allfuncs')
329 while True:
330 if not text.strip():
331 break
332 name, role, match_end = parse_item_name(text)
333 funcs.append((name, role))
334 text = text[match_end:].strip()
335 if text and text[0] == ',':
336 text = text[1:].strip()
337 rest = list(filter(None, [description]))
338 items.append((funcs, rest))
339 else:
340 self._error_location(f"Error parsing See Also entry {line!r}")
341 return items
342
343 def _parse_index(self, section, content):
344 """
345 .. index: default
346 :refguide: something, else, and more
347
348 """
349 def strip_each_in(lst):
350 return [s.strip() for s in lst]
351
352 out = {}
353 section = section.split('::')
354 if len(section) > 1:
355 out['default'] = strip_each_in(section[1].split(','))[0]
356 for line in content:
357 line = line.split(':')
358 if len(line) > 2:
359 out[line[1]] = strip_each_in(line[2].split(','))
360 return out
361
362 def _parse_summary(self):
363 """Grab signature (if given) and summary"""
364 if self._is_at_section():
365 return
366
367 # If several signatures present, take the last one
368 while True:
369 summary = self._doc.read_to_next_empty_line()
370 summary_str = " ".join([s.strip() for s in summary]).strip()
371 compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$')
372 if compiled.match(summary_str):
373 self['Signature'] = summary_str
374 if not self._is_at_section():
375 continue
376 break
377
378 if summary is not None:
379 self['Summary'] = summary
380
381 if not self._is_at_section():
382 self['Extended Summary'] = self._read_to_next_section()
383
384 def _parse(self):
385 self._doc.reset()
386 self._parse_summary()
387
388 sections = list(self._read_sections())
389 section_names = set([section for section, content in sections])
390
391 has_returns = 'Returns' in section_names
392 has_yields = 'Yields' in section_names
393 # We could do more tests, but we are not. Arbitrarily.
394 if has_returns and has_yields:
395 msg = 'Docstring contains both a Returns and Yields section.'
396 raise ValueError(msg)
397 if not has_yields and 'Receives' in section_names:
398 msg = 'Docstring contains a Receives section but not Yields.'
399 raise ValueError(msg)
400
401 for (section, content) in sections:
402 if not section.startswith('..'):
403 section = (s.capitalize() for s in section.split(' '))
404 section = ' '.join(section)
405 if self.get(section):
406 self._error_location("The section %s appears twice in %s"
407 % (section, '\n'.join(self._doc._str)))
408
409 if section in ('Parameters', 'Other Parameters', 'Attributes',
410 'Methods'):
411 self[section] = self._parse_param_list(content)
412 elif section in ('Returns', 'Yields', 'Raises', 'Warns', 'Receives'):
413 self[section] = self._parse_param_list(
414 content, single_element_is_type=True)
415 elif section.startswith('.. index::'):
416 self['index'] = self._parse_index(section, content)
417 elif section == 'See Also':
418 self['See Also'] = self._parse_see_also(content)
419 else:
420 self[section] = content
421
422 @property
423 def _obj(self):
424 if hasattr(self, '_cls'):
425 return self._cls
426 elif hasattr(self, '_f'):
427 return self._f
428 return None
429
430 def _error_location(self, msg, error=True):
431 if self._obj is not None:
432 # we know where the docs came from:
433 try:
434 filename = inspect.getsourcefile(self._obj)
435 except TypeError:
436 filename = None
437 msg += f" in the docstring of {self._obj.__name__}"
438 msg += f" in {filename}." if filename else ""
439 if error:
440 raise ValueError(msg)
441 else:
442 warn(msg)
443
444 # string conversion routines
445
446 def _str_header(self, name, symbol='-'):
447 return [name, len(name)*symbol]
448
449 def _str_indent(self, doc, indent=4):
450 return [' '*indent + line for line in doc]
451
452 def _str_signature(self):
453 if self['Signature']:
454 return [self['Signature'].replace('*', r'\*')] + ['']
455 return ['']
456
457 def _str_summary(self):
458 if self['Summary']:
459 return self['Summary'] + ['']
460 return []
461
462 def _str_extended_summary(self):
463 if self['Extended Summary']:
464 return self['Extended Summary'] + ['']
465 return []
466
467 def _str_param_list(self, name):
468 out = []
469 if self[name]:
470 out += self._str_header(name)
471 for param in self[name]:
472 parts = []
473 if param.name:
474 parts.append(param.name)
475 if param.type:
476 parts.append(param.type)
477 out += [' : '.join(parts)]
478 if param.desc and ''.join(param.desc).strip():
479 out += self._str_indent(param.desc)
480 out += ['']
481 return out
482
483 def _str_section(self, name):
484 out = []
485 if self[name]:
486 out += self._str_header(name)
487 out += self[name]
488 out += ['']
489 return out
490
491 def _str_see_also(self, func_role):
492 if not self['See Also']:
493 return []
494 out = []
495 out += self._str_header("See Also")
496 out += ['']
497 last_had_desc = True
498 for funcs, desc in self['See Also']:
499 assert isinstance(funcs, list)
500 links = []
501 for func, role in funcs:
502 if role:
503 link = ':%s:`%s`' % (role, func)
504 elif func_role:
505 link = ':%s:`%s`' % (func_role, func)
506 else:
507 link = "`%s`_" % func
508 links.append(link)
509 link = ', '.join(links)
510 out += [link]
511 if desc:
512 out += self._str_indent([' '.join(desc)])
513 last_had_desc = True
514 else:
515 last_had_desc = False
516 out += self._str_indent([self.empty_description])
517
518 if last_had_desc:
519 out += ['']
520 out += ['']
521 return out
522
523 def _str_index(self):
524 idx = self['index']
525 out = []
526 output_index = False
527 default_index = idx.get('default', '')
528 if default_index:
529 output_index = True
530 out += ['.. index:: %s' % default_index]
531 for section, references in idx.items():
532 if section == 'default':
533 continue
534 output_index = True
535 out += [' :%s: %s' % (section, ', '.join(references))]
536 if output_index:
537 return out
538 return ''
539
540 def __str__(self, func_role=''):
541 out = []
542 out += self._str_signature()
543 out += self._str_summary()
544 out += self._str_extended_summary()
545 for param_list in ('Parameters', 'Returns', 'Yields', 'Receives',
546 'Other Parameters', 'Raises', 'Warns'):
547 out += self._str_param_list(param_list)
548 out += self._str_section('Warnings')
549 out += self._str_see_also(func_role)
550 for s in ('Notes', 'References', 'Examples'):
551 out += self._str_section(s)
552 for param_list in ('Attributes', 'Methods'):
553 out += self._str_param_list(param_list)
554 out += self._str_index()
555 return '\n'.join(out)
556
557
558def dedent_lines(lines):
559 """Deindent a list of lines maximally"""
560 return textwrap.dedent("\n".join(lines)).split("\n")
561
562
563class FunctionDoc(NumpyDocString):
564 def __init__(self, func, role='func', doc=None, config=None):
565 self._f = func
566 self._role = role # e.g. "func" or "meth"
567
568 if doc is None:
569 if func is None:
570 raise ValueError("No function or docstring given")
571 doc = inspect.getdoc(func) or ''
572 if config is None:
573 config = {}
574 NumpyDocString.__init__(self, doc, config)
575
576 def get_func(self):
577 func_name = getattr(self._f, '__name__', self.__class__.__name__)
578 if inspect.isclass(self._f):
579 func = getattr(self._f, '__call__', self._f.__init__)
580 else:
581 func = self._f
582 return func, func_name
583
584 def __str__(self):
585 out = ''
586
587 func, func_name = self.get_func()
588
589 roles = {'func': 'function',
590 'meth': 'method'}
591
592 if self._role:
593 if self._role not in roles:
594 print("Warning: invalid role %s" % self._role)
595 out += '.. %s:: %s\n \n\n' % (roles.get(self._role, ''),
596 func_name)
597
598 out += super().__str__(func_role=self._role)
599 return out
600
601
602class ObjDoc(NumpyDocString):
603 def __init__(self, obj, doc=None, config=None):
604 self._f = obj
605 if config is None:
606 config = {}
607 NumpyDocString.__init__(self, doc, config=config)
608
609
610class ClassDoc(NumpyDocString):
611
612 extra_public_methods = ['__call__']
613
614 def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc,
615 config=None):
616 if not inspect.isclass(cls) and cls is not None:
617 raise ValueError("Expected a class or None, but got %r" % cls)
618 self._cls = cls
619
620 if 'sphinx' in sys.modules:
621 from sphinx.ext.autodoc import ALL
622 else:
623 ALL = object()
624
625 if config is None:
626 config = {}
627 self.show_inherited_members = config.get(
628 'show_inherited_class_members', True)
629
630 if modulename and not modulename.endswith('.'):
631 modulename += '.'
632 self._mod = modulename
633
634 if doc is None:
635 if cls is None:
636 raise ValueError("No class or documentation string given")
637 doc = pydoc.getdoc(cls)
638
639 NumpyDocString.__init__(self, doc)
640
641 _members = config.get('members', [])
642 if _members is ALL:
643 _members = None
644 _exclude = config.get('exclude-members', [])
645
646 if config.get('show_class_members', True) and _exclude is not ALL:
647 def splitlines_x(s):
648 if not s:
649 return []
650 else:
651 return s.splitlines()
652 for field, items in [('Methods', self.methods),
653 ('Attributes', self.properties)]:
654 if not self[field]:
655 doc_list = []
656 for name in sorted(items):
657 if (name in _exclude or
658 (_members and name not in _members)):
659 continue
660 try:
661 doc_item = pydoc.getdoc(getattr(self._cls, name))
662 doc_list.append(
663 Parameter(name, '', splitlines_x(doc_item)))
664 except AttributeError:
665 pass # method doesn't exist
666 self[field] = doc_list
667
668 @property
669 def methods(self):
670 if self._cls is None:
671 return []
672 return [name for name, func in inspect.getmembers(self._cls)
673 if ((not name.startswith('_') or
674 name in self.extra_public_methods) and
675 isinstance(func, Callable) and
676 self._is_show_member(name))]
677
678 @property
679 def properties(self):
680 if self._cls is None:
681 return []
682 return [name for name, func in inspect.getmembers(self._cls)
683 if (not name.startswith('_') and
684 (func is None or isinstance(func, property) or
685 inspect.isdatadescriptor(func)) and
686 self._is_show_member(name))]
687
688 def _is_show_member(self, name):
689 if self.show_inherited_members:
690 return True # show all class members
691 if name not in self._cls.__dict__:
692 return False # class member is inherited, we do not show it
693 return True
694
695
696def get_doc_object(obj, what=None, doc=None, config=None):
697 if what is None:
698 if inspect.isclass(obj):
699 what = 'class'
700 elif inspect.ismodule(obj):
701 what = 'module'
702 elif isinstance(obj, Callable):
703 what = 'function'
704 else:
705 what = 'object'
706 if config is None:
707 config = {}
708
709 if what == 'class':
710 return ClassDoc(obj, func_doc=FunctionDoc, doc=doc, config=config)
711 elif what in ('function', 'method'):
712 return FunctionDoc(obj, doc=doc, config=config)
713 else:
714 if doc is None:
715 doc = pydoc.getdoc(obj)
716 return ObjDoc(obj, doc, config=config)