diff --git a/docs/project/usage.md b/docs/project/usage.md index 201a058..f968b5d 100644 --- a/docs/project/usage.md +++ b/docs/project/usage.md @@ -94,17 +94,31 @@ await micropip.install("pkg", deps=False) ### Constraining indirect dependencies -Dependency resolution can be further customized with optional `constraints`: these -provide the versions (or URLs of wheels) +Dependency resolution can be further customized with optional `constraints`: as +described in the [`pip`](https://pip.pypa.io/en/stable/user_guide/#constraints-files) +documentation, these must provide a name and version (or URL), and may not request +`[extras]`. ```python -await micropip.install("pkg", constraints=["other-pkg ==0.1.1"]) +await micropip.install( + "pkg", + constraints=[ + "other-pkg ==0.1.1", + "some-other-pkg <2", + "yet-another-pkg@https://example.com/yet_another_pkg-0.1.2-py3-none-any.whl", + # invalid examples # why? + # yet_another_pkg-0.1.2-py3-none-any.whl # missing name + # something-completely[different] ==0.1.1 # extras + # package-with-no-version # missing version or URL + ] +) ``` -Default `constraints` may be provided to be used by all subsequent calls to -`micropip.install`: +`micropip.set_constraints` replaces any default constraints for all subsequent +calls to `micropip.install` that don't specify constraints: ```python micropip.set_constraints = ["other-pkg ==0.1.1"] -await micropip.install("pkg") +await micropip.install("pkg") # uses defaults +await micropip.install("another-pkg", constraints=[]) # ignores defaults ``` diff --git a/micropip/transaction.py b/micropip/transaction.py index c962396..3b0876f 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -38,13 +38,29 @@ class Transaction: verbose: bool | int | None = None constraints: list[str] | None = None - def __post_init__(self): + def __post_init__(self) -> None: # If index_urls is None, pyodide-lock.json have to be searched first. # TODO: when PyPI starts to support hosting WASM wheels, this might be deleted. self.search_pyodide_lock_first = ( self.index_urls == package_index.DEFAULT_INDEX_URLS ) + self.constrained_reqs: dict[str, Requirement] = {} + + for constraint in self.constraints or []: + con = Requirement(constraint) + if not con.name: + logger.debug("Transaction: discarding nameless constraint: %s", con) + continue + if con.extras: + logger.debug("Transaction: discarding [extras] constraint: %s", con) + continue + if not con.url or len(con.specifier): + logger.debug("Transaction: discarding versionless constraint: %s", con) + continue + con.name = canonicalize_name(con.name) + self.constrained_reqs[con.name] = con + async def gather_requirements( self, requirements: list[str] | list[Requirement], @@ -88,6 +104,14 @@ def check_version_satisfied(self, req: Requirement) -> tuple[bool, str]: f"Requested '{req}', " f"but {req.name}=={ver} is already installed" ) + def constrain_requirement(self, req: Requirement) -> Requirement: + """Provide a constrained requirement, if available, or the original.""" + constrained_req = self.constrained_reqs.get(canonicalize_name(req.name)) + if constrained_req: + logger.debug("Transaction: %s constrained to %s", req, constrained_req) + return constrained_req + return req + async def add_requirement_inner( self, req: Requirement, @@ -100,6 +124,8 @@ async def add_requirement_inner( for e in req.extras: self.ctx_extras.append({"extra": e}) + req = self.constrain_requirement(req) + if self.pre: req.specifier.prereleases = True @@ -136,6 +162,7 @@ def eval_marker(e: dict[str, str]) -> bool: eval_marker(e) for e in self.ctx_extras ): return + # Is some version of this package is already installed? req.name = canonicalize_name(req.name)