diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index 84da218c0938e..d0cf103f6f605 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -1203,7 +1203,7 @@ impl CacheBucket { Self::Interpreter => "interpreter-v4", // Note that when bumping this, you'll also need to bump it // in `crates/uv/tests/build/cache_clean.rs`. - Self::Simple => "simple-v22", + Self::Simple => "simple-v23", // Note that when bumping this, you'll also need to bump it // in `crates/uv/tests/build/cache_prune.rs`. Self::Wheels => "wheels-v6", diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index b4e9f6a55e786..c12d4c4d99ff8 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -18,7 +18,7 @@ use uv_auth::{CredentialsCache, Indexes, PyxTokenStore}; use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache}; use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; -use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; +use uv_distribution_filename::{DistFilename, WheelFilename}; use uv_distribution_types::{ BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, Name, RegistryBuiltWheel, @@ -1381,46 +1381,29 @@ type FlatIndexSlot = Arc>>; #[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] pub struct VersionFiles { - pub wheels: Vec, - pub source_dists: Vec, + pub wheels: Vec, + pub source_dists: Vec, } impl VersionFiles { - fn push(&mut self, filename: DistFilename, file: File) { + fn push(&mut self, filename: &DistFilename, file: File) { match filename { - DistFilename::WheelFilename(name) => self.wheels.push(VersionWheel { name, file }), - DistFilename::SourceDistFilename(name) => { - self.source_dists.push(VersionSourceDist { name, file }); - } + DistFilename::WheelFilename(_) => self.wheels.push(file), + DistFilename::SourceDistFilename(_) => self.source_dists.push(file), } } - pub fn all(self) -> impl Iterator { + pub fn all(self, package_name: &PackageName) -> impl Iterator { self.source_dists .into_iter() - .map(|VersionSourceDist { name, file }| (DistFilename::SourceDistFilename(name), file)) - .chain( - self.wheels - .into_iter() - .map(|VersionWheel { name, file }| (DistFilename::WheelFilename(name), file)), - ) + .chain(self.wheels) + .filter_map(|file| { + let filename = DistFilename::try_from_filename(&file.filename, package_name)?; + Some((filename, file)) + }) } } -#[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] -#[rkyv(derive(Debug))] -pub struct VersionWheel { - pub name: WheelFilename, - pub file: File, -} - -#[derive(Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] -#[rkyv(derive(Debug))] -pub struct VersionSourceDist { - pub name: SourceDistFilename, - pub file: File, -} - /// The list of projects available in a Simple API index. #[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] #[rkyv(derive(Debug))] @@ -1520,11 +1503,11 @@ impl SimpleDetailMetadata { }; match version_map.entry(filename.version().clone()) { std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(filename, file); + entry.get_mut().push(&filename, file); } std::collections::btree_map::Entry::Vacant(entry) => { let mut files = VersionFiles::default(); - files.push(filename, file); + files.push(&filename, file); entry.insert(files); } } @@ -1534,10 +1517,10 @@ impl SimpleDetailMetadata { for files in version_map.values_mut() { files .wheels - .sort_unstable_by(|left, right| left.file.filename.cmp(&right.file.filename)); + .sort_unstable_by(|left, right| left.filename.cmp(&right.filename)); files .source_dists - .sort_unstable_by(|left, right| left.file.filename.cmp(&right.file.filename)); + .sort_unstable_by(|left, right| left.filename.cmp(&right.filename)); } Self { @@ -1588,11 +1571,11 @@ impl SimpleDetailMetadata { }; match version_map.entry(filename.version().clone()) { std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(filename, file); + entry.get_mut().push(&filename, file); } std::collections::btree_map::Entry::Vacant(entry) => { let mut files = VersionFiles::default(); - files.push(filename, file); + files.push(&filename, file); entry.insert(files); } } @@ -2102,6 +2085,50 @@ mod tests { assert_eq!(versions, ["1.7.8".to_string()]); } + #[test] + fn distribution_files_round_trip() -> Result<(), Error> { + let response = r#" + { + "files": [ + { + "filename": "example_1-1.0.0-py3-none-any.whl", + "hashes": {}, + "url": "https://files.pythonhosted.org/example_1-1.0.0-py3-none-any.whl" + }, + { + "filename": "example-1-1.0.0.tar.gz", + "hashes": {}, + "url": "https://files.pythonhosted.org/example-1-1.0.0.tar.gz" + } + ] + } + "#; + let package_name = PackageName::from_str("example-1")?; + let data: PypiSimpleDetail = serde_json::from_str(response)?; + let base = DisplaySafeUrl::parse("https://pypi.org/simple/example-1/")?; + let simple_metadata = SimpleDetailMetadata::from_pypi_files( + data.files, + &package_name, + data.project_status, + &base, + ); + let archived = super::OwnedArchive::from_unarchived(&simple_metadata)?; + let simple_metadata = super::OwnedArchive::deserialize(&archived); + + let filenames: Vec<_> = simple_metadata + .versions + .into_iter() + .flat_map(|datum| datum.files.all(&package_name)) + .map(|(filename, _)| filename.to_string()) + .collect(); + assert_eq!( + filenames, + ["example_1-1.0.0.tar.gz", "example_1-1.0.0-py3-none-any.whl"] + ); + + Ok(()) + } + /// Test for project statuses from PyPI's JSON detail response. #[test] fn project_status_pypi_json() { @@ -2160,53 +2187,44 @@ mod tests { files: VersionFiles { wheels: [], source_dists: [ - VersionSourceDist { - name: SourceDistFilename { - name: PackageName( - "pepy", - ), - version: "2.1.1", - extension: TarGz, - }, - file: File { - dist_info_metadata: false, - filename: "pepy-2.1.1.tar.gz", - hashes: HashDigests( + File { + dist_info_metadata: false, + filename: "pepy-2.1.1.tar.gz", + hashes: HashDigests( + [ + HashDigest { + algorithm: Sha256, + digest: "cec463c444b71d1664229121897b22df753dc91fabb2113d1c89992638c90829", + }, + ], + ), + requires_python: Some( + VersionSpecifiers( [ - HashDigest { - algorithm: Sha256, - digest: "cec463c444b71d1664229121897b22df753dc91fabb2113d1c89992638c90829", + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.7", }, ], ), - requires_python: Some( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "3.7", - }, - ], - ), + ), + size: Some( + 15399, + ), + upload_time_utc_ms: Some( + 1668446093935, + ), + url: AbsoluteUrl( + UrlString( + "https://files.pythonhosted.org/packages/78/7e/123d89ce0e999e957e53f0b985f734565c93b9a698af53586fc2a1be0dbf/pepy-2.1.1.tar.gz", ), - size: Some( - 15399, + ), + yanked: Some( + Bool( + false, ), - upload_time_utc_ms: Some( - 1668446093935, - ), - url: AbsoluteUrl( - UrlString( - "https://files.pythonhosted.org/packages/78/7e/123d89ce0e999e957e53f0b985f734565c93b9a698af53586fc2a1be0dbf/pepy-2.1.1.tar.gz", - ), - ), - yanked: Some( - Bool( - false, - ), - ), - zstd: None, - }, + ), + zstd: None, }, ], }, @@ -2252,45 +2270,36 @@ mod tests { files: VersionFiles { wheels: [], source_dists: [ - VersionSourceDist { - name: SourceDistFilename { - name: PackageName( - "pepy", - ), - version: "2.1.1", - extension: TarGz, - }, - file: File { - dist_info_metadata: false, - filename: "pepy-2.1.1.tar.gz", - hashes: HashDigests( + File { + dist_info_metadata: false, + filename: "pepy-2.1.1.tar.gz", + hashes: HashDigests( + [ + HashDigest { + algorithm: Sha256, + digest: "cec463c444b71d1664229121897b22df753dc91fabb2113d1c89992638c90829", + }, + ], + ), + requires_python: Some( + VersionSpecifiers( [ - HashDigest { - algorithm: Sha256, - digest: "cec463c444b71d1664229121897b22df753dc91fabb2113d1c89992638c90829", + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.7", }, ], ), - requires_python: Some( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "3.7", - }, - ], - ), - ), - size: None, - upload_time_utc_ms: None, - url: AbsoluteUrl( - UrlString( - "https://files.pythonhosted.org/packages/78/7e/123d89ce0e999e957e53f0b985f734565c93b9a698af53586fc2a1be0dbf/pepy-2.1.1.tar.gz", - ), + ), + size: None, + upload_time_utc_ms: None, + url: AbsoluteUrl( + UrlString( + "https://files.pythonhosted.org/packages/78/7e/123d89ce0e999e957e53f0b985f734565c93b9a698af53586fc2a1be0dbf/pepy-2.1.1.tar.gz", ), - yanked: None, - zstd: None, - }, + ), + yanked: None, + zstd: None, }, ], }, diff --git a/crates/uv-distribution-filename/src/lib.rs b/crates/uv-distribution-filename/src/lib.rs index bc49292fd1d2d..bb83a50aa3bce 100644 --- a/crates/uv-distribution-filename/src/lib.rs +++ b/crates/uv-distribution-filename/src/lib.rs @@ -40,8 +40,7 @@ pub enum DistFilename { impl DistFilename { /// Parse a filename as wheel or source dist name. - #[cfg(test)] - fn try_from_filename(filename: &str, package_name: &PackageName) -> Option { + pub fn try_from_filename(filename: &str, package_name: &PackageName) -> Option { Self::try_from_filename_with_reason(filename, package_name).ok() } diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index b25daaecfc8ea..b0827ffcb8471 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -1001,20 +1001,14 @@ pub async fn check_url( return Ok(false); }; - let archived_file = match filename { - DistFilename::SourceDistFilename(source_dist) => metadatum - .files - .source_dists - .iter() - .find(|entry| &entry.name == source_dist) - .map(|entry| &entry.file), - DistFilename::WheelFilename(wheel) => metadatum - .files - .wheels - .iter() - .find(|entry| &entry.name == wheel) - .map(|entry| &entry.file), + let archived_files = match filename { + DistFilename::SourceDistFilename(_) => &metadatum.files.source_dists, + DistFilename::WheelFilename(_) => &metadatum.files.wheels, }; + let archived_file = archived_files.iter().find(|file| { + DistFilename::try_from_filename(&file.filename, filename.name()) + .is_some_and(|candidate| &candidate == filename) + }); let Some(archived_file) = archived_file else { return Ok(false); }; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 7b7b4fa429267..881e53fa19f3b 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -94,6 +94,7 @@ impl VersionMap { } Self { inner: VersionMapInner::Lazy(VersionMapLazy { + package_name: package_name.clone(), map, stable, local, @@ -490,6 +491,8 @@ impl VersionMapLazyIndex { /// provide substantial savings in some cases. #[derive(Debug)] struct VersionMapLazy { + /// The normalized package name used to reconstruct cached wheel filenames. + package_name: PackageName, /// An immutable archive-order index from version to possibly-initialized distribution. map: VersionMapLazyIndex, /// Whether the version map contains at least one stable (non-pre-release) version. @@ -570,8 +573,7 @@ impl VersionMapLazy { files .wheels .iter() - .map(|wheel| &wheel.file) - .chain(files.source_dists.iter().map(|sdist| &sdist.file)) + .chain(files.source_dists.iter()) .any(|file| { let upload_time = file.upload_time_utc_ms.as_ref().map(|t| t.to_native()); let excluded = if let Some(cutoff) = &self.included_version_cutoff { @@ -604,8 +606,7 @@ impl VersionMapLazy { .files .wheels .iter() - .map(|wheel| &wheel.file) - .chain(datum.files.source_dists.iter().map(|sdist| &sdist.file)) + .chain(datum.files.source_dists.iter()) .any(|file| { file.upload_time_utc_ms .as_ref() @@ -646,7 +647,7 @@ impl VersionMapLazy { ) .expect("archived version files always deserializes"); let mut priority_dist = init.cloned().unwrap_or_default(); - for (filename, file) in files.all() { + for (filename, file) in files.all(&self.package_name) { // Support resolving as if it were an earlier timestamp, at least as long files have // upload time information. let (excluded, upload_time) = if let Some(included_version_cutoff) = diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 8768af37b72ae..a404d778e4784 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -154,7 +154,7 @@ impl LatestClient<'_> { rkyv::deserialize::(&datum.files) .expect("archived version files always deserializes"); - for (filename, file) in files.all() { + for (filename, file) in files.all(package) { if self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { update_latest(filename); } diff --git a/crates/uv/tests/build/cache_clean.rs b/crates/uv/tests/build/cache_clean.rs index 9d0c3c5250149..032af01ed97de 100644 --- a/crates/uv/tests/build/cache_clean.rs +++ b/crates/uv/tests/build/cache_clean.rs @@ -143,7 +143,7 @@ fn clean_package_pypi() -> Result<()> { // Assert that the `.rkyv` file is created for `iniconfig`. let rkyv = context .cache_dir - .child("simple-v22") + .child("simple-v23") .child("pypi") .child("iniconfig.rkyv"); assert!( @@ -219,7 +219,7 @@ fn clean_package_index() -> Result<()> { // Assert that the `.rkyv` file is created for `iniconfig`. let rkyv = context .cache_dir - .child("simple-v22") + .child("simple-v23") .child("index") .child("e8208120cae3ba69") .child("iniconfig.rkyv");