Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## [0.10.1] - 2024/07/05
## [0.11.0] - 2025/10/XX

### Added

- `micropip.install` now supports extras in custom download locations through the `pkg[extras] @ https://example.com/pkg-1.0.0-py3-none-any.whl` syntax.
[#257](https://github.com/pyodide/micropip/pull/257)

## [0.10.1] - 2025/07/05

### Fixed

- `micropip.freeze()` now updates the URLs inside the lockfile to absolute URLs.
This behavior is consistent with the `lockfileURL` behavior change in Pyodide 0.28.0.
[#241](https://github.com/pyodide/micropip/pull/241)

## [0.10.0] - 2024/07/02
## [0.10.0] - 2025/07/02

### Added

Expand All @@ -35,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
when one index URL fails to find a package, and will fallback to the next index URL.
[#225](https://github.com/pyodide/micropip/pull/225)

## [0.9.0] - 2024/02/01
## [0.9.0] - 2025/02/01

### Fixed

Expand Down
39 changes: 25 additions & 14 deletions micropip/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,28 @@ async def add_requirement(self, req: str | Requirement) -> None:

try:
as_req = constrain_requirement(Requirement(req), self.constrained_reqs)
except InvalidRequirement:
as_req = None

if as_req:
if as_req.name and len(as_req.specifier):
return await self.add_requirement_inner(as_req)
if as_req.url:
req = as_req.url
if as_req.name.endswith(".whl"):
# This happens when the requirement is passed as a relative URL:
# For instance, micropip.install("pkg-1.0.0-py3-none-any.whl")
# packaging cannot distinguish this is a relative URL or a very weird wheel name ... sigh ...
# But we want to treat it as a custom download location.
return await self.add_requirement_from_url(req)

return await self.add_requirement_inner(as_req)
except InvalidRequirement:
if not urlparse(req).path.endswith(".whl"):
raise

if urlparse(req).path.endswith(".whl"):
# custom download location
wheel = WheelInfo.from_url(req)
check_compatible(wheel.filename)
return await self.add_wheel(wheel, extras=set(), specifier="")
# custom download location, for instance, micropip.install("https://example.com/pkg-1.0.0-py3-none-any.whl")
return await self.add_requirement_from_url(req)

return await self.add_requirement_inner(Requirement(req))
async def add_requirement_from_url(
self, req: str, extras: set[str] | None = None
) -> None:
wheel = WheelInfo.from_url(req)
check_compatible(wheel.filename)
return await self.add_wheel(wheel, extras=extras or set(), specifier="")

def check_version_satisfied(
self, req: Requirement, *, allow_reinstall: bool = False
Expand Down Expand Up @@ -138,7 +144,7 @@ def check_version_satisfied(
"or micropip.uninstall(...) to uninstall the package first."
)

async def add_requirement_inner(
async def add_requirement_inner( # noqa: C901
self,
req: Requirement,
) -> None:
Expand Down Expand Up @@ -201,6 +207,11 @@ def eval_marker(e: dict[str, str]) -> bool:
logger.info("Requirement already satisfied: %s (%s)", req, ver)
return

if req.url:
# custom download location, for instance, micropip.install("pkg @ https://example.com/pkg-1.0.0-py3-none-any.whl")
# in this case, we don't need to search the index_urls or pyodide lock file.
return await self.add_requirement_from_url(req.url, extras=req.extras)

try:
if self.search_pyodide_lock_first:
if await self._add_requirement_from_pyodide_lock(req):
Expand Down
29 changes: 29 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,32 @@ async def _run(selenium):
assert (dist_dir / "PYODIDE_SHA256").exists()

_run(selenium_standalone_micropip)


@integration_test_only
def test_install_url_based_wheel(selenium_standalone_micropip, pytestconfig):
# Dependencies of URL based wheels are fetched from PyPI.
# It is tricky to test this without accessing PyPI, hence integration test
@run_in_pyodide
async def _run(selenium, url):
import micropip

await micropip.install(f"typer @ {url}")

try:
import rich
except ModuleNotFoundError:
pass
else:
raise Exception("Should raise!")

await micropip.uninstall("typer")

await micropip.install(f"typer[all] @ {url}")

import rich # noqa: F401
import typer # noqa: F401

# typer 0.10.0 has "[all]" dependency that comes with colorama, shellingham, and rich
typer_0_10_0_url = "https://files.pythonhosted.org/packages/d9/07/8100c125307a26f03c305764f22cd995ae1878071ddf1df3588add73b53c/typer-0.10.0-py3-none-any.whl"
_run(selenium_standalone_micropip, typer_0_10_0_url)