From f266fb711c0e9adf2977e12abef1e2c02bdfa656 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 31 Jul 2024 12:16:39 -0400 Subject: [PATCH] Use full requirement when serializing receipt (#5494) ## Summary The current receipt doesn't capture quite enough information. For example, it doesn't differentiate between editable and non-editable requirements. This PR instead uses the full `Requirement` type. I think we should use a custom representation like we do in the lockfile, but I'm just using the default representation to demonstrate the idea. --- Cargo.lock | 2 + crates/pep440-rs/src/version_specifier.rs | 11 + crates/pep508-rs/src/origin.rs | 5 +- crates/pypi-types/Cargo.toml | 3 +- crates/pypi-types/src/requirement.rs | 253 +++++++++++++++++++++- crates/uv-git/Cargo.toml | 3 +- crates/uv-git/src/git.rs | 4 +- crates/uv-git/src/sha.rs | 21 +- crates/uv-tool/Cargo.toml | 2 +- crates/uv-tool/src/tool.rs | 30 ++- crates/uv/src/commands/tool/install.rs | 5 +- crates/uv/tests/tool_install.rs | 58 +++-- 12 files changed, 341 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47357ca5f..4ffc7beef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2807,6 +2807,7 @@ dependencies = [ "toml", "tracing", "url", + "uv-fs", "uv-git", "uv-normalize", ] @@ -4844,6 +4845,7 @@ dependencies = [ "fs-err", "reqwest", "reqwest-middleware", + "serde", "thiserror", "tokio", "tracing", diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index 01968bc8a..4ac98c917 100644 --- a/crates/pep440-rs/src/version_specifier.rs +++ b/crates/pep440-rs/src/version_specifier.rs @@ -61,6 +61,11 @@ impl VersionSpecifiers { pub fn contains(&self, version: &Version) -> bool { self.iter().all(|specifier| specifier.contains(version)) } + + /// Returns `true` if the specifiers are empty is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } impl FromIterator for VersionSpecifiers { @@ -98,6 +103,12 @@ impl std::fmt::Display for VersionSpecifiers { } } +impl Default for VersionSpecifiers { + fn default() -> Self { + Self::empty() + } +} + /// https://pyo3.rs/v0.18.2/class/protocols.html#iterable-objects #[cfg(feature = "pyo3")] #[pyclass] diff --git a/crates/pep508-rs/src/origin.rs b/crates/pep508-rs/src/origin.rs index db5a082d9..7535aebae 100644 --- a/crates/pep508-rs/src/origin.rs +++ b/crates/pep508-rs/src/origin.rs @@ -3,7 +3,10 @@ use std::path::{Path, PathBuf}; use uv_normalize::PackageName; /// The origin of a dependency, e.g., a `-r requirements.txt` file. -#[derive(Hash, Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[derive( + Hash, Debug, Clone, Eq, PartialEq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "kebab-case")] pub enum RequirementOrigin { /// The requirement was provided via a standalone file (e.g., a `requirements.txt` file). File(PathBuf), diff --git a/crates/pypi-types/Cargo.toml b/crates/pypi-types/Cargo.toml index 3345cb468..547d313f4 100644 --- a/crates/pypi-types/Cargo.toml +++ b/crates/pypi-types/Cargo.toml @@ -15,8 +15,9 @@ workspace = true [dependencies] pep440_rs = { workspace = true } pep508_rs = { workspace = true } -uv-normalize = { workspace = true } +uv-fs = { workspace = true, features = ["serde"] } uv-git = { workspace = true } +uv-normalize = { workspace = true } chrono = { workspace = true, features = ["serde"] } indexmap = { workspace = true, features = ["serde"] } diff --git a/crates/pypi-types/src/requirement.rs b/crates/pypi-types/src/requirement.rs index 8f1c5dc95..41546d198 100644 --- a/crates/pypi-types/src/requirement.rs +++ b/crates/pypi-types/src/requirement.rs @@ -1,10 +1,12 @@ use std::fmt::{Display, Formatter}; -use std::path::PathBuf; - +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use thiserror::Error; use url::Url; use pep440_rs::VersionSpecifiers; use pep508_rs::{MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl}; +use uv_fs::PortablePathBuf; use uv_git::{GitReference, GitSha, GitUrl}; use uv_normalize::{ExtraName, PackageName}; @@ -12,16 +14,30 @@ use crate::{ ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl, }; +#[derive(Debug, Error)] +pub enum RequirementError { + #[error(transparent)] + VerbatimUrlError(#[from] pep508_rs::VerbatimUrlError), + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[error(transparent)] + OidParseError(#[from] uv_git::OidParseError), +} + /// A representation of dependency on a package, an extension over a PEP 508's requirement. /// /// The main change is using [`RequirementSource`] to represent all supported package sources over /// [`VersionOrUrl`], which collapses all URL sources into a single stringly type. -#[derive(Hash, Debug, Clone, Eq, PartialEq)] +#[derive(Hash, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] pub struct Requirement { pub name: PackageName, + #[serde(skip_serializing_if = "Vec::is_empty", default)] pub extras: Vec, pub marker: Option, + #[serde(flatten)] pub source: RequirementSource, + #[serde(skip)] pub origin: Option, } @@ -228,7 +244,8 @@ impl Display for Requirement { /// We store both the parsed fields (such as the plain url and the subdirectory) and the joined /// PEP 508 style url (e.g. `file:///#subdirectory=`) since we need both in /// different locations. -#[derive(Hash, Debug, Clone, Eq, PartialEq)] +#[derive(Hash, Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")] pub enum RequirementSource { /// The requirement has a version specifier, such as `foo >1,<2`. Registry { @@ -398,7 +415,7 @@ impl RequirementSource { pub fn version_or_url(&self) -> Option> { match self { Self::Registry { specifier, .. } => { - if specifier.len() == 0 { + if specifier.is_empty() { None } else { Some(VersionOrUrl::VersionSpecifier(specifier.clone())) @@ -415,3 +432,229 @@ impl RequirementSource { matches!(self, Self::Directory { editable: true, .. }) } } + +impl Display for RequirementSource { + /// Display the [`RequirementSource`], with the intention of being shown directly to a user, + /// rather than for inclusion in a `requirements.txt` file. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Registry { specifier, index } => { + write!(f, "{specifier}")?; + if let Some(index) = index { + write!(f, " (index: {index})")?; + } + } + Self::Url { url, .. } => { + write!(f, " {url}")?; + } + Self::Git { + url: _, + repository, + reference, + precise: _, + subdirectory, + } => { + write!(f, " git+{repository}")?; + if let Some(reference) = reference.as_str() { + write!(f, "@{reference}")?; + } + if let Some(subdirectory) = subdirectory { + writeln!(f, "#subdirectory={}", subdirectory.display())?; + } + } + Self::Path { url, .. } => { + write!(f, "{url}")?; + } + Self::Directory { url, .. } => { + write!(f, "{url}")?; + } + } + Ok(()) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum RequirementSourceWire { + /// Ex) `source = { specifier = "foo >1,<2" }` + Registry { + #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] + specifier: VersionSpecifiers, + index: Option, + }, + /// Ex) `source = { git = "" }` + Git { git: String }, + /// Ex) `source = { url = "" }` + Direct { + url: Url, + subdirectory: Option, + }, + /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }` + Path { path: PortablePathBuf }, + /// Ex) `source = { directory = "/home/ferris/iniconfig" }` + Directory { directory: PortablePathBuf }, + /// Ex) `source = { editable = "/home/ferris/iniconfig" }` + Editable { editable: PortablePathBuf }, +} + +impl From for RequirementSourceWire { + fn from(value: RequirementSource) -> Self { + match value { + RequirementSource::Registry { specifier, index } => Self::Registry { specifier, index }, + RequirementSource::Url { + subdirectory, + location, + url: _, + } => Self::Direct { + url: location, + subdirectory: subdirectory + .as_deref() + .and_then(Path::to_str) + .map(str::to_string), + }, + RequirementSource::Git { + repository, + reference, + precise, + subdirectory, + url: _, + } => { + let mut url = repository; + + // Clear out any existing state. + url.set_fragment(None); + url.set_query(None); + + // Put the subdirectory in the query. + if let Some(subdirectory) = subdirectory.as_deref().and_then(Path::to_str) { + url.query_pairs_mut() + .append_pair("subdirectory", subdirectory); + } + + // Put the requested reference in the query. + match reference { + GitReference::Branch(branch) => { + url.query_pairs_mut() + .append_pair("branch", branch.to_string().as_str()); + } + GitReference::Tag(tag) => { + url.query_pairs_mut() + .append_pair("tag", tag.to_string().as_str()); + } + GitReference::ShortCommit(rev) + | GitReference::BranchOrTag(rev) + | GitReference::BranchOrTagOrCommit(rev) + | GitReference::NamedRef(rev) + | GitReference::FullCommit(rev) => { + url.query_pairs_mut() + .append_pair("rev", rev.to_string().as_str()); + } + GitReference::DefaultBranch => {} + } + + // Put the precise commit in the fragment. + if let Some(precise) = precise { + url.set_fragment(Some(&precise.to_string())); + } + + Self::Git { + git: url.to_string(), + } + } + RequirementSource::Path { + install_path, + lock_path: _, + url: _, + } => Self::Path { + path: PortablePathBuf::from(install_path), + }, + RequirementSource::Directory { + install_path, + lock_path: _, + editable, + url: _, + } => { + if editable { + Self::Editable { + editable: PortablePathBuf::from(install_path), + } + } else { + Self::Directory { + directory: PortablePathBuf::from(install_path), + } + } + } + } + } +} + +impl TryFrom for RequirementSource { + type Error = RequirementError; + + fn try_from(wire: RequirementSourceWire) -> Result { + match wire { + RequirementSourceWire::Registry { specifier, index } => { + Ok(RequirementSource::Registry { specifier, index }) + } + RequirementSourceWire::Git { git } => { + let mut url = Url::parse(&git)?; + + let mut reference = GitReference::DefaultBranch; + let mut subdirectory = None; + for (key, val) in url.query_pairs() { + match &*key { + "tag" => reference = GitReference::Tag(val.into_owned()), + "branch" => reference = GitReference::Branch(val.into_owned()), + "rev" => reference = GitReference::from_rev(val.into_owned()), + "subdirectory" => subdirectory = Some(PathBuf::from(val.into_owned())), + _ => continue, + }; + } + + let precise = url.fragment().map(GitSha::from_str).transpose()?; + + url.set_query(None); + url.set_fragment(None); + + Ok(RequirementSource::Git { + repository: url.clone(), + reference, + precise, + subdirectory, + url: VerbatimUrl::from_url(url), + }) + } + RequirementSourceWire::Direct { url, subdirectory } => Ok(RequirementSource::Url { + url: VerbatimUrl::from_url(url.clone()), + subdirectory: subdirectory.map(PathBuf::from), + location: url.clone(), + }), + RequirementSourceWire::Path { path } => { + let path = PathBuf::from(path); + Ok(RequirementSource::Path { + url: VerbatimUrl::from_path(path.as_path())?, + install_path: path.clone(), + lock_path: path, + }) + } + RequirementSourceWire::Directory { directory } => { + let directory = PathBuf::from(directory); + Ok(RequirementSource::Directory { + url: VerbatimUrl::from_path(directory.as_path())?, + install_path: directory.clone(), + lock_path: directory, + editable: false, + }) + } + RequirementSourceWire::Editable { editable } => { + let editable = PathBuf::from(editable); + Ok(RequirementSource::Directory { + url: VerbatimUrl::from_path(editable.as_path())?, + install_path: editable.clone(), + lock_path: editable, + editable: true, + }) + } + } + } +} diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index 3d4e0e948..c459387f8 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -22,7 +22,8 @@ dashmap = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } reqwest = { workspace = true, features = ["blocking"] } reqwest-middleware = { workspace = true } +serde = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -url = { workspace = true } +url = { workspace = true } \ No newline at end of file diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 9931639a2..aeb913031 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -20,7 +20,9 @@ use crate::sha::GitOid; const CHECKOUT_READY_LOCK: &str = ".ok"; /// A reference to commit or commit-ish. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive( + Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] pub enum GitReference { /// A specific branch. Branch(String), diff --git a/crates/uv-git/src/sha.rs b/crates/uv-git/src/sha.rs index d476c08fe..5ae7d3ec8 100644 --- a/crates/uv-git/src/sha.rs +++ b/crates/uv-git/src/sha.rs @@ -26,7 +26,7 @@ impl From for GitSha { } } -impl std::fmt::Display for GitSha { +impl Display for GitSha { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } @@ -40,6 +40,25 @@ impl FromStr for GitSha { } } +impl serde::Serialize for GitSha { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.as_str().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for GitSha { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + GitSha::from_str(&value).map_err(serde::de::Error::custom) + } +} + /// Unique identity of any Git object (commit, tree, blob, tag). /// /// Note this type does not validate whether the input is a valid hash. diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index b953fc90a..270514e36 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -30,5 +30,5 @@ pathdiff = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } -toml_edit = { workspace = true } +toml_edit = { workspace = true, features = ["serde"] } tracing = { workspace = true } diff --git a/crates/uv-tool/src/tool.rs b/crates/uv-tool/src/tool.rs index 786ce5355..f04a582b0 100644 --- a/crates/uv-tool/src/tool.rs +++ b/crates/uv-tool/src/tool.rs @@ -6,7 +6,7 @@ use toml_edit::Array; use toml_edit::Table; use toml_edit::Value; -use pypi_types::VerbatimParsedUrl; +use pypi_types::Requirement; use uv_fs::PortablePath; /// A tool entry. @@ -15,7 +15,7 @@ use uv_fs::PortablePath; #[serde(rename_all = "kebab-case")] pub struct Tool { /// The requirements requested by the user during installation. - requirements: Vec>, + requirements: Vec, /// The Python requested by the user during installation. python: Option, /// A mapping of entry point names to their metadata. @@ -59,7 +59,7 @@ fn each_element_on_its_line_array(elements: impl Iterator>, + requirements: Vec, python: Option, entrypoints: impl Iterator, ) -> Self { @@ -79,12 +79,20 @@ impl Tool { table.insert("requirements", { let requirements = match self.requirements.as_slice() { [] => Array::new(), - [requirement] => Array::from_iter([Value::from(requirement.to_string())]), - requirements => each_element_on_its_line_array( - requirements - .iter() - .map(|requirement| Value::from(requirement.to_string())), - ), + [requirement] => Array::from_iter([serde::Serialize::serialize( + &requirement, + toml_edit::ser::ValueSerializer::new(), + ) + .unwrap()]), + requirements => { + each_element_on_its_line_array(requirements.iter().map(|requirement| { + serde::Serialize::serialize( + &requirement, + toml_edit::ser::ValueSerializer::new(), + ) + .unwrap() + })) + } }; value(requirements) }); @@ -98,7 +106,7 @@ impl Tool { self.entrypoints .iter() .map(ToolEntrypoint::to_toml) - .map(toml_edit::Table::into_inline_table), + .map(Table::into_inline_table), ); value(entrypoints) }); @@ -110,7 +118,7 @@ impl Tool { &self.entrypoints } - pub fn requirements(&self) -> &[pep508_rs::Requirement] { + pub fn requirements(&self) -> &[Requirement] { &self.requirements } } diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index b50887078..6bf37f0c8 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -435,10 +435,7 @@ pub(crate) async fn install( debug!("Adding receipt for tool `{}`", from.name); let tool = Tool::new( - requirements - .into_iter() - .map(pep508_rs::Requirement::from) - .collect(), + requirements.into_iter().collect(), python, target_entry_points .into_iter() diff --git a/crates/uv/tests/tool_install.rs b/crates/uv/tests/tool_install.rs index 70422d813..3540d85fc 100644 --- a/crates/uv/tests/tool_install.rs +++ b/crates/uv/tests/tool_install.rs @@ -82,7 +82,7 @@ fn tool_install() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -164,7 +164,7 @@ fn tool_install() { // We should have a new tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["flask"] + requirements = [{ name = "flask" }] entrypoints = [ { name = "flask", install-path = "[TEMP_DIR]/bin/flask" }, ] @@ -300,7 +300,7 @@ fn tool_install_version() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black==24.2.0"] + requirements = [{ name = "black", specifier = "==24.2.0" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -380,7 +380,7 @@ fn tool_install_editable() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black @ file://[WORKSPACE]/scripts/packages/black_editable"] + requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] @@ -396,9 +396,7 @@ fn tool_install_editable() { ----- stderr ----- "###); - // Request `black`. It should retain the current installation. - // TODO(charlie): This is arguably incorrect, especially because the tool receipt removes the - // file path. + // Request `black`. It should reinstall from the registry. uv_snapshot!(context.filters(), context.tool_install() .arg("black") .env("UV_TOOL_DIR", tool_dir.as_os_str()) @@ -410,7 +408,7 @@ fn tool_install_editable() { ----- stderr ----- warning: `uv tool install` is experimental and may change without warning - Installed 1 executable: black + `black` is already installed "###); insta::with_settings!({ @@ -419,7 +417,7 @@ fn tool_install_editable() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] @@ -460,7 +458,7 @@ fn tool_install_editable() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black==24.2.0"] + requirements = [{ name = "black", specifier = "==24.2.0" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -532,7 +530,7 @@ fn tool_install_editable_from() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black @ file://[WORKSPACE]/scripts/packages/black_editable"] + requirements = [{ name = "black", editable = "[WORKSPACE]/scripts/packages/black_editable" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] @@ -683,7 +681,7 @@ fn tool_install_already_installed() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -717,7 +715,7 @@ fn tool_install_already_installed() { // We should not have an additional tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1025,7 +1023,7 @@ fn tool_install_entry_point_exists() { // We write a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1058,7 +1056,7 @@ fn tool_install_entry_point_exists() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1288,7 +1286,7 @@ fn tool_install_unnamed_package() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl"] + requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1400,7 +1398,7 @@ fn tool_install_unnamed_from() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl"] + requirements = [{ name = "black", url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1488,8 +1486,8 @@ fn tool_install_unnamed_with() { assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] requirements = [ - "black", - "iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", + { name = "black" }, + { name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, ] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, @@ -1555,8 +1553,8 @@ fn tool_install_requirements_txt() { assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] requirements = [ - "black", - "iniconfig", + { name = "black" }, + { name = "iniconfig" }, ] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, @@ -1598,8 +1596,8 @@ fn tool_install_requirements_txt() { assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] requirements = [ - "black", - "idna", + { name = "black" }, + { name = "idna" }, ] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, @@ -1660,8 +1658,8 @@ fn tool_install_requirements_txt_arguments() { assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] requirements = [ - "black", - "idna", + { name = "black" }, + { name = "idna" }, ] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, @@ -1776,7 +1774,7 @@ fn tool_install_upgrade() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black==24.1.1"] + requirements = [{ name = "black", specifier = "==24.1.1" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1806,7 +1804,7 @@ fn tool_install_upgrade() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, @@ -1842,8 +1840,8 @@ fn tool_install_upgrade() { assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] requirements = [ - "black", - "iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", + { name = "black" }, + { name = "iniconfig", url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, ] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, @@ -1882,7 +1880,7 @@ fn tool_install_upgrade() { // We should have a tool receipt assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" [tool] - requirements = ["black"] + requirements = [{ name = "black" }] entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },