Coverage for pyquickhelper/helpgen/utils_sphinx_doc.py: 89%

851 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

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 f"Unable to package root for '{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(f"^( *)from ([.]{{1,3}}){namedot} import {names}{end}") 

84 

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

86 line = lines[i] 

87 find = regi.search(line) 

88 

89 if find: 

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

91 idot = len(dot) 

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

93 if level > 0: 

94 if end is None: 

95 end = "" 

96 if names is None: 

97 names = "" 

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

99 if rel: 

100 packname += '.' + rel 

101 line = f"{space}from {packname} import {name0}{names}{end}" 

102 lines[i] = line 

103 else: 

104 raise ValueError( # pragma: no cover 

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

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

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

108 

109 return "\n".join(lines) 

110 

111 

112def _private_process_one_file( 

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

114 """ 

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

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

117 It replaces relatives imports by a regular import. 

118 

119 @param fullname name of the file 

120 @param to location (folder) 

121 @param silent no logs if True 

122 @param fmod modification functions 

123 @param replace_relative_import replace relative import 

124 @param use_sys @see fn remove_undesired_part_for_documentation 

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

126 """ 

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

128 

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

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

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

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

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

134 # the original (only Windows). 

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

136 bin = f.read() 

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

138 f.write(bin) 

139 else: 

140 shutil.copy(fullname, to) 

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

142 else: 

143 try: 

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

145 content = g.read() 

146 except UnicodeDecodeError: # pragma: no cover 

147 try: 

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

149 content = g.read() 

150 except UnicodeDecodeError as e: 

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

152 f"Unable to read '{fullname}' due to '{e.reason}'") from e 

153 

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

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

156 nblines = len(lines) 

157 

158 keepc = content 

159 try: 

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

161 except SyntaxError as e: # pragma: no cover 

162 if not silent: 

163 raise e 

164 content = keepc 

165 counts = dict(docrows=0) 

166 

167 content = fmod(content, fullname) 

168 content = remove_undesired_part_for_documentation( 

169 content, fullname, use_sys) 

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

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

172 os.makedirs(fold) 

173 if replace_relative_import: 

174 content = replace_relative_import_fct(fullname, content) 

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

176 g.write(content) 

177 

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

179 

180 

181def remove_undesired_part_for_documentation(content, filename, use_sys): 

182 """ 

183 Some files contains blocs inserted between the two lines: 

184 

185 * ``# -- HELP BEGIN EXCLUDE --`` 

186 * ``# -- HELP END EXCLUDE --`` 

187 

188 Those lines will be commented out. 

189 

190 @param content file content 

191 @param filename for error message 

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

193 @return modified file content 

194 

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

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

197 It relies on the following code:: 

198 

199 import sys 

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

201 

202 # section to enable or disables 

203 

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

205 parameter *use_sys*. 

206 """ 

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

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

209 

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

211 res = [] 

212 inside = False 

213 has_sys = False 

214 flask_trick = False 

215 for line in lines: 

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

217 has_sys = True 

218 if line.startswith(marker_in): 

219 if inside: 

220 raise HelpGenException( # pragma: no cover 

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

222 inside = True 

223 if use_sys: # pragma: no cover 

224 if not has_sys: 

225 res.append("import sys") 

226 res.append( 

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

228 res.append(line) 

229 elif line.startswith(marker_out): 

230 if use_sys and flask_trick: # pragma: no cover 

231 res.append(" pass") 

232 if not inside: 

233 raise HelpGenException( # pragma: no cover 

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

235 inside = False 

236 flask_trick = False 

237 res.append(line) 

238 else: 

239 if inside: 

240 if use_sys: # pragma: no cover 

241 # specific trick for Flask 

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

243 line = "# " + line 

244 flask_trick = True 

245 res.append(" " + line) 

246 else: 

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

248 else: 

249 res.append(line) 

250 return "\n".join(res) 

251 

252 

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

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

255 softfile=lambda f: False, 

256 fexclude=lambda f: False, 

257 addfilter=None, replace_relative_import=False, 

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

259 """ 

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

261 apply on each of them a modification. 

262 

263 :param input: input folder 

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

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

266 this function takes a string and returns a string 

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

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

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

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

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

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

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

274 the documentation is lighter (no special members) 

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

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

277 :param replace_relative_import: replace relative import 

278 :param copy_add_ext: additional extension file to copy 

279 :param use_sys: see :func:`remove_undesired_part_for_documentation 

280 <pyquickhelper.helpgen.utils_sphinx_doc.remove_undesired_part_for_documentation>` 

281 :param fLOG: logging function 

282 :return: list of copied files 

283 """ 

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

285 os.makedirs(output) 

286 

287 if remove: 

288 remove_folder(output, False, raise_exception=False) 

289 

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

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

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

293 deffilter = "|".join(f"(.+[.]{_}$)" for _ in def_ext) 

294 if copy_add_ext is not None: 

295 res = [f"(.+[.]{e}$)" for e in copy_add_ext] 

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

297 

298 fLOG(f"[copy_source_files] copy filter '{deffilter}'") 

299 

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

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

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

303 else: 

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

305 

306 if filter is None: 

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

308 avoid_copy=True, fLOG=fLOG) 

309 else: 

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

311 avoid_copy=True, fLOG=fLOG) 

312 

313 if len(actions) == 0: 

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

315 

316 ractions = [] 

317 for a, file, dest in actions: 

318 if a != ">+": 

319 continue 

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

321 continue 

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

323 continue 

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

325 raise FileNotFoundError( # pragma: no cover 

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

327 

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

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

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

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

332 f"softfile={softfile} fexclude={fexclude}") 

333 os.makedirs(dd) 

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

335 

336 rext, rline, rdocline = _private_process_one_file( 

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

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

339 

340 return ractions 

341 

342 

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

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

345 """ 

346 See @see fn add_file_rst. 

347 

348 @param rootm root of the module 

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

350 @param template rst template to produce 

351 @param fullname full name of the file 

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

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

354 the documentation is lighter (no special members) 

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

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

357 (will be removed afterwards) 

358 @param fLOG logging function 

359 @return content of a .rst file 

360 

361 .. faqref:: 

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

363 

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

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

366 for modules written in Python but not necessarily for module 

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

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

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

370 the module has is the package. The C file 

371 *pyquickhelper/helpgen/compiled.c* 

372 implements submodule 

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

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

375 

376 :: 

377 

378 static struct PyModuleDef moduledef = { 

379 PyModuleDef_HEAD_INIT, 

380 "<fullname>", 

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

382 sizeof(struct module_state), 

383 fonctions, 

384 NULL, 

385 threader_module_traverse, 

386 threader_module_clear, 

387 NULL 

388 }; 

389 

390 .. warning:: 

391 This function still needs some improvments 

392 for C++ modules on MacOSX. 

393 """ 

394 from pandas import DataFrame 

395 

396 keepf = fullname 

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

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

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

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

401 if rootrep[0] in fullname: 

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

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

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

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

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

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

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

409 pythonname = None 

410 

411 not_expected = os.environ.get( 

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

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

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

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

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

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

418 

419 mo, prefix = import_module( 

420 rootm, keepf, fLOG, additional_sys_path=additional_sys_path) 

421 doc = "" 

422 shortdoc = "" 

423 

424 additional = {} 

425 tspecials = {} 

426 

427 if mo is not None: 

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

429 # it is an error 

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

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

432 mo = "::\n\n" + mo + "\n\n" 

433 doc = mo 

434 shortdoc = "Error" 

435 pythonname = fullnamenoext 

436 else: 

437 pythonname = mo.__name__ 

438 if mo.__doc__ is not None: 

439 doc = mo.__doc__ 

440 doc = private_migrating_doxygen_doc( 

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

442 doct = doc 

443 doc = [] 

444 

445 for d in doct: 

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

447 doc.append(d) 

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

449 doc.pop() 

450 

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

452 if len(doc) > 1: 

453 shortdoc += "..." 

454 

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

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

457 if ":githublink:" not in doc: 

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

459 else: 

460 doc = "" 

461 shortdoc = "empty" 

462 

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

464 objs = get_module_objects(mo) 

465 

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

467 for ob in objs: 

468 

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

470 tspecials[ob.name] = ob 

471 

472 ob.add_prefix(prefix) 

473 if ob.key in store_obj: 

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

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

476 else: 

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

478 else: 

479 store_obj[ob.key] = ob 

480 

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

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

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

484 if len(values) > 0: 

485 tbl = DataFrame( 

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

487 for row in tbl.values: 

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

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

490 

491 if len(tbl) > 0: 

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

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

494 tbl.iloc[0, 1]) 

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

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

497 sph = df2rst(tbl) 

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

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

500 titl += "\n\n" 

501 additional[v] = titl + sph 

502 else: 

503 additional[v] = "" 

504 else: 

505 additional[v] = "" 

506 

507 del mo 

508 

509 else: 

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

511 

512 if indexes is None: 

513 indexes = {} 

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

515 indexes[label] = IndexInformation( 

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

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

518 

519 try: 

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

521 content = ft.read() 

522 except UnicodeDecodeError: 

523 try: 

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

525 content = ft.read() 

526 except UnicodeDecodeError: # pragma: no cover 

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

528 content = ft.read() 

529 

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

531 

532 # dealing with special members (does not work) 

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

534 text_specials = "" 

535 

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

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

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

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

540 

541 not_expected = os.environ.get( 

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

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

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

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

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

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

548 

549 ttitle = f"module ``{fullnamenoext}``" 

550 rep = { 

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

552 "__FILENAMENOEXT__": filenoext, 

553 "__FULLNAMENOEXT__": pythonname, 

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

555 "__DOCUMENTATIONLINE__": 

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

557 "__PLATFORM__": plat, 

558 "__ADDEDMEMBERS__": text_specials} 

559 

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

561 rep[k] = v 

562 

563 res = template 

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

565 res = res.replace(a, b) 

566 

567 has_class = any( 

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

569 if not has_class: 

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

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

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

573 

574 if softfile(fullname): 

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

576 

577 return res 

578 

579 

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

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

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

583 mapped_function=None, indexes=None, 

584 additional_sys_path=None, fLOG=noLOG): 

585 """ 

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

587 

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

589 @param store_obj to keep table of all objects 

590 @param actions output from @see fn copy_source_files 

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

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

593 which are not desired in the documentation 

594 @param fmod applies modification to the instanciated template 

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

596 the documentation is lighter (no special members) 

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

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

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

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

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

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

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

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

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

606 the function populates it 

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

608 (will be removed afterwards) 

609 @param fLOG logging function 

610 @return list of written files stored in RstFileHelp 

611 """ 

612 if indexes is None: 

613 indexes = {} 

614 if mapped_function is None: 

615 mapped_function = [] 

616 

617 if additional_sys_path is None: 

618 additional_sys_path = [] 

619 

620 memo = {} 

621 app = [] 

622 for action in actions: 

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

624 if not isinstance(file, str): 

625 file = file.name 

626 

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

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

629 rst += ".rst" 

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

631 

632 if sys.platform == "win32": 

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

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

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

636 else: 

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

638 

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

640 cpxx in file and ( 

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

642 )): 

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

644 content = apply_modification_template( 

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

646 additional_sys_path=additional_sys_path, fLOG=fLOG) 

647 content = fmod(content, file) 

648 

649 # tweaks for example and studies 

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

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

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

653 

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

655 # proper way 

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

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

658 noex, name) 

659 

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

661 g.write(content) 

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

663 

664 for vv in indexes.values(): 

665 if vv.fullname == to: 

666 vv.set_rst_file(rst) 

667 break 

668 

669 else: 

670 for pat, func in mapped_function: 

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

672 continue 

673 if pat not in memo: 

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

675 exp = memo[pat] 

676 if exp.search(file): 

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

678 # we copy but we do nothing with it 

679 pass 

680 else: 

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

682 content = g.read() 

683 if func is None: 

684 func = filecontent_to_rst 

685 content = func(to, content) 

686 

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

688 content, doc = content 

689 else: 

690 doc = "" 

691 

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

693 g.write(content) 

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

695 

696 filenoext, ext = os.path.splitext( 

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

698 ext = ext.strip(".") 

699 label = IndexInformation.get_label( 

700 indexes, "ext-" + filenoext) 

701 indexes[label] = IndexInformation( 

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

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

704 

705 return app 

706 

707 

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

709 correspondances=None, fLOG=fLOG): 

710 """ 

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

712 

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

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

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

716 @param fexclude_index to exclude files from the indices 

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

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

719 @param fLOG logging function 

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

721 

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

723 

724 :: 

725 

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

727 "staticmethod": "Static Methods", 

728 "property": "Properties", 

729 "function": "Functions", 

730 "class": "Classes", 

731 "module": "Modules"} 

732 

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

734 "function": "l-functions", 

735 "staticmethod": "l-staticmethods", 

736 "property": "l-properties", 

737 "class": "l-classes", 

738 "module": "l-modules"} 

739 """ 

740 from pandas import DataFrame 

741 

742 if titles is None: 

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

744 "staticmethod": "Static Methods", 

745 "property": "Properties", 

746 "function": "Functions", 

747 "class": "Classes", 

748 "module": "Modules"} 

749 

750 if correspondances is None: 

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

752 "function": "l-functions", 

753 "staticmethod": "l-staticmethods", 

754 "property": "l-properties", 

755 "class": "l-classes", 

756 "module": "l-modules"} 

757 

758 # we process store_obj 

759 types = {} 

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

761 if not isinstance(v, list): 

762 v = [v] 

763 for _ in v: 

764 if fexclude_index(_): 

765 continue 

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

767 

768 fLOG(f"[produces_indexes] store_obj: extraction of types: {types}") 

769 res = {} 

770 for k in types: 

771 fLOG(f"[produces_indexes] type: [{k}] - rst") 

772 values = [] 

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

774 if not isinstance(so, list): 

775 so = [so] 

776 

777 for o in so: 

778 if fexclude_index(o): 

779 continue 

780 if o.type != k: 

781 continue 

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

783 rlink = o.rst_link(class_in_bracket=False) 

784 fLOG(f"[produces_indexes] + '{o.name}': {rlink}") 

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

786 

787 values.sort() 

788 for row in values: 

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

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

791 

792 # we filter private method or functions 

793 values = [ 

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

795 values = [ 

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

797 

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

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

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

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

802 

803 if len(tbl) > 0: 

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

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

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

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

808 sph = df2rst(tbl) 

809 res[k] = sph 

810 fLOG(f"[produces_indexes] type: [{k}] - shape: {tbl.shape}") 

811 

812 # we process indexes 

813 

814 fLOG("[produces_indexes] indexes") 

815 types = {} 

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

817 if fexclude_index(v): 

818 continue 

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

820 

821 fLOG(f"[produces_indexes] extraction of types: {types}") 

822 

823 for k in types: 

824 if k in res: 

825 raise HelpGenException( # pragma: no cover 

826 f"you should not index anything related to classes, functions or method (conflict: {k})") 

827 values = [] 

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

829 if fexclude_index(o): 

830 continue 

831 if o.type != k: 

832 continue 

833 values.append([o.name, 

834 o.rst_link(), 

835 o.truncdoc]) 

836 values.sort() 

837 

838 tbl = DataFrame( 

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

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

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

842 

843 if len(tbl) > 0: 

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

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

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

847 sph = df2rst(tbl) 

848 res[k] = sph 

849 

850 # end 

851 

852 keys = list(res.keys()) 

853 for k in keys: 

854 fLOG(f"[produces_indexes] index name '{k}'") 

855 label = correspondances.get(k, k) 

856 title = titles.get(k, k) 

857 under = "=" * len(title) 

858 

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

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

861 

862 not_expected = os.environ.get( 

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

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

865 raise HelpGenException( # pragma: no cover 

866 f"The title is probably wrong (2), found '{not_expected}' in '{title}'") 

867 

868 res[k] = f"\n.. _{label}:\n\n{title}\n{under}\n\n{content}\n\n{res[k]}" 

869 

870 return res 

871 

872 

873def filecontent_to_rst(filename, content): 

874 """ 

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

876 It adds a title and a label based on the 

877 filename (no folder included). 

878 

879 @param filename filename 

880 @param content content 

881 @return new content 

882 """ 

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

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

885 

886 not_expected = os.environ.get( 

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

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

889 raise HelpGenException( # pragma: no cover 

890 f"The title is probably wrong (1): '{not_expected}' found in '{file}'") 

891 

892 rows = ["", f".. _f-{file}:", "", "", full, "", 

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

894 "", ""] 

895 if ".. RSTFORMAT." in content: 

896 rows.append(f".. include:: {file} ") 

897 else: 

898 rows.append(f".. literalinclude:: {file} ") 

899 rows.append("") 

900 

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

902 

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

904 cont = reg.search(nospl) 

905 if cont: 

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

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

908 

909 if "@brief" in content: 

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

911 begin = None 

912 end = None 

913 for i, r in enumerate(spl): 

914 if "@brief" in r: 

915 begin = i 

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

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

918 end = i 

919 

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

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

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

923 else: 

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

925 

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

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

928 begin = None 

929 end = None 

930 for i, r in enumerate(spl): 

931 if "/**" in r: 

932 begin = i 

933 if end is None and begin is not None and "*/" in r: 

934 end = i 

935 

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

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

938 filerows = private_migrating_doxygen_doc( 

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

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

941 rstr = re.sub( 

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

943 content = content.replace( 

944 ".. literalinclude::", f"\n{rstr}\n\n.. literalinclude::") 

945 

946 return content, summary 

947 

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

949 

950 

951def prepare_file_for_sphinx_help_generation(store_obj, input, output, 

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

953 template=add_file_rst_template, 

954 rootrep=( 

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

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

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

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

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

960 additional_sys_path=None, replace_relative_import=False, 

961 module_name=None, copy_add_ext=None, use_sys=None, 

962 auto_rst_generation=True, fLOG=fLOG): 

963 """ 

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

965 

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

967 @param input input folder 

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

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

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

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

972 @param fmod_copy modifies the content of each file, 

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

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

975 @param template rst template to produce 

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

977 @param fmod_res applies modification to the instanciated template 

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

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

980 copy for the documentation, example: 

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

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

983 the documentation is lighter (no special members) 

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

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

986 

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

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

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

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

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

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

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

994 

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

996 

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

998 @param replace_relative_import replace relative import 

999 @param module_name module name (cannot be None) 

1000 @param copy_add_ext additional file extension to copy 

1001 @param use_sys @see fn remove_undesired_part_for_documentation 

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

1003 @param fLOG logging function 

1004 

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

1006 

1007 Example: 

1008 

1009 :: 

1010 

1011 prepare_file_for_sphinx_help_generation ( 

1012 {}, 

1013 ".", 

1014 "_doc/sphinxdoc/source/", 

1015 subfolders = [ 

1016 ("src/" + project_var_name, 

1017 project_var_name), 

1018 ], 

1019 silent = True, 

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

1021 (project_var_name,), ""), 

1022 optional_dirs = optional_dirs, 

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

1024 

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

1026 """ 

1027 if optional_dirs is None: 

1028 optional_dirs = [] 

1029 

1030 if mapped_function is None: 

1031 mapped_function = [] 

1032 

1033 if additional_sys_path is None: 

1034 additional_sys_path = [] 

1035 

1036 if module_name is None: 

1037 raise ValueError( # pragma: no cover 

1038 "module_name cannot be None") 

1039 

1040 fLOG(f"[prepare_file_for_sphinx_help_generation] output='{output}'") 

1041 rootm = os.path.abspath(output) 

1042 fLOG(f"[prepare_file_for_sphinx_help_generation] input='{input}'") 

1043 

1044 actions = [] 

1045 rsts = [] 

1046 indexes = {} 

1047 

1048 for sub in subfolders: 

1049 if isinstance(sub, str): 

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

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

1052 else: 

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

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

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

1056 raise RuntimeError( # pragma: no cover 

1057 f"Subfolder {src!r} cannot start with '_'.") 

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

1059 raise RuntimeError( # pragma: no cover 

1060 f"Destination {dst!r} cannot start with '_'.") 

1061 

1062 if os.path.isfile(src): 

1063 fLOG(" [p] ", src) 

1064 _private_process_one_file( 

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

1066 

1067 temp = os.path.split(dst) 

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

1069 if auto_rst_generation: 

1070 rstadd = add_file_rst(rootm, store_obj, actions_t, 

1071 template, rootrep, fmod_res, 

1072 softfile=softfile, 

1073 mapped_function=mapped_function, 

1074 indexes=indexes, 

1075 additional_sys_path=additional_sys_path, 

1076 fLOG=fLOG) 

1077 rsts += rstadd 

1078 else: 

1079 fLOG( 

1080 f"[prepare_file_for_sphinx_help_generation] processing '{src}'") 

1081 

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

1083 softfile=softfile, fexclude=fexclude, 

1084 addfilter="|".join( 

1085 [f'({_[0]})' for _ in mapped_function]), 

1086 replace_relative_import=replace_relative_import, 

1087 copy_add_ext=copy_add_ext, 

1088 use_sys=use_sys, fLOG=fLOG) 

1089 

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

1091 importlib.invalidate_caches() 

1092 importlib.util.find_spec(module_name) 

1093 

1094 if auto_rst_generation: 

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

1096 rootrep, fmod_res, softfile=softfile, 

1097 mapped_function=mapped_function, 

1098 indexes=indexes, 

1099 additional_sys_path=additional_sys_path, 

1100 fLOG=fLOG) 

1101 

1102 actions += actions_t 

1103 

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

1105 for tu in optional_dirs: 

1106 if len(tu) == 2: 

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

1108 else: 

1109 fold, dest, filt = tu 

1110 if filt is None: 

1111 filt = ".*" 

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

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

1114 os.makedirs(dest) 

1115 

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

1117 softfile=softfile, fexclude=fexclude, 

1118 addfilter="|".join([f'({_[0]})' 

1119 for _ in mapped_function]), 

1120 replace_relative_import=replace_relative_import, 

1121 copy_add_ext=copy_add_ext, fLOG=fLOG) 

1122 

1123 # processing all store_obj to compute some indices 

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

1125 fLOG("[prepare_file_for_sphinx_help_generation] extracted ", 

1126 len(store_obj), " objects") 

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

1128 

1129 fLOG("[prepare_file_for_sphinx_help_generation] generating ", 

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

1131 allfiles = [] 

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

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

1134 allfiles.append("index_" + k) 

1135 fLOG(" generates index", out) 

1136 if k == "module": 

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

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

1139 for _ in rsts: 

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

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

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

1143 if "source" in na: 

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

1145 na = "/".join(na) 

1146 toc.append(" " + na) 

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

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

1149 fh.write(vv) 

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

1151 

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

1153 rows = [] 

1154 for act in actions: 

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

1156 v = 1 

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

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

1159 if name.startswith("auto_"): 

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

1161 elif "__init__.py" in name: 

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

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

1164 v = 1 

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

1166 

1167 # use DataFrame to produce a RST table 

1168 from pandas import DataFrame 

1169 df = DataFrame( 

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

1171 try: 

1172 # for pandas >= 0.17 

1173 df = df.groupby( 

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

1175 except AttributeError: # pragma: no cover 

1176 # for pandas < 0.17 

1177 df = df.groupby( 

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

1179 

1180 # reports 

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

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

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

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

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

1186 falli.write("\n") 

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

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

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

1190 sph = df2rst(df, list_table=True) 

1191 falli.write(sph) 

1192 falli.write("\n") 

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

1194 

1195 # all indexes 

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

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

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

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

1200 falli.write("\n") 

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

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

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

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

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

1206 falli.write("\n") 

1207 for k in sorted(allfiles): 

1208 falli.write(f" {k}\n") 

1209 falli.write("\n") 

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

1211 

1212 # last function to process images 

1213 fLOG("looking for images", output) 

1214 

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

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

1217 if os.path.exists(images): 

1218 process_copy_images(output, images) 

1219 

1220 # fixes indexed objects with incomplete names 

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

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

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

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

1225 

1226 # look for FAQ and example 

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

1228 app = [] 

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

1230 ("example", "Examples"), 

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

1232 onefiles = process_look_for_tag(tag, title, rsts) 

1233 for page, onefile in onefiles: 

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

1235 (tag, 

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

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

1238 fh.write(onefile) 

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

1240 rsts += app 

1241 

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

1243 return actions, rsts 

1244 

1245 

1246def process_copy_images(folder_source, folder_images): 

1247 """ 

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

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

1250 closes to the file. 

1251 

1252 @param folder_source folder where to look for sources 

1253 @param folder_images folder where to look for images 

1254 @return list of copied images 

1255 """ 

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

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

1258 cop = [] 

1259 for fn in files: 

1260 try: 

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

1262 content = f.read() 

1263 except Exception as e: # pragma: no cover 

1264 try: 

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

1266 content = f.read() 

1267 except Exception: 

1268 raise RuntimeError(f"Issue with file '{fn}'") from e 

1269 

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

1271 for line in lines: 

1272 img = reg.search(line) 

1273 if img: 

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

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

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

1277 if os.path.exists(path): 

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

1279 shutil.copy(path, dest) 

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

1281 cop.append(dest) 

1282 else: 

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

1284 return cop 

1285 

1286 

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

1288 """ 

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

1290 

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

1292 

1293 

1294 @param folder_source folder where to look for sources 

1295 @param store_obj container for indexed objects 

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

1297 @param fLOG logging function 

1298 @return list of fixed references 

1299 """ 

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

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

1302 } 

1303 

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

1305 reg = re.compile( 

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

1307 cop = [] 

1308 for fn in files: 

1309 try: 

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

1311 content = f.read() 

1312 encoding = "utf8" 

1313 except Exception: # pragma: no cover 

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

1315 content = f.read() 

1316 encoding = None 

1317 

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

1319 

1320 modif = False 

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

1322 rline = [] 

1323 for line in lines: 

1324 ref = reg.search(line) 

1325 if ref: 

1326 all = ref.groups()[0] 

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

1328 typ = ref.groups()[2] 

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

1330 

1331 key = None 

1332 obj = None 

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

1334 k = f"{cand};{nam}" 

1335 if k in store_obj: 

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

1337 se = [ 

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

1339 if len(se) == 1: 

1340 obj = se[0] 

1341 break 

1342 else: 

1343 key = k 

1344 obj = store_obj[k] 

1345 break 

1346 

1347 if key in store_obj: 

1348 modif = True 

1349 lnk = obj.rst_link(class_in_bracket=False) 

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

1351 line = line.replace(all, lnk) 

1352 else: 

1353 fLOG( 

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

1355 if issues is not None: 

1356 issues.append(("fix_incomplete_references", 

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

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

1359 

1360 rline.append(line) 

1361 

1362 if modif: 

1363 if encoding == "utf8": 

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

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

1366 else: 

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

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

1369 return cop 

1370 

1371 

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

1373 """ 

1374 Migrates the doxygen documentation to rst format. 

1375 

1376 @param content file content 

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

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

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

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

1381 @return statistics, new content file 

1382 

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

1384 which will be done. 

1385 """ 

1386 if log: 

1387 fLOG("migrating_doxygen_doc: ", filename) 

1388 

1389 rows = [] 

1390 counts = {"docrows": 0} 

1391 

1392 def print_in_rows(v, file=None): 

1393 rows.append(v) 

1394 

1395 def local_private_migrating_doxygen_doc(r, index_first_line, filename): 

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

1397 return _private_migrating_doxygen_doc(r, index_first_line, 

1398 filename, debug=debug, silent=silent) 

1399 

1400 process_string(content, print_in_rows, local_private_migrating_doxygen_doc, 

1401 filename, 0, debug=debug) 

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

1403 

1404# -- HELP BEGIN EXCLUDE -- 

1405 

1406 

1407def private_migrating_doxygen_doc(rows, index_first_line, filename, 

1408 debug=False, silent=False): 

1409 """ 

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

1411 

1412 @param rows list of text lines 

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

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

1415 @param silent if True, do not display anything 

1416 @param debug display more information if True 

1417 @return another list of text lines 

1418 

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

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

1421 

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

1423 the parsing. 

1424 

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

1426 

1427 :: 

1428 

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

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

1431 

1432 __sphinx__skip__ 

1433 

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

1435 

1436 Doxygen conversions:: 

1437 

1438 @param <param_name> description 

1439 :param <param_name>: description 

1440 

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

1442 

1443 @return description 

1444 :return: description 

1445 

1446 @rtype description 

1447 :rtype: description 

1448 

1449 @code 

1450 code:: + indentation 

1451 

1452 @endcode 

1453 nothing 

1454 

1455 @file 

1456 nothing 

1457 

1458 @brief 

1459 nothing 

1460 

1461 @ingroup ... 

1462 nothing 

1463 

1464 @defgroup .... 

1465 nothing 

1466 

1467 @image html ... 

1468 

1469 @see,@ref label forbidden 

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

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

1472 

1473 :class:`label` 

1474 :func:`label` 

1475 :attr:`label` 

1476 :meth:`label` 

1477 :mod:`label` 

1478 

1479 @warning description (until next empty line) 

1480 .. warning:: 

1481 description 

1482 

1483 @todo 

1484 .. todo:: a todo box 

1485 

1486 ------------- not done yet 

1487 

1488 @img image name 

1489 .. image:: test.png 

1490 :width: 200pt 

1491 

1492 .. raw:: html 

1493 html indente 

1494 

1495 """ 

1496 return _private_migrating_doxygen_doc(rows, index_first_line, filename, 

1497 debug=debug, silent=silent) 

1498 

1499# -- HELP END EXCLUDE -- 

1500 

1501 

1502def _private_migrating_doxygen_doc(rows, index_first_line, filename, 

1503 debug=False, silent=False): 

1504 if debug: # pragma: no cover 

1505 fLOG("------------------ P0") 

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

1507 fLOG("------------------ P") 

1508 

1509 debugrows = rows 

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

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

1512 refe = re.compile( 

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

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

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

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

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

1518 

1519 # min indent 

1520 if len(rows) > 1: 

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

1522 else: 

1523 space_rows = [] 

1524 if len(space_rows) > 0: 

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

1526 else: 

1527 min_indent = 0 

1528 

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

1530 if len(rows) > 1: 

1531 r = rows[0] 

1532 r = (r.lstrip(), r) 

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

1534 if delta != min_indent: 

1535 rows = rows.copy() 

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

1537 

1538 # processing doxygen documentation 

1539 indent = False 

1540 openi = False 

1541 beginends = {} 

1542 

1543 typstr = str 

1544 

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

1546 if "@var" in whole: 

1547 whole = process_var_tag(whole, True) 

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

1549 

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

1551 row = rows[i] 

1552 

1553 if debug: 

1554 fLOG( # pragma: no cover 

1555 f"-- indent={indent} openi={openi} row={row}") 

1556 

1557 if "__sphinx__skip__" in row: 

1558 if not silent: 

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

1560 (filename, index_first_line + i + 1)) 

1561 break 

1562 

1563 strow = row.strip(" ") 

1564 

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

1566 if "@endFAQ" in strow: 

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

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

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

1570 if "@endexample" in strow: 

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

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

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

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

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

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

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

1578 continue 

1579 

1580 if indent: 

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

1582 indent = False 

1583 rows[i] = "" 

1584 openi = False 

1585 if "@endcode" in strow: 

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

1587 else: 

1588 rows[i] = " " + rows[i] 

1589 

1590 else: 

1591 

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

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

1594 sp = " " * pos 

1595 rows[i] = rows[i].replace("@warning", f"\n{sp}.. warning:: ") 

1596 indent = True 

1597 

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

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

1600 sp = " " * pos 

1601 rows[i] = rows[i].replace("@todo", f"\n{sp}.. todo:: ") 

1602 indent = True 

1603 

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

1605 rows[i] = "" 

1606 

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

1608 rows[i] = "" 

1609 

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

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

1612 sp = " " * pos 

1613 spl = strow.split() 

1614 img = spl[-1] 

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

1616 rows[i] = f"\n{sp}.. fancybox:: " + img + "\n\n" 

1617 else: 

1618 

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

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

1621 img = img.strip("~") 

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

1623 pos = spl_path.index("src") 

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

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

1626 else: 

1627 ref = "" 

1628 

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

1630 rows[i] = f"\n{sp}.. image:: {ref}{img}\n{sp} :align: center\n" 

1631 

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

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

1634 sp = " " * pos 

1635 prev = i - 1 

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

1637 prev -= 1 

1638 rows[i] = "" 

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

1640 rows[prev] += f"\n\n{sp}::\n" 

1641 else: 

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

1643 indent = True 

1644 openi = True 

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

1646 

1647 # basic tags 

1648 row = rows[i] 

1649 

1650 # tag param 

1651 look = pars.search(row) 

1652 lexxce = exce.search(row) 

1653 example = exem.search(row) 

1654 faq = faq_.search(row) 

1655 nbreg = nb_.search(row) 

1656 

1657 if look: 

1658 rep = look.groups()[0] 

1659 sp = look.groups()[1] 

1660 name = look.groups()[2] 

1661 to = f":param{sp}{name}:" 

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

1663 

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

1665 # not start by : 

1666 if i > 0 and not rows[ 

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

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

1669 

1670 elif lexxce: 

1671 rep = lexxce.groups()[0] 

1672 sp = lexxce.groups()[1] 

1673 name = lexxce.groups()[2] 

1674 to = f":raises{sp}{name}:" 

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

1676 

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

1678 # not start by : 

1679 if i > 0 and not rows[ 

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

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

1682 

1683 elif example: # pragma: no cover 

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

1685 rep = example.groups()[0] 

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

1687 pag = example.groups()[1] 

1688 if pag is None: 

1689 pag = "" 

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

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

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

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

1694 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" % ( 

1695 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

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

1697 

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

1699 # not start by : 

1700 if i > 0 and not rows[ 

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

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

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

1704 

1705 elif faq: 

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

1707 rep = faq.groups()[0] 

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

1709 pag = faq.groups()[1] 

1710 if pag is None: 

1711 pag = "" 

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

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

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

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

1716 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" % ( 

1717 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

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

1719 

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

1721 # not start by : 

1722 if i > 0 and not rows[ 

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

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

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

1726 

1727 elif nbreg: # pragma: no cover 

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

1729 rep = nbreg.groups()[0] 

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

1731 pag = nbreg.groups()[1] 

1732 if pag is None: 

1733 pag = "" 

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

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

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

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

1738 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" % ( 

1739 sp, ref, sp, ref2, sp, exa, sp, pag, exa, ref) 

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

1741 

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

1743 # not start by : 

1744 if i > 0 and not rows[ 

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

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

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

1748 

1749 elif "@return" in row: 

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

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

1752 # not start by : 

1753 if i > 0 and not rows[ 

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

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

1756 

1757 elif "@rtype" in row: 

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

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

1760 # not start by : 

1761 if i > 0 and not rows[ 

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

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

1764 

1765 elif "@brief" in row: 

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

1767 elif "@file" in row: 

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

1769 

1770 # loop on references 

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

1772 while refl: 

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

1774 see = "" # " " if see else "" 

1775 ty = refl.groups()[4] 

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

1777 if len(name) == 0: 

1778 raise SyntaxError( # pragma: no cover 

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

1780 rep = refl.groups()[0] 

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

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

1783 to = f"{see}:{ty}:`{name}`" 

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

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

1786 

1787 if not debug: 

1788 for i, row in enumerate(rows): 

1789 if "__sphinx__skip__" in row: 

1790 break 

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

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

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

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

1795 if not silent: # pragma: no cover 

1796 fLOG("#########################") 

1797 _private_migrating_doxygen_doc( 

1798 debugrows, index_first_line, filename, debug=True) 

1799 fLOG("#########################") 

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

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

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

1803 else: # pragma: no cover 

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

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

1806 raise SyntaxError(mes) # pragma: no cover 

1807 

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

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

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

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

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

1813 raise SyntaxError(mes) 

1814 

1815 # add githublink 

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

1817 if len(link) == 0: 

1818 rows.append("") 

1819 rows.append(f"{' ' * min_indent}:githublink:`%|py|{index_first_line}`") 

1820 

1821 # clean rows 

1822 clean_rows = [] 

1823 for row in rows: 

1824 if row.strip(): 

1825 clean_rows.append(row) 

1826 elif len(clean_rows) > 0: 

1827 clean_rows.append('') 

1828 return clean_rows 

1829 

1830 

1831def doc_checking(): 

1832 """ 

1833 Example of a doc string. 

1834 """ 

1835 pass 

1836 

1837 

1838class useless_class_UnicodeStringIOThreadSafe (str): 

1839 

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

1841 class protected again Thread issue""" 

1842 

1843 def __init__(self): 

1844 """ 

1845 creates a lock 

1846 """ 

1847 str.__init__(self) 

1848 import threading 

1849 self.lock = threading.Lock()