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 helpers to produce a Sphinx documentation 

4 

5""" 

6import os 

7import re 

8import sys 

9import shutil 

10import importlib 

11from ..loghelper.flog import fLOG, noLOG 

12from ..filehelper.synchelper import remove_folder, synchronize_folder, explore_folder 

13from ._my_doxypy import process_string 

14from .utils_sphinx_doc_helpers import add_file_rst_template, process_var_tag, import_module 

15from .utils_sphinx_doc_helpers import get_module_objects, add_file_rst_template_cor, add_file_rst_template_title 

16from .utils_sphinx_doc_helpers import IndexInformation, RstFileHelp, HelpGenException, process_look_for_tag, make_label_index 

17from ..pandashelper.tblformat import df2rst 

18 

19 

20def validate_file_for_help(filename, fexclude=lambda f: False): 

21 """ 

22 Accepts or rejects a file to be copied in the help folder. 

23 

24 @param filename filename 

25 @param fexclude function to exclude some files 

26 @return boolean 

27 """ 

28 if fexclude is not None and fexclude(filename): 

29 return False # pragma: no cover 

30 

31 if filename.endswith(".pyd") or filename.endswith(".so"): 

32 return True 

33 

34 if "rpy2" in filename: # pragma: no cover 

35 with open(filename, "r") as ff: 

36 content = ff.read() 

37 if "from pandas.core." in content: 

38 return False 

39 

40 return True 

41 

42 

43def replace_relative_import_fct(fullname, content=None): 

44 """ 

45 Takes a :epkg:`python` file and replaces all relative 

46 imports it was able to find by an import which can be 

47 processed by :epkg:`Python` if the file were the main file. 

48 

49 @param fullname name of the file 

50 @param content a preprocessed content of the file of 

51 the content if it is None 

52 @return content of the file without relative imports 

53 

54 Does not change imports in comments. 

55 """ 

56 if content is None: 

57 with open(fullname, "r", encoding="utf8") as f: 

58 content = f.read() 

59 

60 fullpath = os.path.dirname(fullname) 

61 fullsplit = fullpath.replace('\\', '/').split('/') 

62 root = None 

63 for i in range(len(fullsplit), 1, -1): 

64 path = "/".join(fullsplit[:i]) 

65 init = os.path.join(path, '__init__.py') 

66 src = os.path.join(path, 'src') 

67 cond = init or (not init and src) 

68 if not cond: 

69 root = i + 1 

70 break 

71 if i < len(fullsplit) and fullsplit[i] in ('src', 'site-packages'): 

72 root = i + 1 

73 break 

74 if root is None: 

75 raise FileNotFoundError( # pragma: no cover 

76 "Unable to package root for '{}'.".format(fullname)) 

77 

78 lines = content.split("\n") 

79 name = "([a-zA-Z_][a-zA-Z_0-9]*)" 

80 namedot = "([a-zA-Z_][a-zA-Z_0-9.]*)" 

81 names = name + "(, " + name + ")*" 

82 end = "( .*)?$" 

83 regi = re.compile("{0}{1}{2}{3}{4}".format("^( *)from ([.]{1,3})", 

84 namedot, " import ", names, end)) 

85 

86 for i in range(0, len(lines)): 

87 line = lines[i] 

88 find = regi.search(line) 

89 

90 if find: 

91 space, dot, rel, name0, names, _, end = find.groups() 

92 idot = len(dot) 

93 level = len(fullsplit) - root - idot + 1 

94 if level > 0: 

95 if end is None: 

96 end = "" 

97 if names is None: 

98 names = "" 

99 packname = ".".join(fullsplit[root:root + level]) 

100 if rel: 

101 packname += '.' + rel 

102 line = "{space}from {packname} import {name0}{names}{end}".format( 

103 space=space, packname=packname, name0=name0, names=names, end=end) 

104 lines[i] = line 

105 else: 

106 raise ValueError( # pragma: no cover 

107 "Unable to replace relative import in '{0}', " 

108 "root='{1}'\n{2}|{3}|{4}|{5}| level={6}".format( 

109 line, fullsplit[root], dot, rel, name0, names, level)) 

110 

111 return "\n".join(lines) 

112 

113 

114def _private_process_one_file( 

115 fullname, to, silent, fmod, replace_relative_import, use_sys): 

116 """ 

117 Copies one file from the source to the documentation folder. 

118 It processes some comments in doxygen format (@ param, @ return). 

119 It replaces relatives imports by a regular import. 

120 

121 @param fullname name of the file 

122 @param to location (folder) 

123 @param silent no logs if True 

124 @param fmod modification functions 

125 @param replace_relative_import replace relative import 

126 @param use_sys @see fn remove_undesired_part_for_documentation 

127 @return extension, number of lines, number of lines in documentation 

128 """ 

129 ext = os.path.splitext(fullname)[-1] 

130 

131 if ext in {".jpeg", ".jpg", ".pyd", ".png", ".dat", ".dll", ".o", 

132 ".so", ".exe", ".enc", ".txt", ".gif", ".csv", '.pyx', 

133 '*.mp3', '*.mp4', '.tmpl'}: 

134 if ext in (".pyd", ".so"): 

135 # If the file is being executed, the copy might keep the properties of 

136 # the original (only Windows). 

137 with open(fullname, "rb") as f: 

138 bin = f.read() 

139 with open(to, "wb") as f: 

140 f.write(bin) 

141 else: 

142 shutil.copy(fullname, to) 

143 return os.path.splitext(fullname)[-1], 0, 0 

144 else: 

145 try: 

146 with open(fullname, "r", encoding="utf8") as g: 

147 content = g.read() 

148 except UnicodeDecodeError: # pragma: no cover 

149 try: 

150 with open(fullname, "r") as g: 

151 content = g.read() 

152 except UnicodeDecodeError as e: 

153 raise UnicodeDecodeError(e.encoding, e.object, e.start, e.end, 

154 "Unable to read '{0}' due to '{1}'".format(fullname, e.reason)) from e 

155 

156 lines = [_.strip(" \t\n\r") for _ in content.split("\n")] 

157 lines = [_ for _ in lines if len(_) > 0] 

158 nblines = len(lines) 

159 

160 keepc = content 

161 try: 

162 counts, content = migrating_doxygen_doc(content, fullname, silent) 

163 except SyntaxError as e: # pragma: no cover 

164 if not silent: 

165 raise e 

166 content = keepc 

167 counts = dict(docrows=0) 

168 

169 content = fmod(content, fullname) 

170 content = remove_undesired_part_for_documentation( 

171 content, fullname, use_sys) 

172 fold = os.path.split(to)[0] 

173 if not os.path.exists(fold): 

174 os.makedirs(fold) 

175 if replace_relative_import: 

176 content = replace_relative_import_fct(fullname, content) 

177 with open(to, "w", encoding="utf8") as g: 

178 g.write(content) 

179 

180 return os.path.splitext(fullname)[-1], nblines, counts["docrows"] 

181 

182 

183def remove_undesired_part_for_documentation(content, filename, use_sys): 

184 """ 

185 Some files contains blocs inserted between the two lines: 

186 

187 * ``# -- HELP BEGIN EXCLUDE --`` 

188 * ``# -- HELP END EXCLUDE --`` 

189 

190 Those lines will be commented out. 

191 

192 @param content file content 

193 @param filename for error message 

194 @param use_sys string or None, enables, disables a section based on variables added to sys module 

195 @return modified file content 

196 

197 If the parameter *use_sys* is false, the section of code 

198 will be commented out. If true, the section can be enabled. 

199 It relies on the following code:: 

200 

201 import sys 

202 if hasattr(sys, "<use_sys>") and sys.<use_sys>: 

203 

204 # section to enable or disables 

205 

206 The string ``<use_sys>`` will be replaced by the value of 

207 parameter *use_sys*. 

208 """ 

209 marker_in = "# -- HELP BEGIN EXCLUDE --" 

210 marker_out = "# -- HELP END EXCLUDE --" 

211 

212 lines = content.split("\n") 

213 res = [] 

214 inside = False 

215 has_sys = False 

216 flask_trick = False 

217 for line in lines: 

218 if line.startswith("import sys"): 

219 has_sys = True 

220 if line.startswith(marker_in): 

221 if inside: 

222 raise HelpGenException( # pragma: no cover 

223 "issues with undesired blocs in file " + filename + " with: " + marker_in + "|" + marker_out) 

224 inside = True 

225 if use_sys: # pragma: no cover 

226 if not has_sys: 

227 res.append("import sys") 

228 res.append( 

229 "if hasattr(sys, '{0}') and sys.{0}:".format(use_sys)) 

230 res.append(line) 

231 elif line.startswith(marker_out): 

232 if use_sys and flask_trick: # pragma: no cover 

233 res.append(" pass") 

234 if not inside: 

235 raise HelpGenException( # pragma: no cover 

236 "issues with undesired blocs in file " + filename + " with: " + marker_in + "|" + marker_out) 

237 inside = False 

238 flask_trick = False 

239 res.append(line) 

240 else: 

241 if inside: 

242 if use_sys: # pragma: no cover 

243 # specific trick for Flask 

244 if line.startswith("@app."): 

245 line = "# " + line 

246 flask_trick = True 

247 res.append(" " + line) 

248 else: 

249 res.append("### " + line) 

250 else: 

251 res.append(line) 

252 return "\n".join(res) 

253 

254 

255def copy_source_files(input, output, fmod=lambda v, filename: v, 

256 silent=False, filter=None, remove=True, 

257 softfile=lambda f: False, 

258 fexclude=lambda f: False, 

259 addfilter=None, replace_relative_import=False, 

260 copy_add_ext=None, use_sys=None, fLOG=fLOG): 

261 """ 

262 Copies all sources files (input) into a folder (output), 

263 apply on each of them a modification. 

264 

265 :param input: input folder 

266 :param output: output folder (it will be cleaned each time) 

267 :param fmod: modifies the content of each file, 

268 this function takes a string and returns a string 

269 :param silent: if True, do not stop when facing an issue with :epkg:`doxygen` documentation 

270 :param filter: if None, process only file related to python code, otherwise, 

271 use this filter to select file (regular expression). If this parameter 

272 is None or is empty, the default value is something like: 

273 ``"(.+[.]py$)|(.+[.]pyd$)|(.+[.]cpp$)|(.+[.]h$)|(.+[.]dll$))"``. 

274 :param remove: if True, remove every files in the output folder first 

275 :param softfile: softfile is a function (f : filename --> True or False), when it is True, 

276 the documentation is lighter (no special members) 

277 :param fexclude: function to exclude some files from the help 

278 :param addfilter: additional filter, it should look like: ``"(.+[.]pyx$)|(.+[.]pyh$)"`` 

279 :param replace_relative_import: replace relative import 

280 :param copy_add_ext: additional extension file to copy 

281 :param use_sys: see :func:`remove_undesired_part_for_documentation 

282 <pyquickhelper.helpgen.utils_sphinx_doc.remove_undesired_part_for_documentation>` 

283 :param fLOG: logging function 

284 :return: list of copied files 

285 """ 

286 if not os.path.exists(output): 

287 os.makedirs(output) 

288 

289 if remove: 

290 remove_folder(output, False, raise_exception=False) 

291 

292 def_ext = ['py', 'pyd', 'cpp', 'h', 'dll', 'so', 'yml', 'o', 'def', 'gif', 

293 'exe', 'data', 'config', 'css', 'js', 'png', 'map', 'sass', 

294 'csv', 'tpl', 'jpg', 'jpeg', 'hpp', 'cc', 'tmpl'] 

295 deffilter = "|".join("(.+[.]{0}$)".format(_) for _ in def_ext) 

296 if copy_add_ext is not None: 

297 res = ["(.+[.]%s$)" % e for e in copy_add_ext] 

298 deffilter += "|" + "|".join(res) 

299 

300 fLOG("[copy_source_files] copy filter '{0}'".format(deffilter)) 

301 

302 if addfilter is not None and len(addfilter) > 0: 

303 if filter is None or len(filter) == 0: 

304 filter = "|".join([deffilter, addfilter]) 

305 else: 

306 filter = "|".join([filter, addfilter]) 

307 

308 if filter is None: 

309 actions = synchronize_folder(input, output, filter=deffilter, 

310 avoid_copy=True, fLOG=fLOG) 

311 else: 

312 actions = synchronize_folder(input, output, filter=filter, 

313 avoid_copy=True, fLOG=fLOG) 

314 

315 if len(actions) == 0: 

316 raise FileNotFoundError("empty folder: " + input) # pragma: no cover 

317 

318 ractions = [] 

319 for a, file, dest in actions: 

320 if a != ">+": 

321 continue 

322 if not validate_file_for_help(file.fullname, fexclude): 

323 continue 

324 if file.name.endswith("setup.py"): 

325 continue 

326 if "setup.py" in file.name: 

327 raise FileNotFoundError( # pragma: no cover 

328 "are you sure (setup.py)?, file: " + file.fullname) 

329 

330 to = os.path.join(dest, file.name) 

331 dd = os.path.split(to)[0] 

332 if not os.path.exists(dd): 

333 fLOG("[copy_source_files] create ", dd, 

334 "softfile={0} fexclude={1}".format(softfile, fexclude)) 

335 os.makedirs(dd) 

336 fLOG("[copy_source_files] copy ", file.fullname, " to ", to) 

337 

338 rext, rline, rdocline = _private_process_one_file( 

339 file.fullname, to, silent, fmod, replace_relative_import, use_sys) 

340 ractions.append((a, file, dest, rext, rline, rdocline)) 

341 

342 return ractions 

343 

344 

345def apply_modification_template(rootm, store_obj, template, fullname, rootrep, 

346 softfile, indexes, additional_sys_path, fLOG=noLOG): 

347 """ 

348 See @see fn add_file_rst. 

349 

350 @param rootm root of the module 

351 @param store_obj keep track of all objects extracted from the module 

352 @param template rst template to produce 

353 @param fullname full name of the file 

354 @param rootrep file name in the documentation contains some folders which are not desired in the documentation 

355 @param softfile a function (f : filename --> True or False), when it is True, 

356 the documentation is lighter (no special members) 

357 @param indexes dictionary with the label and some information (IndexInformation) 

358 @param additional_sys_path additional path to include to sys.path before importing a module 

359 (will be removed afterwards) 

360 @param fLOG logging function 

361 @return content of a .rst file 

362 

363 .. faqref:: 

364 :title: Why doesn't the documentation show compiled submodules? 

365 

366 The instruction ``.. automodule:: <name>`` only shows objects *obj* 

367 which verify ``obj.__module__ == name``. This is always the case 

368 for modules written in Python but not necessarily for module 

369 compiled from C language. When the module is declared, 

370 the following structure contains the module name in second position. 

371 This name must not be the submodule shortname but the name 

372 the module has is the package. The C file 

373 *pyquickhelper/helpgen/compiled.c* 

374 implements submodule 

375 ``pyquickhelper.helpgen.compiled``, this value must replace 

376 ``<fullname>`` in the structure below, not simply *compiled*. 

377 

378 :: 

379 

380 static struct PyModuleDef moduledef = { 

381 PyModuleDef_HEAD_INIT, 

382 "<fullname>", 

383 "Helper for parallelization with threads with C++.", 

384 sizeof(struct module_state), 

385 fonctions, 

386 NULL, 

387 threader_module_traverse, 

388 threader_module_clear, 

389 NULL 

390 }; 

391 

392 .. warning:: 

393 This function still needs some improvments 

394 for C++ modules on MacOSX. 

395 """ 

396 from pandas import DataFrame 

397 

398 keepf = fullname 

399 filename = os.path.split(fullname)[-1] 

400 filenoext = os.path.splitext(filename)[0] 

401 fullname = fullname.strip(".").replace( 

402 "\\", "/").replace("/", ".").strip(".") 

403 if rootrep[0] in fullname: 

404 pos = fullname.index(rootrep[0]) 

405 fullname = rootrep[1] + fullname[pos + len(rootrep[0]):] 

406 fullnamenoext = fullname[:-3] if fullname.endswith(".py") else fullname 

407 if fullnamenoext.endswith(".pyd"): 

408 fullnamenoext = '.'.join(fullnamenoext.split('.')[:-2]) 

409 elif fullnamenoext.endswith('-linux-gnu.so'): 

410 fullnamenoext = '.'.join(fullnamenoext.split('.')[:-2]) 

411 pythonname = None 

412 

413 not_expected = os.environ.get( 

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

415 if not_expected not in ('jenkins', 'vsts', 'runner') and not_expected in fullnamenoext: 

416 mes = ("The title is probably wrong (5): {0}\nnoext='{1}'\npython='{2}'\nrootm='{3}'\nrootrep='{4}'" 

417 "\nfullname='{5}'\nkeepf='{6}'\nnot_expected='{7}'") # pragma: no cover 

418 raise HelpGenException(mes.format( # pragma: no cover 

419 fullnamenoext, filenoext, pythonname, rootm, rootrep, fullname, keepf, not_expected)) 

420 

421 mo, prefix = import_module( 

422 rootm, keepf, fLOG, additional_sys_path=additional_sys_path) 

423 doc = "" 

424 shortdoc = "" 

425 

426 additional = {} 

427 tspecials = {} 

428 

429 if mo is not None: 

430 if isinstance(mo, str): # pragma: no cover 

431 # it is an error 

432 spl = mo.split("\n") 

433 mo = "\n".join([" " + _ for _ in spl]) 

434 mo = "::\n\n" + mo + "\n\n" 

435 doc = mo 

436 shortdoc = "Error" 

437 pythonname = fullnamenoext 

438 else: 

439 pythonname = mo.__name__ 

440 if mo.__doc__ is not None: 

441 doc = mo.__doc__ 

442 doc = private_migrating_doxygen_doc( 

443 doc.split("\n"), 0, fullname) 

444 doct = doc 

445 doc = [] 

446 

447 for d in doct: 

448 if len(doc) != 0 or len(d) > 0: 

449 doc.append(d) 

450 while len(doc) > 0 and len(doc[-1]) == 0: 

451 doc.pop() 

452 

453 shortdoc = doc[0] if len(doc) > 0 else "" 

454 if len(doc) > 1: 

455 shortdoc += "..." 

456 

457 doc = "\n".join(doc) 

458 doc = "module ``" + mo.__name__ + "``\n\n" + doc 

459 if ":githublink:" not in doc: 

460 doc += "\n\n:githublink:`GitHub|py|*`" 

461 else: 

462 doc = "" 

463 shortdoc = "empty" 

464 

465 # it produces the table for the function, classes, and 

466 objs = get_module_objects(mo) 

467 

468 prefix = ".".join(fullnamenoext.split(".")[:-1]) 

469 for ob in objs: 

470 

471 if ob.type in ["method"] and ob.name.startswith("_"): 

472 tspecials[ob.name] = ob 

473 

474 ob.add_prefix(prefix) 

475 if ob.key in store_obj: 

476 if isinstance(store_obj[ob.key], list): 

477 store_obj[ob.key].append(ob) 

478 else: 

479 store_obj[ob.key] = [store_obj[ob.key], ob] 

480 else: 

481 store_obj[ob.key] = ob 

482 

483 for k, v in add_file_rst_template_cor.items(): 

484 values = [[o.rst_link(None, class_in_bracket=False), o.truncdoc] 

485 for o in objs if o.type == k] 

486 if len(values) > 0: 

487 tbl = DataFrame( 

488 columns=[k, "truncated documentation"], data=values) 

489 for row in tbl.values: 

490 if ":meth:`_" in row[0]: 

491 row[0] = row[0].replace(":meth:`_", ":py:meth:`_") 

492 

493 if len(tbl) > 0: 

494 maxi = max([len(_) for _ in tbl[k]]) 

495 s = 0 if tbl.iloc[0, 1] is None else len( 

496 tbl.iloc[0, 1]) 

497 t = "" if tbl.iloc[0, 1] is None else tbl.iloc[0, 1] 

498 tbl.iloc[0, 1] = t + (" " * (3 * maxi - s)) 

499 sph = df2rst(tbl) 

500 titl = "\n\n" + add_file_rst_template_title[k] + "\n" 

501 titl += "+" * len(add_file_rst_template_title[k]) 

502 titl += "\n\n" 

503 additional[v] = titl + sph 

504 else: 

505 additional[v] = "" 

506 else: 

507 additional[v] = "" 

508 

509 del mo 

510 

511 else: 

512 doc = "[sphinxerror]-C unable to import." 

513 

514 if indexes is None: 

515 indexes = {} 

516 label = IndexInformation.get_label(indexes, "f-" + filenoext) 

517 indexes[label] = IndexInformation( 

518 "module", label, filenoext, doc, None, keepf) 

519 fLOG("[apply_modification_template] adding into index ", indexes[label]) 

520 

521 try: 

522 with open(keepf, "r") as ft: 

523 content = ft.read() 

524 except UnicodeDecodeError: 

525 try: 

526 with open(keepf, "r", encoding="latin-1") as ft: 

527 content = ft.read() 

528 except UnicodeDecodeError: # pragma: no cover 

529 with open(keepf, "r", encoding="utf8") as ft: 

530 content = ft.read() 

531 

532 plat = "Windows" if "This example only runs on Windows." in content else "any" 

533 

534 # dealing with special members (does not work) 

535 # text_specials = "".join([" :special-members: %s\n" % k for k in tspecials ]) 

536 text_specials = "" 

537 

538 if fullnamenoext.endswith(".__init__"): 

539 fullnamenoext = fullnamenoext[: -len(".__init__")] 

540 if filenoext.endswith(".__init__"): 

541 filenoext = filenoext[: -len(".__init__")] 

542 

543 not_expected = os.environ.get( 

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

545 if not_expected not in ('jenkins', 'vsts', 'runner') and not_expected in fullnamenoext: 

546 mes = ("The title is probably wrong (3): {0}\nnoext={1}\npython={2}\nrootm={3}\nrootrep={4}" 

547 "\nfullname={5}\nkeepf={6}\nnot_expected='{7}'") # pragma: no cover 

548 raise HelpGenException(mes.format( # pragma: no cover 

549 fullnamenoext, filenoext, pythonname, rootm, rootrep, fullname, keepf, not_expected)) 

550 

551 ttitle = "module ``{0}``".format(fullnamenoext) 

552 rep = { 

553 "__FULLNAME_UNDERLINED__": ttitle + "\n" + ("=" * len(ttitle)) + "\n", 

554 "__FILENAMENOEXT__": filenoext, 

555 "__FULLNAMENOEXT__": pythonname, 

556 "__DOCUMENTATION__": doc.split("\n.. ")[0], 

557 "__DOCUMENTATIONLINE__": 

558 shortdoc.split(".. todoext::", maxsplit=1)[0], 

559 "__PLATFORM__": plat, 

560 "__ADDEDMEMBERS__": text_specials} 

561 

562 for k, v in additional.items(): 

563 rep[k] = v 

564 

565 res = template 

566 for a, b in rep.items(): 

567 res = res.replace(a, b) 

568 

569 has_class = any( 

570 filter(lambda _: _.startswith("class "), content.split("\n"))) 

571 if not has_class: 

572 spl = res.split("\n") 

573 spl = [_ for _ in spl if not _.startswith(".. inheritance-diagram::")] 

574 res = "\n".join(spl) 

575 

576 if softfile(fullname): 

577 res = res.replace(":special-members:", "") 

578 

579 return res 

580 

581 

582def add_file_rst(rootm, store_obj, actions, template=add_file_rst_template, 

583 rootrep=("_doc.sphinxdoc.source.pyquickhelper.", ""), 

584 fmod=lambda v, filename: v, softfile=lambda f: False, 

585 mapped_function=None, indexes=None, 

586 additional_sys_path=None, fLOG=noLOG): 

587 """ 

588 Creates a :epkg:`rst` file for every source file. 

589 

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

591 @param store_obj to keep table of all objects 

592 @param actions output from @see fn copy_source_files 

593 @param template :epkg:`rst` template to produce 

594 @param rootrep file name in the documentation contains some folders 

595 which are not desired in the documentation 

596 @param fmod applies modification to the instanciated template 

597 @param softfile softfile is a function (f : filename --> True or False), when it is True, 

598 the documentation is lighter (no special members) 

599 @param mapped_function list of 2-tuple (pattern, function). Every file matching the pattern 

600 will be copied to the documentation folder, its content will be sent 

601 to the function and will produce a file <filename>.rst. Example: 

602 ``[ (".*[.]sql$", filecontent_to_rst) ]`` 

603 The function takes two parameters: full_filename, content_filename. It returns 

604 a string (the rst file) or a tuple (rst file, short description). 

605 By default (if function is None), the function ``filecontent_to_rst`` will be called 

606 except for .rst file for which nothing is done. 

607 @param indexes to index some information { dictionary label:IndexInformation (...) }, 

608 the function populates it 

609 @param additional_sys_path additional path to include to sys.path before importing a module 

610 (will be removed afterwards) 

611 @param fLOG logging function 

612 @return list of written files stored in RstFileHelp 

613 """ 

614 if indexes is None: 

615 indexes = {} 

616 if mapped_function is None: 

617 mapped_function = [] 

618 

619 if additional_sys_path is None: 

620 additional_sys_path = [] 

621 

622 memo = {} 

623 app = [] 

624 for action in actions: 

625 _, file, dest = action[:3] 

626 if not isinstance(file, str): 

627 file = file.name 

628 

629 to = os.path.join(dest, file) 

630 rst = os.path.splitext(to)[0] 

631 rst += ".rst" 

632 ext = os.path.splitext(to)[-1] 

633 

634 if sys.platform == "win32": 

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

636 elif sys.version_info[:2] <= (3, 7): 

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

638 else: 

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

640 

641 if file.endswith(".py") or ( 

642 cpxx in file and ( 

643 file.endswith(".pyd") or file.endswith("linux-gnu.so") 

644 )): 

645 if os.stat(to).st_size > 0: 

646 content = apply_modification_template( 

647 rootm, store_obj, template, to, rootrep, softfile, indexes, 

648 additional_sys_path=additional_sys_path, fLOG=fLOG) 

649 content = fmod(content, file) 

650 

651 # tweaks for example and studies 

652 zzz = to.replace("\\", "/") 

653 name = os.path.split(file)[-1] 

654 noex = os.path.splitext(name)[0] 

655 

656 # todo: specific case: should be removed and added back in a 

657 # proper way 

658 if "examples/" in zzz or "studies/" in zzz: 

659 content += "\n.. _%s_literal:\n\nCode\n----\n\n.. literalinclude:: %s\n\n" % ( 

660 noex, name) 

661 

662 with open(rst, "w", encoding="utf8") as g: 

663 g.write(content) 

664 app.append(RstFileHelp(to, rst, "")) 

665 

666 for vv in indexes.values(): 

667 if vv.fullname == to: 

668 vv.set_rst_file(rst) 

669 break 

670 

671 else: 

672 for pat, func in mapped_function: 

673 if func is None and ext == ".rst": 

674 continue 

675 if pat not in memo: 

676 memo[pat] = re.compile(pat) 

677 exp = memo[pat] 

678 if exp.search(file): 

679 if isinstance(func, bool) and not func: 

680 # we copy but we do nothing with it 

681 pass 

682 else: 

683 with open(to, "r", encoding="utf8") as g: 

684 content = g.read() 

685 if func is None: 

686 func = filecontent_to_rst 

687 content = func(to, content) 

688 

689 if isinstance(content, tuple) and len(content) == 2: 

690 content, doc = content 

691 else: 

692 doc = "" 

693 

694 with open(rst, "w", encoding="utf8") as g: 

695 g.write(content) 

696 app.append(RstFileHelp(to, rst, "")) 

697 

698 filenoext, ext = os.path.splitext( 

699 os.path.split(to)[-1]) 

700 ext = ext.strip(".") 

701 label = IndexInformation.get_label( 

702 indexes, "ext-" + filenoext) 

703 indexes[label] = IndexInformation( 

704 "ext-" + ext, label, filenoext, doc, rst, to) 

705 fLOG("[add_file_rst] add ext into index ", indexes[label]) 

706 

707 return app 

708 

709 

710def produces_indexes(store_obj, indexes, fexclude_index, titles=None, 

711 correspondances=None, fLOG=fLOG): 

712 """ 

713 Produces a file for each category of object found in the module. 

714 

715 @param store_obj list of collected object, it is a dictionary 

716 key : ModuleMemberDoc or key : [ list of ModuleMemberDoc ] 

717 @param indexes list of things to index, dictionary { label : IndexInformation } 

718 @param fexclude_index to exclude files from the indices 

719 @param titles each type is mapped to a title to add to the :epkg:`rst` file 

720 @param correspondances each type is mapped to a label to add to the :epkg:`rst` file 

721 @param fLOG logging function 

722 @return dictionary: { type : rst content of the index } 

723 

724 Default values if *titles* of *correspondances* is None: 

725 

726 :: 

727 

728 title = {"method": "Methods", 

729 "staticmethod": "Static Methods", 

730 "property": "Properties", 

731 "function": "Functions", 

732 "class": "Classes", 

733 "module": "Modules"} 

734 

735 correspondances = {"method": "l-methods", 

736 "function": "l-functions", 

737 "staticmethod": "l-staticmethods", 

738 "property": "l-properties", 

739 "class": "l-classes", 

740 "module": "l-modules"} 

741 """ 

742 from pandas import DataFrame 

743 

744 if titles is None: 

745 titles = {"method": "Methods", 

746 "staticmethod": "Static Methods", 

747 "property": "Properties", 

748 "function": "Functions", 

749 "class": "Classes", 

750 "module": "Modules"} 

751 

752 if correspondances is None: 

753 correspondances = {"method": "l-methods", 

754 "function": "l-functions", 

755 "staticmethod": "l-staticmethods", 

756 "property": "l-properties", 

757 "class": "l-classes", 

758 "module": "l-modules"} 

759 

760 # we process store_obj 

761 types = {} 

762 for k, v in store_obj.items(): 

763 if not isinstance(v, list): 

764 v = [v] 

765 for _ in v: 

766 if fexclude_index(_): 

767 continue 

768 types[_.type] = types.get(_.type, 0) + 1 

769 

770 fLOG("[produces_indexes] store_obj: extraction of types: {}".format(types)) 

771 res = {} 

772 for k in types: 

773 fLOG("[produces_indexes] type: [{}] - rst".format(k)) 

774 values = [] 

775 for t, so in store_obj.items(): 

776 if not isinstance(so, list): 

777 so = [so] 

778 

779 for o in so: 

780 if fexclude_index(o): 

781 continue 

782 if o.type != k: 

783 continue 

784 oclname = o.classname.__name__ if o.classname is not None else "" 

785 rlink = o.rst_link(class_in_bracket=False) 

786 fLOG("[produces_indexes] + '{}': {}".format(o.name, rlink)) 

787 values.append([o.name, rlink, oclname, o.truncdoc]) 

788 

789 values.sort() 

790 for row in values: 

791 if ":meth:`_" in row[1]: 

792 row[1] = row[1].replace(":meth:`_", ":py:meth:`_") 

793 

794 # we filter private method or functions 

795 values = [ 

796 row for row in values if ":meth:`__" in row or ":meth:`_" not in row] 

797 values = [ 

798 row for row in values if ":func:`__" in row or ":func:`_" not in row] 

799 

800 columns = ["_", k, "class parent", "truncated documentation"] 

801 tbl = DataFrame(columns=columns, data=values) 

802 if len(tbl.columns) >= 2: 

803 tbl = tbl.iloc[:, 1:].copy() 

804 

805 if len(tbl) > 0: 

806 maxi = max([len(_) for _ in tbl[k]]) 

807 s = 0 if tbl.iloc[0, 1] is None else len(tbl.iloc[0, 1]) 

808 t = "" if tbl.iloc[0, 1] is None else tbl.iloc[0, 1] 

809 tbl.iloc[0, 1] = t + (" " * (3 * maxi - s)) 

810 sph = df2rst(tbl) 

811 res[k] = sph 

812 fLOG("[produces_indexes] type: [{}] - shape: {}".format(k, tbl.shape)) 

813 

814 # we process indexes 

815 

816 fLOG("[produces_indexes] indexes") 

817 types = {} 

818 for k, v in indexes.items(): 

819 if fexclude_index(v): 

820 continue 

821 types[v.type] = types.get(v.type, 0) + 1 

822 

823 fLOG("[produces_indexes] extraction of types: {}".format(types)) 

824 

825 for k in types: 

826 if k in res: 

827 raise HelpGenException( # pragma: no cover 

828 "you should not index anything related to classes, functions or method (conflict: %s)" % k) 

829 values = [] 

830 for t, o in indexes.items(): 

831 if fexclude_index(o): 

832 continue 

833 if o.type != k: 

834 continue 

835 values.append([o.name, 

836 o.rst_link(), 

837 o.truncdoc]) 

838 values.sort() 

839 

840 tbl = DataFrame( 

841 columns=["_", k, "truncated documentation"], data=values) 

842 if len(tbl.columns) >= 2: 

843 tbl = tbl[tbl.columns[1:]] 

844 

845 if len(tbl) > 0: 

846 maxi = max([len(_) for _ in tbl[k]]) 

847 tbl.iloc[0, 1] = tbl.iloc[0, 1] + \ 

848 (" " * (3 * maxi - len(tbl.iloc[0, 1]))) 

849 sph = df2rst(tbl) 

850 res[k] = sph 

851 

852 # end 

853 

854 for k in res: # pylint: disable=C0206 

855 fLOG("[produces_indexes] index name '{}'".format(k)) 

856 label = correspondances.get(k, k) 

857 title = titles.get(k, k) 

858 under = "=" * len(title) 

859 

860 content = "\n".join([".. contents::", " :local:", 

861 " :depth: 1", "", "", "Summary", "+++++++"]) 

862 

863 not_expected = os.environ.get( 

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

865 if not_expected != "jenkins" and not_expected in title: 

866 raise HelpGenException( # pragma: no cover 

867 "The title is probably wrong (2), found '{0}' in '{1}'".format(not_expected, title)) 

868 

869 res[k] = "\n.. _%s:\n\n%s\n%s\n\n%s\n\n%s" % ( 

870 label, title, under, content, res[k]) 

871 

872 return res 

873 

874 

875def filecontent_to_rst(filename, content): 

876 """ 

877 Produces a *.rst* file which contains the file. 

878 It adds a title and a label based on the 

879 filename (no folder included). 

880 

881 @param filename filename 

882 @param content content 

883 @return new content 

884 """ 

885 file = os.path.split(filename)[-1] 

886 full = file + "\n" + ("=" * len(file)) + "\n" 

887 

888 not_expected = os.environ.get( 

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

890 if not_expected != "jenkins" and not_expected in file: 

891 raise HelpGenException( # pragma: no cover 

892 "The title is probably wrong (1): '{0}' found in '{1}'".format(not_expected, file)) 

893 

894 rows = ["", ".. _f-%s:" % file, "", "", full, "", 

895 # "fullpath: ``%s``" % filename, 

896 "", ""] 

897 if ".. RSTFORMAT." in content: 

898 rows.append(".. include:: %s " % file) 

899 else: 

900 rows.append(".. literalinclude:: %s " % file) 

901 rows.append("") 

902 

903 nospl = content.replace("\n", "_!_!:!_") 

904 

905 reg = re.compile("(.. beginshortsummary[.](.*?).. endshortsummary[.])") 

906 cont = reg.search(nospl) 

907 if cont: 

908 g = cont.groups()[1].replace("_!_!:!_", "\n") 

909 return "\n".join(rows), g.strip("\n\r ") 

910 

911 if "@brief" in content: 

912 spl = content.split("\n") 

913 begin = None 

914 end = None 

915 for i, r in enumerate(spl): 

916 if "@brief" in r: 

917 begin = i 

918 if end is None and begin is not None and len( 

919 r.strip(" \n\r\t")) == 0: 

920 end = i 

921 

922 if begin is not None and end is not None: 

923 summary = "\n".join(spl[begin:end]).replace( 

924 "@brief", "").strip("\n\t\r ") 

925 else: 

926 summary = "no documentation" # pragma: no cover 

927 

928 # looking for C++/java/C# comments 

929 spl = content.split("\n") 

930 begin = None 

931 end = None 

932 for i, r in enumerate(spl): 

933 if "/**" in spl[i]: 

934 begin = i 

935 if end is None and begin is not None and "*/" in spl[i]: 

936 end = i 

937 

938 content = "\n".join(rows) 

939 if begin is not None and end is not None: # pragma: no cover 

940 filerows = private_migrating_doxygen_doc( 

941 spl[begin + 1:end - 1], 1, filename) 

942 rstr = "\n".join(filerows) 

943 rstr = re.sub( 

944 ":param +([a-zA-Z_][[a-zA-Z_0-9]*) *:", r"* **\1**:", rstr) 

945 content = content.replace( 

946 ".. literalinclude::", "\n%s\n\n.. literalinclude::" % rstr) 

947 

948 return content, summary 

949 

950 return "\n".join(rows), "no documentation" 

951 

952 

953def prepare_file_for_sphinx_help_generation(store_obj, input, output, 

954 subfolders, fmod_copy=lambda v, filename: v, 

955 template=add_file_rst_template, 

956 rootrep=( 

957 "_doc.sphinxdoc.source.project_name.", ""), 

958 fmod_res=lambda v, filename: v, silent=False, 

959 optional_dirs=None, softfile=lambda f: False, 

960 fexclude=lambda f: False, mapped_function=None, 

961 fexclude_index=lambda f: False, issues=None, 

962 additional_sys_path=None, replace_relative_import=False, 

963 module_name=None, copy_add_ext=None, use_sys=None, 

964 auto_rst_generation=True, fLOG=fLOG): 

965 """ 

966 Prepares all files for :epkg:`Sphinx` generation. 

967 

968 @param store_obj to keep track of all objects, it should be a dictionary 

969 @param input input folder 

970 @param output output folder (it will be cleaned each time) 

971 @param subfolders list of subfolders to copy from input to output, two cases: 

972 * a string input/<sub> --> output/<sub> 

973 * a tuple input/<sub[0]> --> output/<sub[1]> 

974 @param fmod_copy modifies the content of each file, 

975 this function takes a string and the filename and returns a string 

976 ``f(content, filename) --> string`` 

977 @param template rst template to produce 

978 @param rootrep file name in the documentation contains some folders which are not desired in the documentation 

979 @param fmod_res applies modification to the instanciated template 

980 @param silent if True, do not stop when facing an issue with doxygen migration 

981 @param optional_dirs list of tuple with a list of folders (source, copy, filter) to 

982 copy for the documentation, example: 

983 ``( <folder_help>, "coverage", ".*" )`` 

984 @param softfile softfile is a function (f : filename --> True or False), when it is True, 

985 the documentation is lighter (no special members) 

986 @param fexclude function to exclude some files from the help 

987 @param fexclude_index function to exclude some files from the indices 

988 

989 @param mapped_function list of 2-tuple (pattern, function). Every file matching the pattern 

990 will be copied to the documentation folder, its content will be sent 

991 to the function and will produce a file <filename>.rst. Example: 

992 ``[ (".*[.]sql$", filecontent_to_rst) ]`` 

993 The function takes two parameters: full_filename, content_filename. It returns 

994 a string (the rst file) or a tuple (rst file, short description). 

995 By default (if function is None), the function ``filecontent_to_rst`` will be called. 

996 

997 @param issues if not None (a list), the function will store some issues here. 

998 

999 @param additional_sys_path additional paths to includes to sys.path when import a module (will be removed afterwards) 

1000 @param replace_relative_import replace relative import 

1001 @param module_name module name (cannot be None) 

1002 @param copy_add_ext additional file extension to copy 

1003 @param use_sys @see fn remove_undesired_part_for_documentation 

1004 @param auto_rst_generation add a file *.rst* for each source file 

1005 @param fLOG logging function 

1006 

1007 @return list of written files stored in @see cl RstFileHelp 

1008 

1009 Example: 

1010 

1011 :: 

1012 

1013 prepare_file_for_sphinx_help_generation ( 

1014 {}, 

1015 ".", 

1016 "_doc/sphinxdoc/source/", 

1017 subfolders = [ 

1018 ("src/" + project_var_name, 

1019 project_var_name), 

1020 ], 

1021 silent = True, 

1022 rootrep = ("_doc.sphinxdoc.source.%s." % 

1023 (project_var_name,), ""), 

1024 optional_dirs = optional_dirs, 

1025 mapped_function = [ (".*[.]tohelp$", None) ] ) 

1026 

1027 It produces a file with the number of lines and files per extension. 

1028 """ 

1029 if optional_dirs is None: 

1030 optional_dirs = [] 

1031 

1032 if mapped_function is None: 

1033 mapped_function = [] 

1034 

1035 if additional_sys_path is None: 

1036 additional_sys_path = [] 

1037 

1038 if module_name is None: 

1039 raise ValueError( # pragma: no cover 

1040 "module_name cannot be None") 

1041 

1042 fLOG("[prepare_file_for_sphinx_help_generation] output='{}'".format(output)) 

1043 rootm = os.path.abspath(output) 

1044 fLOG("[prepare_file_for_sphinx_help_generation] input='{}'".format(input)) 

1045 

1046 actions = [] 

1047 rsts = [] 

1048 indexes = {} 

1049 

1050 for sub in subfolders: 

1051 if isinstance(sub, str): 

1052 src = (input + "/" + sub).replace("//", "/") 

1053 dst = (output + "/" + sub).replace("//", "/") 

1054 else: 

1055 src = (input + "/" + sub[0]).replace("//", "/") 

1056 dst = (output + "/" + sub[1]).replace("//", "/") 

1057 if os.path.split(src)[-1][0] == '_': 

1058 raise RuntimeError( 

1059 "Subfolder %r cannot start with '_'." % src) 

1060 if os.path.split(dst)[-1][0] == '_': 

1061 raise RuntimeError( # pragma: no cover 

1062 "Destination %r cannot start with '_'." % dst) 

1063 

1064 if os.path.isfile(src): 

1065 fLOG(" [p] ", src) 

1066 _private_process_one_file( 

1067 src, dst, silent, fmod_copy, replace_relative_import, use_sys) 

1068 

1069 temp = os.path.split(dst) 

1070 actions_t = [(">", temp[1], temp[0], 0, 0)] 

1071 if auto_rst_generation: 

1072 rstadd = add_file_rst(rootm, store_obj, actions_t, 

1073 template, rootrep, fmod_res, 

1074 softfile=softfile, 

1075 mapped_function=mapped_function, 

1076 indexes=indexes, 

1077 additional_sys_path=additional_sys_path, 

1078 fLOG=fLOG) 

1079 rsts += rstadd 

1080 else: 

1081 fLOG("[prepare_file_for_sphinx_help_generation] processing '{}'".format(src)) 

1082 

1083 actions_t = copy_source_files(src, dst, fmod_copy, silent=silent, 

1084 softfile=softfile, fexclude=fexclude, 

1085 addfilter="|".join( 

1086 ['(%s)' % _[0] for _ in mapped_function]), 

1087 replace_relative_import=replace_relative_import, 

1088 copy_add_ext=copy_add_ext, 

1089 use_sys=use_sys, fLOG=fLOG) 

1090 

1091 # without those two lines, importing the module might crash later 

1092 importlib.invalidate_caches() 

1093 importlib.util.find_spec(module_name) 

1094 

1095 if auto_rst_generation: 

1096 rsts += add_file_rst(rootm, store_obj, actions_t, template, 

1097 rootrep, fmod_res, softfile=softfile, 

1098 mapped_function=mapped_function, 

1099 indexes=indexes, 

1100 additional_sys_path=additional_sys_path, 

1101 fLOG=fLOG) 

1102 

1103 actions += actions_t 

1104 

1105 # everything is cleaned from the build folder, so, it is no use 

1106 for tu in optional_dirs: 

1107 if len(tu) == 2: 

1108 fold, dest, filt = tu + (".*", ) 

1109 else: 

1110 fold, dest, filt = tu 

1111 if filt is None: 

1112 filt = ".*" 

1113 if not os.path.exists(dest): 

1114 fLOG("creating folder (sphinx) ", dest) 

1115 os.makedirs(dest) 

1116 

1117 copy_source_files(fold, dest, silent=silent, filter=filt, 

1118 softfile=softfile, fexclude=fexclude, 

1119 addfilter="|".join(['(%s)' % _[0] 

1120 for _ in mapped_function]), 

1121 replace_relative_import=replace_relative_import, 

1122 copy_add_ext=copy_add_ext, fLOG=fLOG) 

1123 

1124 # processing all store_obj to compute some indices 

1125 fLOG("[prepare_file_for_sphinx_help_generation] processing all store_obj to compute some indices") 

1126 fLOG("[prepare_file_for_sphinx_help_generation] extracted ", 

1127 len(store_obj), " objects") 

1128 res = produces_indexes(store_obj, indexes, fexclude_index, fLOG=fLOG) 

1129 

1130 fLOG("[prepare_file_for_sphinx_help_generation] generating ", 

1131 len(res), " indexes for ", ", ".join(list(res.keys()))) 

1132 allfiles = [] 

1133 for k, vv in res.items(): 

1134 out = os.path.join(output, "index_" + k + ".rst") 

1135 allfiles.append("index_" + k) 

1136 fLOG(" generates index", out) 

1137 if k == "module": 

1138 toc = ["\n\n.. toctree::"] 

1139 toc.append(" :maxdepth: 1\n") 

1140 for _ in rsts: 

1141 if _.file is not None and len(_.file) > 0: 

1142 na = os.path.splitext(_.rst)[0].replace( 

1143 "\\", "/").split("/") 

1144 if "source" in na: 

1145 na = na[na.index("source") + 1:] 

1146 na = "/".join(na) 

1147 toc.append(" " + na) 

1148 vv += "\n".join(toc) 

1149 with open(out, "w", encoding="utf8") as fh: 

1150 fh.write(vv) 

1151 rsts.append(RstFileHelp(None, out, None)) 

1152 

1153 # generates a table with the number of lines per extension 

1154 rows = [] 

1155 for act in actions: 

1156 if "__init__.py" not in act[1].get_fullname() or act[-1] > 0: 

1157 v = 1 

1158 rows.append(act[-3:] + (v,)) 

1159 name = os.path.split(act[1].get_fullname())[-1] 

1160 if name.startswith("auto_"): 

1161 rows.append(("auto_*" + act[-3], act[-2], act[-1], v)) 

1162 elif "__init__.py" in name: 

1163 rows.append(("__init__.py", act[-2], act[-1], v)) 

1164 elif "__init__.py" in act[1].get_fullname(): 

1165 v = 1 

1166 rows.append(("empty __init__.py", act[-2], act[-1], v)) 

1167 

1168 # use DataFrame to produce a RST table 

1169 from pandas import DataFrame 

1170 df = DataFrame( 

1171 data=rows, columns=["extension/kind", "nb lines", "nb doc lines", "nb files"]) 

1172 try: 

1173 # for pandas >= 0.17 

1174 df = df.groupby( 

1175 "extension/kind", as_index=False).sum().sort_values("extension/kind") 

1176 except AttributeError: # pragma: no cover 

1177 # for pandas < 0.17 

1178 df = df.groupby( 

1179 "extension/kind", as_index=False).sum().sort("extension/kind") 

1180 

1181 # reports 

1182 fLOG("[prepare_file_for_sphinx_help_generation] writing ", "all_report.rst") 

1183 all_report = os.path.join(output, "all_report.rst") 

1184 with open(all_report, "w") as falli: 

1185 falli.write("\n:orphan:\n\n") 

1186 falli.write(".. _l-statcode:\n") 

1187 falli.write("\n") 

1188 falli.write("Statistics on code\n") 

1189 falli.write("==================\n") 

1190 falli.write("\n\n") 

1191 sph = df2rst(df, list_table=True) 

1192 falli.write(sph) 

1193 falli.write("\n") 

1194 rsts.append(RstFileHelp(None, all_report, None)) 

1195 

1196 # all indexes 

1197 fLOG("[prepare_file_for_sphinx_help_generation] writing ", "all_indexes.rst") 

1198 all_index = os.path.join(output, "all_indexes.rst") 

1199 with open(all_index, "w") as falli: 

1200 falli.write("\n:orphan:\n\n") 

1201 falli.write("\n") 

1202 falli.write("All indexes\n") 

1203 falli.write("===========\n") 

1204 falli.write("\n\n") 

1205 falli.write(".. toctree::\n") 

1206 falli.write(" :maxdepth: 2\n") 

1207 falli.write("\n") 

1208 for k in sorted(allfiles): 

1209 falli.write(" %s\n" % k) 

1210 falli.write("\n") 

1211 rsts.append(RstFileHelp(None, all_index, None)) 

1212 

1213 # last function to process images 

1214 fLOG("looking for images", output) 

1215 

1216 images = os.path.join(output, "images") 

1217 fLOG("+looking for images into ", images, " for folder ", output) 

1218 if os.path.exists(images): 

1219 process_copy_images(output, images) 

1220 

1221 # fixes indexed objects with incomplete names 

1222 # :class:`name` --> :class:`name <...>` 

1223 fLOG("+looking for incomplete references", output) 

1224 fix_incomplete_references(output, store_obj, issues=issues, fLOG=fLOG) 

1225 # for t,so in store_obj.items() : 

1226 

1227 # look for FAQ and example 

1228 fLOG("[prepare_file_for_sphinx_help_generation] FAQ + examples") 

1229 app = [] 

1230 for tag, title in [("FAQ", "FAQ"), 

1231 ("example", "Examples"), 

1232 ("NB", "Magic commands"), ]: 

1233 onefiles = process_look_for_tag(tag, title, rsts) 

1234 for page, onefile in onefiles: 

1235 saveas = os.path.join(output, "all_%s%s.rst" % 

1236 (tag, 

1237 page.replace(":", "").replace("/", "").replace(" ", ""))) 

1238 with open(saveas, "w", encoding="utf8") as fh: 

1239 fh.write(onefile) 

1240 app.append(RstFileHelp(saveas, onefile, "")) 

1241 rsts += app 

1242 

1243 fLOG("[prepare_file_for_sphinx_help_generation] END", output) 

1244 return actions, rsts 

1245 

1246 

1247def process_copy_images(folder_source, folder_images): 

1248 """ 

1249 Looks into every file .rst or .py for images (.. image:: imagename), 

1250 if this image was found in directory folder_images, then the image is copied 

1251 closes to the file. 

1252 

1253 @param folder_source folder where to look for sources 

1254 @param folder_images folder where to look for images 

1255 @return list of copied images 

1256 """ 

1257 _, files = explore_folder(folder_source, "[.]((rst)|(py))$") 

1258 reg = re.compile(".. image::(.*)") 

1259 cop = [] 

1260 for fn in files: 

1261 try: 

1262 with open(fn, "r", encoding="utf8") as f: 

1263 content = f.read() 

1264 except Exception as e: # pragma: no cover 

1265 try: 

1266 with open(fn, "r") as f: 

1267 content = f.read() 

1268 except Exception: 

1269 raise Exception("Issue with file '{0}'".format(fn)) from e 

1270 

1271 lines = content.split("\n") 

1272 for line in lines: 

1273 img = reg.search(line) 

1274 if img: 

1275 name = img.groups()[0].strip() 

1276 fin = os.path.split(name)[-1] 

1277 path = os.path.join(folder_images, fin) 

1278 if os.path.exists(path): 

1279 dest = os.path.join(os.path.split(fn)[0], fin) 

1280 shutil.copy(path, dest) 

1281 fLOG("+copy img ", fin, " to ", dest) 

1282 cop.append(dest) 

1283 else: 

1284 fLOG("-unable to find image ", name) 

1285 return cop 

1286 

1287 

1288def fix_incomplete_references(folder_source, store_obj, issues=None, fLOG=fLOG): 

1289 """ 

1290 Looks into every file .rst or .py for incomplete reference. Example:: 

1291 

1292 :class:`name` --> :class:`name <...>`. 

1293 

1294 

1295 @param folder_source folder where to look for sources 

1296 @param store_obj container for indexed objects 

1297 @param issues if not None (a list), it will add issues (function, message) 

1298 @param fLOG logging function 

1299 @return list of fixed references 

1300 """ 

1301 cor = {"func": ["function"], 

1302 "meth": ["method", "property", "staticmethod"] 

1303 } 

1304 

1305 _, files = explore_folder(folder_source, "[.](py)$") 

1306 reg = re.compile( 

1307 "(:(py:)?((class)|(meth)|(func)):`([a-zA-Z_][a-zA-Z0-9_]*?)`)") 

1308 cop = [] 

1309 for fn in files: 

1310 try: 

1311 with open(fn, "r", encoding="utf8") as f: 

1312 content = f.read() 

1313 encoding = "utf8" 

1314 except Exception: # pragma: no cover 

1315 with open(fn, "r") as f: 

1316 content = f.read() 

1317 encoding = None 

1318 

1319 mainname = os.path.splitext(os.path.split(fn)[-1])[0] 

1320 

1321 modif = False 

1322 lines = content.split("\n") 

1323 rline = [] 

1324 for line in lines: 

1325 ref = reg.search(line) 

1326 if ref: 

1327 all = ref.groups()[0] 

1328 # pre = ref.groups()[1] 

1329 typ = ref.groups()[2] 

1330 nam = ref.groups()[-1] 

1331 

1332 key = None 

1333 obj = None 

1334 for cand in cor.get(typ, [typ]): 

1335 k = "%s;%s" % (cand, nam) 

1336 if k in store_obj: 

1337 if isinstance(store_obj[k], list): 

1338 se = [ 

1339 _s for _s in store_obj[k] if mainname in _s.rst_link()] 

1340 if len(se) == 1: 

1341 obj = se[0] 

1342 break 

1343 else: 

1344 key = k 

1345 obj = store_obj[k] 

1346 break 

1347 

1348 if key in store_obj: 

1349 modif = True 

1350 lnk = obj.rst_link(class_in_bracket=False) 

1351 fLOG(" i,ref, found ", all, " --> ", lnk) 

1352 line = line.replace(all, lnk) 

1353 else: 

1354 fLOG( 

1355 " w,unable to replace key ", key, ": ", all, "in file", fn) 

1356 if issues is not None: 

1357 issues.append(("fix_incomplete_references", 

1358 "Unable to replace key '%s', link '%s' in file " 

1359 "'%s'." % (key, all, fn))) 

1360 

1361 rline.append(line) 

1362 

1363 if modif: 

1364 if encoding == "utf8": 

1365 with open(fn, "w", encoding="utf8") as f: 

1366 f.write("\n".join(rline)) 

1367 else: 

1368 with open(fn, "w") as f: 

1369 f.write("\n".join(rline)) 

1370 return cop 

1371 

1372 

1373def migrating_doxygen_doc(content, filename, silent=False, log=False, debug=False): 

1374 """ 

1375 Migrates the doxygen documentation to rst format. 

1376 

1377 @param content file content 

1378 @param filename filename (to display useful error messages) 

1379 @param silent if silent, do not raise an exception 

1380 @param log if True, write some information in the logs (not only exceptions) 

1381 @param debug display more information on the output if True 

1382 @return statistics, new content file 

1383 

1384 Function ``private_migrating_doxygen_doc`` enumerates the list of conversion 

1385 which will be done. 

1386 """ 

1387 if log: 

1388 fLOG("migrating_doxygen_doc: ", filename) 

1389 

1390 rows = [] 

1391 counts = {"docrows": 0} 

1392 

1393 def print_in_rows(v, file=None): 

1394 rows.append(v) 

1395 

1396 def local_private_migrating_doxygen_doc(r, index_first_line, filename): 

1397 counts["docrows"] += len(r) 

1398 return _private_migrating_doxygen_doc(r, index_first_line, 

1399 filename, debug=debug, silent=silent) 

1400 

1401 process_string(content, print_in_rows, local_private_migrating_doxygen_doc, 

1402 filename, 0, debug=debug) 

1403 return counts, "\n".join(rows) 

1404 

1405# -- HELP BEGIN EXCLUDE -- 

1406 

1407 

1408def private_migrating_doxygen_doc(rows, index_first_line, filename, 

1409 debug=False, silent=False): 

1410 """ 

1411 Processes a block help (from doxygen to rst). 

1412 

1413 @param rows list of text lines 

1414 @param index_first_line index of the first line (to display useful message error) 

1415 @param filename filename (to display useful message error) 

1416 @param silent if True, do not display anything 

1417 @param debug display more information if True 

1418 @return another list of text lines 

1419 

1420 @warning This function uses regular expression to process the documentation, 

1421 it does not import the module (as Sphinx does). It might misunderstand some code. 

1422 

1423 @todo Try to import the module and if it possible, uses that information to help 

1424 the parsing. 

1425 

1426 The following line displays error message you can click on using SciTe 

1427 

1428 :: 

1429 

1430 raise SyntaxError(" File \"%s\", line %d, in ???\n unable to process: %s " %( 

1431 filename, index_first_line+i+1, row)) 

1432 

1433 __sphinx__skip__ 

1434 

1435 The previous string tells the function to stop processing the help. 

1436 

1437 Doxygen conversions:: 

1438 

1439 @param <param_name> description 

1440 :param <param_name>: description 

1441 

1442 @var <param_name> produces a table with the attributes 

1443 

1444 @return description 

1445 :return: description 

1446 

1447 @rtype description 

1448 :rtype: description 

1449 

1450 @code 

1451 code:: + indentation 

1452 

1453 @endcode 

1454 nothing 

1455 

1456 @file 

1457 nothing 

1458 

1459 @brief 

1460 nothing 

1461 

1462 @ingroup ... 

1463 nothing 

1464 

1465 @defgroup .... 

1466 nothing 

1467 

1468 @image html ... 

1469 

1470 @see,@ref label forbidden 

1471 should be <op> <fn> <label>, example: @ref cl label 

1472 <op> must be in [fn, cl, at, me, te, md] 

1473 

1474 :class:`label` 

1475 :func:`label` 

1476 :attr:`label` 

1477 :meth:`label` 

1478 :mod:`label` 

1479 

1480 @warning description (until next empty line) 

1481 .. warning:: 

1482 description 

1483 

1484 @todo 

1485 .. todo:: a todo box 

1486 

1487 ------------- not done yet 

1488 

1489 @img image name 

1490 .. image:: test.png 

1491 :width: 200pt 

1492 

1493 .. raw:: html 

1494 html indente 

1495 

1496 """ 

1497 return _private_migrating_doxygen_doc(rows, index_first_line, filename, 

1498 debug=debug, silent=silent) 

1499 

1500# -- HELP END EXCLUDE -- 

1501 

1502 

1503def _private_migrating_doxygen_doc(rows, index_first_line, filename, 

1504 debug=False, silent=False): 

1505 if debug: # pragma: no cover 

1506 fLOG("------------------ P0") 

1507 fLOG("\n".join(rows)) 

1508 fLOG("------------------ P") 

1509 

1510 debugrows = rows 

1511 rows = [_.replace("\t", " ") for _ in rows] 

1512 pars = re.compile("([@]param( +)([a-zA-Z0-9_]+)) ") 

1513 refe = re.compile( 

1514 "([@]((see)|(ref)) +((fn)|(cl)|(at)|(me)|(te)|(md)) +([a-zA-Z0-9_]+))($|[^a-zA-Z0-9_])") 

1515 exce = re.compile("([@]exception( +)([a-zA-Z0-9_]+)) ") 

1516 exem = re.compile("([@]example[(](.*?___)?(.*?)[)])") 

1517 faq_ = re.compile("([@]FAQ[(](.*?___)?(.*?)[)])") 

1518 nb_ = re.compile("([@]NB[(](.*?___)?(.*?)[)])") 

1519 

1520 # min indent 

1521 if len(rows) > 1: 

1522 space_rows = [(r.lstrip(), r) for r in rows[1:] if len(r.strip()) > 0] 

1523 else: 

1524 space_rows = [] 

1525 if len(space_rows) > 0: 

1526 min_indent = min(len(r[1]) - len(r[0]) for r in space_rows) 

1527 else: 

1528 min_indent = 0 

1529 

1530 # We fix the first rows which might be different from the others. 

1531 if len(rows) > 1: 

1532 r = rows[0] 

1533 r = (r.lstrip(), r) 

1534 delta = len(r[1]) - len(r[0]) 

1535 if delta != min_indent: 

1536 rows = rows.copy() 

1537 rows[0] = " " * min_indent + rows[0].lstrip() 

1538 

1539 # processing doxygen documentation 

1540 indent = False 

1541 openi = False 

1542 beginends = {} 

1543 

1544 typstr = str 

1545 

1546 whole = "\n".join(rows) 

1547 if "@var" in whole: 

1548 whole = process_var_tag(whole, True) 

1549 rows = whole.split("\n") 

1550 

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

1552 row = rows[i] 

1553 

1554 if debug: 

1555 fLOG( # pragma: no cover 

1556 "-- indent=%s openi=%s row=%s" % (indent, openi, row)) 

1557 

1558 if "__sphinx__skip__" in row: 

1559 if not silent: 

1560 fLOG(" File \"%s\", line %s, skipping" % 

1561 (filename, index_first_line + i + 1)) 

1562 break 

1563 

1564 strow = row.strip(" ") 

1565 

1566 if "@endFAQ" in strow or "@endexample" in strow or "@endNB" in strow: 

1567 if "@endFAQ" in strow: 

1568 beginends["FAQ"] = beginends.get("FAQ", 0) - 1 

1569 sp = " " * row.index("@endFAQ") 

1570 rows[i] = "\n" + sp + ".. endFAQ.\n" 

1571 if "@endexample" in strow: 

1572 beginends["example"] = beginends.get("example", 0) - 1 

1573 sp = " " * row.index("@endexample") 

1574 rows[i] = "\n" + sp + ".. endexample.\n" 

1575 if "@endNB" in strow: # pragma: no cover 

1576 beginends["NB"] = beginends.get("NB", 0) - 1 

1577 sp = " " * row.index("@endNB") 

1578 rows[i] = "\n" + sp + ".. endNB.\n" 

1579 continue 

1580 

1581 if indent: 

1582 if (not openi and len(strow) == 0) or "@endcode" in strow: 

1583 indent = False 

1584 rows[i] = "" 

1585 openi = False 

1586 if "@endcode" in strow: 

1587 beginends["code"] = beginends.get("code", 0) - 1 

1588 else: 

1589 rows[i] = " " + rows[i] 

1590 

1591 else: 

1592 

1593 if strow.startswith("@warning"): 

1594 pos = rows[i].find("@warning") 

1595 sp = " " * pos 

1596 rows[i] = rows[i].replace("@warning", "\n%s.. warning:: " % sp) 

1597 indent = True 

1598 

1599 elif strow.startswith("@todo"): 

1600 pos = rows[i].find("@todo") 

1601 sp = " " * pos 

1602 rows[i] = rows[i].replace("@todo", "\n%s.. todo:: " % sp) 

1603 indent = True 

1604 

1605 elif strow.startswith("@ingroup"): 

1606 rows[i] = "" 

1607 

1608 elif strow.startswith("@defgroup"): 

1609 rows[i] = "" 

1610 

1611 elif strow.startswith("@image"): 

1612 pos = rows[i].find("@image") 

1613 sp = " " * pos 

1614 spl = strow.split() 

1615 img = spl[-1] 

1616 if img.startswith("http://"): 

1617 rows[i] = "\n%s.. fancybox:: " % sp + img + "\n\n" 

1618 else: 

1619 

1620 if img.startswith("images") or img.startswith("~"): 

1621 # we assume it is a relative path to the source 

1622 img = img.strip("~") 

1623 spl_path = filename.replace("\\", "/").split("/") 

1624 pos = spl_path.index("src") 

1625 dots = [".."] * (len(spl_path) - pos - 2) 

1626 ref = "/".join(dots) + "/" 

1627 else: 

1628 ref = "" 

1629 

1630 sp = " " * row.index("@image") 

1631 rows[i] = "\n%s.. image:: %s%s\n%s :align: center\n" % ( 

1632 sp, ref, img, sp) 

1633 

1634 elif strow.startswith("@code"): 

1635 pos = rows[i].find("@code") 

1636 sp = " " * pos 

1637 prev = i - 1 

1638 while prev > 0 and len(rows[prev].strip(" \n\r\t")) == 0: 

1639 prev -= 1 

1640 rows[i] = "" 

1641 if rows[prev].strip("\n").endswith("."): 

1642 rows[prev] += "\n\n%s::\n" % sp 

1643 else: 

1644 rows[prev] += (":" if rows[prev].endswith(":") else "::") 

1645 indent = True 

1646 openi = True 

1647 beginends["code"] = beginends.get("code", 0) + 1 

1648 

1649 # basic tags 

1650 row = rows[i] 

1651 

1652 # tag param 

1653 look = pars.search(row) 

1654 lexxce = exce.search(row) 

1655 example = exem.search(row) 

1656 faq = faq_.search(row) 

1657 nbreg = nb_.search(row) 

1658 

1659 if look: 

1660 rep = look.groups()[0] 

1661 sp = look.groups()[1] 

1662 name = look.groups()[2] 

1663 to = ":param%s%s:" % (sp, name) 

1664 rows[i] = row.replace(rep, to) 

1665 

1666 # it requires an empty line before if the previous line does 

1667 # not start by : 

1668 if i > 0 and not rows[ 

1669 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1670 rows[i] = "\n" + rows[i] 

1671 

1672 elif lexxce: 

1673 rep = lexxce.groups()[0] 

1674 sp = lexxce.groups()[1] 

1675 name = lexxce.groups()[2] 

1676 to = ":raises%s%s:" % (sp, name) 

1677 rows[i] = row.replace(rep, to) 

1678 

1679 # it requires an empty line before if the previous line does 

1680 # not start by : 

1681 if i > 0 and not rows[ 

1682 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1683 rows[i] = "\n" + rows[i] 

1684 

1685 elif example: 

1686 sp = " " * row.index("@example") 

1687 rep = example.groups()[0] 

1688 exa = example.groups()[2].replace("[|", "(").replace("|]", ")") 

1689 pag = example.groups()[1] 

1690 if pag is None: 

1691 pag = "" 

1692 fil = os.path.splitext(os.path.split(filename)[-1])[0] 

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

1694 ref = fil + "-l%d" % (i + index_first_line) 

1695 ref2 = make_label_index(exa, typstr(example.groups())) 

1696 to = "\n\n%s.. _le-%s:\n\n%s.. _le-%s:\n\n%s**Example: %s** \n\n%s.. example(%s%s;;le-%s).\n" % ( 

1697 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

1698 rows[i] = row.replace(rep, to) 

1699 

1700 # it requires an empty line before if the previous line does 

1701 # not start by : 

1702 if i > 0 and not rows[ 

1703 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1704 rows[i] = "\n" + rows[i] 

1705 beginends["example"] = beginends.get("example", 0) + 1 

1706 

1707 elif faq: 

1708 sp = " " * row.index("@FAQ") 

1709 rep = faq.groups()[0] 

1710 exa = faq.groups()[2].replace("[|", "(").replace("|]", ")") 

1711 pag = faq.groups()[1] 

1712 if pag is None: 

1713 pag = "" 

1714 fil = os.path.splitext(os.path.split(filename)[-1])[0] 

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

1716 ref = fil + "-l%d" % (i + index_first_line) 

1717 ref2 = make_label_index(exa, typstr(faq.groups())) 

1718 to = "\n\n%s.. _le-%s:\n\n%s.. _le-%s:\n\n%s**FAQ: %s** \n\n%s.. FAQ(%s%s;;le-%s).\n" % ( 

1719 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

1720 rows[i] = row.replace(rep, to) 

1721 

1722 # it requires an empty line before if the previous line does 

1723 # not start by : 

1724 if i > 0 and not rows[ 

1725 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1726 rows[i] = "\n" + rows[i] 

1727 beginends["FAQ"] = beginends.get("FAQ", 0) + 1 

1728 

1729 elif nbreg: # pragma: no cover 

1730 sp = " " * row.index("@NB") 

1731 rep = nbreg.groups()[0] 

1732 exa = nbreg.groups()[2].replace("[|", "(").replace("|]", ")") 

1733 pag = nbreg.groups()[1] 

1734 if pag is None: 

1735 pag = "" 

1736 fil = os.path.splitext(os.path.split(filename)[-1])[0] 

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

1738 ref = fil + "-l%d" % (i + index_first_line) 

1739 ref2 = make_label_index(exa, typstr(nbreg.groups())) 

1740 to = "\n\n%s.. _le-%s:\n\n%s.. _le-%s:\n\n%s**NB: %s** \n\n%s.. NB(%s%s;;le-%s).\n" % ( 

1741 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

1742 rows[i] = row.replace(rep, to) 

1743 

1744 # it requires an empty line before if the previous line does 

1745 # not start by : 

1746 if i > 0 and not rows[ 

1747 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1748 rows[i] = "\n" + rows[i] 

1749 beginends["NB"] = beginends.get("NB", 0) + 1 

1750 

1751 elif "@return" in row: 

1752 rows[i] = row.replace("@return", ":return:") 

1753 # it requires an empty line before if the previous line does 

1754 # not start by : 

1755 if i > 0 and not rows[ 

1756 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1757 rows[i] = "\n" + rows[i] 

1758 

1759 elif "@rtype" in row: 

1760 rows[i] = row.replace("@rtype", ":rtype:") 

1761 # it requires an empty line before if the previous line does 

1762 # not start by : 

1763 if i > 0 and not rows[ 

1764 i - 1].strip().startswith(":") and len(rows[i - 1].strip()) > 0: 

1765 rows[i] = "\n" + rows[i] 

1766 

1767 elif "@brief" in row: 

1768 rows[i] = row.replace("@brief", "").strip() 

1769 elif "@file" in row: 

1770 rows[i] = row.replace("@file", "").strip() 

1771 

1772 # loop on references 

1773 refl = refe.search(rows[i]) 

1774 while refl: 

1775 see = "see" in refl.groups()[1] 

1776 see = "" # " " if see else "" 

1777 ty = refl.groups()[4] 

1778 name = refl.groups()[-2] 

1779 if len(name) == 0: 

1780 raise SyntaxError( 

1781 "name should be empty: " + typstr(refl.groups())) 

1782 rep = refl.groups()[0] 

1783 ty = {"cl": "class", "me": "meth", "at": "attr", 

1784 "fn": "func", "te": "term", "md": "mod"}[ty] 

1785 to = "%s:%s:`%s`" % (see, ty, name) 

1786 rows[i] = rows[i].replace(rep, to) 

1787 refl = refe.search(rows[i]) 

1788 

1789 if not debug: 

1790 for i, row in enumerate(rows): 

1791 if "__sphinx__skip__" in row: 

1792 break 

1793 if "@param" in row or "@return" in row or "@see" in row or "@warning" in row \ 

1794 or "@todo" in row or "@code" in row or "@endcode" in row or "@brief" in row or "@file" in row \ 

1795 or "@rtype" in row or "@exception" in row \ 

1796 or "@example" in row or "@NB" in row or "@endNB" in row or "@endexample" in row: 

1797 if not silent: # pragma: no cover 

1798 fLOG("#########################") 

1799 _private_migrating_doxygen_doc( 

1800 debugrows, index_first_line, filename, debug=True) 

1801 fLOG("#########################") 

1802 mes = " File \"%s\", line %d, in ???\n unable to process: %s \nwhole blocks:\n%s" % ( 

1803 filename, index_first_line + i + 1, row, "\n".join(rows)) 

1804 fLOG("[sphinxerror]-D ", mes) 

1805 else: # pragma: no cover 

1806 mes = " File \"%s\", line %d, in ???\n unable to process: %s \nwhole blocks:\n%s" % ( 

1807 filename, index_first_line + i + 1, row, "\n".join(rows)) 

1808 raise SyntaxError(mes) # pragma: no cover 

1809 

1810 for k, v in beginends.items(): 

1811 if v != 0: # pragma: no cover 

1812 mes = " File \"%s\", line %d, in ???\n unbalanced tag %s: %s \nwhole blocks:\n%s" % ( 

1813 filename, index_first_line + i + 1, k, row, "\n".join(rows)) 

1814 fLOG("[sphinxerror]-E ", mes) 

1815 raise SyntaxError(mes) 

1816 

1817 # add githublink 

1818 link = [_ for _ in rows if ":githublink:" in _] 

1819 if len(link) == 0: 

1820 rows.append("") 

1821 rows.append("{1}:githublink:`%|py|{0}`".format( 

1822 index_first_line, " " * min_indent)) 

1823 

1824 # clean rows 

1825 clean_rows = [] 

1826 for row in rows: 

1827 if row.strip(): 

1828 clean_rows.append(row) 

1829 elif len(clean_rows) > 0: 

1830 clean_rows.append('') 

1831 return clean_rows 

1832 

1833 

1834def doc_checking(): 

1835 """ 

1836 Example of a doc string. 

1837 """ 

1838 pass 

1839 

1840 

1841class useless_class_UnicodeStringIOThreadSafe (str): 

1842 

1843 """avoid conversion problem between str and char, 

1844 class protected again Thread issue""" 

1845 

1846 def __init__(self): 

1847 """ 

1848 creates a lock 

1849 """ 

1850 str.__init__(self) 

1851 import threading 

1852 self.lock = threading.Lock()