Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add basic "install from lock file" operation #3340

Merged
merged 2 commits into from
May 3, 2024
Merged

Conversation

BurntSushi
Copy link
Member

This PR principally adds a routine for converting a Lock to a
Resolution, where a Resolution is a map of package names pinned to
a specific version.

I'm not sure that a Resolution is ultimately what we want here (we
might need more stuff), but this was the quickest route I could find to
plug a Lock into our existing uv pip install infrastructure.

This commit also does a little refactoring of the Lock types. The
main thing is to permit extra state on some of the types (like a
by_id map on Lock for quick lookups of distributions) that aren't
included in the serialization format of a Lock. We achieve this
by defining separate Wire types that are automatically converted
to-and-from via serde.

Note that like with the lock file format types themselves, we leave a
few todo!() expressions around. The main idea is to get something
minimally working without spending too much effort here. (A fair bit
of refactoring will be required to generate a lock file, and it's
not clear how much this code will wind up needing to change anyway.)
In particular, we only handle the case of installing wheels from a
registry.

A demonstration of the full flow:

$ requirements.in
anyio
$ cargo run -p uv -- pip compile -p3.10 requirements.in --unstable-uv-lock-file
$ uv venv
$ cargo run -p uv -- pip install --unstable-uv-lock-file anyio -r requirements.in
Installed 5 packages in 7ms
 + anyio==4.3.0
 + exceptiongroup==1.2.1
 + idna==3.7
 + sniffio==1.3.1
 + typing-extensions==4.11.0

In order to install from a lock file, we start from the root and do a
breadth first traversal over its dependencies. We aren't yet filtering
on marker expressions (since they aren't in the lock file yet), but we
should be able to add that in the future. In so doing, the traversal
should select only the subset of distributions relevant for the current
platform.

/// Returns the distribution with the given name. If there are multiple
/// matching distributions, then an error is returned. If there are no
/// matching distributions, then `Ok(None)` is returned.
fn find_by_name(&self, name: &str) -> Result<Option<&Distribution>, String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use LockError rather than String?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only an internal helper for right now, and its only use asserts that is succeeds. I don't know that this belongs in LockError, and right now, it's only used to find the "root" package. And I expect all of that to change.

@@ -1308,6 +1308,9 @@ pub(crate) struct PipInstallArgs {
/// print the resulting plan.
#[arg(long)]
pub(crate) dry_run: bool,

#[arg(long, hide = true)]
pub(crate) unstable_uv_lock_file: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we call this unstable_lock_file (omitting uv)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It matches the name I added in: https://github.com/astral-sh/uv/pull/3314/files#diff-ae55eef503096c43098e973865bd5ed612871dcd1d3d335631a8027deb3828d3R605

This flag is hidden, undocumented and intended to be temporary. Its only purpose is to expose "install from uv.lock" on the CLI so that we can interact with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly just have a strong dislike for using uv in things (outside of crate names, since it's roughly a namespace), like structs or arguments. But not a big deal if it's temporary.

eprint!("{report:?}");
return Ok(ExitStatus::Failure);
let resolution = if let Some(ref root) = uv_lock {
let encoded = fs::tokio::read_to_string("uv.lock").await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this assumes that it's in the current working directory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's the simplest thing to do I think. I expect this code to be eventually deleted. (Like, I don't expect uv pip install should ever read a uv.lock file.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok agreed.

root_name: &str,
) -> Resolution {
let root = self
.find_by_name(root_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could / should this just be a special package named root?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that privileging the notion of "root" in the lock file makes sense, particularly given that we want to have workspace support. Cargo went in the same direction:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok agreed. This makes more sense now that I see that this isn't ever intended to be part of pip install, but rather, something that stems from a known project root.

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about the root concept, and why we need to pass anyio as an argument to --unstable-lock. What's the eventual vision there?

@charliermarsh
Copy link
Member

Separately, what are the expected semantics for pip install with a lockfile? Does the lockfile get regenerated? Should we limit it to pip sync for now for simplicity?

@BurntSushi
Copy link
Member Author

@charliermarsh

I'm a little confused about the root concept, and why we need to pass anyio as an argument to --unstable-lock. What's the eventual vision there?

Hmm, so I added this as a TODO comment in the source:

            // TODO: In the future, we should derive the root distribution
            // from the pyproject.toml, but I don't think the infrastructure
            // for that is in place yet. For now, we ask the caller to specify
            // the root package name explicitly, and we assume here that it is
            // correct.

The basic idea here is that the lock file should contain an entry for each package in the current uv workspace. That concept doesn't really exist yet as far as I know, so those packages do not yet get added to the lock file. So we need some other way to pick the root. Right now, we just ask for it. But this is the sort of thing we should be auto-detecting by reading the pyproject.toml.

Separately, what are the expected semantics for pip install with a lockfile? Does the lockfile get regenerated? Should we limit it to pip sync for now for simplicity?

I'm not sure there are any expected semantics for pip install with uv.lock. It's not an obviously well defined operation to me. It's only here now as a temporary way of exposing interaction with uv.lock.

BurntSushi added 2 commits May 3, 2024 07:56
This commit adds a routine for converting a `Lock` to a
`Resolution`, where a `Resolution` is a map of package names
pinned to a specific version.

I'm not sure that a `Resolution` is ultimately what we want
here (we might need more stuff), but this was the quickest
route I could find to plug a `Lock` into the existing `pip install`
infrastructure.

This commit also does a little refactoring of the `Lock` types.
The main thing is to permit extra state on some of the types
(like a `by_id` map on `Lock` for quick lookups of distributions)
that aren't included in the serialization format of a `Lock`.
We achieve this by defining separate `Wire` types that are
automatically converted to-and-from via `serde`.

Note like with the lock file format types themselves, we leave
a few `todo!()` expressions around. The main idea is to get
something minimally working without spending too much effort
here. (A fair bit of refactoring will be required to generate
a lock file, and it's not clear how much this code will wind up
needing to change anyway.) In particular, we only handle the case
of installing wheels from a registry.
Like the similar flag on `uv pip compile`, this is meant to be
a temporary toggle to expose "install from lock file" on the CLI.
We expect to remove this eventually.

The larger diff for `pip_install.rs` is mostly just due to moving
some pieces together that we specifically don't want to run (like
resolution) when installing from a lock file.
@BurntSushi BurntSushi merged commit 7772e62 into main May 3, 2024
43 checks passed
@BurntSushi BurntSushi deleted the ag/lock-install branch May 3, 2024 12:18
@BurntSushi BurntSushi added the preview Experimental behavior label May 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
preview Experimental behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants