From bfd7b53a1818aa4f98fc8c7e5f085e14db998a58 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 24 Aug 2024 13:46:15 -0400 Subject: [PATCH] Use a single type --- crates/distribution-types/src/file.rs | 20 +- crates/distribution-types/src/index_url.rs | 7 +- crates/pep508-rs/src/verbatim_url.rs | 15 +- crates/uv-resolver/src/lock.rs | 285 ++++++++++++--------- crates/uv/tests/lock.rs | 209 +++++++++++++-- 5 files changed, 393 insertions(+), 143 deletions(-) diff --git a/crates/distribution-types/src/file.rs b/crates/distribution-types/src/file.rs index f55b75e13f51e..427e72dc60830 100644 --- a/crates/distribution-types/src/file.rs +++ b/crates/distribution-types/src/file.rs @@ -143,8 +143,6 @@ impl Display for FileLocation { PartialOrd, Ord, Hash, - Serialize, - Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, @@ -153,6 +151,24 @@ impl Display for FileLocation { #[archive_attr(derive(Debug))] pub struct UrlString(String); +impl serde::Serialize for UrlString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + String::serialize(&self.0, serializer) + } +} + +impl<'de> serde::de::Deserialize<'de> for UrlString { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + String::deserialize(deserializer).map(UrlString) + } +} + impl UrlString { /// Converts a [`UrlString`] to a [`Url`]. pub fn to_url(&self) -> Url { diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 15cd574a0feee..d96121cc2bad4 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -113,7 +113,7 @@ impl FromStr for IndexUrl { fn from_str(s: &str) -> Result { let path = Path::new(s); - let url = if path.exists() { + let url = if Path::new(s).exists() { VerbatimUrl::from_path(path)? } else { VerbatimUrl::parse_url(s)? @@ -248,9 +248,8 @@ impl FromStr for FlatIndexLocation { type Err = IndexUrlError; fn from_str(s: &str) -> Result { - let path = Path::new(s); - let url = if path.exists() { - VerbatimUrl::from_path(path)? + let url = if Path::new(s).exists() { + VerbatimUrl::from_path(s)? } else { VerbatimUrl::parse_url(s)? }; diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index 11de14881fd48..c1a0ae18ea229 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -57,6 +57,9 @@ impl VerbatimUrl { let mut url = Url::from_file_path(path.clone()) .map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?; + // Drop trailing slashes. + url.path_segments_mut().unwrap().pop_if_empty(); + // Set the fragment, if it exists. if let Some(fragment) = fragment { url.set_fragment(Some(fragment)); @@ -67,7 +70,11 @@ impl VerbatimUrl { /// Parse a URL from a string, expanding any environment variables. pub fn parse_url(given: impl AsRef) -> Result { - let url = Url::parse(given.as_ref())?; + let mut url = Url::parse(given.as_ref())?; + + // Drop trailing slashes. + url.path_segments_mut().unwrap().pop_if_empty(); + Ok(Self { url, given: None }) } @@ -97,6 +104,9 @@ impl VerbatimUrl { let mut url = Url::from_file_path(path.clone()) .map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?; + // Drop trailing slashes. + url.path_segments_mut().unwrap().pop_if_empty(); + // Set the fragment, if it exists. if let Some(fragment) = fragment { url.set_fragment(Some(fragment)); @@ -127,6 +137,9 @@ impl VerbatimUrl { let mut url = Url::from_file_path(path.clone()) .unwrap_or_else(|()| panic!("path is absolute: {}", path.display())); + // Drop trailing slashes. + url.path_segments_mut().unwrap().pop_if_empty(); + // Set the fragment, if it exists. if let Some(fragment) = fragment { url.set_fragment(Some(fragment)); diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index c7215183a6632..46800d3a2176b 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -23,7 +23,7 @@ use distribution_types::{ UrlString, }; use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; +use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; use platform_tags::{TagCompatibility, TagPriority, Tags}; use pypi_types::{ redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, @@ -31,7 +31,7 @@ use pypi_types::{ }; use uv_configuration::ExtrasSpecification; use uv_distribution::DistributionDatabase; -use uv_fs::{relative_to, PortablePath, PortablePathBuf}; +use uv_fs::{normalize_absolute_path, relative_to, PortablePath, PortablePathBuf}; use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_types::BuildContext; @@ -789,9 +789,22 @@ impl Lock { while let Some(package) = queue.pop_front() { // If the lockfile references an index that was not provided, we can't validate it. if let Source::Registry(index) = &package.id.source { + let index = match index { + RegistrySource::Url(url) => Cow::Borrowed(url), + RegistrySource::Path(path) => { + // TODO(charlie): Computing the URL for every package is expensive. We + // should go in the other direction, and convert `indexes` to relative + // paths. + let path = + normalize_absolute_path(&workspace.install_path().join(path)).unwrap(); + let url = Url::from_file_path(path).unwrap(); + Cow::Owned(UrlString::from(url)) + } + }; + if indexes .as_ref() - .is_some_and(|indexes| !indexes.contains(index)) + .is_some_and(|indexes| !indexes.contains(&index)) { return Ok(SatisfiesResult::MissingIndex( &package.id.name, @@ -936,7 +949,7 @@ pub enum SatisfiesResult<'lock> { /// The lockfile is missing a workspace member. MissingRoot(PackageName), /// The lockfile referenced an index that was not provided - MissingIndex(&'lock PackageName, &'lock Version, &'lock UrlString), + MissingIndex(&'lock PackageName, &'lock Version, Cow<'lock, UrlString>), /// The resolver failed to generate metadata for a given package. MissingMetadata(&'lock PackageName, &'lock Version), /// A package in the lockfile contains different `requires-dist` metadata than expected. @@ -1241,24 +1254,11 @@ impl Package { fn to_dist(&self, workspace_root: &Path, tags: &Tags) -> Result { if let Some(best_wheel_index) = self.find_best_wheel(tags) { return match &self.id.source { - Source::Registry(url) => { - let wheels = self - .wheels - .iter() - .map(|wheel| wheel.to_remote_registry_dist(url.to_url())) - .collect::>()?; - let reg_built_dist = RegistryBuiltDist { - wheels, - best_wheel_index, - sdist: None, - }; - Ok(Dist::Built(BuiltDist::Registry(reg_built_dist))) - } - Source::Local(path) => { + Source::Registry(source) => { let wheels = self .wheels .iter() - .map(|wheel| wheel.to_local_registry_dist(path, workspace_root)) + .map(|wheel| wheel.to_registry_dist(source, workspace_root)) .collect::>()?; let reg_built_dist = RegistryBuiltDist { wheels, @@ -1401,7 +1401,7 @@ impl Package { }; distribution_types::SourceDist::DirectUrl(direct_dist) } - Source::Registry(url) => { + Source::Registry(RegistrySource::Url(url)) => { let Some(ref sdist) = self.sdist else { return Ok(None); }; @@ -1441,7 +1441,7 @@ impl Package { }; distribution_types::SourceDist::Registry(reg_dist) } - Source::Local(path) => { + Source::Registry(RegistrySource::Path(path)) => { let Some(ref sdist) = self.sdist else { return Ok(None); }; @@ -1655,14 +1655,6 @@ impl Package { self.fork_markers.as_slice() } - /// Return the index URL for this package, if it is a registry source. - pub fn index(&self) -> Option<&UrlString> { - match &self.id.source { - Source::Registry(url) => Some(url), - _ => None, - } - } - /// Returns all the hashes associated with this [`Package`]. fn hashes(&self) -> Vec { let mut hashes = Vec::new(); @@ -1894,8 +1886,8 @@ impl From for PackageIdForDependency { #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)] #[serde(try_from = "SourceWire")] enum Source { - /// A remote registry of `--find-links` index. - Registry(UrlString), + /// A registry or `--find-links` index. + Registry(RegistrySource), /// A Git repository. Git(UrlString, GitSource), /// A direct HTTP(S) URL. @@ -1906,11 +1898,6 @@ enum Source { Directory(PathBuf), /// A path to a local directory that should be installed as editable. Editable(PathBuf), - /// A local registry of `--find-links` index. - /// - /// STOPSHIP(charlie): We should just use `Registry` for this, and have serialization that - /// allows either a URL or a path. - Local(PathBuf), } impl Source { @@ -2027,7 +2014,8 @@ impl Source { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { // Remove any sensitive credentials from the index URL. let redacted = index_url.redacted(); - Ok(Source::Registry(UrlString::from(redacted.as_ref()))) + let source = RegistrySource::Url(UrlString::from(redacted.as_ref())); + Ok(Source::Registry(source)) } IndexUrl::Path(url) => { let path = relative_to( @@ -2036,7 +2024,8 @@ impl Source { root, ) .map_err(LockErrorKind::IndexRelativePath)?; - Ok(Source::Local(path)) + let source = RegistrySource::Path(path); + Ok(Source::Registry(source)) } } } @@ -2071,9 +2060,17 @@ impl Source { fn to_toml(&self, table: &mut Table) { let mut source_table = InlineTable::new(); match *self { - Source::Registry(ref url) => { - source_table.insert("registry", Value::from(url.as_ref())); - } + Source::Registry(ref source) => match source { + RegistrySource::Url(url) => { + source_table.insert("registry", Value::from(url.as_ref())); + } + RegistrySource::Path(path) => { + source_table.insert( + "registry", + Value::from(PortablePath::from(path).to_string()), + ); + } + }, Source::Git(ref url, _) => { source_table.insert("git", Value::from(url.as_ref())); } @@ -2098,9 +2095,6 @@ impl Source { Value::from(PortablePath::from(path).to_string()), ); } - Source::Local(ref path) => { - source_table.insert("local", Value::from(PortablePath::from(path).to_string())); - } } table.insert("source", value(source_table)); } @@ -2109,13 +2103,15 @@ impl Source { impl std::fmt::Display for Source { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Source::Registry(url) | Source::Git(url, _) | Source::Direct(url, _) => { + Source::Registry(RegistrySource::Url(url)) + | Source::Git(url, _) + | Source::Direct(url, _) => { write!(f, "{}+{}", self.name(), url) } - Source::Path(path) + Source::Registry(RegistrySource::Path(path)) + | Source::Path(path) | Source::Directory(path) - | Source::Editable(path) - | Source::Local(path) => { + | Source::Editable(path) => { write!(f, "{}+{}", self.name(), PortablePath::from(path)) } } @@ -2131,7 +2127,6 @@ impl Source { Self::Path(..) => "path", Self::Directory(..) => "directory", Self::Editable(..) => "editable", - Self::Local(..) => "local", } } @@ -2144,7 +2139,7 @@ impl Source { /// Returns `None` to indicate that the source kind _may_ include a hash. fn requires_hash(&self) -> Option { match *self { - Self::Registry(..) | Self::Local(..) => None, + Self::Registry(..) => None, Self::Direct(..) | Self::Path(..) => Some(true), Self::Git(..) | Self::Directory(..) | Self::Editable(..) => Some(false), } @@ -2155,7 +2150,7 @@ impl Source { #[serde(untagged)] enum SourceWire { Registry { - registry: UrlString, + registry: RegistrySource, }, Git { git: String, @@ -2174,9 +2169,6 @@ enum SourceWire { Editable { editable: PortablePathBuf, }, - Local { - local: PortablePathBuf, - }, } impl TryFrom for Source { @@ -2213,11 +2205,68 @@ impl TryFrom for Source { Path { path } => Ok(Source::Path(path.into())), Directory { directory } => Ok(Source::Directory(directory.into())), Editable { editable } => Ok(Source::Editable(editable.into())), - Local { local } => Ok(Source::Local(local.into())), } } } +/// The source for a registry, which could be a URL or a relative path. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +enum RegistrySource { + /// Ex) `https://pypi.org/simple` + Url(UrlString), + /// Ex) `../path/to/local/index` + Path(PathBuf), +} + +impl std::fmt::Display for RegistrySource { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + RegistrySource::Url(url) => write!(f, "{url}"), + RegistrySource::Path(path) => write!(f, "{}", path.display()), + } + } +} + +impl<'de> serde::de::Deserialize<'de> for RegistrySource { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = RegistrySource; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid URL or a file path") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if split_scheme(value).is_some() { + Ok( + serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new( + value, + )) + .map(RegistrySource::Url)?, + ) + } else { + Ok( + serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new( + value, + )) + .map(RegistrySource::Path)?, + ) + } + } + } + + deserializer.deserialize_str(Visitor) + } +} + #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)] struct DirectSource { subdirectory: Option, @@ -2742,61 +2791,64 @@ impl Wheel { }) } - fn to_remote_registry_dist(&self, index_url: Url) -> Result { - let filename: WheelFilename = self.filename.clone(); - let file_url = match &self.url { - WheelWireSource::Url { url } => url, - WheelWireSource::Path { .. } => unreachable!(), - }; - let file = Box::new(distribution_types::File { - dist_info_metadata: false, - filename: filename.to_string(), - hashes: self.hash.iter().map(|h| h.0.clone()).collect(), - requires_python: None, - size: self.size, - upload_time_utc_ms: None, - url: FileLocation::AbsoluteUrl(file_url.clone()), - yanked: None, - }); - let index = IndexUrl::Url(VerbatimUrl::from_url(index_url)); - Ok(RegistryBuiltWheel { - filename, - file, - index, - }) - } - - fn to_local_registry_dist( + fn to_registry_dist( &self, - index_path: &Path, + source: &RegistrySource, root: &Path, ) -> Result { let filename: WheelFilename = self.filename.clone(); - let file_path = match &self.url { - WheelWireSource::Path { path } => path, - WheelWireSource::Url { .. } => unreachable!(), - }; - let file_path = root.join(index_path).join(file_path); - let file_url = Url::from_file_path(&file_path).unwrap(); - let file = Box::new(distribution_types::File { - dist_info_metadata: false, - filename: filename.to_string(), - hashes: self.hash.iter().map(|h| h.0.clone()).collect(), - requires_python: None, - size: self.size, - upload_time_utc_ms: None, - url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), - yanked: None, - }); - let index = IndexUrl::Path( - VerbatimUrl::from_path(root.join(index_path)) - .map_err(LockErrorKind::RegistryVerbatimUrl)?, - ); - Ok(RegistryBuiltWheel { - filename, - file, - index, - }) + + match source { + RegistrySource::Url(index_url) => { + let file_url = match &self.url { + WheelWireSource::Url { url } => url, + WheelWireSource::Path { .. } => unreachable!(), + }; + let file = Box::new(distribution_types::File { + dist_info_metadata: false, + filename: filename.to_string(), + hashes: self.hash.iter().map(|h| h.0.clone()).collect(), + requires_python: None, + size: self.size, + upload_time_utc_ms: None, + url: FileLocation::AbsoluteUrl(file_url.clone()), + yanked: None, + }); + let index = IndexUrl::Url(VerbatimUrl::from_url(index_url.to_url())); + Ok(RegistryBuiltWheel { + filename, + file, + index, + }) + } + RegistrySource::Path(index_path) => { + let file_path = match &self.url { + WheelWireSource::Path { path } => path, + WheelWireSource::Url { .. } => unreachable!(), + }; + let file_path = root.join(index_path).join(file_path); + let file_url = Url::from_file_path(file_path).unwrap(); + let file = Box::new(distribution_types::File { + dist_info_metadata: false, + filename: filename.to_string(), + hashes: self.hash.iter().map(|h| h.0.clone()).collect(), + requires_python: None, + size: self.size, + upload_time_utc_ms: None, + url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), + yanked: None, + }); + let index = IndexUrl::Path( + VerbatimUrl::from_path(root.join(index_path)) + .map_err(LockErrorKind::RegistryVerbatimUrl)?, + ); + Ok(RegistryBuiltWheel { + filename, + file, + index, + }) + } + } } } @@ -2821,20 +2873,15 @@ struct WheelWire { enum WheelWireSource { /// Used for all wheels except path wheels. Url { - /// A URL or file path (via `file://`) where the wheel that was locked - /// against was found. The location does not need to exist in the future, - /// so this should be treated as only a hint to where to look and/or - /// recording where the wheel file originally came from. + /// A URL where the wheel that was locked against was found. The location + /// does not need to exist in the future, so this should be treated as + /// only a hint to where to look and/or recording where the wheel file + /// originally came from. url: UrlString, }, /// Used for path wheels. Path { - /// The filename of the wheel. - /// - /// This isn't part of the wire format since it's redundant with the - /// URL. But we do use it for various things, and thus compute it at - /// deserialization time. Not being able to extract a wheel filename from a - /// wheel URL is thus a deserialization error. + /// The path to the wheel, relative to the index. path: PathBuf, }, } @@ -3296,11 +3343,11 @@ enum LockErrorKind { }, /// An error that occurs when a distribution indicates that it is sourced from a local registry, /// but is missing a path. - #[error("found local registry distribution {name}=={version} without a valid path")] + #[error("found registry distribution {name}=={version} without a valid path")] MissingPath { - /// The name of the distribution that is missing a URL. + /// The name of the distribution that is missing a path. name: PackageName, - /// The version of the distribution that is missing a URL. + /// The version of the distribution that is missing a path. version: Version, }, /// An error that occurs when a distribution indicates that it is sourced from a registry, but diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 18e83ea415349..47824ffedb72d 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -6491,7 +6491,28 @@ fn lock_warn_missing_transitive_lower_bounds() -> Result<()> { fn lock_find_links_local_wheel() -> Result<()> { let context = TestContext::new("3.12"); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); + // Create a symlink to the `--find-links` directory. + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink( + context.workspace_root.join("scripts/links"), + context.temp_dir.join("links"), + )?; + } + + #[cfg(windows)] + { + use std::os::windows::fs::symlink_dir; + symlink_dir( + context.workspace_root.join("scripts/links"), + context.temp_dir.join("links"), + )?; + } + + let workspace = context.temp_dir.child("workspace"); + + let pyproject_toml = workspace.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" [project] name = "project" @@ -6502,19 +6523,20 @@ fn lock_find_links_local_wheel() -> Result<()> { [tool.uv] find-links = ["{}"] "#, - context.workspace_root.join("scripts/links/").portable_display(), + context.temp_dir.join("links/").portable_display(), })?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 2 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -6541,7 +6563,7 @@ fn lock_find_links_local_wheel() -> Result<()> { [[package]] name = "tqdm" version = "1000.0.0" - source = { local = "../../../../../../workspace/puffin/scripts/links" } + source = { registry = "../links" } wheels = [ { path = "tqdm-1000.0.0-py3-none-any.whl" }, ] @@ -6550,25 +6572,28 @@ fn lock_find_links_local_wheel() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 2 packages in [TIME] "###); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv Prepared 1 package in [TIME] Installed 2 packages in [TIME] - + project==0.1.0 (from file://[TEMP_DIR]/) + + project==0.1.0 (from file://[TEMP_DIR]/workspace) + tqdm==1000.0.0 "###); @@ -6580,7 +6605,28 @@ fn lock_find_links_local_wheel() -> Result<()> { fn lock_find_links_local_sdist() -> Result<()> { let context = TestContext::new("3.12"); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); + // Create a symlink to the `--find-links` directory. + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink( + context.workspace_root.join("scripts/links"), + context.temp_dir.join("links"), + )?; + } + + #[cfg(windows)] + { + use std::os::windows::fs::symlink_dir; + symlink_dir( + context.workspace_root.join("scripts/links"), + context.temp_dir.join("links"), + )?; + } + + let workspace = context.temp_dir.child("workspace"); + + let pyproject_toml = workspace.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" [project] name = "project" @@ -6591,19 +6637,20 @@ fn lock_find_links_local_sdist() -> Result<()> { [tool.uv] find-links = ["{}"] "#, - context.workspace_root.join("scripts/links/").portable_display(), + context.temp_dir.join("links/").portable_display(), })?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 2 packages in [TIME] "###); - let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap(); insta::with_settings!({ filters => context.filters(), @@ -6630,32 +6677,35 @@ fn lock_find_links_local_sdist() -> Result<()> { [[package]] name = "tqdm" version = "999.0.0" - source = { local = "../../../../../../workspace/puffin/scripts/links" } + source = { registry = "../links" } sdist = { path = "tqdm-999.0.0.tar.gz" } "### ); }); // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 2 packages in [TIME] "###); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(&workspace), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv Prepared 2 packages in [TIME] Installed 2 packages in [TIME] - + project==0.1.0 (from file://[TEMP_DIR]/) + + project==0.1.0 (from file://[TEMP_DIR]/workspace) + tqdm==999.0.0 "###); @@ -6937,7 +6987,7 @@ fn lock_local_index() -> Result<()> { [[package]] name = "tqdm" version = "1000.0.0" - source = { local = "../../[TMP]/simple-html" } + source = { registry = "../../[TMP]/simple-html" } wheels = [ { path = "tqdm/tqdm-1000.0.0-py3-none-any.whl" }, ] @@ -10429,3 +10479,128 @@ fn lock_dropped_dev_extra() -> Result<()> { Ok(()) } + +/// Use a trailing slash on the declared index. +#[test] +fn lock_trailing_slash() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [tool.uv] + index-url = "https://pypi.org/simple/" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + Ok(()) +}