Coverage for pyquickhelper/pycode/setup_helper.py: 64%

318 statements  

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

1""" 

2@file 

3@brief Helper for the setup 

4""" 

5 

6import os 

7import sys 

8import re 

9import warnings 

10import hashlib 

11import datetime 

12 

13 

14def get_available_setup_commands(): 

15 """ 

16 Returns the list of commands :epkg:`pyquickhelper` implements 

17 or allows. 

18 """ 

19 commands = ['bdist_egg', 'bdist_msi', 'bdist_wheel', 'bdist_wininst', 'build27', 

20 'build_ext', 'build_script', 'build_sphinx', 'clean_pyd', 'clean_space', 

21 'copy27', 'copy_dist', 'copy_sphinx', 'history', 'lab', 'local_pypi', 

22 'notebook', 'publish', 'publish_doc', 'register', 'run27', 'run_pylint', 

23 'sdist', 'setupdep', 'test_local_pypi', 

24 'unittests', 'unittests_GUI', 'unittests_LONG', 'unittests_SKIP', 

25 'upload_docs', 'write_version', 'local_jenkins'] 

26 return commands 

27 

28 

29def get_available_build_commands(): 

30 """ 

31 Returns commands which build the package. 

32 """ 

33 return {"sdist", "bdist_wheel", "publish", "publish_doc", "register", 

34 "upload_docs", "bdist_wininst", "build_ext"} 

35 

36 

37def available_commands_list(argv): 

38 """ 

39 Checks that on command handled by pyquickhelper is part of the arguments. 

40 

41 @param argv ``sys.args`` 

42 @return bool 

43 """ 

44 commands = get_available_setup_commands() 

45 for c in commands: 

46 if c in argv: 

47 return True 

48 return False 

49 

50 

51def process_argv_for_unittest(argv, skip_function): 

52 """ 

53 Interprets command line arguments. 

54 

55 :param argv: list 

56 :param skip_function: skipping function 

57 :return: commands 

58 """ 

59 if "-d" in argv: 

60 ld = argv.index("-d") 

61 if ld >= len(argv) - 1: 

62 raise ValueError( # pragma: no cover 

63 "Option -d should be follow by a duration in seconds.") 

64 d = float(argv[ld + 1]) 

65 else: 

66 d = None 

67 

68 if "-f" in argv: 

69 lf = argv.index("-f") 

70 if lf >= len(argv) - 1: 

71 raise ValueError( # pragma: no cover 

72 "Option -f should be follow by a file.") 

73 f = argv[lf + 1] 

74 else: 

75 f = None 

76 

77 if "-e" in argv: 

78 le = argv.index("-e") 

79 if le >= len(argv) - 1: 

80 raise ValueError( # pragma: no cover 

81 "Option -e should be follow by a regular expression.") 

82 pattern = argv[le + 1] 

83 if len(pattern) >= 2 and pattern[0] == pattern[-1] == '"': 

84 pattern = pattern[1:-1] 

85 e = re.compile(pattern) 

86 else: 

87 e = None 

88 

89 if "-g" in argv: 

90 lg = argv.index("-g") 

91 if lg >= len(argv) - 1: 

92 raise ValueError( # pragma: no cover 

93 "Option -g should be follow by a regular expression.") 

94 pattern = argv[lg + 1] 

95 if len(pattern) >= 2 and pattern[0] == pattern[-1] == '"': 

96 pattern = pattern[1:-1] 

97 g = re.compile(pattern) 

98 else: 

99 g = None 

100 

101 if f is None and d is None and e is None and g is None: 

102 return skip_function # pragma: no cover 

103 

104 def ereg(name): 

105 return (e is None) or (e.search(name) is not None) 

106 

107 def greg(name): 

108 return (g is None) or (g.search(name) is None) 

109 

110 if f is not None: 

111 if d is not None: # pragma: no cover 

112 raise NotImplementedError( 

113 "Options -f and -d cannot be specified at the same time.") 

114 

115 def allow(name, code, duration): # pragma: no cover 

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

117 return f not in name and ereg(name) and greg(name) 

118 return allow # pragma: no cover 

119 else: 

120 # d is not None 

121 def skip_allowd(name, code, duration): 

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

123 cond = ( 

124 (duration is None or d is None or duration <= d) and 

125 ereg(name) and greg(name)) 

126 return not cond 

127 return skip_allowd 

128 

129 

130def process_standard_options_for_setup( 

131 argv, file_or_folder, project_var_name, module_name=None, unittest_modules=None, 

132 pattern_copy=None, 

133 requirements=None, port=8067, blog_list=None, default_engine_paths=None, 

134 extra_ext=None, add_htmlhelp=False, coverage_options=None, 

135 coverage_exclude_lines=None, func_sphinx_begin=None, func_sphinx_end=None, 

136 additional_notebook_path=None, additional_local_path=None, copy_add_ext=None, 

137 nbformats=("ipynb", "html", "python", "rst", "slides", 

138 "pdf", "github"), 

139 layout=None, direct_call=False, 

140 additional_ut_path=None, 

141 skip_function=None, covtoken=None, 

142 stdout=None, stderr=None, use_run_cmd=False, filter_warning=None, 

143 file_filter_pep8=None, github_owner=None, 

144 existing_history=None, coverage_root='src', 

145 fexclude=None, skip_issues=None, fLOG=None): 

146 """ 

147 Processes the standard options the module pyquickhelper is 

148 able to process assuming the module which calls this function 

149 follows the same design as *pyquickhelper*, it will process the following 

150 options: 

151 

152 .. runpython:: 

153 

154 from pyquickhelper.pycode import process_standard_options_for_setup_help 

155 process_standard_options_for_setup_help("--help-commands") 

156 

157 @param argv = *sys.argv* 

158 @param file_or_folder file ``setup.py`` or folder which contains it 

159 @param project_var_name display name of the module 

160 @param module_name module name, None if equal to *project_var_name* 

161 (``import <module_name>``) 

162 @param unittest_modules modules added for the unit tests, 

163 see @see fn py3to2_convert_tree 

164 @param pattern_copy see @see fn py3to2_convert_tree 

165 @param requirements dependencies, fetched with a local pipy server from 

166 ``http://localhost:port/`` 

167 @param port port for the local pipy server 

168 @param blog_list list of blog to listen for this module 

169 (usually stored in ``module.__blog__``) 

170 @param default_engine_paths define the default location for python engine, 

171 should be dictionary *{ engine: path }*, see below. 

172 @param extra_ext extra file extension to process (add a page for each of them, 

173 ex ``["doc"]``) 

174 @param add_htmlhelp run HTML Help too (only on Windows) 

175 @param coverage_options see @see fn main_wrapper_tests 

176 @param coverage_exclude_lines see @see fn main_wrapper_tests 

177 @param func_sphinx_begin function called before the documentation generation, 

178 it gets the same parameters as this function (all named), 

179 use ``**args**`` 

180 @param func_sphinx_end function called after the documentation generation, 

181 it gets the same parameters as this function (all named), 

182 use ``**args**`` 

183 @param additional_notebook_path additional paths to add when launching the notebook 

184 @param additional_local_path additional paths to add when running a local command 

185 @param copy_add_ext additional file extensions to copy 

186 @param nbformats requested formats for the notebooks conversion 

187 @param layout list of formats sphinx should generate such as html, latex, pdf, docx, 

188 it is a list of tuple (layout, build directory, parameters to override), 

189 if None --> ``["html", "pdf"]`` 

190 @param additional_ut_path additional paths to add when running unit tests 

191 @param skip_function function to skip unit tests, see @ee fn main_wrapper_tests 

192 @param covtoken token used when publishing coverage report to 

193 `codecov <https://codecov.io/>`_, 

194 more in @see fn main_wrapper_tests 

195 @param fLOG logging function 

196 @param stdout redirect stdout for unit test if not None 

197 @param stderr redirect stderr for unit test if not None 

198 @param use_run_cmd to run the sphinx documentation with @see fn run_cmd and 

199 not ``os.system`` 

200 @param filter_warning see @see fn main_wrapper_tests 

201 @param file_filter_pep8 function to filter out files for which checking pep8 

202 (see @see fn remove_extra_spaces_folder) 

203 @param github_owner :epkg:`github` owner of the package 

204 @param existing_history existing history, retrieves existing issues stored 

205 in that file 

206 @param coverage_root see @see fn main_wrapper_tests 

207 @param direct_call @see fn generate_help_sphinx 

208 @param fexclude function which tells which file not to copy in the folder 

209 used to build the documentation 

210 @param skip_issues skip a given list of issues when building the history 

211 @return True (an option was processed) or False, 

212 the file ``setup.py`` should call function ``setup`` 

213 

214 The command ``build_script`` is used, the flag ``--private`` can be used to 

215 avoid producing scripts to publish the module on `Pypi <https://pypi.python.org/pypi>`_. 

216 

217 An example for *default_engine_paths*:: 

218 

219 default_engine_paths = { 

220 "windows": { 

221 "__PY35__": None, 

222 "__PY36_X64__": "c:\\Python365_x64", 

223 "__PY37_X64__": "c:\\Python372_x64", 

224 "__PY38_X64__": "c:\\Python387_x64", 

225 "__PY39_X64__": "c:\\Python391_x64", 

226 "__PY310_X64__": "c:\\Python3101_x64", 

227 }, 

228 } 

229 

230 Parameters *coverage_options*, *coverage_exclude_lines*, *copy_add_ext* were added 

231 for function @see fn main_wrapper_tests. 

232 Parameter *unittest_modules* accepts a list of string and 2-uple. 

233 If it is a 2-uple, the first string is used to convert Python 3 code into Python 2 

234 (in case the local folder is different from the module name), 

235 the second string is used to add local path to the variable ``PYTHON_PATH``. 

236 If it is a single string, it means both name strings are equal. 

237 Parameters *func_sphinx_begin* and *func_sphinx_end* were added 

238 to pre-process or post-process the documentation. 

239 Parameter *additional_notebook_path* was added to specify some additional 

240 paths when preparing the script *auto_cmd_notebook.bat*. 

241 Parameters *layout*, *nbformats* were added for 

242 function @see fn generate_help_sphinx. 

243 The coverage computation can be disable by specifying 

244 ``coverage_options["disable_coverage"] = True``. 

245 Parameter *covtoken* was added to post the coverage report to :epkg:`codecov`. 

246 Option ``-e`` and ``-g`` were added to 

247 filter file by regular expressions (in with *e*, out with *g*). 

248 """ 

249 if module_name is not None and ( 

250 len(module_name) == 0 or module_name[0] == '_'): 

251 raise RuntimeError( # pragma: no cover 

252 f"module cannot be empty or start with '_': {module_name!r}.") 

253 if fLOG is None: # pragma: no cover 

254 from ..loghelper.flog import noLOG 

255 fLOG = noLOG 

256 if skip_function is None: # pragma: no cover 

257 from .utils_tests_private import default_skip_function 

258 skip_function = default_skip_function 

259 if pattern_copy is None: 

260 # delayed import 

261 from .default_regular_expression import _setup_pattern_copy 

262 pattern_copy = _setup_pattern_copy 

263 

264 if layout is None: 

265 layout = ["html", "pdf"] 

266 

267 if "--help" in argv or "--help-commands" in argv: 

268 process_standard_options_for_setup_help(argv) 

269 return not len(get_available_build_commands() & set(argv)) 

270 fLOG("[process_standard_options_for_setup]", argv) 

271 fLOG("[process_standard_options_for_setup] python version:", sys.version_info) 

272 

273 folder = file_or_folder if os.path.isdir( 

274 file_or_folder) else os.path.dirname(file_or_folder) 

275 unit_test_folder = os.path.join(folder, "_unittests") 

276 fLOG(f"unittest_modules={unittest_modules}") 

277 

278 if unittest_modules is None: 

279 unittest_modules_py3to2 = None 

280 unittest_modules_script = None 

281 else: # pragma: no cover 

282 unittest_modules_py3to2 = [] 

283 unittest_modules_script = [] 

284 for mod in unittest_modules: 

285 if isinstance(mod, tuple): 

286 unittest_modules_py3to2.append(mod[0]) 

287 unittest_modules_script.append(mod[1]) 

288 else: 

289 unittest_modules_py3to2.append(mod) 

290 unittest_modules_script.append(mod) 

291 

292 # dump unit test coverage? 

293 

294 def dump_coverage_fct(full=True): 

295 mn = project_var_name if module_name is None else module_name 

296 full_path = _get_dump_default_path(folder, mn, argv) 

297 if full_path is None or full: 

298 return full_path 

299 else: 

300 sub = os.path.split(full_path)[0] 

301 sub = os.path.split(sub)[0] 

302 return sub 

303 

304 # starts interpreting the commands 

305 

306 if "clean_space" in argv: # pragma: no cover 

307 rem = clean_space_for_setup( 

308 file_or_folder, file_filter=file_filter_pep8) 

309 print("[clean_space] number of impacted files (pep8 + rst):", len(rem)) 

310 rem = clean_notebooks_for_numbers(file_or_folder) 

311 print("[clean_space] number of impacted notebooks:", len(rem)) 

312 return True 

313 

314 if "run_pylint" in argv: # pragma: no cover 

315 verbose = '-v' in argv 

316 pos = argv.index('run_pylint') 

317 ignores = [_[2:] for _ in argv if _[:2] == '-i'] 

318 ignores = None if len(ignores) == 0 else tuple(ignores) 

319 argv = [_ for _ in argv if _ not in ( 

320 '-v', '-') and not _.startswith('-i')] 

321 pattern = argv[pos + 1] if len(argv) > pos + 1 else ".*[.]py$" 

322 neg_pattern = argv[pos + 2] if len(argv) > pos + 2 else None 

323 print("[run_pylint] run_pylint for sources pattern='{0}' neg_pattern='{1}'".format( 

324 pattern, neg_pattern)) 

325 src_folder = os.path.join(folder, "src") 

326 if not os.path.exists(src_folder): 

327 src_folder = folder 

328 run_pylint_for_setup(src_folder, fLOG=print, pattern=pattern, 

329 verbose=verbose, pylint_ignore=ignores) 

330 print("[run_pylint] run_pylint for unittest") 

331 run_pylint_for_setup(os.path.join(folder, "_unittests"), 

332 fLOG=print, pattern=pattern, verbose=verbose, 

333 pylint_ignore=ignores) 

334 return True 

335 

336 elif 'history' in argv: # pragma: no cover 

337 dest = ' '.join(argv).split( # pylint: disable=C0207 

338 'history')[-1].strip() 

339 if not dest: 

340 dest = os.path.join(folder, 'HISTORY.rst') 

341 if existing_history is None: 

342 hfold = get_folder(file_or_folder) 

343 histo = os.path.join(hfold, 'HISTORY.rst') 

344 if os.path.exists(histo): 

345 existing_history = histo 

346 if existing_history is not None: 

347 print('[history] existing ', existing_history) 

348 print('[history] ', dest) 

349 build_history_from_setup( 

350 dest, owner=github_owner, module=project_var_name, 

351 existing_history=existing_history, 

352 skip_issues=skip_issues, fLOG=fLOG) 

353 return True 

354 

355 elif "write_version" in argv: # pragma: no cover 

356 fLOG("---- JENKINS BEGIN WRITE VERSION ----") 

357 write_version_for_setup(file_or_folder, module_name=module_name) 

358 fLOG("---- JENKINS BEGIN END VERSION ----") 

359 return True 

360 

361 elif "clean_pyd" in argv: 

362 clean_space_for_setup(file_or_folder) 

363 return True 

364 

365 elif "build_sphinx" in argv: 

366 # delayed import 

367 try: 

368 from nbconvert.nbconvertapp import main as nbconvert_main 

369 if nbconvert_main is None: # pragma: no cover 

370 raise AttributeError("nbconvert_main is None") 

371 except AttributeError as e: # pragma: no cover 

372 raise ImportError( 

373 "Unable to import nbconvert, cannot generate the documentation") from e 

374 

375 if func_sphinx_begin is not None: 

376 func_sphinx_begin( 

377 argv=argv, file_or_folder=file_or_folder, 

378 project_var_name=project_var_name, 

379 module_name=module_name, unittest_modules=unittest_modules, 

380 pattern_copy=pattern_copy, 

381 requirements=requirements, port=port, blog_list=blog_list, 

382 default_engine_paths=default_engine_paths, 

383 extra_ext=extra_ext, add_htmlhelp=add_htmlhelp, 

384 coverage_options=coverage_options, 

385 coverage_exclude_lines=coverage_exclude_lines, 

386 func_sphinx_begin=func_sphinx_begin, 

387 func_sphinx_end=func_sphinx_end, 

388 additional_notebook_path=additional_notebook_path, 

389 nbformats=nbformats, layout=layout, 

390 skip_function=skip_function, 

391 addition_ut_path=additional_ut_path, fLOG=fLOG) 

392 standard_help_for_setup( 

393 argv, file_or_folder, project_var_name, 

394 module_name=module_name, extra_ext=extra_ext, 

395 add_htmlhelp=add_htmlhelp, copy_add_ext=copy_add_ext, 

396 nbformats=nbformats, layout=layout, 

397 use_run_cmd=use_run_cmd, fLOG=fLOG, direct_call=direct_call, 

398 fexclude=fexclude) 

399 

400 if func_sphinx_end is not None: 

401 func_sphinx_end( 

402 argv=argv, file_or_folder=file_or_folder, 

403 project_var_name=project_var_name, 

404 module_name=module_name, unittest_modules=unittest_modules, 

405 pattern_copy=pattern_copy, 

406 requirements=requirements, port=port, blog_list=blog_list, 

407 default_engine_paths=default_engine_paths, 

408 extra_ext=extra_ext, add_htmlhelp=add_htmlhelp, 

409 coverage_options=coverage_options, 

410 coverage_exclude_lines=coverage_exclude_lines, 

411 func_sphinx_begin=func_sphinx_begin, 

412 func_sphinx_end=func_sphinx_end, 

413 additional_notebook_path=additional_notebook_path, 

414 nbformats=nbformats, layout=layout, 

415 skip_function=skip_function, 

416 addition_ut_path=additional_ut_path, fLOG=fLOG) 

417 

418 return True 

419 

420 elif "unittests" in argv: 

421 skip_f = process_argv_for_unittest(argv, skip_function) 

422 return run_unittests_for_setup( 

423 file_or_folder, 

424 coverage_options=coverage_options, 

425 coverage_exclude_lines=coverage_exclude_lines, 

426 additional_ut_path=additional_ut_path, 

427 skip_function=skip_f, covtoken=covtoken, 

428 stdout=stdout, stderr=stderr, 

429 filter_warning=filter_warning, dump_coverage=dump_coverage_fct(), 

430 add_coverage_folder=dump_coverage_fct(False), 

431 coverage_root=coverage_root, fLOG=fLOG) 

432 

433 elif "unittests_LONG" in argv: 

434 def skip_long(name, code, duration): 

435 return "test_LONG_" not in name 

436 return run_unittests_for_setup( 

437 file_or_folder, skip_function=skip_long, 

438 coverage_options=coverage_options, coverage_exclude_lines=coverage_exclude_lines, 

439 additional_ut_path=additional_ut_path, 

440 stdout=stdout, stderr=stderr, dump_coverage=dump_coverage_fct(), 

441 fLOG=fLOG) 

442 

443 elif "unittests_SKIP" in argv: 

444 def skip_skip(name, code, duration): 

445 return "test_SKIP_" not in name 

446 return run_unittests_for_setup( 

447 file_or_folder, skip_function=skip_skip, 

448 coverage_options=coverage_options, coverage_exclude_lines=coverage_exclude_lines, 

449 additional_ut_path=additional_ut_path, 

450 stdout=stdout, stderr=stderr, dump_coverage=dump_coverage_fct(), 

451 fLOG=fLOG) 

452 

453 elif "unittests_GUI" in argv: # pragma: no cover 

454 def skip_skip(name, code, duration): 

455 return "test_GUI_" not in name 

456 return run_unittests_for_setup( 

457 file_or_folder, skip_function=skip_skip, 

458 coverage_options=coverage_options, coverage_exclude_lines=coverage_exclude_lines, 

459 additional_ut_path=additional_ut_path, 

460 stdout=stdout, stderr=stderr, dump_coverage=dump_coverage_fct(), 

461 fLOG=fLOG) 

462 

463 elif "build_script" in argv: # pragma: no cover 

464 # delayed import 

465 from .build_helper import get_extra_script_command, get_script_command, get_build_script 

466 

467 # script running setup.py 

468 script = get_build_script( 

469 project_var_name, requirements=requirements, port=port, 

470 default_engine_paths=default_engine_paths) 

471 binto = os.path.join(folder, "bin") 

472 if not os.path.exists(binto): 

473 os.mkdir(binto) 

474 with open(os.path.join(folder, "bin", f"auto_unittest_setup_help.{get_script_extension()}"), "w") as f: 

475 f.write(script) 

476 

477 for c in ("build_script", "clean_space", 

478 "write_version", "clean_pyd", 

479 "build_sphinx", "unittests", 

480 "unittests_LONG", "unittests_SKIP", "unittests_GUI", 

481 "unittests -d 5", "copy27", 

482 "local_pypi", 'run_pylint'): 

483 sc = get_script_command( 

484 c, project_var_name, requirements=requirements, port=port, platform=sys.platform, 

485 default_engine_paths=default_engine_paths, additional_local_path=additional_local_path) 

486 cn = c.replace(" ", "_") 

487 with open(os.path.join(folder, "bin", f"auto_setup_{cn}.{get_script_extension()}"), "w") as f: 

488 f.write(sc) 

489 

490 # script running for a developper 

491 

492 for c in {"notebook", "publish", "publish_doc", "local_pypi", "run27", 

493 "build27", "setupdep", "copy_dist", 

494 "any_setup_command", "build_dist", 

495 "copy_sphinx", "lab", "history"}: 

496 if "--private" in argv and "publish" in c: 

497 # we skip this to avoid producing scripts for publish 

498 # functionalities 

499 continue 

500 sc = get_extra_script_command(c, project_var_name, requirements=requirements, 

501 port=port, platform=sys.platform, 

502 default_engine_paths=default_engine_paths, 

503 unit_test_folder=unit_test_folder, 

504 unittest_modules=unittest_modules_script, 

505 additional_notebook_path=additional_notebook_path, 

506 additional_local_path=additional_local_path) 

507 if sc is None: 

508 continue 

509 if c == "setupdep": 

510 folder_setup = os.path.join(folder, "build", "auto_setup") 

511 if not os.path.exists(folder_setup): 

512 os.makedirs(folder_setup) 

513 with open(os.path.join(folder_setup, "auto_setup_dep.py"), "w") as f: 

514 f.write(sc) 

515 else: 

516 with open(os.path.join(folder, "bin", f"auto_cmd_{c}.{get_script_extension()}"), "w") as f: 

517 f.write(sc) 

518 

519 # script for anybody 

520 write_module_scripts( 

521 folder, platform=sys.platform, blog_list=blog_list, default_engine_paths=default_engine_paths) 

522 

523 # pyproj for PTVS 

524 if sys.platform.startswith("win"): # pragma: no cover 

525 write_pyproj(folder) 

526 

527 return True 

528 

529 elif "copy27" in argv: # pragma: no cover 

530 # delayed import 

531 from .py3to2 import py3to2_convert_tree 

532 root = os.path.abspath(os.path.dirname(file_or_folder)) 

533 root = os.path.normpath(root) 

534 dest = os.path.join(root, "dist_module27") 

535 py3to2_convert_tree( 

536 root, dest, unittest_modules=unittest_modules_py3to2, pattern_copy=pattern_copy) 

537 return True 

538 

539 elif "local_pypi" in argv: # pragma: no cover 

540 # delayed import 

541 from ..filehelper import get_url_content_timeout 

542 url = f"http://localhost:{port}/" 

543 content = get_url_content_timeout(url, timeout=5) 

544 if content is None or len(content) == 0: 

545 raise RuntimeError("test failed for url: " + url) 

546 print(content) 

547 return True 

548 

549 elif 'local_jenkins' in argv: # pragma: no cover 

550 pos = argv.index("local_jenkins") 

551 user = argv[pos + 1] 

552 password = argv[pos + 2] 

553 if len(argv) > pos + 3: 

554 location = argv[pos + 3] 

555 else: 

556 if sys.platform.startswith("win"): # pragma: no cover 

557 location = "\\Jenkins" 

558 else: 

559 location = "somewhere/workspace" 

560 if len(argv) > pos + 4: 

561 server_url = argv[pos + 4] 

562 else: 

563 server_url = "http://localhost:8080/" 

564 from ..jenkinshelper import JenkinsExt, setup_jenkins_server_yml, default_jenkins_jobs 

565 modules = default_jenkins_jobs( 

566 github_owner=github_owner, module_name=project_var_name if module_name is None else module_name) 

567 key = "Python%d%d" % sys.version_info[:2] 

568 engines = {key: os.path.abspath(os.path.dirname(sys.executable))} 

569 js = JenkinsExt(server_url, user, password, engines=engines) 

570 setup_jenkins_server_yml(js, github=github_owner, modules=modules, fLOG=fLOG, overwrite=True, 

571 delete_first=False, location=location) 

572 return True 

573 

574 else: 

575 return False 

576 

577 

578def get_script_extension(): 

579 """ 

580 Returns the scripts extension based on the system it is running on. 

581 

582 @return bat or sh 

583 """ 

584 if sys.platform.startswith("win"): # pragma: no cover 

585 return "bat" 

586 return "sh" 

587 

588 

589def get_folder(file_or_folder): 

590 """ 

591 Returns the folder which contains ``setup.py``. 

592 

593 @param file_or_folder file ``setup.py`` or folder which contains it 

594 @return folder 

595 """ 

596 file_or_folder = os.path.abspath(file_or_folder) 

597 if os.path.isdir(file_or_folder): 

598 folder = file_or_folder 

599 else: 

600 folder = os.path.dirname(file_or_folder) 

601 return folder 

602 

603 

604def write_version_for_setup(file_or_folder, exc=False, module_name=None): 

605 """ 

606 Extracts the version number, 

607 the function writes the files ``version.txt`` in this folder. 

608 

609 @param file_or_folder file ``setup.py`` or folder which contains it 

610 @param exc raises an exception if cannot look into git folder 

611 @param module_name module name 

612 @return version number 

613 """ 

614 # delayed import to speed up import of pycode 

615 from ..loghelper.pyrepo_helper import SourceRepository 

616 src = SourceRepository(commandline=True) 

617 ffolder = get_folder(file_or_folder) 

618 try: 

619 version = src.version(ffolder) 

620 except Exception as e: # pragma: no cover 

621 if exc: 

622 raise e 

623 return None 

624 if version in ["0", 0, None]: 

625 raise RuntimeError( # pragma: no cover 

626 f"issue with version {version}") 

627 

628 # write version number 

629 if version is not None: 

630 with open(os.path.join(ffolder, "version.txt"), "w") as f: 

631 f.write(str(version) + "\n") 

632 

633 modifies_init_file(ffolder, version, module_name=module_name) 

634 return version 

635 

636 

637def clean_space_for_setup(file_or_folder, file_filter=None): 

638 """ 

639 .. index:: pep8 

640 

641 Does some cleaning within the module, apply :epkg:`pep8` rules. 

642 

643 @param file_or_folder file ``setup.py`` or folder which contains it 

644 @param file_filter file filter (see @see fn remove_extra_spaces_folder) 

645 @return impacted files 

646 """ 

647 # delayed import 

648 from .code_helper import remove_extra_spaces_folder 

649 ffolder = get_folder(file_or_folder) 

650 rem = remove_extra_spaces_folder( 

651 ffolder, extensions=[".py", ".rst", ".md", ".bat", ".sh"], 

652 file_filter=file_filter) 

653 return rem 

654 

655 

656def clean_notebooks_for_numbers(file_or_folder): 

657 """ 

658 Upgrades notebooks to the latest format and 

659 cleans notebooks execution numbers and rearranges the JSON file. 

660 

661 @param file_or_folder file ``setup.py`` or folder which contains it 

662 @return impacted files 

663 

664 .. index:: notebooks 

665 """ 

666 from ..ipythonhelper.notebook_helper import upgrade_notebook, remove_execution_number 

667 from ..filehelper import explore_folder_iterfile 

668 ffolder = get_folder(file_or_folder) 

669 fold2 = os.path.normpath( 

670 os.path.join(ffolder, "_doc", "notebooks")) 

671 mod = [] 

672 for nbf in explore_folder_iterfile(fold2, pattern=".*[.]ipynb"): 

673 t = upgrade_notebook(nbf) 

674 if t: 

675 mod.append(nbf) # pragma: no cover 

676 # remove numbers 

677 s = remove_execution_number(nbf, nbf) 

678 if s: 

679 mod.append(nbf) 

680 return mod 

681 

682 

683def standard_help_for_setup(argv, file_or_folder, project_var_name, module_name=None, 

684 extra_ext=None, add_htmlhelp=False, copy_add_ext=None, 

685 nbformats=("ipynb", "html", "python", 

686 "rst", "slides", "pdf"), 

687 layout=None, use_run_cmd=False, direct_call=False, 

688 fexclude=None, fLOG=None): 

689 """ 

690 Standard function which generates help assuming they follow 

691 the same design as *pyquickhelper*. 

692 

693 @param argv it should be ``sys.argv`` 

694 @param file_or_folder file ``setup.py`` or folder which contains it 

695 @param project_var_name display name of the module 

696 @param module_name module name, None if equal to *project_var_name* 

697 (``import <module_name>``) 

698 @param extra_ext extra file extension to process (ex ``["doc"]``) 

699 @param add_htmlhelp run HTML Help too (only on Windows) 

700 @param copy_add_ext additional extension of files to copy 

701 @param nbformats notebooks format to generate 

702 @param layout layout for the documentation, if None --> ``["html", "pdf"]`` 

703 @param use_run_cmd use function @see fn run_cmd instead of ``os.system`` 

704 to build the documentation 

705 @param direct_call see @see fn generate_help_sphinx 

706 @param fexclude function which tells which file not to copy in the folder 

707 used to build the documentation 

708 @param fLOG logging function 

709 

710 The function outputs some information through function @see fn fLOG. 

711 

712 A page will be added for each extra file extension mentioned in *extra_ext* if 

713 some of these were found. 

714 """ 

715 if fLOG is None: # pragma: no cover 

716 from ..loghelper.flog import noLOG 

717 fLOG = noLOG 

718 if "--help" in argv: # pragma: no cover 

719 from ..helpgen.help_usage import get_help_usage 

720 print(get_help_usage()) 

721 else: 

722 from ..helpgen.sphinx_main import generate_help_sphinx 

723 

724 if layout is None: 

725 layout = ["html", "pdf"] # pragma: no cover 

726 if module_name is None: 

727 module_name = project_var_name 

728 

729 ffolder = get_folder(file_or_folder) 

730 source = os.path.join(ffolder, "_doc", "sphinxdoc", "source") 

731 

732 if not os.path.exists(source): 

733 raise FileNotFoundError( # pragma: no cover 

734 "you must get the source from GitHub to build the documentation,\nfolder {0} " 

735 "should exist\n(file_or_folder={1})\n(ffolder={2})\n(cwd={3})".format( 

736 source, file_or_folder, ffolder, os.getcwd())) 

737 

738 if "conf" in sys.modules: # pragma: no cover 

739 warnings.warn("module conf was imported, this function expects not to:\n{0}".format( 

740 sys.modules["conf"].__file__)) 

741 del sys.modules["conf"] 

742 

743 project_name = os.path.split( 

744 os.path.split(os.path.abspath(ffolder))[0])[-1] 

745 

746 generate_help_sphinx(project_name, module_name=module_name, layout=layout, 

747 extra_ext=extra_ext, nbformats=nbformats, add_htmlhelp=add_htmlhelp, 

748 copy_add_ext=copy_add_ext, fLOG=fLOG, root=ffolder, 

749 direct_call=direct_call, fexclude=fexclude) 

750 

751 

752def run_unittests_for_setup(file_or_folder, skip_function=None, 

753 coverage_options=None, coverage_exclude_lines=None, 

754 additional_ut_path=None, covtoken=None, stdout=None, 

755 stderr=None, filter_warning=None, dump_coverage=None, 

756 add_coverage_folder=None, coverage_root='src', fLOG=None): 

757 """ 

758 Runs the unit tests and computes the coverage, stores 

759 the results in ``_doc/sphinxdoc/source/coverage`` 

760 assuming the module follows the same design as *pyquickhelper*. 

761 

762 @param file_or_folder file ``setup.py`` or folder which contains it 

763 @param skip_function see @see fn main_wrapper_tests 

764 @param coverage_options see @see fn main_wrapper_tests 

765 @param coverage_exclude_lines see @see fn main_wrapper_tests 

766 @param additional_ut_path see @see fn main_wrapper_tests 

767 @param covtoken see @see fn main_wrapper_tests 

768 @param stdout see @see fn main_wrapper_tests 

769 @param stderr see @see fn main_wrapper_tests 

770 @param filter_warning see @see fn main_wrapper_tests 

771 @param coverage_root see @see fn main_wrapper_tests 

772 @param dump_coverage location where to dump the coverage 

773 @param add_coverage_folder additional folder where to look for other coverage reports 

774 @param fLOG logging function 

775 

776 See function @see fn main_wrapper_tests. 

777 The coverage computation can be disabled by specifying 

778 ``coverage_options["disable_coverage"] = True``. 

779 *covtoken* was added to post the coverage report to 

780 `codecov <https://codecov.io/>`_. 

781 Parameter *dump_coverage* 

782 dumps the unit test coverage in another location. 

783 """ 

784 # delayed import 

785 from .tkinter_helper import fix_tkinter_issues_virtualenv 

786 from .utils_tests import main_wrapper_tests 

787 ffolder = get_folder(file_or_folder) 

788 funit = os.path.join(ffolder, "_unittests") 

789 if not os.path.exists(funit): 

790 raise FileNotFoundError( # pragma: no cover 

791 "You must get the whole source to run the unittests," 

792 "\nfolder {0} should exist".format(funit)) 

793 

794 if skip_function is None: # pragma: no cover 

795 from .utils_tests_private import default_skip_function 

796 skip_function = default_skip_function 

797 if fLOG is None: # pragma: no cover 

798 from ..loghelper.flog import noLOG 

799 fLOG = noLOG 

800 

801 fix_tkinter_issues_virtualenv(fLOG=fLOG) 

802 

803 cov = True 

804 if coverage_options: 

805 if "disable_coverage" in coverage_options and coverage_options["disable_coverage"]: 

806 cov = False 

807 

808 if dump_coverage is not None and not cov: 

809 dump_coverage = None 

810 

811 logfile = os.path.join(funit, "unittests.out") 

812 res = main_wrapper_tests( 

813 logfile, add_coverage=cov, skip_function=skip_function, 

814 coverage_options=coverage_options, 

815 coverage_exclude_lines=coverage_exclude_lines, 

816 additional_ut_path=additional_ut_path, 

817 covtoken=covtoken, stdout=stdout, stderr=stderr, 

818 filter_warning=filter_warning, dump_coverage=dump_coverage, 

819 add_coverage_folder=add_coverage_folder, 

820 coverage_root=coverage_root, fLOG=fLOG) 

821 if not isinstance(res, dict): 

822 raise TypeError( # pragma: no cover 

823 f"Dictionary expected not {type(res)!r}.") 

824 return res 

825 

826 

827def copy27_for_setup(file_or_folder): # pragma: no cover 

828 """ 

829 Prepares a copy of the source for :epkg:`Python` 2.7, 

830 assuming the module follows the same design as *pyquickhelper*. 

831 

832 @param file_or_folder file ``setup.py`` or folder which contains it 

833 """ 

834 # delayed import 

835 from .py3to2 import py3to2_convert_tree 

836 root = get_folder(file_or_folder) 

837 root = os.path.normpath(root) 

838 dest = os.path.join(root, "dist_module27") 

839 py3to2_convert_tree(root, dest) 

840 

841 

842def write_pyproj(file_or_folder, location=None): 

843 """ 

844 Creates a project 

845 `pyproj <https://docs.microsoft.com/fr-fr/visualstudio/python/managing-python-projects-in-visual-studio>`_ 

846 to work with `PTVS <https://pytools.codeplex.com/>`_ 

847 (Python Tools for Visual Studio) 

848 

849 @param file_or_folder file ``setup.py`` or folder which contains it 

850 @param location if not None, stores the project into this folder 

851 

852 This functionality fails with :epkg:`Python` 2.7 (encoding). 

853 """ 

854 # delayed import 

855 from ..filehelper import explore_folder_iterfile 

856 from .build_helper import get_pyproj_project 

857 

858 avoid = ["dist", "build", "dist_module27", 

859 "_doc", "_virtualenv", "_virtualenv27", "_venv"] 

860 

861 def filter(name): 

862 if os.path.splitext(name)[-1] != ".py": 

863 return False 

864 if "temp_" in name: 

865 return False 

866 if "temp2_" in name: 

867 return False # pragma: no cover 

868 for a in avoid: 

869 if name.startswith(a + "\\"): 

870 return False # pragma: no cover 

871 if name.startswith(a + "/"): 

872 return False 

873 return True 

874 

875 root = get_folder(file_or_folder) 

876 root = os.path.normpath(root) 

877 name = os.path.split(root)[-1] 

878 if location is None: 

879 dest = os.path.join(root, "ptvs_project.pyproj") # pragma: no cover 

880 else: 

881 dest = os.path.join(location, "ptvs_project.pyproj") 

882 all_files = [os.path.relpath(_, root) 

883 for _ in explore_folder_iterfile(root)] 

884 all_files = [_ for _ in all_files if filter(_)] 

885 pyproj = get_pyproj_project(name, all_files) 

886 with open(dest, "w", encoding="utf8") as f: 

887 f.write(pyproj.strip()) 

888 

889 

890def process_standard_options_for_setup_help(argv): 

891 """ 

892 Prints the added options available through this module. 

893 """ 

894 commands = { 

895 "build27": "build the wheel for Python 2.7 (if available), it requires to run copy27 first", 

896 "build_script": "produce various scripts to build the module", 

897 "build_sphinx": "build the documentation", 

898 "build_wheel": "build the wheel", 

899 "clean_space": "clean unnecessary spaces in the code, applies :epkg:`pycodestyle` on all files", 

900 "clean_pyd": "clean file ``*.pyd``", 

901 "copy27": "create a modified copy of the module to run on Python 2.7 (if available), it requires to run copy27 first", 

902 "copy_dist": "copy documentation to folder dist", 

903 "copy_sphinx": "modify and copy sources to _doc/sphinxdoc/source/<module>", 

904 "history": "builds the history of the package in RST", 

905 "local_jenkins": "sets up jobs on a local jenkins server", 

906 "run27": "run the unit tests for the Python 2.7", 

907 "run_pylint": "run pylint on the sources, allowed parameters <pattern> <neg_pattern>", 

908 "unittests": "run the unit tests which do not contain test_LONG, test_SKIP or test_GUI in their file name", 

909 "unittests_LONG": "run the unit tests which contain test_LONG their file name", 

910 "unittests_SKIP": "run the unit tests which contain test_SKIP their file name", 

911 "unittests_GUI": "run the unit tests which contain test_GUI their file name", 

912 "write_version": "write a file ``version.txt`` with the version number (assuming sources are host with git)", 

913 } 

914 

915 if "--help-commands" in argv: 

916 print("Commands processed by pyquickhelper:") 

917 for k, v in sorted(commands.items()): 

918 print(f" {k}{' ' * (len('copy27 ') - len(k))}{v}") 

919 print() 

920 elif "--help" in argv: 

921 docu = 0 

922 for k, v in sorted(commands.items()): 

923 if k in argv: 

924 docu += 1 

925 

926 if docu == 0: # pragma: no cover 

927 print("pyquickhelper commands:") 

928 print() 

929 for k in sorted(commands): 

930 process_standard_options_for_setup_help(['--help', k]) 

931 print() 

932 else: 

933 for k, v in sorted(commands.items()): 

934 if k in argv: 

935 docu += 1 

936 print(f" setup.py {k}{' ' * (20 - len(k))}{v}") 

937 if k == "unittests": 

938 print( 

939 f"\n {k} [-d seconds] [-f file] [-e regex] [-g regex]\n\n {v}") 

940 print( 

941 " -d seconds run all unit tests for which predicted duration is below a given threshold.") 

942 print( 

943 " -f file run all unit tests in file (do not use the full path)") 

944 print( 

945 " -e regex run all unit tests files matching the regular expression") 

946 print( 

947 " -g regex run all unit tests files not matching the regular expression") 

948 print() 

949 elif k == "local_jenkins": # pragma: no cover 

950 print() 

951 print( 

952 f" {k} user password [location] [server]") 

953 print(" default location is somewhere/workspace") 

954 print(" default server is http://localhost:8080/") 

955 print() 

956 

957 

958def write_module_scripts(folder, platform=sys.platform, blog_list=None, 

959 default_engine_paths=None, command=None): 

960 """ 

961 Writes a couple of scripts which allow a user to be faster on some tasks 

962 or to easily get information about the module. 

963 

964 @param folder where to write the script 

965 @param platform platform 

966 @param blog_list blog list to follow, should be attribute ``__blog__`` of the module 

967 @param command None to generate scripts for all commands or a value in *[blog, doc]*. 

968 @param default_engine_paths default engines (or python distributions) 

969 @return list of written scripts 

970 

971 The function produces the following files: 

972 

973 * *auto_rss_list.xml*: list of rss stream to follow 

974 * *auto_rss_database.db3*: stores blog posts 

975 * *auto_rss_server.py*: runs a server which updates the scripts and runs a server. It also open the default browser. 

976 * *auto_rss_server.(bat|sh)*: run *auto_run_server.py*, the file on Linux might be missing if there is an equivalent python script 

977 

978 .. faqref:: 

979 :title: How to generate auto_rss_server.py? 

980 

981 The following code generates the script *auto_rss_local.py* 

982 which runs a local server to read blog posts included 

983 in the documentation (it uses module 

984 `pyrsslocal <http://www.xavierdupre.fr/app/pyrsslocal/helpsphinx/index.html>`_):: 

985 

986 from pyquickhelper.pycode import write_module_scripts, __blog__ 

987 write_module_scripts(".", blog_list=__blog__, command="blog") 

988 """ 

989 # delayed import 

990 from .build_helper import get_script_module 

991 default_set = {"blog", "doc"} 

992 if command is not None: 

993 if command not in default_set: 

994 raise ValueError( # pragma: no cover 

995 f"command {command} is not available in {default_set}") 

996 commands = {command} 

997 else: 

998 commands = default_set 

999 

1000 res = [] 

1001 for c in commands: 

1002 sc = get_script_module( 

1003 c, platform=sys.platform, blog_list=blog_list, default_engine_paths=default_engine_paths) 

1004 if sc is None: 

1005 continue # pragma: no cover 

1006 tobin = os.path.join(folder, "bin") 

1007 if not os.path.exists(tobin): 

1008 os.mkdir(tobin) 

1009 for item in sc: 

1010 if isinstance(item, tuple): 

1011 name = os.path.join(folder, "bin", item[0]) 

1012 with open(name, "w", encoding="utf8") as f: 

1013 f.write(item[1]) 

1014 res.append(name) 

1015 else: # pragma: no cover 

1016 name = os.path.join( 

1017 folder, "bin", f"auto_run_{c}.{get_script_extension()}") 

1018 with open(name, "w") as f: 

1019 f.write(item) 

1020 res.append(name) 

1021 return res 

1022 

1023 

1024def _get_dump_default_path(location, module_name, argv): 

1025 """ 

1026 Proposes a default location to dump results about unit tests execution. 

1027 

1028 @param location location of the module 

1029 @param module_name module name 

1030 @param argv argument on the command line 

1031 @return location of the dump 

1032 

1033 The result is None for remote continuous integration. 

1034 """ 

1035 from . import is_travis_or_appveyor 

1036 if is_travis_or_appveyor(): 

1037 return None # pragma: no cover 

1038 hash = hash_list(argv) 

1039 setup = os.path.join(location, "setup.py") 

1040 if not os.path.exists(setup): 

1041 raise FileNotFoundError(setup) # pragma: no cover 

1042 fold = os.path.join(location, "..", "_coverage_dumps") 

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

1044 os.mkdir(fold) 

1045 dt = datetime.datetime.now().strftime("%Y%m%dT%H%M") 

1046 if module_name is None: 

1047 raise ValueError("module_name cannot be None") # pragma: no cover 

1048 dump = os.path.join(fold, module_name, hash, dt) 

1049 if not os.path.exists(dump): 

1050 os.makedirs(dump) 

1051 return dump 

1052 

1053 

1054def hash_list(argv, size=8): 

1055 """ 

1056 Proposes a hash for the list of arguments. 

1057 

1058 @param argv list of arguments on the command line. 

1059 @param size size of the hash 

1060 @return string 

1061 """ 

1062 st = "--".join(map(str, argv)) 

1063 hash = hashlib.md5() 

1064 hash.update(st.encode("utf-8")) 

1065 res = hash.hexdigest() 

1066 if len(res) > 8: 

1067 return res[:8] 

1068 return res # pragma: no cover 

1069 

1070 

1071def build_history_from_setup(dest, owner, module, existing_history=None, 

1072 skip_issues=None, fLOG=None): # pragma: no cover 

1073 """ 

1074 Builds the history from :epkg:`github` and :epkg:`pypi`. 

1075 

1076 @param dest history will be written in this file 

1077 @param owner owner of the package on :epkg:`github` 

1078 @param module module name 

1079 @param existing_history existing history, retrieves existing issues stored 

1080 in that file 

1081 @param skip_issues skip a given list of issues when building the history 

1082 @param fLOG logging function 

1083 @return history 

1084 """ 

1085 # delayed import 

1086 from ..loghelper.history_helper import build_history, compile_history 

1087 if owner is None: 

1088 raise ValueError( # pragma: no cover 

1089 "owner must be specified.") 

1090 if "/" in owner: 

1091 raise ValueError( # pragma: no cover 

1092 f"owner {owner!r} cannot contain '/'.") 

1093 if fLOG is None: # pragma: no cover 

1094 from ..loghelper.flog import noLOG 

1095 fLOG = noLOG 

1096 repo = module 

1097 hist = build_history(owner, repo, unpublished=True, 

1098 existing_history=existing_history, 

1099 skip_issues=skip_issues, fLOG=fLOG) 

1100 output = compile_history(hist) 

1101 if dest is not None: 

1102 with open(dest, "w", encoding="utf-8") as f: 

1103 f.write(output) 

1104 return output 

1105 

1106 

1107def run_pylint_for_setup(folder, pattern=".*[.]py$", neg_pattern=None, 

1108 verbose=False, pylint_ignore=None, fLOG=None): 

1109 """ 

1110 Applies :epkg:`pylint` on subfolder *folder*. 

1111 

1112 @param folder folder where to look 

1113 @param pattern file to checks 

1114 @param neg_pattern negative pattern 

1115 @param pylint_ignore ignore these :epkg:`pylint` warnings or errors 

1116 @param verbose verbose 

1117 @param fLOG logging function 

1118 """ 

1119 # delayed import 

1120 from .utils_tests_helper import check_pep8 

1121 if fLOG is None: # pragma: no cover 

1122 from ..loghelper.flog import noLOG 

1123 fLOG = noLOG 

1124 check_pep8(folder, pattern=pattern, neg_pattern=neg_pattern, 

1125 pylint_ignore=pylint_ignore, verbose=verbose, fLOG=fLOG) 

1126 

1127 

1128def modifies_init_file(folder, version, module_name=None): 

1129 """ 

1130 Automatically modifies the init file. 

1131 

1132 @param folder where to find the init file 

1133 @param version commit number 

1134 @return modified init file 

1135 """ 

1136 def _update_version(v, nv): 

1137 vs = v.split('.') 

1138 if len(vs) <= 2: 

1139 return '.'.join(list(vs) + [nv]) 

1140 if len(vs) >= 3: 

1141 vs = list(vs) 

1142 vs[-1] = nv 

1143 return '.'.join(vs) 

1144 raise ValueError( # pragma: no cover 

1145 f"Unable to process '{v}' with new version '{nv}'.") 

1146 

1147 filename = None 

1148 if os.path.exists(folder): 

1149 if os.path.isdir(folder): 

1150 src = os.path.join(folder, 'src') 

1151 if module_name is None: 

1152 setu = os.path.join(folder, 'setup.py') 

1153 if not os.path.exists(setu): 

1154 raise FileNotFoundError( # pragma: no cover 

1155 "Unable to find 'setup.py' in '{}' and module_name is " 

1156 "None.".format(folder)) 

1157 reg = re.compile( 

1158 "(project_var_name = ['\\\"]([a-zA-Z][a-zA-Z_0-9]+)['\\\"])") 

1159 with open(setu, 'r', encoding='utf-8') as f: 

1160 cst = f.read() 

1161 find = reg.findall(cst) 

1162 if len(find) == 0: 

1163 raise FileNotFoundError( # pragma: no cover 

1164 "Unable to find 'project_var_name' in 'setup.py' in '{}' " 

1165 "and module_name is None.".format(folder)) 

1166 module_name = find[0][1] 

1167 if os.path.exists(src) and module_name is not None: 

1168 filename = os.path.join(src, module_name, '__init__.py') 

1169 elif os.path.exists(src) and module_name is not None: 

1170 filename = os.path.join(src, module_name, '__init__.py') 

1171 elif module_name is not None: 

1172 filename = os.path.join(folder, module_name, '__init__.py') 

1173 else: 

1174 raise FileNotFoundError( # pragma: no cover 

1175 f"Unable to find '__init__.py' in '{folder}' (module_name is None).") 

1176 if not os.path.exists(filename): 

1177 raise FileNotFoundError( # pragma: no cover 

1178 f"Unable to find '__init__.py' in '{folder}' (got '{filename}').") 

1179 with open(filename, 'r', encoding='utf-8') as f: 

1180 content = f.read() 

1181 elif '__version__' in folder: 

1182 content = folder 

1183 else: 

1184 raise ValueError(f"Unable to process '{folder}'.") 

1185 

1186 reg = re.compile("(__version__ = ['\\\"]([0-9.]+)['\\\"])") 

1187 lines = content.split('\n') 

1188 modif = [] 

1189 rep = [] 

1190 for line in lines: 

1191 if line.startswith("__version__"): 

1192 find = reg.findall(line) 

1193 if len(find) != 1: 

1194 raise ValueError( # pragma: no cover 

1195 f"Unable to find __version__ in '{line}'") 

1196 v = find[0][1] 

1197 nv = _update_version(v, str(version)) 

1198 newline = line.replace(v, nv) 

1199 modif.append(newline) 

1200 rep.append((line, newline)) 

1201 else: 

1202 modif.append(line) 

1203 if len(rep) == 0: 

1204 raise ValueError( # pragma: no cover 

1205 f"Unable to find '__version__' in \n{content}") 

1206 

1207 content = '\n'.join(modif) 

1208 if filename is not None: 

1209 with open(filename, 'w', encoding='utf-8') as f: 

1210 f.write(content) 

1211 return content