Coverage for src/pymyinstall/installhelper/module_install_version.py: 73%

503 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-07-19 01:47 +0200

1""" 

2@file 

3@brief Functions to get module version, license, dependencies 

4""" 

5import sys 

6import re 

7import warnings 

8import functools 

9import time 

10import platform 

11import xmlrpc.client as xmlrpc_client 

12from .install_cmd_helper import run_cmd, get_pip_program 

13from .module_install_exceptions import ( 

14 MissingPackageOnPyPiException, AnnoyingPackageException) 

15from .module_install_exceptions import ( 

16 ConfigurationError, MissingVersionOnPyPiException, 

17 WrongVersionError, MissingVersionWheelException) 

18from .install_cmd_regex import regex_wheel_versions 

19from .pip_helper import get_installed_distributions 

20 

21 

22annoying_modules = {"pygame", "liblinear", "mlpy", "VideoCapture", 

23 "libsvm", "opencv_python", "scikits.cuda", 

24 "NLopt"} 

25 

26 

27_get_module_version_manual_memoize = {} 

28 

29 

30def get_module_version(module, use_cmd=False): 

31 """ 

32 Returns a dictionary ``{ module: version }``. 

33 

34 @param module unused, None 

35 @param use_cmd use command line 

36 @return dictionary 

37 """ 

38 if module is not None: 

39 modl = module.lower() 

40 res = get_module_version(None, use_cmd=use_cmd) 

41 return res.get(modl, None) 

42 

43 global _get_module_version_manual_memoize # pylint: disable=W0602 

44 if len(_get_module_version_manual_memoize) > 0: 

45 return _get_module_version_manual_memoize 

46 

47 res = {} 

48 

49 if use_cmd: 

50 prog = get_pip_program() 

51 cmd = prog + " list" 

52 out, err = run_cmd(cmd, wait=True, fLOG=None) 

53 

54 if err is not None and len(err) > 0: 

55 if len(err.split("\n")) > 3 or \ 

56 "You should consider upgrading via the 'pip install --upgrade pip' command." not in err: 

57 raise RuntimeError("unable to run, #lines {0}\nCMD:\n{3}\nERR-J:\n{1}\nOUT:\n{2}".format( 

58 len(err.split("\n")), err, out, cmd)) 

59 lines = out.split("\n") 

60 

61 for line in lines: 

62 if "(" in line: 

63 spl = line.split() 

64 if len(spl) == 2: 

65 a = spl[0] 

66 b = spl[1].strip(" \n\r") 

67 res[a] = b.strip("()") 

68 al = a.lower() 

69 if al != a: 

70 res[al] = res[a] 

71 else: 

72 # local_only must be False to get all modules 

73 # not only the ones installed in the virtual environment 

74 dist = get_installed_distributions(local_only=False) 

75 if len(dist) == 0: 

76 raise ConfigurationError("no installed module, unexpected (pip should be there): " + 

77 "sys.executable={0}, sys.prefix={1}, sys.base_prefix={2}".format( 

78 sys.executable, sys.prefix, sys.base_prefix)) 

79 for mod in dist: 

80 al = mod.key.lower() 

81 a = mod.key 

82 try: 

83 v = mod.version 

84 except ValueError: 

85 v = "UNKNOWN" 

86 res[a] = v 

87 if a != al: 

88 res[al] = v 

89 

90 _get_module_version_manual_memoize.update(res) 

91 return res 

92 

93 

94def is_installed(name): 

95 """ 

96 Tells if a module is installed or not. 

97 

98 @param name module name 

99 @return boolean 

100 """ 

101 return get_module_version(name) is not None 

102 

103 

104_get_module_metadata_manual_memoize = {} 

105 

106 

107def get_module_metadata(module, use_cmd=False, refresh_cache=False): 

108 """ 

109 Returns a dictionary ``{ module: metadata }``. 

110 

111 @param module unused, None 

112 @param refresh_cache refresh the cache before getting metadata 

113 @return dictionary 

114 """ 

115 if module is not None: 

116 modl = module.lower() 

117 res = get_module_metadata( 

118 None, use_cmd=use_cmd, refresh_cache=refresh_cache) 

119 return res.get(modl, None) 

120 

121 global _get_module_metadata_manual_memoize # pylint: disable=W0602 

122 if not refresh_cache and len(_get_module_metadata_manual_memoize) > 0: 

123 return _get_module_metadata_manual_memoize 

124 

125 # local_only must be False to get all modules 

126 # not only the ones installed in the virtual environment 

127 dist = get_installed_distributions(local_only=False, use_cmd=use_cmd) 

128 if len(dist) == 0: 

129 raise ConfigurationError("no installed module, unexpected (pip should be there): " + 

130 "sys.executable={0}, sys.prefix={1}, sys.base_prefix={2}".format( 

131 sys.executable, sys.prefix, sys.base_prefix)) 

132 res = {} 

133 for mod in dist: 

134 d = {} 

135 lines = mod._get_metadata(mod.PKG_INFO) 

136 for line in lines: 

137 if sys.version_info[0] == 2: 

138 typstr = str # unicode# 

139 if not isinstance(line, typstr): 

140 line = typstr(line, encoding="utf8", errors="ignore") 

141 try: 

142 spl = line.split(":") 

143 except UnicodeDecodeError: 

144 warnings.warn("UnicodeDecodeError with: " + line) 

145 continue 

146 key = spl[0].strip() 

147 value = ":".join(spl[1:]).strip() 

148 if key not in d: 

149 d[key] = value 

150 else: 

151 if not isinstance(d[key], list): 

152 d[key] = [d[key]] 

153 d[key].append(value) 

154 

155 a = mod.key 

156 res[a] = d 

157 al = mod.key.lower() 

158 if a != al: 

159 res[al] = d 

160 

161 _get_module_metadata_manual_memoize.update(res) 

162 return res 

163 

164 

165def _get_pypi_version_memoize_op(f): 

166 memo = {} 

167 

168 def helper(module_name, full_list=False, url="https://pypi.python.org/pypi"): 

169 key = module_name, full_list, url 

170 if key not in memo: 

171 memo[key] = f(module_name, full_list, url) 

172 return memo[key] 

173 return helper 

174 

175 

176_get_pypi_version_memoize = {} 

177 

178 

179def get_pypi_version(module_name, full_list=False, url="https://pypi.python.org/pypi", skip_betas=True): 

180 """ 

181 Returns the version of a package on :epkg:`pypi`, 

182 we skip alpha, beta or dev version. 

183 

184 @param module_name module name 

185 @param url pypi server 

186 @param full_list results as a list or return the last stable version 

187 @param skip_betas skip the intermediate functions 

188 @return version (str or list) 

189 

190 See also `installing_python_packages_programatically.py <https://gist.github.com/rwilcox/755524>`_, 

191 `pkgtools.pypi: PyPI interface <http://pkgtools.readthedocs.org/en/latest/pypi.html>`_. 

192 

193 It the function fails, check the status of 

194 `Python Infrastructure <https://status.python.org/>`_. 

195 It can return errors:: 

196 

197 ProtocolError: ProtocolError for pypi.python.org/pypi: 503 No healthy backends 

198 """ 

199 

200 global _get_pypi_version_memoize # pylint: disable=W0602 

201 key = module_name, full_list, url 

202 if key in _get_pypi_version_memoize: 

203 available = _get_pypi_version_memoize[key] 

204 if full_list: 

205 return available 

206 elif available is not None and len(available) > 0: 

207 return available[0] 

208 return None 

209 

210 def pypi_package_releases(module_name, b): 

211 nbtry = 0 

212 while nbtry < 40: 

213 try: 

214 available = pypi.package_releases(module_name, True) 

215 return available 

216 except Exception as e: 

217 if ("HTTPTooManyRequests" in str(type(e)) or 

218 "HTTPTooManyRequests" in str(e)): 

219 nbtry += 1 

220 warnings.warn(str(e)) 

221 time.sleep(90) 

222 continue 

223 if ("TimeoutError" in str(type(e)) or 

224 "TimeoutError" in str(e)): 

225 nbtry += 1 

226 warnings.warn(str(e)) 

227 time.sleep(20) 

228 continue 

229 raise e 

230 return None 

231 

232 def _inside_loop_(pypi, module_name, tried): 

233 

234 available = pypi_package_releases(module_name, True) 

235 

236 if available is None or len(available) == 0: 

237 ntry = module_name.capitalize() 

238 if ntry not in tried: 

239 tried.append(ntry) 

240 available = pypi_package_releases(tried[-1], True) 

241 

242 if available is None or len(available) == 0: 

243 ntry = module_name.replace("-", "_") 

244 if ntry not in tried: 

245 tried.append(ntry) 

246 available = pypi_package_releases(tried[-1], True) 

247 

248 if available is None or len(available) == 0: 

249 ntry = module_name.replace("_", "-") 

250 if ntry not in tried: 

251 tried.append(ntry) 

252 available = pypi_package_releases(tried[-1], True) 

253 

254 if available is None or len(available) == 0: 

255 ntry = module_name.lower() 

256 if ntry not in tried: 

257 tried.append(ntry) 

258 available = pypi_package_releases(tried[-1], True) 

259 

260 if available is None or len(available) == 0: 

261 ml = module_name.lower() 

262 if ml == "markupsafe": 

263 tried.append("MarkupSafe") 

264 available = pypi_package_releases(tried[-1], True) 

265 elif ml == "flask-sqlalchemy": 

266 tried.append("Flask-SQLAlchemy") 

267 available = pypi_package_releases(tried[-1], True) 

268 elif ml == "apscheduler": 

269 tried.append("APScheduler") 

270 available = pypi_package_releases(tried[-1], True) 

271 elif ml == "datashape": 

272 tried.append("DataShape") 

273 available = pypi_package_releases(tried[-1], True) 

274 elif ml == "pycontracts": 

275 tried.append("PyContracts") 

276 available = pypi_package_releases(tried[-1], True) 

277 elif ml == "pybrain": 

278 tried.append("PyBrain") 

279 available = pypi_package_releases(tried[-1], True) 

280 elif ml == "pyexecjs": 

281 tried.append("PyExecJS") 

282 available = pypi_package_releases(tried[-1], True) 

283 elif ml == "dataspyre": 

284 tried.append("DataSpyre") 

285 available = pypi_package_releases(tried[-1], True) 

286 elif ml == "heapdict": 

287 tried.append("HeapDict") 

288 available = pypi_package_releases(tried[-1], True) 

289 elif ml == "pyreact": 

290 tried.append("PyReact") 

291 available = pypi_package_releases(tried[-1], True) 

292 elif ml == "qtpy": 

293 tried.append("QtPy") 

294 available = pypi_package_releases(tried[-1], True) 

295 elif ml == "pythonqwt": 

296 tried.append("PythonQwt") 

297 available = pypi_package_releases(tried[-1], True) 

298 elif ml == "onedrive-sdk-python": 

299 tried.append("onedrivesdk") 

300 available = pypi_package_releases(tried[-1], True) 

301 elif ml.startswith("orange3-"): 

302 s = ml.split("-")[1] 

303 ntry = "Orange3-" + s[0].upper() + s[1:] 

304 tried.append(ntry) 

305 available = pypi_package_releases(tried[-1], True) 

306 elif module_name in annoying_modules: 

307 raise AnnoyingPackageException(module_name) 

308 

309 # this raises a warning about an opened connection 

310 # see documentation of the function 

311 # del pypi 

312 

313 return available 

314 

315 tried = [module_name] 

316 

317 if sys.version_info[:2] <= (3, 4): 

318 # the client does not an implemented of __exit__ for version <= 3.4 

319 pypi = xmlrpc_client.ServerProxy(url) 

320 available = _inside_loop_(pypi, module_name, tried) 

321 else: 

322 with xmlrpc_client.ServerProxy(url) as pypi: 

323 available = _inside_loop_(pypi, module_name, tried) 

324 

325 if available is None or len(available) == 0: 

326 raise MissingPackageOnPyPiException("tried:\n" + "\n".join(tried)) 

327 

328 def filter_betas(a): 

329 spl = a.split(".") 

330 if len(spl) in (2, 3): 

331 last = spl[-1] 

332 if not skip_betas or ("a" not in last and "b" not in last and "dev" not in last): 

333 return True 

334 else: 

335 # we don't really know here, so we assume it is not 

336 return True 

337 return False 

338 

339 if available: 

340 available2 = list(filter(filter_betas, available)) 

341 if available2: 

342 _get_pypi_version_memoize[key] = available2 

343 if full_list: 

344 return available2 

345 return available2[0] 

346 

347 raise MissingVersionOnPyPiException( 

348 "{0}\nversion:\n{1}".format(module_name, "\n".join(available))) 

349 

350 

351def numeric_version(vers): 

352 """ 

353 convert a string into a tuple with numbers wherever possible 

354 

355 @param vers string 

356 @return tuple 

357 """ 

358 if isinstance(vers, tuple): 

359 return vers 

360 if isinstance(vers, list): 

361 raise RuntimeError("unexpected value:" + str(vers)) 

362 spl = str(vers).split(".") 

363 r = [] 

364 for _ in spl: 

365 try: 

366 i = int(_) 

367 r.append(i) 

368 except ValueError: 

369 r.append(_) 

370 return tuple(r) 

371 

372 

373def compare_version(num, vers): 

374 """ 

375 Compares two versions. 

376 

377 @param num first version 

378 @param vers second version 

379 @return -1, 0, 1 

380 """ 

381 if num is None: 

382 if vers is None: 

383 return 0 

384 return 1 

385 if vers is None: 

386 return -1 

387 

388 if not isinstance(vers, tuple): 

389 vers = numeric_version(vers) 

390 if not isinstance(num, tuple): 

391 num = numeric_version(num) 

392 

393 if len(num) == len(vers): 

394 for a, b in zip(num, vers): 

395 if isinstance(a, int) and isinstance(b, int): 

396 if a < b: 

397 return -1 

398 if a > b: 

399 return 1 

400 else: 

401 a = str(a) 

402 b = str(b) 

403 if a < b: 

404 return -1 

405 if a > b: 

406 return 1 

407 return 0 

408 

409 if len(num) < len(vers): 

410 num = num + (0,) * (len(vers) - len(num)) 

411 return compare_version(num, vers) 

412 vers = vers + (0,) * (len(num) - len(vers)) 

413 return compare_version(num, vers) 

414 

415 

416def version_consensus(v1, v2): 

417 """ 

418 *v1* and *v2* are two versions of the same module, which one to keep? 

419 

420 @param v1 version 1 

421 @param v2 version 2 

422 @return consensus 

423 

424 * ``v1=None``, ``v2='(>=1.5)'`` --> ``v='>=1.5'`` 

425 

426 To improve: 

427 

428 * ``v1='<=1.6'``, ``v2='(>=1.5)'`` --> ``v='==1.6'`` 

429 """ 

430 reg = re.compile("([><=!]*)([^><=!]+)") 

431 

432 def process_version(v): 

433 if isinstance(v, str # unicode# 

434 ): 

435 v = v.strip('()') 

436 find = reg.search(v) 

437 if not find: 

438 raise WrongVersionError(v) 

439 sign = find.groups()[0] 

440 number = numeric_version(find.groups()[1]) 

441 else: 

442 try: 

443 sign, number = v 

444 except ValueError as e: 

445 raise ValueError("weird format: " + str(v) + 

446 " - " + str(type(v))) from e 

447 return sign, number 

448 

449 if v1 is None: 

450 return v2 

451 if v2 is None: 

452 return v1 

453 

454 s1, n1 = process_version(v1) 

455 s2, n2 = process_version(v2) 

456 

457 if s1 not in ('<=', '==', '<', '>', '>=', '!='): 

458 raise ValueError("wrong sign '{0}' for v1='{1}'".format(s1, v1)) 

459 if s2 not in ('<=', '==', '<', '>', '>=', '!='): 

460 raise ValueError("wrong sign '{0}' for v1='{1}'".format(s2, v2)) 

461 

462 if s1 == "==": 

463 if s2 == "==": 

464 if compare_version(n1, n2) != 0: 

465 raise WrongVersionError( 

466 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

467 res = s1, n1 

468 else: 

469 res = s1, n1 

470 

471 elif s1 == "<=": 

472 if s2 == "<=": 

473 res = s1, min(n1, n2) 

474 elif s2 == "==": 

475 if compare_version(n1, n2) < 0: 

476 raise WrongVersionError( 

477 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

478 res = s2, n2 

479 elif s2 == '<': 

480 if compare_version(n1, n2) == -1: 

481 res = s1, n1 

482 else: 

483 res = s2, n2 

484 elif s2 in ('>', '>='): 

485 if compare_version(n1, n2) <= 0: 

486 raise WrongVersionError( 

487 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

488 res = s1, n1 

489 

490 elif s1 == "<": 

491 if s2 == "<": 

492 res = s1, min(n1, n2) 

493 elif s2 == "==": 

494 if compare_version(n1, n2) <= 0: 

495 raise WrongVersionError( 

496 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

497 res = s2, n2 

498 elif s2 == '<=': 

499 if compare_version(n1, n2) <= 0: 

500 res = s1, n1 

501 else: 

502 res = s2, n2 

503 elif s2 in ('>', '>='): 

504 if compare_version(n1, n2) <= 0: 

505 raise WrongVersionError( 

506 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

507 res = s1, n1 

508 

509 elif s1 == ">=": 

510 if s2 == ">=": 

511 res = s1, max(n1, n2) 

512 elif s2 == "==": 

513 if compare_version(n1, n2) == -1: 

514 raise WrongVersionError( 

515 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

516 res = s2, n2 

517 elif s2 == '>': 

518 if compare_version(n1, n2) <= 0: 

519 res = s2, n2 

520 else: 

521 res = s1, n1 

522 elif s2 in ('<', '<='): 

523 if compare_version(n1, n2) >= 0: 

524 raise WrongVersionError( 

525 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

526 res = s2, n2 

527 elif s2 == '!=': 

528 if compare_version(n1, n2) == 0: 

529 raise WrongVersionError( 

530 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

531 res = s1, max(n1, n2) 

532 

533 elif s1 == ">": 

534 if s2 == ">": 

535 res = s1, max(n1, n2) 

536 elif s2 == "==": 

537 if compare_version(n1, n2) >= 0: 

538 raise WrongVersionError( 

539 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

540 res = s2, n2 

541 elif s2 == '>=': 

542 if compare_version(n1, n2) == -1: 

543 res = s2, n2 

544 else: 

545 res = s1, n1 

546 elif s2 in ('<', '<='): 

547 if compare_version(n1, n2) == 1: 

548 raise WrongVersionError( 

549 "incompatible version: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

550 res = s2, n2 

551 else: 

552 res = None, None 

553 

554 if res[0] is None: 

555 raise WrongVersionError( 

556 "incompatible version and wrong format: {0}{1} and {2}{3}".format(s1, n1, s2, n2)) 

557 

558 return '{0}{1}'.format(res[0], '.'.join(str(_) for _ in res[1])) 

559 

560 

561_get_module_dependencies_deps = None 

562 

563 

564def get_module_dependencies(module, use_cmd=False, deep=False, collapse=True, use_pip=None, refresh_cache=False): 

565 """ 

566 Returns the dependencies for a module. 

567 

568 @param module unused, None 

569 @param use_cmd use command line 

570 @param deep dig into dependencies of dependencies 

571 @param collapse only one row per module 

572 @param use_pip use pip to discover dependencies or not (parse metadata) 

573 @param refresh_cache refresh the cache (see below) 

574 @return list of tuple (module, version, required by as a str) 

575 or dictionary { module: (version, required by as a list) } if *collapse* is True 

576 

577 The function which uses *use_pip=True* is not fully tested, it does not 

578 return contraints (== 2.4). The function caches the results to avoid doing it again 

579 during a second execution unless *refresh_cache* is True. 

580 This function is not tested on Python 2.7. 

581 """ 

582 if use_pip is None: 

583 use_pip = not sys.platform.startswith("win") 

584 

585 def evaluate_condition(cond, full): 

586 python_version = ".".join(str(_) for _ in sys.version_info[:3]) 

587 extra = "" 

588 sys_platform = sys.platform 

589 platform_machine = platform.machine 

590 platform_python_implementation = platform.python_implementation 

591 try: 

592 return eval(cond) 

593 except Exception: 

594 if "python_version" not in cond and "extra" not in cond: 

595 # probably something like cycler (>=0.10) 

596 # we don't check that 

597 return True 

598 mes = ("Unable to evaluate condition '{0}' from '{1}', " 

599 "extra='{2}', python_version='{3}', " 

600 "sys_platform='{4}' platform_machine='{5}', " 

601 "platform_python_implementation='{6}'.").format( 

602 cond, full, extra, python_version, sys_platform, 

603 platform_machine, platform_python_implementation) 

604 raise RuntimeError(mes) 

605 

606 if use_pip: 

607 global _get_module_dependencies_deps 

608 if _get_module_dependencies_deps is None or refresh_cache: 

609 temp = get_installed_distributions( 

610 local_only=False, skip=[], use_cmd=use_cmd) 

611 _get_module_dependencies_deps = dict( 

612 (p.key, (p, p.requires())) for p in temp) 

613 if module not in _get_module_dependencies_deps: 

614 raise ValueError("module {0} was not installed".format(module)) 

615 res = [] 

616 req = _get_module_dependencies_deps[module][1] 

617 if isinstance(req, list): 

618 for r in req: 

619 res.append((r.key, None, module)) 

620 else: 

621 res.append((req.key, None, module)) 

622 else: 

623 meta = get_module_metadata( 

624 module, use_cmd, refresh_cache=refresh_cache) 

625 if meta is None: 

626 raise ImportError( 

627 "unable to get metadata for module '{0}' - refresh_cache={1}".format(module, refresh_cache)) 

628 deps = [v for k, v in meta.items() 

629 if "Requires" in k and "Requires-Python" not in k] 

630 res = [] 

631 for d in deps: 

632 if not isinstance(d, list): 

633 dl = [d] 

634 else: 

635 dl = d 

636 for v in dl: 

637 spl = v.split() 

638 if len(spl) > 1: 

639 spl = [spl[0], " ".join(spl[1:])] 

640 if len(spl) <= 1: 

641 key = (module, None, v) 

642 else: 

643 conds = spl[1].split(";") 

644 ok = [evaluate_condition(cond, v) for cond in conds] 

645 if not all(ok): 

646 continue 

647 key = (spl[0].strip(";"), spl[1], module) 

648 if key not in res: 

649 res.append(key) 

650 

651 # specific filters 

652 def validate_module(name): 

653 if name == "enum34" and sys.version_info[:2] > (3, ): 

654 raise NameError( 

655 "Unexpected dependency '{0}' for module '{1}'.".format(name, module)) 

656 if name == "configparser": 

657 raise NameError( 

658 "Unexpected dependency '{0}' for module '{1}'.".format(name, module)) 

659 return True 

660 

661 res = [key for key in res if validate_module(key[0])] 

662 

663 if deep: 

664 done = {module: None} 

665 mod = 1 

666 while mod > 0: 

667 mod = 0 

668 for r in res: 

669 if r[0] not in done: 

670 if r[0].lower() < 'a' or r[0].lower() > 'zzzzzz' or r[0].endswith(";"): 

671 raise NameError( 

672 "A module has an unexpected name '{0}', r={1} " 

673 "when looking for dependencies of '{2}'.".format( 

674 r[0], r, module)) 

675 temp = get_module_dependencies( 

676 r[0], use_cmd=use_cmd, deep=deep, collapse=False, 

677 use_pip=use_pip, refresh_cache=refresh_cache) 

678 for key in temp: 

679 if key not in res: 

680 res.append(key) 

681 mod += 1 

682 done[r[0]] = None 

683 

684 if collapse: 

685 final = {} 

686 for name, version, required in res: 

687 if name not in final: 

688 final[name] = (version, [required]) 

689 else: 

690 ex = final[name][1] 

691 if required not in ex: 

692 ex.append(required) 

693 try: 

694 v = version_consensus(final[name][0], version) 

695 except WrongVersionError as e: 

696 raise WrongVersionError("unable to reconcile versions:\n{0}\n{1}".format( 

697 ex, str((name, version, required)))) from e 

698 final[name] = (v, ex) 

699 final = {k: (v[0], list(sorted(v[1]))) for k, v in final.items()} 

700 return final 

701 

702 return [(name, version.strip('()') if version is not None else version, required) 

703 for name, version, required in res] 

704 

705 

706def choose_most_recent(list_name): 

707 """ 

708 Chooses the most recent version for a list of module names. 

709 

710 @param list_name list of names 

711 @return most recent version or None if the input list is empty 

712 

713 In the following case, we would choose the first option:: 

714 

715 numpy-1.10.0+mkl-cp34-none-win_amd64.whl 

716 numpy-1.9.1.0+mkl-cp34-none-win_amd64.whl 

717 """ 

718 def find_wheel(tu): 

719 for t in tu: 

720 if ".whl" in t: 

721 return t 

722 raise ValueError("unable to find a wheel in {0}".format(tu)) 

723 if len(list_name) == 0: 

724 return None 

725 if isinstance(list_name[0], tuple): 

726 list_name = [(find_wheel(_), _) for _ in list_name] 

727 else: 

728 list_name = [(_, _) for _ in list_name] 

729 

730 versions = [re.compile(_) for _ in regex_wheel_versions] 

731 

732 def search_regex(_): 

733 resv = None 

734 for version in versions: 

735 try: 

736 resv = version.search(_[0]) 

737 except TypeError as e: 

738 raise TypeError("Unable to parse '{0}'".format(_)) from e 

739 if resv is not None: 

740 return resv 

741 raise MissingVersionWheelException( 

742 "Unable to get version number for module '{}':\nREGEX\n{}".format(_, "\n".join(regex_wheel_versions))) 

743 

744 list_name = [(search_regex(_).groups()[0], _[0], _[1]) 

745 for _ in list_name] 

746 

747 def cmp(el1, el2): 

748 return compare_version(el1[0], el2[0]) 

749 

750 list_name = list(sorted(list_name, key=functools.cmp_to_key(cmp))) 

751 return list_name[-1][-1] 

752 

753 

754def get_wheel_version(whlname): 

755 """ 

756 extract the version from a wheel file, 

757 return ``2.6.0`` for ``rpy2-2.6.0-cp34-none-win_amd64.whl`` 

758 

759 @param whlname file name 

760 @return string 

761 """ 

762 find = [] 

763 for reg in regex_wheel_versions: 

764 if len(find) == 0: 

765 exp = re.compile(reg) 

766 find = exp.findall(whlname) 

767 else: 

768 break 

769 if len(find) == 0: 

770 mes = "Unable to extract version for '{0}'\nREGEX\n{1}" 

771 raise ValueError(mes.format(whlname, "\n".join(regex_wheel_versions))) 

772 if len(find) > 1: 

773 mes = "Too many options for '{0}'\nOPTIONS\n{1}\nREGEX\n{2}" 

774 raise ValueError(mes.format( 

775 whlname, find, "\n".join(regex_wheel_versions))) 

776 if isinstance(find[0], tuple): 

777 return find[0][0] 

778 else: 

779 return find[0]