Skip to content

Commit

Permalink
uv-resolver: add initial version of universal lock file format (astra…
Browse files Browse the repository at this point in the history
…l-sh#3314)

This is meant to be a base on which to build. There are some parts
which are implicitly incomplete and others which are explicitly
incomplete. The latter are indicated by TODO comments.

Here is a non-exhaustive list of incomplete things. In many cases, these
are incomplete simply because the data isn't present in a
`ResolutionGraph`. Future work will need to refactor our resolver so
that this data is correctly passed down.

* Not all wheels are included. Only the "selected" wheel for the current
  distribution is included.
* Marker expressions are always absent.
* We don't emit hashes for certainly kinds of distributions (direct
  URLs, git, and path).
* We don't capture git information from a dependency specification.
  Right now, we just always emit "default branch."

There are perhaps also other changes we might want to make to the format
of a more cosmetic nature. Right now, all arrays are encoded using
whatever the `toml` crate decides to do. But we might want to exert more
control over this. For example, by using inline tables or squashing more
things into strings (like I did for `Source` and `Hash`). I think the
main trade-off here is that table arrays are somewhat difficult to read
(especially without indentation), where as squashing things down into a
more condensed format potentially makes future compatible additions
harder.

I also went pretty light on the documentation here than what I would
normally do. That's primarily because I think this code is going to
go through some evolution and I didn't want to spend too much time
documenting something that is likely to change.

Finally, here's an example of the lock file format in TOML for the
`anyio` dependency. I generated it with the following command:

```
cargo run -p uv -- pip compile -p3.10 ~/astral/tmp/reqs/anyio.in --unstable-uv-lock-file
```

And that writes out a `uv.lock` file:

```toml
version = 1

[[distribution]]
name = "anyio"
version = "4.3.0"
source = "registry+https://pypi.org/simple"

[[distribution.wheel]]
url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl"
hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"

[[distribution.dependencies]]
name = "exceptiongroup"
version = "1.2.1"
source = "registry+https://pypi.org/simple"

[[distribution.dependencies]]
name = "idna"
version = "3.7"
source = "registry+https://pypi.org/simple"

[[distribution.dependencies]]
name = "sniffio"
version = "1.3.1"
source = "registry+https://pypi.org/simple"

[[distribution.dependencies]]
name = "typing-extensions"
version = "4.11.0"
source = "registry+https://pypi.org/simple"

[[distribution]]
name = "exceptiongroup"
version = "1.2.1"
source = "registry+https://pypi.org/simple"

[[distribution.wheel]]
url = "https://files.pythonhosted.org/packages/01/90/79fe92dd413a9cab314ef5c591b5aa9b9ba787ae4cadab75055b0ae00b33/exceptiongroup-1.2.1-py3-none-any.whl"
hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"

[[distribution]]
name = "idna"
version = "3.7"
source = "registry+https://pypi.org/simple"

[[distribution.wheel]]
url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl"
hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"

[[distribution]]
name = "sniffio"
version = "1.3.1"
source = "registry+https://pypi.org/simple"

[[distribution.wheel]]
url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl"
hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"

[[distribution]]
name = "typing-extensions"
version = "4.11.0"
source = "registry+https://pypi.org/simple"

[[distribution.wheel]]
url = "https://files.pythonhosted.org/packages/01/f3/936e209267d6ef7510322191003885de524fc48d1b43269810cd589ceaf5/typing_extensions-4.11.0-py3-none-any.whl"
hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
```
  • Loading branch information
BurntSushi authored Apr 29, 2024
1 parent a3b61a2 commit d2e7c05
Show file tree
Hide file tree
Showing 10 changed files with 890 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions crates/distribution-types/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,52 @@ pub enum FileLocation {
Path(#[with(rkyv::with::AsString)] PathBuf),
}

impl FileLocation {
/// Convert this location to a URL.
///
/// A relative URL has its base joined to the path. An absolute URL is
/// parsed as-is. And a path location is turned into a URL via the `file`
/// protocol.
///
/// # Errors
///
/// This returns an error if any of the URL parsing fails, or if, for
/// example, the location is a path and the path isn't valid UTF-8.
/// (Because URLs must be valid UTF-8.)
pub fn to_url(&self) -> Result<Url, ToUrlError> {
match *self {
FileLocation::RelativeUrl(ref base, ref path) => {
let base_url = Url::parse(base).map_err(|err| ToUrlError::InvalidBase {
base: base.clone(),
err,
})?;
let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin {
base: base.clone(),
path: path.clone(),
err,
})?;
Ok(joined)
}
FileLocation::AbsoluteUrl(ref absolute) => {
let url = Url::parse(absolute).map_err(|err| ToUrlError::InvalidAbsolute {
absolute: absolute.clone(),
err,
})?;
Ok(url)
}
FileLocation::Path(ref path) => {
let path = path
.to_str()
.ok_or_else(|| ToUrlError::PathNotUtf8 { path: path.clone() })?;
let url = Url::from_file_path(path).map_err(|()| ToUrlError::InvalidPath {
path: path.to_string(),
})?;
Ok(url)
}
}
}
}

impl Display for FileLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Expand All @@ -91,3 +137,55 @@ impl Display for FileLocation {
}
}
}

/// An error that occurs when a `FileLocation` is not a valid URL.
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum ToUrlError {
/// An error that occurs when the base URL in `FileLocation::Relative`
/// could not be parsed as a valid URL.
#[error("could not parse base URL `{base}` as a valid URL")]
InvalidBase {
/// The base URL that could not be parsed as a valid URL.
base: String,
/// The underlying URL parse error.
#[source]
err: url::ParseError,
},
/// An error that occurs when the base URL could not be joined with
/// the relative path in a `FileLocation::Relative`.
#[error("could not join base URL `{base}` to relative path `{path}`")]
InvalidJoin {
/// The base URL that could not be parsed as a valid URL.
base: String,
/// The relative path segment.
path: String,
/// The underlying URL parse error.
#[source]
err: url::ParseError,
},
/// An error that occurs when the absolute URL in `FileLocation::Absolute`
/// could not be parsed as a valid URL.
#[error("could not parse absolute URL `{absolute}` as a valid URL")]
InvalidAbsolute {
/// The absolute URL that could not be parsed as a valid URL.
absolute: String,
/// The underlying URL parse error.
#[source]
err: url::ParseError,
},
/// An error that occurs when the file path in `FileLocation::Path` is
/// not valid UTF-8. We need paths to be valid UTF-8 to be transformed
/// into URLs, which must also be UTF-8.
#[error("could not build URL from file path `{path}` because it is not valid UTF-8")]
PathNotUtf8 {
/// The original path that was not valid UTF-8.
path: PathBuf,
},
/// An error that occurs when the file URL created from a file path is not
/// a valid URL.
#[error("could not parse file path `{path}` as a valid URL")]
InvalidPath {
/// The file path URL that could not be parsed as a valid URL.
path: String,
},
}
2 changes: 2 additions & 0 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use error::ResolveError;
pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
pub use flat_index::FlatIndex;
pub use lock::{Lock, LockError};
pub use manifest::Manifest;
pub use options::{Options, OptionsBuilder};
pub use preferences::{Preference, PreferenceError};
Expand All @@ -28,6 +29,7 @@ mod error;
mod exclude_newer;
mod exclusions;
mod flat_index;
mod lock;
mod manifest;
mod options;
mod pins;
Expand Down
Loading

0 comments on commit d2e7c05

Please sign in to comment.