123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- """Extract reference documentation from the NumPy source tree.
- Copyright (C) 2008 Stefan van der Walt <stefan@mentat.za.net>, Pauli Virtanen <pav@iki.fi>
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are
- met:
- 1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in
- the documentation and/or other materials provided with the
- distribution.
- THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
- INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
- IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGE.
- """
- import inspect
- import textwrap
- import re
- import pydoc
- from warnings import warn
- from collections import namedtuple
- from collections.abc import Callable, Mapping
- import copy
- import sys
- def strip_blank_lines(l):
- "Remove leading and trailing blank lines from a list of lines"
- while l and not l[0].strip():
- del l[0]
- while l and not l[-1].strip():
- del l[-1]
- return l
- class Reader:
- """A line-based string reader.
- """
- def __init__(self, data):
- """
- Parameters
- ----------
- data : str
- String with lines separated by '\n'.
- """
- if isinstance(data, list):
- self._str = data
- else:
- self._str = data.split('\n') # store string as list of lines
- self.reset()
- def __getitem__(self, n):
- return self._str[n]
- def reset(self):
- self._l = 0 # current line nr
- def read(self):
- if not self.eof():
- out = self[self._l]
- self._l += 1
- return out
- else:
- return ''
- def seek_next_non_empty_line(self):
- for l in self[self._l:]:
- if l.strip():
- break
- else:
- self._l += 1
- def eof(self):
- return self._l >= len(self._str)
- def read_to_condition(self, condition_func):
- start = self._l
- for line in self[start:]:
- if condition_func(line):
- return self[start:self._l]
- self._l += 1
- if self.eof():
- return self[start:self._l+1]
- return []
- def read_to_next_empty_line(self):
- self.seek_next_non_empty_line()
- def is_empty(line):
- return not line.strip()
- return self.read_to_condition(is_empty)
- def read_to_next_unindented_line(self):
- def is_unindented(line):
- return (line.strip() and (len(line.lstrip()) == len(line)))
- return self.read_to_condition(is_unindented)
- def peek(self, n=0):
- if self._l + n < len(self._str):
- return self[self._l + n]
- else:
- return ''
- def is_empty(self):
- return not ''.join(self._str).strip()
- class ParseError(Exception):
- def __str__(self):
- message = self.args[0]
- if hasattr(self, 'docstring'):
- message = f"{message} in {self.docstring!r}"
- return message
- Parameter = namedtuple('Parameter', ['name', 'type', 'desc'])
- class NumpyDocString(Mapping):
- """Parses a numpydoc string to an abstract representation
- Instances define a mapping from section title to structured data.
- """
- sections = {
- 'Signature': '',
- 'Summary': [''],
- 'Extended Summary': [],
- 'Parameters': [],
- 'Returns': [],
- 'Yields': [],
- 'Receives': [],
- 'Raises': [],
- 'Warns': [],
- 'Other Parameters': [],
- 'Attributes': [],
- 'Methods': [],
- 'See Also': [],
- 'Notes': [],
- 'Warnings': [],
- 'References': '',
- 'Examples': '',
- 'index': {}
- }
- def __init__(self, docstring, config={}):
- orig_docstring = docstring
- docstring = textwrap.dedent(docstring).split('\n')
- self._doc = Reader(docstring)
- self._parsed_data = copy.deepcopy(self.sections)
- try:
- self._parse()
- except ParseError as e:
- e.docstring = orig_docstring
- raise
- def __getitem__(self, key):
- return self._parsed_data[key]
- def __setitem__(self, key, val):
- if key not in self._parsed_data:
- self._error_location(f"Unknown section {key}", error=False)
- else:
- self._parsed_data[key] = val
- def __iter__(self):
- return iter(self._parsed_data)
- def __len__(self):
- return len(self._parsed_data)
- def _is_at_section(self):
- self._doc.seek_next_non_empty_line()
- if self._doc.eof():
- return False
- l1 = self._doc.peek().strip() # e.g. Parameters
- if l1.startswith('.. index::'):
- return True
- l2 = self._doc.peek(1).strip() # ---------- or ==========
- return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
- def _strip(self, doc):
- i = 0
- j = 0
- for i, line in enumerate(doc):
- if line.strip():
- break
- for j, line in enumerate(doc[::-1]):
- if line.strip():
- break
- return doc[i:len(doc)-j]
- def _read_to_next_section(self):
- section = self._doc.read_to_next_empty_line()
- while not self._is_at_section() and not self._doc.eof():
- if not self._doc.peek(-1).strip(): # previous line was empty
- section += ['']
- section += self._doc.read_to_next_empty_line()
- return section
- def _read_sections(self):
- while not self._doc.eof():
- data = self._read_to_next_section()
- name = data[0].strip()
- if name.startswith('..'): # index section
- yield name, data[1:]
- elif len(data) < 2:
- yield StopIteration
- else:
- yield name, self._strip(data[2:])
- def _parse_param_list(self, content, single_element_is_type=False):
- r = Reader(content)
- params = []
- while not r.eof():
- header = r.read().strip()
- if ' : ' in header:
- arg_name, arg_type = header.split(' : ')[:2]
- else:
- if single_element_is_type:
- arg_name, arg_type = '', header
- else:
- arg_name, arg_type = header, ''
- desc = r.read_to_next_unindented_line()
- desc = dedent_lines(desc)
- desc = strip_blank_lines(desc)
- params.append(Parameter(arg_name, arg_type, desc))
- return params
- # See also supports the following formats.
- #
- # <FUNCNAME>
- # <FUNCNAME> SPACE* COLON SPACE+ <DESC> SPACE*
- # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)+ (COMMA | PERIOD)? SPACE*
- # <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)* SPACE* COLON SPACE+ <DESC> SPACE*
- # <FUNCNAME> is one of
- # <PLAIN_FUNCNAME>
- # COLON <ROLE> COLON BACKTICK <PLAIN_FUNCNAME> BACKTICK
- # where
- # <PLAIN_FUNCNAME> is a legal function name, and
- # <ROLE> is any nonempty sequence of word characters.
- # Examples: func_f1 :meth:`func_h1` :obj:`~baz.obj_r` :class:`class_j`
- # <DESC> is a string describing the function.
- _role = r":(?P<role>\w+):"
- _funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_\.-]+)`"
- _funcplain = r"(?P<name2>[a-zA-Z0-9_\.-]+)"
- _funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
- _funcnamenext = _funcname.replace('role', 'rolenext')
- _funcnamenext = _funcnamenext.replace('name', 'namenext')
- _description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
- _func_rgx = re.compile(r"^\s*" + _funcname + r"\s*")
- _line_rgx = re.compile(
- r"^\s*" +
- r"(?P<allfuncs>" + # group for all function names
- _funcname +
- r"(?P<morefuncs>([,]\s+" + _funcnamenext + r")*)" +
- r")" + # end of "allfuncs"
- r"(?P<trailing>[,\.])?" + # Some function lists have a trailing comma (or period) '\s*'
- _description)
- # Empty <DESC> elements are replaced with '..'
- empty_description = '..'
- def _parse_see_also(self, content):
- """
- func_name : Descriptive text
- continued text
- another_func_name : Descriptive text
- func_name1, func_name2, :meth:`func_name`, func_name3
- """
- items = []
- def parse_item_name(text):
- """Match ':role:`name`' or 'name'."""
- m = self._func_rgx.match(text)
- if not m:
- raise ParseError(f"{text} is not a item name")
- role = m.group('role')
- name = m.group('name') if role else m.group('name2')
- return name, role, m.end()
- rest = []
- for line in content:
- if not line.strip():
- continue
- line_match = self._line_rgx.match(line)
- description = None
- if line_match:
- description = line_match.group('desc')
- if line_match.group('trailing') and description:
- self._error_location(
- 'Unexpected comma or period after function list at index %d of '
- 'line "%s"' % (line_match.end('trailing'), line),
- error=False)
- if not description and line.startswith(' '):
- rest.append(line.strip())
- elif line_match:
- funcs = []
- text = line_match.group('allfuncs')
- while True:
- if not text.strip():
- break
- name, role, match_end = parse_item_name(text)
- funcs.append((name, role))
- text = text[match_end:].strip()
- if text and text[0] == ',':
- text = text[1:].strip()
- rest = list(filter(None, [description]))
- items.append((funcs, rest))
- else:
- raise ParseError(f"{line} is not a item name")
- return items
- def _parse_index(self, section, content):
- """
- .. index: default
- :refguide: something, else, and more
- """
- def strip_each_in(lst):
- return [s.strip() for s in lst]
- out = {}
- section = section.split('::')
- if len(section) > 1:
- out['default'] = strip_each_in(section[1].split(','))[0]
- for line in content:
- line = line.split(':')
- if len(line) > 2:
- out[line[1]] = strip_each_in(line[2].split(','))
- return out
- def _parse_summary(self):
- """Grab signature (if given) and summary"""
- if self._is_at_section():
- return
- # If several signatures present, take the last one
- while True:
- summary = self._doc.read_to_next_empty_line()
- summary_str = " ".join([s.strip() for s in summary]).strip()
- compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$')
- if compiled.match(summary_str):
- self['Signature'] = summary_str
- if not self._is_at_section():
- continue
- break
- if summary is not None:
- self['Summary'] = summary
- if not self._is_at_section():
- self['Extended Summary'] = self._read_to_next_section()
- def _parse(self):
- self._doc.reset()
- self._parse_summary()
- sections = list(self._read_sections())
- section_names = {section for section, content in sections}
- has_returns = 'Returns' in section_names
- has_yields = 'Yields' in section_names
- # We could do more tests, but we are not. Arbitrarily.
- if has_returns and has_yields:
- msg = 'Docstring contains both a Returns and Yields section.'
- raise ValueError(msg)
- if not has_yields and 'Receives' in section_names:
- msg = 'Docstring contains a Receives section but not Yields.'
- raise ValueError(msg)
- for (section, content) in sections:
- if not section.startswith('..'):
- section = (s.capitalize() for s in section.split(' '))
- section = ' '.join(section)
- if self.get(section):
- self._error_location(f"The section {section} appears twice")
- if section in ('Parameters', 'Other Parameters', 'Attributes',
- 'Methods'):
- self[section] = self._parse_param_list(content)
- elif section in ('Returns', 'Yields', 'Raises', 'Warns', 'Receives'):
- self[section] = self._parse_param_list(
- content, single_element_is_type=True)
- elif section.startswith('.. index::'):
- self['index'] = self._parse_index(section, content)
- elif section == 'See Also':
- self['See Also'] = self._parse_see_also(content)
- else:
- self[section] = content
- def _error_location(self, msg, error=True):
- if hasattr(self, '_obj'):
- # we know where the docs came from:
- try:
- filename = inspect.getsourcefile(self._obj)
- except TypeError:
- filename = None
- msg = msg + f" in the docstring of {self._obj} in {filename}."
- if error:
- raise ValueError(msg)
- else:
- warn(msg)
- # string conversion routines
- def _str_header(self, name, symbol='-'):
- return [name, len(name)*symbol]
- def _str_indent(self, doc, indent=4):
- out = []
- for line in doc:
- out += [' '*indent + line]
- return out
- def _str_signature(self):
- if self['Signature']:
- return [self['Signature'].replace('*', r'\*')] + ['']
- else:
- return ['']
- def _str_summary(self):
- if self['Summary']:
- return self['Summary'] + ['']
- else:
- return []
- def _str_extended_summary(self):
- if self['Extended Summary']:
- return self['Extended Summary'] + ['']
- else:
- return []
- def _str_param_list(self, name):
- out = []
- if self[name]:
- out += self._str_header(name)
- for param in self[name]:
- parts = []
- if param.name:
- parts.append(param.name)
- if param.type:
- parts.append(param.type)
- out += [' : '.join(parts)]
- if param.desc and ''.join(param.desc).strip():
- out += self._str_indent(param.desc)
- out += ['']
- return out
- def _str_section(self, name):
- out = []
- if self[name]:
- out += self._str_header(name)
- out += self[name]
- out += ['']
- return out
- def _str_see_also(self, func_role):
- if not self['See Also']:
- return []
- out = []
- out += self._str_header("See Also")
- out += ['']
- last_had_desc = True
- for funcs, desc in self['See Also']:
- assert isinstance(funcs, list)
- links = []
- for func, role in funcs:
- if role:
- link = f':{role}:`{func}`'
- elif func_role:
- link = f':{func_role}:`{func}`'
- else:
- link = f"`{func}`_"
- links.append(link)
- link = ', '.join(links)
- out += [link]
- if desc:
- out += self._str_indent([' '.join(desc)])
- last_had_desc = True
- else:
- last_had_desc = False
- out += self._str_indent([self.empty_description])
- if last_had_desc:
- out += ['']
- out += ['']
- return out
- def _str_index(self):
- idx = self['index']
- out = []
- output_index = False
- default_index = idx.get('default', '')
- if default_index:
- output_index = True
- out += [f'.. index:: {default_index}']
- for section, references in idx.items():
- if section == 'default':
- continue
- output_index = True
- out += [f" :{section}: {', '.join(references)}"]
- if output_index:
- return out
- else:
- return ''
- def __str__(self, func_role=''):
- out = []
- out += self._str_signature()
- out += self._str_summary()
- out += self._str_extended_summary()
- for param_list in ('Parameters', 'Returns', 'Yields', 'Receives',
- 'Other Parameters', 'Raises', 'Warns'):
- out += self._str_param_list(param_list)
- out += self._str_section('Warnings')
- out += self._str_see_also(func_role)
- for s in ('Notes', 'References', 'Examples'):
- out += self._str_section(s)
- for param_list in ('Attributes', 'Methods'):
- out += self._str_param_list(param_list)
- out += self._str_index()
- return '\n'.join(out)
- def indent(str, indent=4):
- indent_str = ' '*indent
- if str is None:
- return indent_str
- lines = str.split('\n')
- return '\n'.join(indent_str + l for l in lines)
- def dedent_lines(lines):
- """Deindent a list of lines maximally"""
- return textwrap.dedent("\n".join(lines)).split("\n")
- def header(text, style='-'):
- return text + '\n' + style*len(text) + '\n'
- class FunctionDoc(NumpyDocString):
- def __init__(self, func, role='func', doc=None, config={}):
- self._f = func
- self._role = role # e.g. "func" or "meth"
- if doc is None:
- if func is None:
- raise ValueError("No function or docstring given")
- doc = inspect.getdoc(func) or ''
- NumpyDocString.__init__(self, doc, config)
- if not self['Signature'] and func is not None:
- func, func_name = self.get_func()
- try:
- try:
- signature = str(inspect.signature(func))
- except (AttributeError, ValueError):
- # try to read signature, backward compat for older Python
- if sys.version_info[0] >= 3:
- argspec = inspect.getfullargspec(func)
- else:
- argspec = inspect.getargspec(func)
- signature = inspect.formatargspec(*argspec)
- signature = f'{func_name}{signature}'
- except TypeError:
- signature = f'{func_name}()'
- self['Signature'] = signature
- def get_func(self):
- func_name = getattr(self._f, '__name__', self.__class__.__name__)
- if inspect.isclass(self._f):
- func = getattr(self._f, '__call__', self._f.__init__)
- else:
- func = self._f
- return func, func_name
- def __str__(self):
- out = ''
- func, func_name = self.get_func()
- roles = {'func': 'function',
- 'meth': 'method'}
- if self._role:
- if self._role not in roles:
- print(f"Warning: invalid role {self._role}")
- out += f".. {roles.get(self._role, '')}:: {func_name}\n \n\n"
- out += super().__str__(func_role=self._role)
- return out
- class ClassDoc(NumpyDocString):
- extra_public_methods = ['__call__']
- def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc,
- config={}):
- if not inspect.isclass(cls) and cls is not None:
- raise ValueError(f"Expected a class or None, but got {cls!r}")
- self._cls = cls
- if 'sphinx' in sys.modules:
- from sphinx.ext.autodoc import ALL
- else:
- ALL = object()
- self.show_inherited_members = config.get(
- 'show_inherited_class_members', True)
- if modulename and not modulename.endswith('.'):
- modulename += '.'
- self._mod = modulename
- if doc is None:
- if cls is None:
- raise ValueError("No class or documentation string given")
- doc = pydoc.getdoc(cls)
- NumpyDocString.__init__(self, doc)
- _members = config.get('members', [])
- if _members is ALL:
- _members = None
- _exclude = config.get('exclude-members', [])
- if config.get('show_class_members', True) and _exclude is not ALL:
- def splitlines_x(s):
- if not s:
- return []
- else:
- return s.splitlines()
- for field, items in [('Methods', self.methods),
- ('Attributes', self.properties)]:
- if not self[field]:
- doc_list = []
- for name in sorted(items):
- if (name in _exclude or
- (_members and name not in _members)):
- continue
- try:
- doc_item = pydoc.getdoc(getattr(self._cls, name))
- doc_list.append(
- Parameter(name, '', splitlines_x(doc_item)))
- except AttributeError:
- pass # method doesn't exist
- self[field] = doc_list
- @property
- def methods(self):
- if self._cls is None:
- return []
- return [name for name, func in inspect.getmembers(self._cls)
- if ((not name.startswith('_')
- or name in self.extra_public_methods)
- and isinstance(func, Callable)
- and self._is_show_member(name))]
- @property
- def properties(self):
- if self._cls is None:
- return []
- return [name for name, func in inspect.getmembers(self._cls)
- if (not name.startswith('_') and
- (func is None or isinstance(func, property) or
- inspect.isdatadescriptor(func))
- and self._is_show_member(name))]
- def _is_show_member(self, name):
- if self.show_inherited_members:
- return True # show all class members
- if name not in self._cls.__dict__:
- return False # class member is inherited, we do not show it
- return True
|