Skip to content

Commit

Permalink
apply constraints in transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
bollwyvl committed Jan 24, 2025
1 parent 64bbbe8 commit e50f58c
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 7 deletions.
26 changes: 20 additions & 6 deletions docs/project/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
29 changes: 28 additions & 1 deletion micropip/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 62 in micropip/transaction.py

View check run for this annotation

Codecov / codecov/patch

micropip/transaction.py#L51-L62

Added lines #L51 - L62 were not covered by tests

async def gather_requirements(
self,
requirements: list[str] | list[Requirement],
Expand Down Expand Up @@ -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

Check warning on line 112 in micropip/transaction.py

View check run for this annotation

Codecov / codecov/patch

micropip/transaction.py#L111-L112

Added lines #L111 - L112 were not covered by tests
return req

async def add_requirement_inner(
self,
req: Requirement,
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit e50f58c

Please sign in to comment.