def test_schemes(self): cases = ( ('normalized', (_normalized_key, NV, NM)), ('legacy', (_legacy_key, LV, LM)), ('semantic', (_semantic_key, SV, SM)), ) for name, values in cases: scheme = get_scheme(name) key, version, matcher = values self.assertIs(key, scheme.key) self.assertIs(matcher, scheme.matcher) self.assertIs(version, scheme.matcher.version_class) self.assertIs(get_scheme('default'), get_scheme('normalized')) self.assertRaises(ValueError, get_scheme, 'random')
class _PackageManager: version_scheme = version.get_scheme("normalized") def __init__(self): self.builtin_packages = {} self.builtin_packages.update(js_pyodide._module.packages.dependencies) self.installed_packages = {} def install( self, requirements: Union[str, List[str]], ctx=None, wheel_installer=None, resolve=_nullop, reject=_nullop, ): try: if ctx is None: ctx = {"extra": None} if wheel_installer is None: wheel_installer = _RawWheelInstaller() complete_ctx = dict(markers.DEFAULT_CONTEXT) complete_ctx.update(ctx) if isinstance(requirements, str): requirements = [requirements] transaction: Dict[str, Any] = { "wheels": [], "pyodide_packages": set(), "locked": dict(self.installed_packages), } for requirement in requirements: self.add_requirement(requirement, complete_ctx, transaction) except Exception as e: reject(str(e)) resolve_count = [len(transaction["wheels"])] def do_resolve(*args): resolve_count[0] -= 1 if resolve_count[0] == 0: resolve(f'Installed {", ".join(self.installed_packages.keys())}') # Install built-in packages pyodide_packages = transaction["pyodide_packages"] if len(pyodide_packages): resolve_count[0] += 1 self.installed_packages.update(dict((k, None) for k in pyodide_packages)) js_pyodide.loadPackage(list(pyodide_packages)).then(do_resolve) # Now install PyPI packages for name, wheel, ver in transaction["wheels"]: wheel_installer(name, wheel, do_resolve, reject) self.installed_packages[name] = ver def add_requirement(self, requirement: str, ctx, transaction): if requirement.startswith(("http://", "https://")): # custom download location name, wheel, version = _parse_wheel_url(requirement) transaction["wheels"].append((name, wheel, version)) return req = util.parse_requirement(requirement) # If it's a Pyodide package, use that instead of the one on PyPI if req.name in self.builtin_packages: transaction["pyodide_packages"].add(req.name) return if req.marker: if not markers.evaluator.evaluate(req.marker, ctx): return matcher = self.version_scheme.matcher(req.requirement) # If we already have something that will work, don't # fetch again for name, ver in transaction["locked"].items(): if name == req.name: if matcher.match(ver): break else: raise ValueError( f"Requested '{requirement}', " f"but {name}=={ver} is already installed" ) else: metadata = _get_pypi_json(req.name) wheel, ver = self.find_wheel(metadata, req) transaction["locked"][req.name] = ver recurs_reqs = metadata.get("info", {}).get("requires_dist") or [] for recurs_req in recurs_reqs: self.add_requirement(recurs_req, ctx, transaction) transaction["wheels"].append((req.name, wheel, ver)) def find_wheel(self, metadata, req): releases = [] for ver, files in metadata.get("releases", {}).items(): ver = self.version_scheme.suggest(ver) if ver is not None: releases.append((ver, files)) releases = sorted(releases, reverse=True) matcher = self.version_scheme.matcher(req.requirement) for ver, meta in releases: if matcher.match(ver): for fileinfo in meta: if fileinfo["filename"].endswith("py3-none-any.whl"): return fileinfo, ver raise ValueError(f"Couldn't find a pure Python 3 wheel for '{req.requirement}'")
class _PackageManager: version_scheme = version.get_scheme("normalized") def __init__(self): self.builtin_packages = {} self.builtin_packages.update( js_pyodide._module.packages.dependencies.object_entries()) self.installed_packages = {} async def install(self, requirements: Union[str, List[str]], ctx=None): if ctx is None: ctx = {"extra": None} complete_ctx = dict(markers.DEFAULT_CONTEXT) complete_ctx.update(ctx) if isinstance(requirements, str): requirements = [requirements] transaction: Dict[str, Any] = { "wheels": [], "pyodide_packages": set(), "locked": dict(self.installed_packages), } requirement_promises = [] for requirement in requirements: requirement_promises.append( self.add_requirement(requirement, complete_ctx, transaction)) await gather(*requirement_promises) wheel_promises = [] # Install built-in packages pyodide_packages = transaction["pyodide_packages"] if len(pyodide_packages): # Note: branch never happens in out-of-browser testing because we # report that all dependencies are empty. self.installed_packages.update( dict((k, None) for k in pyodide_packages)) wheel_promises.append( js_pyodide.loadPackage(list(pyodide_packages))) # Now install PyPI packages for name, wheel, ver in transaction["wheels"]: wheel_promises.append(_install_wheel(name, wheel)) self.installed_packages[name] = ver await gather(*wheel_promises) return f'Installed {", ".join(self.installed_packages.keys())}' async def add_requirement(self, requirement: str, ctx, transaction): if requirement.endswith(".whl"): # custom download location name, wheel, version = _parse_wheel_url(requirement) transaction["wheels"].append((name, wheel, version)) return req = util.parse_requirement(requirement) # If it's a Pyodide package, use that instead of the one on PyPI if req.name in self.builtin_packages: transaction["pyodide_packages"].add(req.name) return if req.marker: if not markers.evaluator.evaluate(req.marker, ctx): return matcher = self.version_scheme.matcher(req.requirement) # If we already have something that will work, don't # fetch again for name, ver in transaction["locked"].items(): if name == req.name: if matcher.match(ver): break else: raise ValueError(f"Requested '{requirement}', " f"but {name}=={ver} is already installed") else: metadata = await _get_pypi_json(req.name) wheel, ver = self.find_wheel(metadata, req) transaction["locked"][req.name] = ver recurs_reqs = metadata.get("info", {}).get("requires_dist") or [] for recurs_req in recurs_reqs: await self.add_requirement(recurs_req, ctx, transaction) transaction["wheels"].append((req.name, wheel, ver)) def find_wheel(self, metadata, req): releases = [] for ver, files in metadata.get("releases", {}).items(): ver = self.version_scheme.suggest(ver) if ver is not None: releases.append((ver, files)) def version_number(release): return version.NormalizedVersion(release[0]) releases = sorted(releases, key=version_number, reverse=True) matcher = self.version_scheme.matcher(req.requirement) for ver, meta in releases: if matcher.match(ver): for fileinfo in meta: if fileinfo["filename"].endswith("py3-none-any.whl"): return fileinfo, ver raise ValueError( f"Couldn't find a pure Python 3 wheel for '{req.requirement}'")