Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2@file 

3@brief Various variables and classes used to produce a Sphinx documentation. 

4""" 

5import inspect 

6import os 

7import copy 

8import re 

9import sys 

10import importlib 

11import traceback 

12from ..pandashelper.tblformat import df2rst 

13from .helpgen_exceptions import HelpGenException, ImportErrorHelpGen 

14 

15 

16#: max length for short summaries 

17_length_truncated_doc = 120 

18 

19 

20#: template for a module, substring ``__...__`` ought to be replaced 

21add_file_rst_template = """ 

22__FULLNAME_UNDERLINED__ 

23 

24 

25 

26 

27.. inheritance-diagram:: __FULLNAMENOEXT__ 

28 

29 

30Short summary 

31+++++++++++++ 

32 

33__DOCUMENTATION__ 

34 

35 

36__CLASSES__ 

37 

38__FUNCTIONS__ 

39 

40__PROPERTIES__ 

41 

42__STATICMETHODS__ 

43 

44__METHODS__ 

45 

46Documentation 

47+++++++++++++ 

48 

49.. automodule:: __FULLNAMENOEXT__ 

50 :members: 

51 :special-members: __init__ 

52 :show-inheritance: 

53 

54__ADDEDMEMBERS__ 

55 

56""" 

57 

58#: fields to be replaced 

59add_file_rst_template_cor = {"class": "__CLASSES__", 

60 "method": "__METHODS__", 

61 "function": "__FUNCTIONS__", 

62 "staticmethod": "__STATICMETHODS__", 

63 "property": "__PROPERTIES__", 

64 } 

65 

66#: names for python objects 

67add_file_rst_template_title = {"class": "Classes", 

68 "method": "Methods", 

69 "function": "Functions", 

70 "staticmethod": "Static Methods", 

71 "property": "Properties", 

72 } 

73 

74# 

75# :platform: Unix, Windows 

76# :synopsis: Analyze and reanimate dead parrots. 

77# .. moduleauthor:: xx <x@x> 

78# .. moduleauthor:: xx <x@x> 

79# for autosummary 

80# :toctree: __FILENAMENOEXT__/ 

81# 

82 

83 

84def compute_truncated_documentation(doc, length=_length_truncated_doc, 

85 raise_exception=False): 

86 """ 

87 Produces a truncated version of a docstring. 

88 

89 @param doc doc string 

90 @param length approximated length of the truncated docstring 

91 @param raise_exception raises an exception when the result is empty and the input is not 

92 @return truncated doc string 

93 """ 

94 if len(doc) == 0: 

95 return doc 

96 else: 

97 doc_ = doc 

98 

99 if "@brief " in doc: 

100 doc = doc.split("@brief ") 

101 doc = doc[-1] 

102 if ":githublink:" in doc: 

103 doc = doc.split(":githublink:") 

104 doc = doc[0] 

105 

106 doc = doc.strip("\n\r\t ").replace("\t", " ") 

107 

108 # we stop at the first ... 

109 lines = [li.rstrip() for li in doc.split("\n")] 

110 pos = None 

111 for i, li in enumerate(lines): 

112 lll = li.lstrip() 

113 if lll.startswith(".. ") and li.endswith("::"): 

114 pos = i 

115 break 

116 if lll.startswith("* ") or lll.startswith("- "): 

117 pos = i 

118 break 

119 if pos is not None: 

120 lines = lines[:pos] 

121 

122 # we filter out other stuff 

123 def filter_line(line): 

124 s = line.strip() 

125 if s.startswith(":title:"): 

126 return line.replace(":title:", "") 

127 elif s.startswith(":tag:") or s.startswith(":lid:"): 

128 return "" 

129 return line 

130 doc = "\n".join(filter_line(line) for line in lines) 

131 doc = doc.replace("\n", " ").replace("\r", "").strip("\n\r\t ") 

132 

133 for subs in ["@" + "param", "@" + "return", ":param", ":return", ".. ", "::"]: 

134 if subs in doc: 

135 doc = doc[:doc.find(subs)].strip("\r\t ") 

136 

137 if len(doc) >= _length_truncated_doc: 

138 spl = doc.split(" ") 

139 doc = "" 

140 cq, cq2 = 0, 0 

141 i = 0 

142 while i < len(spl) and (len(doc) < _length_truncated_doc or cq % 2 != 0 or cq2 % 2 != 0): 

143 cq += spl[i].count("`") 

144 cq2 += spl[i].count("``") 

145 doc += spl[i] + " " 

146 i += 1 

147 doc += "..." 

148 

149 doc = re.sub(' +', ' ', doc) 

150 

151 if raise_exception and len(doc) == 0: 

152 raise ValueError( # pragma: no cover 

153 "bad format for docstring:\n{}".format(doc_)) 

154 

155 return doc 

156 

157 

158class ModuleMemberDoc: 

159 

160 """ 

161 Represents a member in a module. 

162 

163 See :epkg:`*py:inspect`. 

164 

165 Attributes: 

166 

167 * *obj (object)*: object 

168 * *type (str)*: type 

169 * *cl (object)*: class it belongs to 

170 * *name (str)*: name 

171 * *module (str)*: module name 

172 * *doc (str)*: documentation 

173 * *truncdoc (str)*: truncated documentation 

174 * *owner (object)*: module 

175 """ 

176 

177 def __init__(self, obj, ty=None, cl=None, name=None, module=None): 

178 """ 

179 @param obj any kind of object 

180 @param ty type (if you want to overwrite what the class will choose), 

181 this type is a string (class, method, function) 

182 @param cl if is a method, class it belongs to 

183 @param name name of the object 

184 @param module module name if belongs to 

185 """ 

186 if module is None: 

187 raise ValueError("module cannot be null.") # pragma: no cover 

188 

189 self.owner = module 

190 self.obj = obj 

191 self.cl = cl 

192 if ty is not None: 

193 self.type = ty 

194 self.name = name 

195 self.populate() 

196 

197 typstr = str 

198 

199 if self.cl is None and self.type in [ 

200 "method", "staticmethod", "property"]: 

201 self.cl = self.obj.__class__ 

202 if self.cl is None and self.type in [ 

203 "method", "staticmethod", "property"]: 

204 raise TypeError( # pragma: no cover 

205 "N/a method must have a class (not None): %s" % typstr(self.obj)) 

206 

207 def add_prefix(self, prefix): 

208 """ 

209 Adds a prefix (for the documentation). 

210 @param prefix string 

211 """ 

212 self.prefix = prefix 

213 

214 @property 

215 def key(self): 

216 """ 

217 Returns a key to identify it. 

218 """ 

219 return "%s;%s" % (self.type, self.name) 

220 

221 def populate(self): 

222 """ 

223 Extracts some information about an object. 

224 """ 

225 obj = self.obj 

226 ty = self.type if "type" in self.__dict__ else None 

227 typstr = str 

228 if ty is None: 

229 if inspect.isclass(obj): 

230 self.type = "class" 

231 elif inspect.ismethod(obj): 

232 self.type = "method" 

233 elif inspect.isfunction(obj) or "built-in function" in str(obj): 

234 self.type = "function" 

235 elif inspect.isgenerator(obj): 

236 self.type = "generator" 

237 else: 

238 raise TypeError( # pragma: no cover 

239 "E/unable to deal with this type: " + typstr(type(obj))) 

240 

241 if ty == "method": 

242 if isinstance(obj, staticmethod): 

243 self.type = "staticmethod" 

244 elif isinstance(obj, property): 

245 self.type = "property" 

246 elif sys.version_info >= (3, 4): 

247 # should be replaced by something more robust 

248 if len(obj.__code__.co_varnames) == 0: 

249 self.type = "staticmethod" 

250 elif obj.__code__.co_varnames[0] != 'self': 

251 self.type = "staticmethod" 

252 

253 # module 

254 try: 

255 self.module = obj.__module__ 

256 self.name = obj.__name__ 

257 except Exception: 

258 if self.type in ["property", "staticmethod"]: 

259 self.module = self.cl.__module__ 

260 else: 

261 self.module = None 

262 if self.name is None: 

263 raise IndexError( # pragma: no cover 

264 "Unable to find a name for this object type={0}, " 

265 "self.type={1}, owner='{2}'".format( 

266 type(obj), self.type, self.owner)) 

267 

268 # full path for the module 

269 if self.module is not None: 

270 self.fullpath = self.module 

271 else: 

272 self.fullpath = "" 

273 

274 # documentation 

275 if self.type == "staticmethod": 

276 try: 

277 self.doc = obj.__func__.__doc__ 

278 except Exception as ie: # pragma: no cover 

279 try: 

280 self.doc = obj.__doc__ 

281 except Exception as ie2: 

282 self.doc = ( 

283 typstr(ie) + " - " + typstr(ie2) + " \n----------\n " + 

284 typstr(dir(obj))) 

285 else: 

286 try: 

287 self.doc = obj.__doc__ 

288 except Exception as ie: # pragma: no cover 

289 self.doc = typstr(ie) + " \n----------\n " + typstr(dir(obj)) 

290 

291 try: 

292 self.file = self.module.__file__ 

293 except Exception: 

294 self.file = "" 

295 

296 # truncated documentation 

297 if self.doc is not None: 

298 self.truncdoc = compute_truncated_documentation(self.doc) 

299 else: 

300 self.doc = "" 

301 self.truncdoc = "" 

302 

303 if self.name is None: 

304 raise TypeError( # pragma: no cover 

305 "S/name is None for object: %s" % typstr(self.obj)) 

306 

307 def __str__(self): 

308 """ 

309 usual 

310 """ 

311 return "[key={0},clname={1},type={2},module_name={3},file={4}".format( 

312 self.key, self.classname, self.type, self.module, self.owner.__file__) 

313 

314 def rst_link(self, prefix=None, class_in_bracket=True): 

315 """ 

316 Returns a sphinx link on the object. 

317 

318 @param prefix to correct the path with a prefix 

319 @param class_in_bracket if True, adds the class in bracket 

320 for methods and properties 

321 @return a string style, see below 

322 

323 String style: 

324 

325 :: 

326 

327 :%s:`%s <%s>` or 

328 :%s:`%s <%s>` (class) 

329 """ 

330 cor = {"function": "func", 

331 "method": "meth", 

332 "staticmethod": "meth", 

333 "property": "meth"} 

334 

335 if self.type in ["method", "staticmethod", "property"]: 

336 path = "%s.%s.%s" % (self.module, self.cl.__name__, self.name) 

337 else: 

338 path = "%s.%s" % (self.module, self.name) 

339 

340 if prefix is not None: 

341 path = "%s.%s" % (prefix, path) 

342 

343 if self.type in ["method", "staticmethod", 

344 "property"] and class_in_bracket: 

345 link = ":%s:`%s <%s>` (%s)" % ( 

346 cor.get(self.type, self.type), self.name, path, self.cl.__name__) 

347 else: 

348 link = ":%s:`%s <%s>`" % ( 

349 cor.get(self.type, self.type), self.name, path) 

350 return link 

351 

352 @property 

353 def classname(self): 

354 """ 

355 Returns the class name if the object is a method. 

356 

357 @return class object 

358 """ 

359 if self.type in ["method", "staticmethod", "property"]: 

360 return self.cl 

361 else: 

362 return None 

363 

364 def __cmp__(self, oth): 

365 """ 

366 Comparison operators, compares first the first, 

367 second the name (lower case). 

368 

369 @param oth other object 

370 @return -1, 0 or 1 

371 """ 

372 if self.type == oth.type: 

373 ln = self.fullpath + "@@@" + self.name.lower() 

374 lo = oth.fullpath + "@@@" + oth.name.lower() 

375 c = -1 if ln < lo else (1 if ln > lo else 0) 

376 if c == 0 and self.type == "method": 

377 ln = self.cl.__name__ 

378 lo = self.cl.__name__ 

379 c = -1 if ln < lo else (1 if ln > lo else 0) 

380 return c 

381 else: 

382 return - \ 

383 1 if self.type < oth.type else ( 

384 1 if self.type > oth.type else 0) 

385 

386 def __lt__(self, oth): 

387 """ 

388 Operator ``<``. 

389 """ 

390 return self.__cmp__(oth) == -1 

391 

392 def __eq__(self, oth): 

393 """ 

394 Operator ``==``. 

395 """ 

396 return self.__cmp__(oth) == 0 

397 

398 def __gt__(self, oth): 

399 """ 

400 Operator ``>``. 

401 """ 

402 return self.__cmp__(oth) == 1 

403 

404 

405class IndexInformation: 

406 

407 """ 

408 Keeps some information to index. 

409 """ 

410 

411 def __init__(self, type, label, name, text, rstfile, fullname): 

412 """ 

413 @param type each type gets an index 

414 @param label label used to index 

415 @param name name to display 

416 @param text text to show as a short description 

417 @param rstfile tells which file the index refers to (rst file) 

418 @param fullname fullname of a file the rst file describes 

419 """ 

420 self.type = type 

421 self.label = label 

422 self.name = name 

423 self.text = text 

424 self.fullname = fullname 

425 self.set_rst_file(rstfile) 

426 

427 def __str__(self): 

428 """ 

429 usual 

430 """ 

431 return "%s -- %s" % (self.label, self.rst_link()) 

432 

433 def set_rst_file(self, rstfile): 

434 """ 

435 Sets the rst file and checks the label is present in it. 

436 

437 @param rstfile rst file 

438 """ 

439 self.rstfile = rstfile 

440 if rstfile is not None: 

441 self.add_label_if_not_present() 

442 

443 @property 

444 def truncdoc(self): 

445 """ 

446 Returns ``self.text``. 

447 """ 

448 return self.text.replace("\n", " ").replace( 

449 "\t", "").replace("\r", "") 

450 

451 def add_label_if_not_present(self): 

452 """ 

453 The function checks the label is present in the original file. 

454 """ 

455 if self.rstfile is not None: 

456 with open(self.rstfile, "r", encoding="utf8") as f: 

457 content = f.read() 

458 label = ".. _%s:" % self.label 

459 if label not in content: 

460 content = "\n%s\n%s" % (label, content) 

461 with open(self.rstfile, "w", encoding="utf8") as f: 

462 f.write(content) 

463 

464 @staticmethod 

465 def get_label(existing, suggestion): 

466 """ 

467 Returns a new label given the existing ones. 

468 

469 @param existing existing labels stored in a dictionary 

470 @param suggestion the suggestion will be chosen if it does not exists, 

471 ``suggestion + zzz`` otherwise 

472 @return string 

473 """ 

474 if existing is None: 

475 raise ValueError( # pragma: no cover 

476 "existing must not be None") 

477 suggestion = suggestion.replace("_", "").replace(".", "") 

478 while suggestion in existing: 

479 suggestion += "z" 

480 return suggestion 

481 

482 def rst_link(self): 

483 """ 

484 return a link rst 

485 @return rst link 

486 """ 

487 if self.label.startswith("_"): 

488 return ":ref:`%s <%s>`" % (self.name, self.label[1:]) 

489 else: 

490 return ":ref:`%s <%s>`" % (self.name, self.label) 

491 

492 

493class RstFileHelp: 

494 """ 

495 Defines what a rst file and what it describes. 

496 """ 

497 

498 def __init__(self, file, rst, doc): 

499 """ 

500 @param file original filename 

501 @param rst produced rst file 

502 @param doc documentation if any 

503 """ 

504 self.file = file 

505 self.rst = rst 

506 self.doc = doc 

507 

508 

509def import_module(rootm, filename, log_function, additional_sys_path=None, 

510 first_try=True): 

511 """ 

512 Imports a module using its filename. 

513 

514 @param rootm root of the module (for relative import) 

515 @param filename file name of the module 

516 @param log_function logging function 

517 @param additional_sys_path additional path to include to ``sys.path`` before 

518 importing a module (will be removed afterwards) 

519 @param first_try first call to the function (to avoid infinite loop) 

520 @return module object, prefix 

521 

522 The function can also import compiled modules. 

523 

524 .. warning:: It adds the file path at the first 

525 position in ``sys.path`` and then deletes it. 

526 """ 

527 if additional_sys_path is None: 

528 additional_sys_path = [] 

529 memo = copy.deepcopy(sys.path) 

530 li = filename.replace("\\", "/") 

531 sdir = os.path.abspath(os.path.split(li)[0]) 

532 relpath = os.path.relpath(li, rootm).replace("\\", "/") 

533 if "/" in relpath: 

534 spl = relpath.split("/") 

535 fmod = spl[0] # this is the prefix 

536 relpath = "/".join(spl[1:]) 

537 else: 

538 fmod = "" 

539 

540 # has init 

541 init_ = os.path.join(sdir, "__init__.py") 

542 if init_ != filename and not os.path.exists(init_): 

543 # no init 

544 return "No __init__.py, unable to import %s" % (filename), fmod 

545 

546 # we remove every path ending by "src" except if it is found in PYTHONPATH 

547 pythonpath = os.environ.get("PYTHONPATH", None) 

548 if pythonpath is not None: 

549 sep = ";" if sys.platform.startswith("win") else ":" 

550 pypaths = [os.path.normpath(_) 

551 for _ in pythonpath.split(sep) if len(_) > 0] 

552 else: 

553 pypaths = [] 

554 rem = [] 

555 for i, p in enumerate(sys.path): 

556 if (p.endswith("src") and p not in pypaths) or ".zip" in p: 

557 rem.append(i) 

558 rem.reverse() 

559 for r in rem: 

560 del sys.path[r] 

561 

562 # Extracts extended extension of the module. 

563 if li.endswith(".py"): 

564 cpxx = ".py" 

565 ext_rem = ".py" 

566 elif li.endswith(".pyd"): 

567 cpxx = ".cp%d%d-" % sys.version_info[:2] 

568 search = li.rfind(cpxx) 

569 ext_rem = li[search:] 

570 else: 

571 cpxx = ".cpython-%d%dm-" % sys.version_info[:2] 

572 search = li.rfind(cpxx) 

573 if search == -1: 

574 cpxx = ".cpython-%d%d-" % sys.version_info[:2] 

575 search = li.rfind(cpxx) 

576 if search == -1: 

577 raise ImportErrorHelpGen( 

578 "Unable to guess extension from '{}'.".format(li)) 

579 ext_rem = li[search:] 

580 if not ext_rem: 

581 raise ValueError( # pragma: no cover 

582 "Unable to guess file extension '{0}'".format(li)) 

583 if ext_rem != ".py": 

584 log_function("[import_module] found extension='{0}'".format(ext_rem)) 

585 

586 # remove fmod from sys.modules 

587 if fmod: 

588 addback = [] 

589 rem = [] 

590 for n, m in sys.modules.items(): 

591 if n.startswith(fmod): 

592 rem.append(n) 

593 addback.append((n, m)) 

594 else: 

595 addback = [] 

596 relpath.replace(ext_rem, "") 

597 rem = [] 

598 for n, m in sys.modules.items(): 

599 if n.startswith(relpath): 

600 rem.append(n) 

601 addback.append((n, m)) 

602 

603 # we remove the modules 

604 # this line is important to remove all modules 

605 # from the sources in folder src and not the modified ones 

606 # in the documentation folder 

607 for r in rem: 

608 del sys.modules[r] 

609 

610 # full path 

611 if rootm is not None: 

612 root = rootm 

613 tl = relpath 

614 fi = tl.replace(ext_rem, "").replace("/", ".") 

615 if fmod: 

616 fi = fmod + "." + fi 

617 context = None 

618 if fi.endswith(".__init__"): 

619 fi = fi[:-len(".__init__")] 

620 else: 

621 root = sdir 

622 tl = os.path.split(li)[1] 

623 fi = tl.replace(ext_rem, "") 

624 context = None 

625 

626 if additional_sys_path is not None and len(additional_sys_path) > 0: 

627 # there is an issue here due to the confusion in the paths 

628 # the paths should be removed just after the import 

629 sys.path.extend(additional_sys_path) 

630 

631 sys.path.insert(0, root) 

632 try: 

633 try: 

634 mo = importlib.import_module(fi, context) 

635 except ImportError: # pragma: no cover 

636 log_function( 

637 "[import_module] unable to import module '{0}' fullname " 

638 "'{1}'".format(fi, filename)) 

639 mo_spec = importlib.util.find_spec(fi, context) 

640 log_function("[import_module] imported spec", mo_spec) 

641 mo = mo_spec.loader.load_module() 

642 log_function("[import_module] successful try", mo_spec) 

643 

644 if not mo.__file__.replace("\\", "/").endswith( 

645 filename.replace("\\", "/").strip("./")): # pragma: no cover 

646 namem = os.path.splitext(os.path.split(filename)[-1])[0] 

647 

648 if "src" in sys.path: 

649 sys.path = [_ for _ in sys.path if _ != "src"] 

650 

651 if namem in sys.modules: 

652 del sys.modules[namem] 

653 # add the context here for relative import 

654 # use importlib.import_module with the package argument filled 

655 # mo = __import__ (fi) 

656 try: 

657 mo = importlib.import_module(fi, context) 

658 except ImportError: 

659 mo = importlib.util.find_spec(fi, context) 

660 

661 if not mo.__file__.replace( 

662 "\\", "/").endswith(filename.replace("\\", "/").strip("./")): 

663 raise ImportError( 

664 "The wrong file was imported (2):\nEXP: {0}\nIMP: {1}\n" 

665 "PATHS:\n - {2}".format( 

666 filename, mo.__file__, "\n - ".join(sys.path))) 

667 else: 

668 raise ImportError( 

669 "The wrong file was imported (1):\nEXP: {0}\nIMP: {1}\n" 

670 "PATHS:\n - {2}".format( 

671 filename, mo.__file__, "\n - ".join(sys.path))) 

672 

673 sys.path = memo 

674 log_function("[import_module] import '{0}' successfully".format( 

675 filename), mo.__file__) 

676 for n, m in addback: 

677 if n not in sys.modules: 

678 sys.modules[n] = m 

679 return mo, fmod 

680 

681 except ImportError as e: # pragma: no cover 

682 exp = re.compile("No module named '(.*)'") 

683 find = exp.search(str(e)) 

684 if find: 

685 module = find.groups()[0] 

686 log_function( 

687 "[warning] unable to import module " + module + 

688 " --- " + str(e).replace("\n", " ")) 

689 

690 log_function(" File \"%s\", line %d" % (__file__, 501)) 

691 log_function("[warning] -- unable to import module (1) ", filename, 

692 ",", fi, " in path ", sdir, " Error: ", str(e)) 

693 log_function(" cwd ", os.getcwd()) 

694 log_function(" path", sdir) 

695 stack = traceback.format_exc() 

696 log_function(" executable", sys.executable) 

697 log_function(" version", sys.version_info) 

698 log_function(" stack:\n", stack) 

699 

700 message = ["-----", stack, "-----"] 

701 message.append(" executable: '{0}'".format(sys.executable)) 

702 message.append(" version: '{0}'".format(sys.version_info)) 

703 message.append(" platform: '{0}'".format(sys.platform)) 

704 message.append(" ext_rem='{0}'".format(ext_rem)) 

705 message.append(" fi='{0}'".format(fi)) 

706 message.append(" li='{0}'".format(li)) 

707 message.append(" cpxx='{0}'".format(cpxx)) 

708 message.append("-----") 

709 for p in sys.path: 

710 message.append(" path: " + p) 

711 message.append("-----") 

712 for p in sorted(sys.modules): 

713 try: 

714 m = sys.modules[p].__path__ 

715 except AttributeError: 

716 m = str(sys.modules[p]) 

717 message.append(" module: {0}={1}".format(p, m)) 

718 

719 sys.path = memo 

720 for n, m in addback: 

721 if n not in sys.modules: 

722 sys.modules[n] = m 

723 

724 if 'File "<frozen importlib._bootstrap>"' in stack: 

725 raise ImportErrorHelpGen( 

726 "frozen importlib._bootstrap is an issue:\n" + "\n".join(message)) from e 

727 

728 return "Unable(1) to import %s\nError:\n%s" % (filename, str(e)), fmod 

729 

730 except SystemError as e: # pragma: no cover 

731 log_function("[warning] -- unable to import module (2) ", filename, 

732 ",", fi, " in path ", sdir, " Error: ", str(e)) 

733 stack = traceback.format_exc() 

734 log_function(" executable", sys.executable) 

735 log_function(" version", sys.version_info) 

736 log_function(" stack:\n", stack) 

737 sys.path = memo 

738 for n, m in addback: 

739 if n not in sys.modules: 

740 sys.modules[n] = m 

741 return "unable(2) to import %s\nError:\n%s" % (filename, str(e)), fmod 

742 

743 except KeyError as e: # pragma: no cover 

744 if first_try and "KeyError: 'pip._vendor.urllib3.contrib'" in str(e): 

745 # Issue with pip 9.0.2 

746 return import_module(rootm=rootm, filename=filename, log_function=log_function, 

747 additional_sys_path=additional_sys_path, 

748 first_try=False) 

749 else: 

750 log_function("[warning] -- unable to import module (4) ", filename, 

751 ",", fi, " in path ", sdir, " Error: ", str(e)) 

752 stack = traceback.format_exc() 

753 log_function(" executable", sys.executable) 

754 log_function(" version", sys.version_info) 

755 log_function(" stack:\n", stack) 

756 sys.path = memo 

757 for n, m in addback: 

758 if n not in sys.modules: 

759 sys.modules[n] = m 

760 return "unable(4) to import %s\nError:\n%s" % (filename, str(e)), fmod 

761 

762 except Exception as e: # pragma: no cover 

763 log_function("[warning] -- unable to import module (3) ", filename, 

764 ",", fi, " in path ", sdir, " Error: ", str(e)) 

765 stack = traceback.format_exc() 

766 log_function(" executable", sys.executable) 

767 log_function(" version", sys.version_info) 

768 log_function(" stack:\n", stack) 

769 sys.path = memo 

770 for n, m in addback: 

771 if n not in sys.modules: 

772 sys.modules[n] = m 

773 return "unable(3) to import %s\nError:\n%s" % (filename, str(e)), fmod 

774 

775 

776def get_module_objects(mod): 

777 """ 

778 Gets all the classes from a module. 

779 

780 @param mod module objects 

781 @return list of ModuleMemberDoc 

782 """ 

783 

784 # exp = { "__class__":"", 

785 # "__dict__":"", 

786 # "__doc__":"", 

787 # "__format__":"", 

788 # "__reduce__":"", 

789 # "__reduce_ex__":"", 

790 # "__subclasshook__":"", 

791 # "__dict__":"", 

792 # "__weakref__":"" 

793 # } 

794 

795 cl = [] 

796 for _, obj in inspect.getmembers(mod): 

797 try: 

798 stobj = str(obj) 

799 except RuntimeError: # pragma: no cover 

800 # One issue met in werkzeug 

801 # Working outside of request context. 

802 stobj = "" 

803 if (inspect.isclass(obj) or inspect.isfunction(obj) or 

804 inspect.isgenerator(obj) or inspect.ismethod(obj) or 

805 ("built-in function" in stobj and not isinstance(obj, dict))): 

806 cl.append(ModuleMemberDoc(obj, module=mod)) 

807 if inspect.isclass(obj): 

808 for n, o in inspect.getmembers(obj): 

809 try: 

810 ok = ModuleMemberDoc( 

811 o, "method", cl=obj, name=n, module=mod) 

812 if ok.module is not None: 

813 cl.append(ok) 

814 except Exception as e: 

815 if str(e).startswith("S/"): 

816 raise e # pragma: no cover 

817 

818 res = [] 

819 for _ in cl: 

820 try: 

821 # if _.module != None : 

822 if _.module == mod.__name__: 

823 res.append(_) 

824 except Exception: # pragma: no cover 

825 pass 

826 

827 res.sort() 

828 return res 

829 

830 

831def process_var_tag( 

832 docstring, rst_replace=False, header=None): 

833 """ 

834 Processes a docstring using tag ``@ var``, and return a list of 2-tuple:: 

835 

836 @ var filename file name 

837 @ var utf8 decode in utf8? 

838 @ var errors decoding in utf8 can raise some errors 

839 

840 @param docstring string 

841 @param rst_replace if True, replace the var bloc var a rst bloc 

842 @param header header for the table, if None, ``["attribute", "meaning"]`` 

843 @return a matrix with two columns or a string if rst_replace is True 

844 

845 """ 

846 from pandas import DataFrame 

847 

848 if header is None: 

849 header = ["attribute", "meaning"] 

850 

851 reg = re.compile("[@]var +([_a-zA-Z][a-zA-Z0-9_]*?) +((?:(?!@var).)+)") 

852 

853 indent = len(docstring) 

854 spl = docstring.split("\n") 

855 docstring = [] 

856 bigrow = "" 

857 for line in spl: 

858 if len(line.strip("\r \t")) == 0: 

859 docstring.append(bigrow) 

860 bigrow = "" 

861 else: 

862 ind = len(line) - len(line.lstrip(" ")) 

863 indent = min(ind, indent) 

864 bigrow += line + "\n" 

865 if len(bigrow) > 0: 

866 docstring.append(bigrow) 

867 

868 values = [] 

869 if rst_replace: 

870 for line in docstring: 

871 line2 = line.replace("\n", " ") 

872 if "@var" in line2: 

873 all = reg.findall(line2) 

874 val = [] 

875 for a in all: 

876 val.append(list(a)) 

877 if len(val) > 0: 

878 tbl = DataFrame(columns=header, data=val) 

879 rst = df2rst(tbl, list_table=True) 

880 if indent > 0: 

881 rst = "\n".join((" " * indent) + 

882 _ for _ in rst.split("\n")) 

883 values.append(rst) 

884 else: 

885 values.append(line) 

886 return "\n".join(values) 

887 else: 

888 for line in docstring: 

889 line = line.replace("\n", " ") 

890 if "@var" in line: 

891 alls = reg.findall(line) 

892 for a in alls: 

893 values.append(a) 

894 return values 

895 

896 

897def make_label_index(title, comment): 

898 """ 

899 Builds a :epkg:`sphinx` label from a string by 

900 removing any odd characters. 

901 

902 @param title title 

903 @param comment add this string in the exception when it raises one 

904 @return label 

905 """ 

906 def accept(c): 

907 if "a" <= c <= "z": 

908 return c 

909 if "A" <= c <= "Z": 

910 return c 

911 if "0" <= c <= "9": 

912 return c 

913 if c in "-_": 

914 return c 

915 return "" 

916 

917 try: 

918 r = "".join(map(accept, title)) 

919 if len(r) == 0: 

920 typstr = str 

921 raise HelpGenException( 

922 "Unable to interpret this title (empty?): {0} (type: {2})\n" 

923 "COMMENT:\n{1}".format( 

924 typstr(title), comment, typstr(type(title)))) 

925 return r 

926 except TypeError as e: # pragma: no cover 

927 typstr = str 

928 raise HelpGenException( 

929 "Unable to interpret this title: {0} (type: {2})\nCOMMENT:" 

930 "\n{1}".format( 

931 typstr(title), comment, typstr(type(title)))) from e 

932 

933 

934def process_look_for_tag(tag, title, files): 

935 """ 

936 Looks for specific information in all files, collect them 

937 into one single page. 

938 

939 @param tag tag 

940 @param title title of the page 

941 @param files list of files to look for 

942 @return a list of tuple (page, content of the page) 

943 

944 The function is looking for regular expression:: 

945 

946 .. tag(...). 

947 ... 

948 .. endtag. 

949 

950 They can be split into several pages:: 

951 

952 .. tag(page::...). 

953 ... 

954 .. endtag. 

955 

956 If the extracted example contains an image (..image:: ../../), the path 

957 is fixed too. 

958 

959 The function parses the files instead of loading the files as a module. 

960 The function needs to replace ``\\\\`` by ``\\``, it does not takes into 

961 acount doc string starting with ``r'''``. 

962 The function calls @see fn remove_some_indent 

963 with ``backslash=True`` to replace double backslashes 

964 by simple backslashes. 

965 """ 

966 def noneempty(a): 

967 if "___" in a: 

968 page, b = a.split("___") 

969 return "_" + page, b.lower(), b 

970 return "", a.lower(), a 

971 repl = "__!LI!NE!__" 

972 exp = re.compile( 

973 "[.][.] %s[(](.*?);;(.*?)[)][.](.*?)[.][.] end%s[.]" % (tag, tag)) 

974 exp2 = re.compile( 

975 "[.][.] %s[(](.*?)[)][.](.*?)[.][.] end%s[.]" % (tag, tag)) 

976 coll = [] 

977 for file in files: 

978 if file.file is None: 

979 continue 

980 if "utils_sphinx_doc.py" in file.file: 

981 continue 

982 if file.file.endswith(".py"): 

983 try: 

984 with open(file.file, "r", encoding="utf8") as f: 

985 content = f.read() 

986 except Exception: 

987 with open(file.file, "r") as f: 

988 content = f.read() 

989 content = content.replace("\n", repl) 

990 else: 

991 content = "Binary file." 

992 

993 all = exp.findall(content) 

994 all2 = exp2.findall(content) 

995 if len(all2) > len(all): 

996 raise HelpGenException( # pragma: no cover 

997 "An issue was detected in file %r." % file.file) 

998 

999 coll += [noneempty(a) + 

1000 (fix_image_page_for_root(c.replace(repl, "\n"), file), b) 

1001 for a, b, c in all] 

1002 

1003 coll.sort() 

1004 coll = [(_[0],) + _[2:] for _ in coll] 

1005 

1006 pages = set(_[0] for _ in coll) 

1007 

1008 pagerows = [] 

1009 

1010 for page in pages: 

1011 if page == "": 

1012 tit = title 

1013 suf = "" 

1014 else: 

1015 tit = title + ": " + page.strip("_") 

1016 suf = page.replace(" ", "").replace("_", "") 

1017 suf = re.sub(r'([^a-zA-Z0-9_])', "", suf) 

1018 page = re.sub(r'([^a-zA-Z0-9_])', "", page) 

1019 

1020 rows = [""" 

1021 .. _l-{0}{3}: 

1022 

1023 {1} 

1024 {2} 

1025 

1026 .. contents:: 

1027 :local: 

1028 

1029 """.replace(" ", "").format(tag, tit, "=" * len(tit), suf)] 

1030 

1031 not_expected = os.environ.get( 

1032 "USERNAME", os.environ.get("USER", "````````````")) 

1033 if not_expected != "jenkins" and not_expected in rows[0]: 

1034 raise HelpGenException( 

1035 "The title is probably wrong (4): {0}\ntag={1}\ntit={2}\n" 

1036 "not_expected='{3}'".format(rows[0], tag, tit, not_expected)) 

1037 

1038 for pa, a, b, c in coll: 

1039 pan = re.sub(r'([^a-zA-Z0-9_])', "", pa) 

1040 if page != pan: 

1041 continue 

1042 lindex = make_label_index(a, pan) 

1043 rows.append("") 

1044 rows.append(".. _lm-{0}:".format(lindex)) 

1045 rows.append("") 

1046 rows.append(a) 

1047 rows.append("+" * len(a)) 

1048 rows.append("") 

1049 rows.append(remove_some_indent(b, backslash=True)) 

1050 rows.append("") 

1051 spl = c.split("-") 

1052 d = "file {0}.py".format(spl[1]) # line, spl[2].lstrip("l")) 

1053 rows.append("see :ref:`%s <%s>`" % (d, c)) 

1054 rows.append("") 

1055 

1056 pagerows.append((page, "\n".join(rows))) 

1057 return pagerows 

1058 

1059 

1060def fix_image_page_for_root(content, file): 

1061 """ 

1062 Looks for images and fix their path as 

1063 if the extract were copied to the root. 

1064 

1065 @param content extracted content 

1066 @param file file where is comes from (unused) 

1067 @return content 

1068 """ 

1069 rows = content.split("\n") 

1070 for i in range(len(rows)): 

1071 row = rows[i] 

1072 if ".. image::" in row: 

1073 spl = row.split(".. image::") 

1074 img = spl[-1] 

1075 if "../images" in img: 

1076 img = img.lstrip("./ ") 

1077 if len(spl) == 1: 

1078 row = ".. image:: " + img 

1079 else: 

1080 row = spl[0] + ".. image:: " + img 

1081 rows[i] = row 

1082 return "\n".join(rows) 

1083 

1084 

1085def remove_some_indent(s, backslash=False): 

1086 """ 

1087 Brings text to the left. 

1088 

1089 @param s text 

1090 @param backslash if True, replace double backslash by simple backslash 

1091 @return text 

1092 """ 

1093 rows = s.split("\n") 

1094 mi = len(s) 

1095 for lr in rows: 

1096 ll = lr.lstrip() 

1097 if len(ll) > 0: 

1098 d = len(lr) - len(ll) 

1099 mi = min(d, mi) 

1100 

1101 if mi > 0: 

1102 keep = [] 

1103 for _ in rows: 

1104 keep.append(_[mi:] if len(_.strip()) > 0 and len(_) > mi else _) 

1105 res = "\n".join(keep) 

1106 else: 

1107 res = s 

1108 

1109 if backslash: 

1110 res = res.replace("\\\\", "\\") 

1111 return res 

1112 

1113 

1114def example_function_latex(): 

1115 """ 

1116 This function only contains an example with 

1117 latex to check it is working fine. 

1118 

1119 .. exref:: 

1120 :title: How to display a formula 

1121 

1122 We want to check this formula to successfully converted. 

1123 

1124 :math:`\\left \\{ \\begin{array}{l} \\min_{x,y} \\left \\{ x^2 + y^2 - xy + y \\right \\} 

1125 \\\\ \\text{sous contrainte} \\; x + 2y = 1 \\end{array}\\right .` 

1126 

1127 Brackets and backslashes might be an issue. 

1128 """ 

1129 pass