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