_docscrape.py 21 KB

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