Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions micropip/_compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

LOCKFILE_PACKAGES = compatibility_layer.lockfile_packages

lockfile_base_url = compatibility_layer.lockfile_base_url

fetch_bytes = compatibility_layer.fetch_bytes

fetch_string_and_headers = compatibility_layer.fetch_string_and_headers
Expand All @@ -40,4 +42,5 @@
"loadedPackages",
"loadPackage",
"to_js",
"lockfile_base_url",
]
4 changes: 3 additions & 1 deletion micropip/_compat/_compat_in_pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

try:
import pyodide_js
from pyodide_js import loadedPackages, loadPackage
from pyodide_js import loadedPackages, loadPackage, lockfileBaseUrl
from pyodide_js._api import ( # type: ignore[import]
install,
loadBinaryFile,
Expand Down Expand Up @@ -58,3 +58,5 @@ async def fetch_string_and_headers(
lockfile_info = LOCKFILE_INFO

lockfile_packages = LOCKFILE_PACKAGES

lockfile_base_url = lockfileBaseUrl
2 changes: 2 additions & 0 deletions micropip/_compat/_compat_not_in_pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ def to_js(
lockfile_info = {}

lockfile_packages = {}

lockfile_base_url = None
2 changes: 2 additions & 0 deletions micropip/_compat/compatibility_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def to_py():

lockfile_packages: dict[str, dict[str, Any]]

lockfile_base_url: str | None = None

@staticmethod
@abstractmethod
async def fetch_bytes(url: str, kwargs: dict[str, str]) -> bytes:
Expand Down
27 changes: 24 additions & 3 deletions micropip/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,31 @@
from copy import deepcopy
from importlib.metadata import Distribution
from typing import Any
from urllib.parse import urljoin

from ._utils import get_dist_info
from ._vendored.packaging.src.packaging.requirements import Requirement
from ._vendored.packaging.src.packaging.utils import canonicalize_name


def freeze_lockfile(
lockfile_packages: dict[str, dict[str, Any]], lockfile_info: dict[str, str]
lockfile_packages: dict[str, dict[str, Any]],
lockfile_info: dict[str, str],
lockfile_base_url: str | None = None,
) -> str:
return json.dumps(freeze_data(lockfile_packages, lockfile_info))
return json.dumps(freeze_data(lockfile_packages, lockfile_info, lockfile_base_url))


def freeze_data(
lockfile_packages: dict[str, dict[str, Any]], lockfile_info: dict[str, str]
lockfile_packages: dict[str, dict[str, Any]],
lockfile_info: dict[str, str],
lockfile_base_url: str | None = None,
) -> dict[str, Any]:
packages = deepcopy(lockfile_packages)
packages.update(load_pip_packages(lockfile_packages))
if lockfile_base_url is not None:
# Override the base URL for the packages
override_base_url(packages, lockfile_base_url)

# Sort
packages = dict(sorted(packages.items()))
Expand All @@ -30,6 +38,19 @@ def freeze_data(
}


def override_base_url(
lockfile_packages: dict[str, dict[str, Any]],
lockfile_base_url: str,
):
"""
Updates the relative URLs in the lockfile packages to absolute URLs by appending the base URL.
This assures that when the generated lockfile is deployed separately from the packages,
the URLs will still point to the correct location.
"""
for pkg in lockfile_packages.values():
pkg["file_name"] = urljoin(lockfile_base_url, pkg["file_name"])


def load_pip_packages(
lockfile_packages: dict[str, dict[str, Any]],
) -> Iterator[tuple[str, dict[str, Any]]]:
Expand Down
4 changes: 3 additions & 1 deletion micropip/package_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ def freeze(self) -> str:
``lockFileURL`` of :js:func:`~globalThis.loadPyodide`.
"""
return freeze_lockfile(
self.compat_layer.lockfile_packages, self.compat_layer.lockfile_info
self.compat_layer.lockfile_packages,
self.compat_layer.lockfile_info,
self.compat_layer.lockfile_base_url,
)

def add_mock_package(
Expand Down
71 changes: 71 additions & 0 deletions tests/test_freeze.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from conftest import mock_fetch_cls
from pytest_pyodide import run_in_pyodide


@pytest.mark.asyncio
Expand Down Expand Up @@ -102,3 +103,73 @@ def test_freeze_lockfile_compat(
assert package.install_dir == "site"
assert not package.unvendored_tests
assert package.version == wheel.version


def test_override_base_url():
from micropip.freeze import override_base_url

lockfile_packages = {
"pkg1": {"file_name": "pkg1-1.0.0-py3-none-any.whl"},
"pkg2": {"file_name": "pkg2-2.0.0-py3-none-any.whl"},
"pkg3": {"file_name": "https://other.com/pkg3-3.0.0-py3-none-any.whl"},
}
base_url = "https://example.com/packages/"

override_base_url(lockfile_packages, base_url)

assert (
lockfile_packages["pkg1"]["file_name"]
== "https://example.com/packages/pkg1-1.0.0-py3-none-any.whl"
)
assert (
lockfile_packages["pkg2"]["file_name"]
== "https://example.com/packages/pkg2-2.0.0-py3-none-any.whl"
)
assert (
lockfile_packages["pkg3"]["file_name"]
== "https://other.com/pkg3-3.0.0-py3-none-any.whl"
)


def test_url_after_freeze_pyodide(selenium_standalone_micropip):

@run_in_pyodide
def _run(selenium, prefix):
import json

from pyodide_js import lockfileBaseUrl
from pyodide_js._api import lockfile_packages

import micropip

new_lockfile_str = micropip.freeze()
new_lockfile_packages = json.loads(new_lockfile_str)["packages"]

orig_lockfile_packages = lockfile_packages.to_py()

for orig_pkg_name, orig_pkg in orig_lockfile_packages.items():
assert orig_pkg_name in new_lockfile_packages

new_pkg = new_lockfile_packages[orig_pkg_name]

assert new_pkg["name"] == orig_pkg["name"]
assert new_pkg["version"] == orig_pkg["version"]
assert new_pkg["sha256"] == orig_pkg["sha256"]
assert new_pkg["imports"] == orig_pkg["imports"]
assert new_pkg["depends"] == orig_pkg["depends"]
assert new_pkg["install_dir"] == orig_pkg["install_dir"]
assert new_pkg["unvendored_tests"] == orig_pkg["unvendored_tests"]

# original lockfile will have relative URLs
# TODO: this might change later if packages are served from PyPI
assert not orig_pkg["file_name"].startswith(prefix)

# new lockfile should have absolute URLs
assert new_pkg["file_name"].startswith(prefix)
assert new_pkg["file_name"].startswith(lockfileBaseUrl)

assert orig_pkg["file_name"] in new_pkg["file_name"]

selenium = selenium_standalone_micropip
prefix = ("/",) if selenium.browser == "node" else ("http://", "https://")
_run(selenium, prefix)