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

1"""Extract reference documentation from the NumPy source tree. 

2 

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 

14 

15 

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 

23 

24 

25class Reader(object): 

26 """A line-based string reader. 

27 

28 """ 

29 def __init__(self, data): 

30 """ 

31 Parameters 

32 ---------- 

33 data : str 

34 String with lines separated by '\\n'. 

35 

36 """ 

37 if isinstance(data, list): 

38 self._str = data 

39 else: 

40 self._str = data.split('\n') # store string as list of lines 

41 

42 self.reset() 

43 

44 def __getitem__(self, n): 

45 return self._str[n] 

46 

47 def reset(self): 

48 self._l = 0 # current line nr 

49 

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 '' 

57 

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 

64 

65 def eof(self): 

66 return self._l >= len(self._str) 

67 

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 [] 

77 

78 def read_to_next_empty_line(self): 

79 self.seek_next_non_empty_line() 

80 

81 def is_empty(line): 

82 return not line.strip() 

83 

84 return self.read_to_condition(is_empty) 

85 

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) 

90 

91 def peek(self, n=0): 

92 if self._l + n < len(self._str): 

93 return self[self._l + n] 

94 else: 

95 return '' 

96 

97 def is_empty(self): 

98 return not ''.join(self._str).strip() 

99 

100 

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 

107 

108 

109Parameter = namedtuple('Parameter', ['name', 'type', 'desc']) 

110 

111 

112class NumpyDocString(Mapping): 

113 """Parses a numpydoc string to an abstract representation 

114 

115 Instances define a mapping from section title to structured data. 

116 

117 """ 

118 

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 } 

139 

140 def __init__(self, docstring, config={}): 

141 orig_docstring = docstring 

142 docstring = textwrap.dedent(docstring).split('\n') 

143 

144 self._doc = Reader(docstring) 

145 self._parsed_data = copy.deepcopy(self.sections) 

146 

147 try: 

148 self._parse() 

149 except ParseError as e: 

150 e.docstring = orig_docstring 

151 raise 

152 

153 def __getitem__(self, key): 

154 return self._parsed_data[key] 

155 

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 

161 

162 def __iter__(self): 

163 return iter(self._parsed_data) 

164 

165 def __len__(self): 

166 return len(self._parsed_data) 

167 

168 def _is_at_section(self): 

169 self._doc.seek_next_non_empty_line() 

170 

171 if self._doc.eof(): 

172 return False 

173 

174 l1 = self._doc.peek().strip() # e.g. Parameters 

175 

176 if l1.startswith('.. index::'): 

177 return True 

178 

179 l2 = self._doc.peek(1).strip() # ---------- or ========== 

180 return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) 

181 

182 def _strip(self, doc): 

183 i = 0 

184 j = 0 

185 for i, line in enumerate(doc): 

186 if line.strip(): 

187 break 

188 

189 for j, line in enumerate(doc[::-1]): 

190 if line.strip(): 

191 break 

192 

193 return doc[i:len(doc)-j] 

194 

195 def _read_to_next_section(self): 

196 section = self._doc.read_to_next_empty_line() 

197 

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 += [''] 

201 

202 section += self._doc.read_to_next_empty_line() 

203 

204 return section 

205 

206 def _read_sections(self): 

207 while not self._doc.eof(): 

208 data = self._read_to_next_section() 

209 name = data[0].strip() 

210 

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:]) 

217 

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, '' 

230 

231 desc = r.read_to_next_unindented_line() 

232 desc = dedent_lines(desc) 

233 desc = strip_blank_lines(desc) 

234 

235 params.append(Parameter(arg_name, arg_type, desc)) 

236 

237 return params 

238 

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* 

245 

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. 

254 

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) 

272 

273 # Empty <DESC> elements are replaced with '..' 

274 empty_description = '..' 

275 

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 

282 

283 """ 

284 

285 items = [] 

286 

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() 

295 

296 rest = [] 

297 for line in content: 

298 if not line.strip(): 

299 continue 

300 

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 

329 

330 def _parse_index(self, section, content): 

331 """ 

332 .. index: default 

333 :refguide: something, else, and more 

334 

335 """ 

336 def strip_each_in(lst): 

337 return [s.strip() for s in lst] 

338 

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 

348 

349 def _parse_summary(self): 

350 """Grab signature (if given) and summary""" 

351 if self._is_at_section(): 

352 return 

353 

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 

364 

365 if summary is not None: 

366 self['Summary'] = summary 

367 

368 if not self._is_at_section(): 

369 self['Extended Summary'] = self._read_to_next_section() 

370 

371 def _parse(self): 

372 self._doc.reset() 

373 self._parse_summary() 

374 

375 sections = list(self._read_sections()) 

376 section_names = set([section for section, content in sections]) 

377 

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) 

387 

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) 

395 

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 

409 

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) 

423 

424 # string conversion routines 

425 

426 def _str_header(self, name, symbol='-'): 

427 return [name, len(name)*symbol] 

428 

429 def _str_indent(self, doc, indent=4): 

430 out = [] 

431 for line in doc: 

432 out += [' '*indent + line] 

433 return out 

434 

435 def _str_signature(self): 

436 if self['Signature']: 

437 return [self['Signature'].replace('*', r'\*')] + [''] 

438 else: 

439 return [''] 

440 

441 def _str_summary(self): 

442 if self['Summary']: 

443 return self['Summary'] + [''] 

444 else: 

445 return [] 

446 

447 def _str_extended_summary(self): 

448 if self['Extended Summary']: 

449 return self['Extended Summary'] + [''] 

450 else: 

451 return [] 

452 

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 

468 

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 

476 

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]) 

503 

504 if last_had_desc: 

505 out += [''] 

506 out += [''] 

507 return out 

508 

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 '' 

526 

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) 

543 

544 

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 

551 

552 

553def dedent_lines(lines): 

554 """Deindent a list of lines maximally""" 

555 return textwrap.dedent("\n".join(lines)).split("\n") 

556 

557 

558def header(text, style='-'): 

559 return text + '\n' + style*len(text) + '\n' 

560 

561 

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" 

566 

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) 

572 

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 

580 

581 def __str__(self): 

582 out = '' 

583 

584 func, func_name = self.get_func() 

585 

586 roles = {'func': 'function', 

587 'meth': 'method'} 

588 

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) 

594 

595 out += super(FunctionDoc, self).__str__(func_role=self._role) 

596 return out 

597 

598 

599class ClassDoc(NumpyDocString): 

600 

601 extra_public_methods = ['__call__'] 

602 

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 

608 

609 if 'sphinx' in sys.modules: 

610 from sphinx.ext.autodoc import ALL 

611 else: 

612 ALL = object() 

613 

614 self.show_inherited_members = config.get( 

615 'show_inherited_class_members', True) 

616 

617 if modulename and not modulename.endswith('.'): 

618 modulename += '.' 

619 self._mod = modulename 

620 

621 if doc is None: 

622 if cls is None: 

623 raise ValueError("No class or documentation string given") 

624 doc = pydoc.getdoc(cls) 

625 

626 NumpyDocString.__init__(self, doc) 

627 

628 _members = config.get('members', []) 

629 if _members is ALL: 

630 _members = None 

631 _exclude = config.get('exclude-members', []) 

632 

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 

654 

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))] 

664 

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))] 

674 

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