diff --git a/micropip/_utils.py b/micropip/_utils.py index d956fd1..9abab9d 100644 --- a/micropip/_utils.py +++ b/micropip/_utils.py @@ -1,10 +1,8 @@ import functools -import json from importlib.metadata import Distribution from pathlib import Path from sysconfig import get_config_var, get_platform -from ._compat import LOCKFILE_PACKAGES from ._vendored.packaging.src.packaging.requirements import ( InvalidRequirement, Requirement, @@ -202,82 +200,6 @@ def platform_to_version(platform: str) -> str: raise ValueError(f"Wheel interpreter version '{tag.interpreter}' is not supported.") -def fix_package_dependencies( - package_name: str, *, extras: list[str | None] | None = None -) -> None: - """Check and fix the list of dependencies for this package - - If you have manually installed a package and dependencies from wheels, - the dependencies will not be correctly setup in the package list - or the pyodide lockfile generated by freezing. This method checks - if the dependencies are correctly set in the package list and will - add missing dependencies. - - Parameters - ---------- - package_name (string): - The name of the package to check. - - extras (list): - List of extras for this package. - - """ - if package_name in LOCKFILE_PACKAGES: - # don't check things that are in original repository - return - - dist = Distribution.from_name(package_name) - - package_requires = dist.requires - if package_requires is None: - # no dependencies - we're good to go - return - - url = dist.read_text("PYODIDE_URL") - - # If it wasn't installed with micropip / pyodide, then we - # can't do anything with it. - if url is None: - return - - # Get current list of pyodide requirements - requires = dist.read_text("PYODIDE_REQUIRES") - - if requires: - depends = json.loads(requires) - else: - depends = [] - - if extras is None: - extras = [None] - else: - extras = extras + [None] - for r in package_requires: - req = Requirement(r) - req_extras = req.extras - req_marker = req.marker - req_name = canonicalize_name(req.name) - needs_requirement = False - if req_marker is not None: - for e in extras: - if req_marker.evaluate(None if e is None else {"extra": e}): - needs_requirement = True - break - else: - needs_requirement = True - - if needs_requirement: - fix_package_dependencies(req_name, extras=list(req_extras)) - - if req_name not in depends: - depends.append(req_name) - - # write updated depends to PYODIDE_DEPENDS - (get_dist_info(dist) / "PYODIDE_REQUIRES").write_text( - json.dumps(sorted(x for x in depends)) - ) - - def validate_constraints( constraints: list[str] | None, environment: dict[str, str] | None = None, diff --git a/micropip/freeze.py b/micropip/freeze.py index caa8dfb..11253d7 100644 --- a/micropip/freeze.py +++ b/micropip/freeze.py @@ -2,9 +2,11 @@ import json from collections.abc import Iterator from copy import deepcopy +from importlib.metadata import Distribution from typing import Any -from ._utils import fix_package_dependencies +from ._utils import get_dist_info +from ._vendored.packaging.src.packaging.requirements import Requirement from ._vendored.packaging.src.packaging.utils import canonicalize_name @@ -18,7 +20,7 @@ def freeze_data( lockfile_packages: dict[str, dict[str, Any]], lockfile_info: dict[str, str] ) -> dict[str, Any]: packages = deepcopy(lockfile_packages) - packages.update(load_pip_packages()) + packages.update(load_pip_packages(lockfile_packages)) # Sort packages = dict(sorted(packages.items())) @@ -28,11 +30,13 @@ def freeze_data( } -def load_pip_packages() -> Iterator[tuple[str, dict[str, Any]]]: - return map( - package_item, - filter(is_valid, map(load_pip_package, importlib.metadata.distributions())), - ) +def load_pip_packages( + lockfile_packages: dict[str, dict[str, Any]] +) -> Iterator[tuple[str, dict[str, Any]]]: + distributions = importlib.metadata.distributions() + pip_packages = [load_pip_package(dist, lockfile_packages) for dist in distributions] + + return (package_item(pkg) for pkg in pip_packages if is_valid(pkg)) def package_item(entry: dict[str, Any]) -> tuple[str, dict[str, Any]]: @@ -43,7 +47,10 @@ def is_valid(entry: dict[str, Any]) -> bool: return entry["file_name"] is not None -def load_pip_package(dist: importlib.metadata.Distribution) -> dict[str, Any]: +def load_pip_package( + dist: importlib.metadata.Distribution, + lockfile_packages: dict[str, dict[str, Any]], +) -> dict[str, Any]: name = dist.name version = dist.version url = dist.read_text("PYODIDE_URL") @@ -51,7 +58,7 @@ def load_pip_package(dist: importlib.metadata.Distribution) -> dict[str, Any]: imports = (dist.read_text("top_level.txt") or "").split() requires = dist.read_text("PYODIDE_REQUIRES") if not requires: - fix_package_dependencies(name) + fix_package_dependencies(name, lockfile_packages) requires = dist.read_text("PYODIDE_REQUIRES") depends = json.loads(requires or "[]") @@ -64,3 +71,85 @@ def load_pip_package(dist: importlib.metadata.Distribution) -> dict[str, Any]: imports=imports, depends=depends, ) + + +def fix_package_dependencies( + package_name: str, + lockfile_packages: dict[str, dict[str, Any]] | None = None, + *, + extras: list[str | None] | None = None, +) -> None: + """Check and fix the list of dependencies for this package + + If you have manually installed a package and dependencies from wheels, + the dependencies will not be correctly setup in the package list + or the pyodide lockfile generated by freezing. This method checks + if the dependencies are correctly set in the package list and will + add missing dependencies. + + Parameters + ---------- + package_name (string): + The name of the package to check. + + lockfile_packages (dict): + The lockfile packages to check against. + + extras (list): + List of extras for this package. + + """ + if lockfile_packages and package_name in lockfile_packages: + # don't check things that are in original repository + return + + dist = Distribution.from_name(package_name) + + package_requires = dist.requires + if package_requires is None: + # no dependencies - we're good to go + return + + url = dist.read_text("PYODIDE_URL") + + # If it wasn't installed with micropip / pyodide, then we + # can't do anything with it. + if url is None: + return + + # Get current list of pyodide requirements + requires = dist.read_text("PYODIDE_REQUIRES") + + if requires: + depends = json.loads(requires) + else: + depends = [] + + if extras is None: + extras = [None] + else: + extras = extras + [None] + for r in package_requires: + req = Requirement(r) + req_extras = req.extras + req_marker = req.marker + req_name = canonicalize_name(req.name) + needs_requirement = False + if req_marker is not None: + for e in extras: + if req_marker.evaluate(None if e is None else {"extra": e}): + needs_requirement = True + break + else: + needs_requirement = True + + if needs_requirement: + fix_package_dependencies(req_name, extras=list(req_extras)) + + if req_name not in depends: + depends.append(req_name) + + # write updated depends to PYODIDE_DEPENDS + (get_dist_info(dist) / "PYODIDE_REQUIRES").write_text( + json.dumps(sorted(x for x in depends)) + )