From 92d45bdbb50bd79ba6694145fdb8b81055d70258 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 11:40:20 -0400 Subject: [PATCH 01/17] Add initial work --- crates/uv-client/src/registry_client.rs | 19 ++- crates/uv/src/commands/pip/latest.rs | 176 ++++++++++++++---------- crates/uv/tests/it/pip_list.rs | 45 ++++++ 3 files changed, 159 insertions(+), 81 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index d4b93e96b0bd1..9da19142caedd 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -21,7 +21,7 @@ use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, + BuiltDist, File, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_git::{GIT_LFS, GitError, GitHttpSettings, GitResolver, Reporter}; @@ -166,6 +166,7 @@ impl<'a> RegistryClientBuilder<'a> { pub fn build(mut self) -> Result { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); + let flat_index = self.index_locations.flat_indexes().cloned().collect(); // Build a base client let builder = self @@ -188,6 +189,7 @@ impl<'a> RegistryClientBuilder<'a> { connectivity, client, read_timeout, + flat_index, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), }) @@ -197,6 +199,7 @@ impl<'a> RegistryClientBuilder<'a> { pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); + let flat_index = self.index_locations.flat_indexes().cloned().collect(); // Wrap in any relevant middleware and handle connectivity. let client = self @@ -218,6 +221,7 @@ impl<'a> RegistryClientBuilder<'a> { connectivity, client, read_timeout, + flat_index, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), } @@ -233,6 +237,8 @@ pub struct RegistryClient { index_strategy: IndexStrategy, /// The strategy to use when selecting a PyTorch backend, if any. torch_backend: Option, + /// The configured flat indexes (e.g., `--find-links`). + flat_index: Vec, /// The underlying HTTP client. client: CachedClient, /// Used for the remote wheel METADATA cache. @@ -331,16 +337,19 @@ impl RegistryClient { capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { - // If `--no-index` is specified, avoid fetching regardless of whether the index is implicit, - // explicit, etc. - if self.index_urls.no_index() { + let has_flat_indexes = !self.flat_index.is_empty(); + + // If `--no-index` is specified and no flat indexes are available, avoid fetching + // regardless of whether the index is implicit, explicit, etc. + if self.index_urls.no_index() && !has_flat_indexes { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { - Either::Right(self.index_urls_for(package_name)) + let find_links = self.flat_index.iter().map(IndexMetadataRef::from); + Either::Right(self.index_urls_for(package_name).chain(find_links)) }; let mut results = Vec::new(); diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 9227c456871fc..ed29a75ef75d1 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -1,10 +1,10 @@ use tokio::sync::Semaphore; use tracing::debug; -use uv_client::{MetadataFormat, RegistryClient, VersionFiles}; +use uv_client::{FlatIndexEntry, MetadataFormat, RegistryClient, VersionFiles}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ - IndexCapabilities, IndexLocations, IndexMetadataRef, IndexUrl, RequiresPython, + File, IndexCapabilities, IndexLocations, IndexMetadataRef, IndexUrl, RequiresPython, }; use uv_normalize::PackageName; use uv_platform_tags::Tags; @@ -45,6 +45,26 @@ impl LatestClient<'_> { ) -> Result, uv_client::Error> { debug!("Fetching latest version of: `{package}`"); + let mut latest: Option = None; + + let mut update_latest = |candidate: DistFilename| { + match latest.as_ref() { + Some(current) => { + // Prefer higher versions, and prefer wheels over sdists at parity. + if candidate.version() > current.version() + || (candidate.version() == current.version() + && matches!(candidate, DistFilename::WheelFilename(_)) + && matches!(current, DistFilename::SourceDistFilename(_))) + { + latest = Some(candidate); + } + } + None => { + latest = Some(candidate); + } + } + }; + let archives = match self .client .simple_detail( @@ -66,98 +86,102 @@ impl LatestClient<'_> { } }; - let mut latest: Option = None; for (index, archive) in archives { - let MetadataFormat::Simple(archive) = archive else { - continue; - }; let exclude_newer = self.effective_exclude_newer(package, index); - for datum in archive.iter().rev() { - // Find the first compatible distribution. - let files = rkyv::deserialize::(&datum.files) - .expect("archived version files always deserializes"); - - // Determine whether there's a compatible wheel and/or source distribution. - let mut best = None; - - for (filename, file) in files.all() { - // Skip distributions uploaded after the cutoff. - if let Some(exclude_newer) = &exclude_newer { - match file.upload_time_utc_ms.as_ref() { - Some(&upload_time) if upload_time >= exclude_newer.as_millisecond() => { - continue; - } - None => { - warn_user_once!( - "{} is missing an upload date, but user provided: {}", - file.filename, - exclude_newer - ); - } - _ => {} + let consider_candidate = |filename: &DistFilename, file: &File| { + if let Some(exclude_newer) = &exclude_newer { + match file.upload_time_utc_ms.as_ref() { + Some(&upload_time) if upload_time >= exclude_newer.as_millisecond() => { + return false; } - } - - // Skip pre-release distributions. - if !filename.version().is_stable() { - if !matches!(self.prerelease, PrereleaseMode::Allow) { - continue; + None => { + warn_user_once!( + "{} is missing an upload date, but user provided: {}", + file.filename, + exclude_newer + ); } + _ => {} } + } - // Skip distributions that are yanked. - if file.yanked.is_some_and(|yanked| yanked.is_yanked()) { - continue; - } + if !filename.version().is_stable() + && !matches!(self.prerelease, PrereleaseMode::Allow) + { + return false; + } - // Skip distributions that are incompatible with the Python requirement. - if let Some(requires_python) = self.requires_python { - if file - .requires_python - .as_ref() - .is_some_and(|file_requires_python| { - !requires_python.is_contained_by(file_requires_python) - }) - { - continue; - } - } + if file + .yanked + .as_ref() + .is_some_and(|yanked| yanked.is_yanked()) + { + return false; + } - // Skip distributions that are incompatible with the current platform. - if let DistFilename::WheelFilename(filename) = &filename { - if self - .tags - .is_some_and(|tags| !filename.compatibility(tags).is_compatible()) - { - continue; - } - } + if let Some(requires_python) = self.requires_python + && file + .requires_python + .as_ref() + .is_some_and(|file_requires_python| { + !requires_python.is_contained_by(file_requires_python) + }) + { + return false; + } - match filename { - DistFilename::WheelFilename(_) => { - best = Some(filename); - break; - } - DistFilename::SourceDistFilename(_) => { - if best.is_none() { - best = Some(filename); + if let DistFilename::WheelFilename(filename) = filename + && self + .tags + .is_some_and(|tags| !filename.compatibility(tags).is_compatible()) + { + return false; + } + + true + }; + + match archive { + MetadataFormat::Simple(archive) => { + for datum in archive.iter().rev() { + let files = + rkyv::deserialize::(&datum.files) + .expect("archived version files always deserializes"); + + let mut best: Option = None; + for (filename, file) in files.all() { + if !consider_candidate(&filename, &file) { + continue; + } + + match filename { + DistFilename::WheelFilename(_) => { + best = Some(filename); + break; + } + DistFilename::SourceDistFilename(_) if best.is_none() => { + best = Some(filename); + } + DistFilename::SourceDistFilename(_) => {} } } - } - } - match (latest.as_ref(), best) { - (Some(current), Some(best)) if best.version() > current.version() => { - latest = Some(best); + if let Some(best) = best { + update_latest(best); + } } - (None, Some(best)) => { - latest = Some(best); + } + MetadataFormat::Flat(entries) => { + for FlatIndexEntry { filename, file, .. } in entries { + if consider_candidate(&filename, &file) { + update_latest(filename); + } } - _ => {} } } } + Ok(latest) } } diff --git a/crates/uv/tests/it/pip_list.rs b/crates/uv/tests/it/pip_list.rs index 826adc084622e..7201d94995943 100644 --- a/crates/uv/tests/it/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -177,6 +177,51 @@ fn list_outdated_json() -> Result<()> { Ok(()) } +#[test] +fn list_outdated_find_links() -> Result<()> { + let context = TestContext::new("3.12"); + + let links_dir = context.workspace_root.join("scripts/links"); + + uv_snapshot!(context.filters(), context.pip_install() + .arg("validation==1.0.0") + .arg("--find-links") + .arg(&links_dir) + .arg("--no-index"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + validation==1.0.0 + "### + ); + + uv_snapshot!(context.filters(), context.pip_list() + .arg("--outdated") + .arg("--find-links") + .arg(&links_dir) + .arg("--no-index"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Package Version Latest Type + ---------- ------- ------ ----- + validation 1.0.0 3.0.0 wheel + + ----- stderr ----- + warning: validation-2.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z + warning: validation-3.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z + warning: validation-1.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z + "### + ); + + Ok(()) +} + #[test] fn list_outdated_freeze() { let context = uv_test::test_context!("3.12"); From e7484fe98f1a2f16bc3c11446c5d4843251d19cd Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 11:54:31 -0400 Subject: [PATCH 02/17] Remove result from test signature --- crates/uv/tests/it/pip_list.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/uv/tests/it/pip_list.rs b/crates/uv/tests/it/pip_list.rs index 7201d94995943..a25265ce6a178 100644 --- a/crates/uv/tests/it/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -178,7 +178,7 @@ fn list_outdated_json() -> Result<()> { } #[test] -fn list_outdated_find_links() -> Result<()> { +fn list_outdated_find_links() { let context = TestContext::new("3.12"); let links_dir = context.workspace_root.join("scripts/links"); @@ -218,8 +218,6 @@ fn list_outdated_find_links() -> Result<()> { warning: validation-1.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z "### ); - - Ok(()) } #[test] From f4a4400a9403584cf2db1907aedb7bb4a7b97940 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 12:20:09 -0400 Subject: [PATCH 03/17] Update snapshot --- crates/uv/tests/it/pip_install.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 56f48718059d6..759884ce7acef 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -6942,7 +6942,7 @@ fn already_installed_dependent_editable() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + ╰─▶ Because first-local was not found in the package registry and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. And because only second-local==0.1.0 is available and you require second-local, we can conclude that your requirements are unsatisfiable. " ); @@ -7062,7 +7062,7 @@ fn already_installed_local_path_dependent() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + ╰─▶ Because first-local was not found in the package registry and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. And because only second-local==0.1.0 is available and you require second-local, we can conclude that your requirements are unsatisfiable. " ); From a4b8d5e3e18f6cac6ff874dc526b1d17de82e993 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 12:30:16 -0400 Subject: [PATCH 04/17] Tweak ordering --- crates/uv/tests/it/sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 958d8a0749735..d2b18f73d15f0 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11820,8 +11820,8 @@ fn sync_build_tag() -> Result<()> { source = { registry = "[TEMP_DIR]/links" } wheels = [ { path = "[TEMP_DIR]/links/build_tag-1.0.0-1-py2.py3-none-any.whl" }, - { path = "[TEMP_DIR]/links/build_tag-1.0.0-3-py2.py3-none-any.whl" }, { path = "[TEMP_DIR]/links/build_tag-1.0.0-5-py2.py3-none-any.whl" }, + { path = "[TEMP_DIR]/links/build_tag-1.0.0-3-py2.py3-none-any.whl" }, ] [[package]] From 26befb9401b8e080309f3caf3912c2391e79ddb2 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 12:34:41 -0400 Subject: [PATCH 05/17] Sort flat index entries --- crates/uv/tests/it/pip_list.rs | 2 +- crates/uv/tests/it/sync.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/pip_list.rs b/crates/uv/tests/it/pip_list.rs index a25265ce6a178..9ddbdcbd83887 100644 --- a/crates/uv/tests/it/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -213,9 +213,9 @@ fn list_outdated_find_links() { validation 1.0.0 3.0.0 wheel ----- stderr ----- + warning: validation-1.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z warning: validation-2.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z warning: validation-3.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z - warning: validation-1.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z "### ); } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index d2b18f73d15f0..958d8a0749735 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11820,8 +11820,8 @@ fn sync_build_tag() -> Result<()> { source = { registry = "[TEMP_DIR]/links" } wheels = [ { path = "[TEMP_DIR]/links/build_tag-1.0.0-1-py2.py3-none-any.whl" }, - { path = "[TEMP_DIR]/links/build_tag-1.0.0-5-py2.py3-none-any.whl" }, { path = "[TEMP_DIR]/links/build_tag-1.0.0-3-py2.py3-none-any.whl" }, + { path = "[TEMP_DIR]/links/build_tag-1.0.0-5-py2.py3-none-any.whl" }, ] [[package]] From 22a559cb949986b4f670528e553cb5ece07e5c02 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 22:29:55 -0400 Subject: [PATCH 06/17] Keep existing message / semantics --- crates/uv-client/src/registry_client.rs | 4 ++++ crates/uv/tests/it/pip_install.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 9da19142caedd..ed43156ff0c2f 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -442,6 +442,10 @@ impl RegistryClient { } if results.is_empty() { + if self.index_urls.no_index() { + return Err(ErrorKind::NoIndex(package_name.to_string()).into()); + } + return match self.connectivity { Connectivity::Online => { Err(ErrorKind::RemotePackageNotFound(package_name.clone()).into()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 759884ce7acef..56f48718059d6 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -6942,7 +6942,7 @@ fn already_installed_dependent_editable() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the package registry and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. And because only second-local==0.1.0 is available and you require second-local, we can conclude that your requirements are unsatisfiable. " ); @@ -7062,7 +7062,7 @@ fn already_installed_local_path_dependent() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the package registry and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. And because only second-local==0.1.0 is available and you require second-local, we can conclude that your requirements are unsatisfiable. " ); From 7ba1843e93cf1ab7853c2edb6f3156f094a089bd Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 3 Oct 2025 11:13:20 -0400 Subject: [PATCH 07/17] Combine flat indexes into `IndexUrls` --- crates/uv-client/src/registry_client.rs | 13 +--- crates/uv-distribution-types/src/index_url.rs | 59 ++++++++++++------- crates/uv/src/commands/project/add.rs | 2 +- crates/uv/tests/it/show_settings.rs | 30 +++++----- 4 files changed, 57 insertions(+), 47 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index ed43156ff0c2f..f4c7043d21caa 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -21,7 +21,7 @@ use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, + BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_git::{GIT_LFS, GitError, GitHttpSettings, GitResolver, Reporter}; @@ -166,7 +166,6 @@ impl<'a> RegistryClientBuilder<'a> { pub fn build(mut self) -> Result { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); - let flat_index = self.index_locations.flat_indexes().cloned().collect(); // Build a base client let builder = self @@ -189,7 +188,6 @@ impl<'a> RegistryClientBuilder<'a> { connectivity, client, read_timeout, - flat_index, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), }) @@ -199,7 +197,6 @@ impl<'a> RegistryClientBuilder<'a> { pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient { self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); - let flat_index = self.index_locations.flat_indexes().cloned().collect(); // Wrap in any relevant middleware and handle connectivity. let client = self @@ -221,7 +218,6 @@ impl<'a> RegistryClientBuilder<'a> { connectivity, client, read_timeout, - flat_index, flat_indexes: Arc::default(), pyx_token_store: PyxTokenStore::from_settings().ok(), } @@ -237,8 +233,6 @@ pub struct RegistryClient { index_strategy: IndexStrategy, /// The strategy to use when selecting a PyTorch backend, if any. torch_backend: Option, - /// The configured flat indexes (e.g., `--find-links`). - flat_index: Vec, /// The underlying HTTP client. client: CachedClient, /// Used for the remote wheel METADATA cache. @@ -337,7 +331,7 @@ impl RegistryClient { capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { - let has_flat_indexes = !self.flat_index.is_empty(); + let has_flat_indexes = self.index_urls.flat_indexes().next().is_some(); // If `--no-index` is specified and no flat indexes are available, avoid fetching // regardless of whether the index is implicit, explicit, etc. @@ -348,8 +342,7 @@ impl RegistryClient { let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { - let find_links = self.flat_index.iter().map(IndexMetadataRef::from); - Either::Right(self.index_urls_for(package_name).chain(find_links)) + Either::Right(self.index_urls_for(package_name)) }; let mut results = Vec::new(); diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index e09b42bc7eed8..c5fefb25240d9 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -256,16 +256,16 @@ impl Deref for IndexUrl { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { indexes: Vec, - flat_index: Vec, + flat_indexes: Vec, no_index: bool, } impl IndexLocations { /// Determine the index URLs to use for fetching packages. - pub fn new(indexes: Vec, flat_index: Vec, no_index: bool) -> Self { + pub fn new(indexes: Vec, flat_indexes: Vec, no_index: bool) -> Self { Self { indexes, - flat_index, + flat_indexes, no_index, } } @@ -277,10 +277,10 @@ impl IndexLocations { /// /// If the current index location has an `index` set, it will be preserved. #[must_use] - pub fn combine(self, indexes: Vec, flat_index: Vec, no_index: bool) -> Self { + pub fn combine(self, indexes: Vec, flat_indexes: Vec, no_index: bool) -> Self { Self { indexes: self.indexes.into_iter().chain(indexes).collect(), - flat_index: self.flat_index.into_iter().chain(flat_index).collect(), + flat_indexes: self.flat_indexes.into_iter().chain(flat_indexes).collect(), no_index: self.no_index || no_index, } } @@ -383,7 +383,7 @@ impl<'a> IndexLocations { /// Return an iterator over the [`FlatIndexLocation`] entries. pub fn flat_indexes(&'a self) -> impl Iterator + 'a { - self.flat_index.iter() + self.flat_indexes.iter() } /// Return the `--no-index` flag. @@ -395,6 +395,7 @@ impl<'a> IndexLocations { pub fn index_urls(&'a self) -> IndexUrls { IndexUrls { indexes: self.indexes.clone(), + flat_indexes: self.flat_indexes.clone(), no_index: self.no_index, } } @@ -407,7 +408,7 @@ impl<'a> IndexLocations { /// that the last-defined index is the first item in the vector. pub fn allowed_indexes(&'a self) -> Vec<&'a Index> { if self.no_index { - self.flat_index.iter().rev().collect() + self.flat_indexes.iter().rev().collect() } else { let mut indexes = vec![]; @@ -416,7 +417,7 @@ impl<'a> IndexLocations { for index in { self.indexes .iter() - .chain(self.flat_index.iter()) + .chain(self.flat_indexes.iter()) .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) } { if index.default { @@ -446,11 +447,11 @@ impl<'a> IndexLocations { /// that the last-defined index is the first item in the vector. pub fn known_indexes(&'a self) -> impl Iterator { if self.no_index { - Either::Left(self.flat_index.iter().rev()) + Either::Left(self.flat_indexes.iter().rev()) } else { Either::Right( std::iter::once(&*DEFAULT_INDEX) - .chain(self.flat_index.iter().rev()) + .chain(self.flat_indexes.iter().rev()) .chain(self.indexes.iter().rev()), ) } @@ -512,17 +513,24 @@ impl From<&IndexLocations> for uv_auth::Indexes { #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct IndexUrls { indexes: Vec, + flat_indexes: Vec, no_index: bool, } impl<'a> IndexUrls { - pub fn from_indexes(indexes: Vec) -> Self { + pub fn from_indexes(indexes: Vec, flat_indexes: Vec) -> Self { Self { indexes, + flat_indexes, no_index: false, } } + /// Return an iterator over the configured flat-index locations. + pub fn flat_indexes(&'a self) -> impl Iterator + 'a { + self.flat_indexes.iter() + } + /// Return the default [`Index`] entry. /// /// If `--no-index` is set, return `None`. @@ -563,14 +571,23 @@ impl<'a> IndexUrls { /// Prioritizes the `[tool.uv.index]` definitions over the `--extra-index-url` definitions /// over the `--index-url` definition. /// - /// If `no_index` was enabled, then this always returns an empty - /// iterator. + /// If `no_index` was enabled, then this always returns only the flat indexes. pub fn indexes(&'a self) -> impl Iterator + 'a { let mut seen = FxHashSet::default(); - self.implicit_indexes() - .chain(self.default_index()) - .filter(|index| !index.explicit) - .filter(move |index| seen.insert(index.raw_url())) // Filter out redundant raw URLs + + let simple_indexes = if self.no_index { + Either::Left(std::iter::empty::<&'a Index>()) + } else { + Either::Right( + self.implicit_indexes() + .chain(self.default_index()) + .filter(|index| !index.explicit), + ) + }; + + simple_indexes + .chain(self.flat_indexes.iter()) + .filter(move |index| seen.insert(index.raw_url())) } /// Return an iterator over all user-defined [`Index`] entries in order. @@ -798,7 +815,7 @@ mod tests { }, ]; - let index_urls = IndexUrls::from_indexes(indexes); + let index_urls = IndexUrls::from_indexes(indexes, Vec::new()); let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap(); assert_eq!( @@ -836,7 +853,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone()); + let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); @@ -883,7 +900,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone()); + let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); @@ -926,7 +943,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone()); + let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let nvidia_url = IndexUrl::from_str("https://pypi.nvidia.com").unwrap(); diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 1ebcb879d84d3..0a04d8e12e814 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -686,7 +686,7 @@ pub(crate) async fn add( // Add any indexes that were provided on the command-line, in priority order. if !raw { - let urls = IndexUrls::from_indexes(indexes); + let urls = IndexUrls::from_indexes(indexes, Vec::new()); let mut indexes = urls.defined_indexes().collect::>(); indexes.reverse(); for index in indexes { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 01a592ba1ce22..08cf0e1fc52bf 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -106,7 +106,7 @@ fn pip_compile_baseline() { settings: PipSettings { index_locations: IndexLocations { indexes: [], - flat_index: [], + flat_indexes: [], no_index: false, }, python: None, @@ -291,7 +291,7 @@ fn pip_install_baseline() { settings: PipSettings { index_locations: IndexLocations { indexes: [], - flat_index: [], + flat_indexes: [], no_index: false, }, python: None, @@ -488,7 +488,7 @@ fn lock_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_index: [], + flat_indexes: [], no_index: false, }, index_strategy: FirstIndex, @@ -617,7 +617,7 @@ fn version_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_index: [], + flat_indexes: [], no_index: false, }, index_strategy: FirstIndex, @@ -784,7 +784,7 @@ fn tool_install_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_index: [], + flat_indexes: [], no_index: false, }, index_strategy: FirstIndex, @@ -902,7 +902,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -1058,7 +1058,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -1251,7 +1251,7 @@ fn resolve_index_url() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, "# @@ -1357,9 +1357,9 @@ fn resolve_find_links() -> anyhow::Result<()> { settings: PipSettings { index_locations: IndexLocations { indexes: [], - - flat_index: [], + - flat_indexes: [], - no_index: false, - + flat_index: [ + + flat_indexes: [ + Index { + name: None, + url: Url( @@ -1554,7 +1554,7 @@ fn resolve_top_level() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, "# @@ -1973,7 +1973,7 @@ fn resolve_both() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -2105,7 +2105,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -2318,7 +2318,7 @@ fn resolve_config_file() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, @@ -120,7 +156,7 @@ @@ -2656,7 +2656,7 @@ fn index_priority() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_index: [], + flat_indexes: [], no_index: false, }, "# From 1bbffb15279b37ef861d6df01fe98a12fd18fde7 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 3 Oct 2025 11:37:02 -0400 Subject: [PATCH 08/17] Split up functions --- crates/uv-client/src/registry_client.rs | 6 ++---- crates/uv-distribution-types/src/index_url.rs | 11 ++++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index f4c7043d21caa..d8592162b87b5 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -331,11 +331,9 @@ impl RegistryClient { capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { - let has_flat_indexes = self.index_urls.flat_indexes().next().is_some(); - // If `--no-index` is specified and no flat indexes are available, avoid fetching // regardless of whether the index is implicit, explicit, etc. - if self.index_urls.no_index() && !has_flat_indexes { + if self.index_urls.no_indexes() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } @@ -435,7 +433,7 @@ impl RegistryClient { } if results.is_empty() { - if self.index_urls.no_index() { + if self.index_urls.simple_indexes_disabled() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index c5fefb25240d9..d7389912d626f 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -576,7 +576,7 @@ impl<'a> IndexUrls { let mut seen = FxHashSet::default(); let simple_indexes = if self.no_index { - Either::Left(std::iter::empty::<&'a Index>()) + Either::Left(std::iter::empty()) } else { Either::Right( self.implicit_indexes() @@ -621,11 +621,16 @@ impl<'a> IndexUrls { Either::Right(non_default.into_iter().chain(default)) } - /// Return the `--no-index` flag. - pub fn no_index(&self) -> bool { + /// Returns `true` if simple indexes (e.g., PyPI or `--extra-index-url`) are disabled via `--no-index`. + pub fn simple_indexes_disabled(&self) -> bool { self.no_index } + /// Returns `true` if there are no index sources at all (simple indexes disabled and no flat indexes). + pub fn no_indexes(&self) -> bool { + self.no_index && self.flat_indexes.is_empty() + } + /// Return the [`IndexStatusCodeStrategy`] for an [`IndexUrl`]. pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy { for index in &self.indexes { From 4da88957418c7b1ca73dce73d9aec3d3140ee96b Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 24 Oct 2025 22:06:14 -0400 Subject: [PATCH 09/17] Move the candidate filtering closure into a dedicated helper --- crates/uv/src/commands/pip/latest.rs | 126 +++++++++++++++------------ 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index ed29a75ef75d1..884c293a4105b 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -1,7 +1,7 @@ use tokio::sync::Semaphore; use tracing::debug; -use uv_client::{FlatIndexEntry, MetadataFormat, RegistryClient, VersionFiles}; +use uv_client::{MetadataFormat, RegistryClient, VersionFiles}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ File, IndexCapabilities, IndexLocations, IndexMetadataRef, IndexUrl, RequiresPython, @@ -36,6 +36,70 @@ impl LatestClient<'_> { .exclude_newer_package_for_index(package, self.index_locations.exclude_newer_for(index)) } + fn consider_candidate( + &self, + filename: &DistFilename, + file: &File, + exclude_newer: Option<&jiff::Timestamp>, + ) -> bool { + // Respect any exclude-newer cutoffs that were provided. + if let Some(exclude_newer) = exclude_newer { + match file.upload_time_utc_ms.as_ref() { + Some(&upload_time) if upload_time >= exclude_newer.as_millisecond() => { + return false; + } + None => { + warn_user_once!( + "{} is missing an upload date, but user provided: {}", + file.filename, + exclude_newer + ); + } + _ => {} + } + } + + // Unless explicitly allowed, skip pre-release artifacts. + if !filename.version().is_stable() { + if !matches!(self.prerelease, PrereleaseMode::Allow) { + return false; + } + } + + // Avoid yanked or otherwise withdrawn files. + if file + .yanked + .as_ref() + .is_some_and(|yanked| yanked.is_yanked()) + { + return false; + } + + // Enforce the interpreter's `Requires-Python` constraints. + if let Some(requires_python) = self.requires_python + && file + .requires_python + .as_ref() + .is_some_and(|file_requires_python| { + !requires_python.is_contained_by(file_requires_python) + }) + { + return false; + } + + // Skip wheels that aren't compatible with the current platform. + if let DistFilename::WheelFilename(filename) = filename { + if self + .tags + .is_some_and(|tags| !filename.compatibility(tags).is_compatible()) + { + return false; + } + } + + true + } + /// Find the latest version of a package from an index. pub(crate) async fn find_latest( &self, @@ -89,59 +153,6 @@ impl LatestClient<'_> { for (index, archive) in archives { let exclude_newer = self.effective_exclude_newer(package, index); - let consider_candidate = |filename: &DistFilename, file: &File| { - if let Some(exclude_newer) = &exclude_newer { - match file.upload_time_utc_ms.as_ref() { - Some(&upload_time) if upload_time >= exclude_newer.as_millisecond() => { - return false; - } - None => { - warn_user_once!( - "{} is missing an upload date, but user provided: {}", - file.filename, - exclude_newer - ); - } - _ => {} - } - } - - if !filename.version().is_stable() - && !matches!(self.prerelease, PrereleaseMode::Allow) - { - return false; - } - - if file - .yanked - .as_ref() - .is_some_and(|yanked| yanked.is_yanked()) - { - return false; - } - - if let Some(requires_python) = self.requires_python - && file - .requires_python - .as_ref() - .is_some_and(|file_requires_python| { - !requires_python.is_contained_by(file_requires_python) - }) - { - return false; - } - - if let DistFilename::WheelFilename(filename) = filename - && self - .tags - .is_some_and(|tags| !filename.compatibility(tags).is_compatible()) - { - return false; - } - - true - }; - match archive { MetadataFormat::Simple(archive) => { for datum in archive.iter().rev() { @@ -151,7 +162,7 @@ impl LatestClient<'_> { let mut best: Option = None; for (filename, file) in files.all() { - if !consider_candidate(&filename, &file) { + if !self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { continue; } @@ -173,8 +184,9 @@ impl LatestClient<'_> { } } MetadataFormat::Flat(entries) => { - for FlatIndexEntry { filename, file, .. } in entries { - if consider_candidate(&filename, &file) { + for entry in entries { + let (filename, file, _) = entry.into_parts(); + if self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { update_latest(filename); } } From 00c13b8d780038178124831ade8b79ce87edbc39 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 24 Oct 2025 22:15:44 -0400 Subject: [PATCH 10/17] Drop exclude newer from snapshot tests --- crates/uv/tests/it/pip_list.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/uv/tests/it/pip_list.rs b/crates/uv/tests/it/pip_list.rs index 9ddbdcbd83887..978624eb97bf1 100644 --- a/crates/uv/tests/it/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -5,6 +5,7 @@ use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; use assert_fs::prelude::*; +use uv_static::EnvVars; use uv_test::uv_snapshot; #[test] @@ -179,11 +180,12 @@ fn list_outdated_json() -> Result<()> { #[test] fn list_outdated_find_links() { - let context = TestContext::new("3.12"); + let context = uv_test::test_context!("3.12"); - let links_dir = context.workspace_root.join("scripts/links"); + let links_dir = context.workspace_root.join("test/links"); uv_snapshot!(context.filters(), context.pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("validation==1.0.0") .arg("--find-links") .arg(&links_dir) @@ -201,6 +203,7 @@ fn list_outdated_find_links() { ); uv_snapshot!(context.filters(), context.pip_list() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("--outdated") .arg("--find-links") .arg(&links_dir) @@ -213,9 +216,6 @@ fn list_outdated_find_links() { validation 1.0.0 3.0.0 wheel ----- stderr ----- - warning: validation-1.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z - warning: validation-2.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z - warning: validation-3.0.0-py3-none-any.whl is missing an upload date, but user provided: global: 2024-03-25T00:00:00Z "### ); } From d2e771781efaf91f346074636fbda8e2c8e36969 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 22:45:35 -0400 Subject: [PATCH 11/17] Merge candidates from all flat indexes --- crates/uv-client/src/registry_client.rs | 19 +++++++++++++++ crates/uv-distribution-types/src/index_url.rs | 19 ++++----------- crates/uv/tests/it/pip_list.rs | 24 +++++++++++++++++-- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index d8592162b87b5..92c1aca5ed0f3 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -337,6 +337,7 @@ impl RegistryClient { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } + let has_explicit_index = index.is_some(); let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { @@ -432,6 +433,24 @@ impl RegistryClient { } } + if !has_explicit_index { + let flat_results = futures::stream::iter(self.index_urls.flat_indexes()) + .map(async |index| { + let _permit = download_concurrency.acquire().await; + let entries = self.flat_single_index(package_name, index.url()).await?; + Ok((index.url(), entries)) + }) + .buffered(8) + .filter_map(async |result: Result<_, Error>| match result { + Ok((_, entries)) if entries.is_empty() => None, + Ok((index, entries)) => Some(Ok((index, MetadataFormat::Flat(entries)))), + Err(err) => Some(Err(err)), + }) + .try_collect::>() + .await?; + results.extend(flat_results); + } + if results.is_empty() { if self.index_urls.simple_indexes_disabled() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index d7389912d626f..f1c20b733aeea 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -571,22 +571,13 @@ impl<'a> IndexUrls { /// Prioritizes the `[tool.uv.index]` definitions over the `--extra-index-url` definitions /// over the `--index-url` definition. /// - /// If `no_index` was enabled, then this always returns only the flat indexes. + /// If `no_index` was enabled, then this always returns an empty + /// iterator. pub fn indexes(&'a self) -> impl Iterator + 'a { let mut seen = FxHashSet::default(); - - let simple_indexes = if self.no_index { - Either::Left(std::iter::empty()) - } else { - Either::Right( - self.implicit_indexes() - .chain(self.default_index()) - .filter(|index| !index.explicit), - ) - }; - - simple_indexes - .chain(self.flat_indexes.iter()) + self.implicit_indexes() + .chain(self.default_index()) + .filter(|index| !index.explicit) .filter(move |index| seen.insert(index.raw_url())) } diff --git a/crates/uv/tests/it/pip_list.rs b/crates/uv/tests/it/pip_list.rs index 978624eb97bf1..a6f212f9d1251 100644 --- a/crates/uv/tests/it/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -179,10 +179,26 @@ fn list_outdated_json() -> Result<()> { } #[test] -fn list_outdated_find_links() { +fn list_outdated_find_links() -> Result<()> { let context = uv_test::test_context!("3.12"); let links_dir = context.workspace_root.join("test/links"); + let first_links_dir = context.temp_dir.child("first-links"); + first_links_dir.create_dir_all()?; + fs_err::copy( + links_dir.join("validation-2.0.0-py3-none-any.whl"), + first_links_dir + .child("validation-2.0.0-py3-none-any.whl") + .path(), + )?; + let second_links_dir = context.temp_dir.child("second-links"); + second_links_dir.create_dir_all()?; + fs_err::copy( + links_dir.join("validation-3.0.0-py3-none-any.whl"), + second_links_dir + .child("validation-3.0.0-py3-none-any.whl") + .path(), + )?; uv_snapshot!(context.filters(), context.pip_install() .env_remove(EnvVars::UV_EXCLUDE_NEWER) @@ -206,7 +222,9 @@ fn list_outdated_find_links() { .env_remove(EnvVars::UV_EXCLUDE_NEWER) .arg("--outdated") .arg("--find-links") - .arg(&links_dir) + .arg(first_links_dir.path()) + .arg("--find-links") + .arg(second_links_dir.path()) .arg("--no-index"), @r###" success: true exit_code: 0 @@ -218,6 +236,8 @@ fn list_outdated_find_links() { ----- stderr ----- "### ); + + Ok(()) } #[test] From 0c7e74c72507db40f7b5d30447f3fe0f706b4912 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 22:48:57 -0400 Subject: [PATCH 12/17] Honor no-index for simple registry lookups --- crates/uv-client/src/registry_client.rs | 93 +++++++++++++++++-- crates/uv-distribution-types/src/index_url.rs | 5 - 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 92c1aca5ed0f3..93980dfee4262 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -331,18 +331,15 @@ impl RegistryClient { capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { - // If `--no-index` is specified and no flat indexes are available, avoid fetching - // regardless of whether the index is implicit, explicit, etc. - if self.index_urls.no_indexes() { - return Err(ErrorKind::NoIndex(package_name.to_string()).into()); - } - let has_explicit_index = index.is_some(); let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { Either::Right(self.index_urls_for(package_name)) }; + let indexes = indexes.filter(|index| { + index.format == IndexFormat::Flat || !self.index_urls.simple_indexes_disabled() + }); let mut results = Vec::new(); @@ -1714,18 +1711,24 @@ impl Connectivity { mod tests { use std::str::FromStr; + use tokio::sync::Semaphore; use url::Url; use uv_normalize::PackageName; use uv_pypi_types::PypiSimpleDetail; use uv_redacted::DisplaySafeUrl; + use uv_torch::{TorchBackend, TorchSource, TorchStrategy}; use crate::{ - BaseClientBuilder, SimpleDetailMetadata, SimpleDetailMetadatum, html::SimpleDetailHTML, + BaseClientBuilder, Connectivity, SimpleDetailMetadata, SimpleDetailMetadatum, + html::SimpleDetailHTML, }; use crate::RegistryClientBuilder; use uv_cache::Cache; - use uv_distribution_types::{FileLocation, ToUrlError}; + use uv_distribution_types::{ + FileLocation, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, + IndexUrl, ToUrlError, + }; use uv_small_str::SmallString; use wiremock::matchers::{basic_auth, method, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -1749,6 +1752,80 @@ mod tests { server } + #[tokio::test] + async fn no_index_disables_explicit_simple_index() -> Result<(), Error> { + let server = MockServer::start().await; + let explicit_index = IndexUrl::from_str(&format!("{}/simple", server.uri()))?; + let flat_index = Index::from_find_links(IndexUrl::from_str("https://example.com/flat")?); + let registry_client = + RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) + .index_locations(IndexLocations::new(vec![], vec![flat_index], true)) + .build()?; + + let error = registry_client + .simple_detail( + &PackageName::from_str("validation")?, + Some(IndexMetadataRef { + url: &explicit_index, + format: IndexFormat::Simple, + }), + &IndexCapabilities::default(), + &Semaphore::new(1), + ) + .await + .expect_err("explicit simple index should be disabled"); + + assert!(matches!( + error.kind(), + crate::ErrorKind::NoIndex(package) if package == "validation" + )); + assert!( + server + .received_requests() + .await + .expect("request recording should be enabled") + .is_empty() + ); + + Ok(()) + } + + #[tokio::test] + async fn no_index_disables_torch_simple_index() -> Result<(), Error> { + let flat_index_dir = tempfile::tempdir()?; + let flat_index = Index::from_find_links(IndexUrl::parse( + flat_index_dir.path().to_string_lossy().as_ref(), + None, + )?); + let registry_client = RegistryClientBuilder::new( + BaseClientBuilder::default().connectivity(Connectivity::Offline), + Cache::temp()?, + ) + .index_locations(IndexLocations::new(vec![], vec![flat_index], true)) + .torch_backend(Some(TorchStrategy::Backend { + backend: TorchBackend::Cpu, + source: TorchSource::PyTorch, + })) + .build()?; + + let error = registry_client + .simple_detail( + &PackageName::from_str("torch")?, + None, + &IndexCapabilities::default(), + &Semaphore::new(1), + ) + .await + .expect_err("torch simple index should be disabled"); + + assert!(matches!( + error.kind(), + crate::ErrorKind::NoIndex(package) if package == "torch" + )); + + Ok(()) + } + #[tokio::test] async fn test_redirect_to_server_with_credentials() -> Result<(), Error> { let username = "user"; diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index f1c20b733aeea..a1cc79d2f349d 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -617,11 +617,6 @@ impl<'a> IndexUrls { self.no_index } - /// Returns `true` if there are no index sources at all (simple indexes disabled and no flat indexes). - pub fn no_indexes(&self) -> bool { - self.no_index && self.flat_indexes.is_empty() - } - /// Return the [`IndexStatusCodeStrategy`] for an [`IndexUrl`]. pub fn status_code_strategy_for(&self, url: &IndexUrl) -> IndexStatusCodeStrategy { for index in &self.indexes { From b142414b844dbf5d868a8c08aa256f9b1409e1bc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 23:14:01 -0400 Subject: [PATCH 13/17] Combine candidates from legacy find-links --- crates/uv-client/src/registry_client.rs | 18 +++++++---- crates/uv/tests/it/pip_install.rs | 43 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 93980dfee4262..e2161790e867a 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -435,17 +435,21 @@ impl RegistryClient { .map(async |index| { let _permit = download_concurrency.acquire().await; let entries = self.flat_single_index(package_name, index.url()).await?; - Ok((index.url(), entries)) + Ok::<_, Error>((index.url(), entries)) }) .buffered(8) - .filter_map(async |result: Result<_, Error>| match result { - Ok((_, entries)) if entries.is_empty() => None, - Ok((index, entries)) => Some(Ok((index, MetadataFormat::Flat(entries)))), - Err(err) => Some(Err(err)), - }) .try_collect::>() .await?; - results.extend(flat_results); + let flat_index = flat_results.first().map(|(index, _)| *index); + let flat_entries = flat_results + .into_iter() + .flat_map(|(_, entries)| entries) + .collect::>(); + if let Some(flat_index) = flat_index + && !flat_entries.is_empty() + { + results.push((flat_index, MetadataFormat::Flat(flat_entries))); + } } if results.is_empty() { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 56f48718059d6..0f8f9f2c05bf9 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7518,6 +7518,49 @@ fn find_links() { ); } +/// Install the latest version across multiple `--find-links` directories. +#[test] +fn find_links_multiple() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let links_dir = context.workspace_root.join("test/links"); + + let first_links_dir = context.temp_dir.child("first-links"); + first_links_dir.create_dir_all()?; + fs::copy( + links_dir.join("ok-1.0.0-py3-none-any.whl"), + first_links_dir.child("ok-1.0.0-py3-none-any.whl").path(), + )?; + + let second_links_dir = context.temp_dir.child("second-links"); + second_links_dir.create_dir_all()?; + fs::copy( + links_dir.join("ok-2.0.0-py3-none-any.whl"), + second_links_dir.child("ok-2.0.0-py3-none-any.whl").path(), + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("ok") + .arg("--no-index") + .arg("--find-links") + .arg(first_links_dir.path()) + .arg("--find-links") + .arg(second_links_dir.path()), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + ok==2.0.0 + " + ); + + Ok(()) +} + /// Install from a `--find-links` HTML page with uppercase tag and attribute names. #[tokio::test] async fn find_links_uppercase_html() -> Result<()> { From 9b796f6b8c07951e79a02b6f6e7b64759008f140 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 23:40:04 -0400 Subject: [PATCH 14/17] Disable explicit flat indexes with no-index --- crates/uv-client/src/registry_client.rs | 41 +++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index e2161790e867a..3c814f6c306a0 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -337,9 +337,7 @@ impl RegistryClient { } else { Either::Right(self.index_urls_for(package_name)) }; - let indexes = indexes.filter(|index| { - index.format == IndexFormat::Flat || !self.index_urls.simple_indexes_disabled() - }); + let indexes = indexes.filter(|_| !self.index_urls.simple_indexes_disabled()); let mut results = Vec::new(); @@ -1794,6 +1792,43 @@ mod tests { Ok(()) } + #[tokio::test] + async fn no_index_disables_explicit_flat_index() -> Result<(), Error> { + let server = MockServer::start().await; + let explicit_index = IndexUrl::from_str(&server.uri())?; + let registry_client = + RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) + .index_locations(IndexLocations::new(vec![], vec![], true)) + .build()?; + + let error = registry_client + .simple_detail( + &PackageName::from_str("validation")?, + Some(IndexMetadataRef { + url: &explicit_index, + format: IndexFormat::Flat, + }), + &IndexCapabilities::default(), + &Semaphore::new(1), + ) + .await + .expect_err("explicit flat index should be disabled"); + + assert!(matches!( + error.kind(), + crate::ErrorKind::NoIndex(package) if package == "validation" + )); + assert!( + server + .received_requests() + .await + .expect("request recording should be enabled") + .is_empty() + ); + + Ok(()) + } + #[tokio::test] async fn no_index_disables_torch_simple_index() -> Result<(), Error> { let flat_index_dir = tempfile::tempdir()?; From 9f36d2b2a2966a063a7aa50e060ad1f63688a105 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 23:42:41 -0400 Subject: [PATCH 15/17] Limit find-links metadata to latest queries --- crates/uv-client/src/registry_client.rs | 74 ++++++++++++++++++++++++- crates/uv/src/commands/pip/latest.rs | 2 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 3c814f6c306a0..92ae4cf0018ee 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -323,13 +323,49 @@ impl RegistryClient { /// "Simple" here refers to [PEP 503 – Simple Repository API](https://peps.python.org/pep-0503/) /// and [PEP 691 – JSON-based Simple API for Python Package Indexes](https://peps.python.org/pep-0691/), /// which the PyPI JSON API implements. - #[instrument(skip_all, fields(package = % package_name))] pub async fn simple_detail<'index>( &'index self, package_name: &PackageName, index: Option>, capabilities: &IndexCapabilities, download_concurrency: &Semaphore, + ) -> Result, Error> { + self.simple_detail_inner( + package_name, + index, + capabilities, + download_concurrency, + false, + ) + .await + } + + /// Fetch package metadata from an index and the configured legacy `--find-links` locations. + pub async fn simple_detail_with_find_links<'index>( + &'index self, + package_name: &PackageName, + index: Option>, + capabilities: &IndexCapabilities, + download_concurrency: &Semaphore, + ) -> Result, Error> { + self.simple_detail_inner( + package_name, + index, + capabilities, + download_concurrency, + true, + ) + .await + } + + #[instrument(skip_all, fields(package = % package_name))] + async fn simple_detail_inner<'index>( + &'index self, + package_name: &PackageName, + index: Option>, + capabilities: &IndexCapabilities, + download_concurrency: &Semaphore, + include_find_links: bool, ) -> Result, Error> { let has_explicit_index = index.is_some(); let indexes = if let Some(index) = index { @@ -428,7 +464,7 @@ impl RegistryClient { } } - if !has_explicit_index { + if include_find_links && !has_explicit_index { let flat_results = futures::stream::iter(self.index_urls.flat_indexes()) .map(async |index| { let _permit = download_concurrency.acquire().await; @@ -1865,6 +1901,40 @@ mod tests { Ok(()) } + #[tokio::test] + async fn simple_detail_does_not_fetch_legacy_find_links() -> Result<(), Error> { + let server = MockServer::start().await; + let flat_index = Index::from_find_links(IndexUrl::from_str(&server.uri())?); + let registry_client = + RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) + .index_locations(IndexLocations::new(vec![], vec![flat_index], true)) + .build()?; + + let error = registry_client + .simple_detail( + &PackageName::from_str("validation")?, + None, + &IndexCapabilities::default(), + &Semaphore::new(1), + ) + .await + .expect_err("legacy find-links should not be fetched"); + + assert!(matches!( + error.kind(), + crate::ErrorKind::NoIndex(package) if package == "validation" + )); + assert!( + server + .received_requests() + .await + .expect("request recording should be enabled") + .is_empty() + ); + + Ok(()) + } + #[tokio::test] async fn test_redirect_to_server_with_credentials() -> Result<(), Error> { let username = "user"; diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 884c293a4105b..7ca89798badc0 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -131,7 +131,7 @@ impl LatestClient<'_> { let archives = match self .client - .simple_detail( + .simple_detail_with_find_links( package, index.map(IndexMetadataRef::from), self.capabilities, From 9989be8fc592443027895ce08378b74624b34b3f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Jun 2026 23:57:35 -0400 Subject: [PATCH 16/17] Simplify find-links metadata handling --- crates/uv-client/src/registry_client.rs | 168 +++++++----------- crates/uv-distribution-types/src/index_url.rs | 40 ++--- crates/uv/src/commands/pip/latest.rs | 57 ++---- crates/uv/src/commands/project/add.rs | 2 +- crates/uv/tests/it/show_settings.rs | 30 ++-- 5 files changed, 118 insertions(+), 179 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 92ae4cf0018ee..a341bd27aa0f2 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -21,7 +21,7 @@ use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, + BuiltDist, File, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_git::{GIT_LFS, GitError, GitHttpSettings, GitResolver, Reporter}; @@ -323,6 +323,7 @@ impl RegistryClient { /// "Simple" here refers to [PEP 503 – Simple Repository API](https://peps.python.org/pep-0503/) /// and [PEP 691 – JSON-based Simple API for Python Package Indexes](https://peps.python.org/pep-0691/), /// which the PyPI JSON API implements. + #[instrument(skip_all, fields(package = % package_name))] pub async fn simple_detail<'index>( &'index self, package_name: &PackageName, @@ -341,6 +342,7 @@ impl RegistryClient { } /// Fetch package metadata from an index and the configured legacy `--find-links` locations. + #[instrument(skip_all, fields(package = % package_name))] pub async fn simple_detail_with_find_links<'index>( &'index self, package_name: &PackageName, @@ -358,7 +360,6 @@ impl RegistryClient { .await } - #[instrument(skip_all, fields(package = % package_name))] async fn simple_detail_inner<'index>( &'index self, package_name: &PackageName, @@ -373,7 +374,7 @@ impl RegistryClient { } else { Either::Right(self.index_urls_for(package_name)) }; - let indexes = indexes.filter(|_| !self.index_urls.simple_indexes_disabled()); + let indexes = indexes.filter(|_| !self.index_urls.no_index()); let mut results = Vec::new(); @@ -465,19 +466,17 @@ impl RegistryClient { } if include_find_links && !has_explicit_index { - let flat_results = futures::stream::iter(self.index_urls.flat_indexes()) + let flat_index = self.index_urls.flat_indexes().next().map(Index::url); + let flat_entries = futures::stream::iter(self.index_urls.flat_indexes()) .map(async |index| { let _permit = download_concurrency.acquire().await; - let entries = self.flat_single_index(package_name, index.url()).await?; - Ok::<_, Error>((index.url(), entries)) + self.flat_single_index(package_name, index.url()).await }) .buffered(8) .try_collect::>() - .await?; - let flat_index = flat_results.first().map(|(index, _)| *index); - let flat_entries = flat_results + .await? .into_iter() - .flat_map(|(_, entries)| entries) + .flatten() .collect::>(); if let Some(flat_index) = flat_index && !flat_entries.is_empty() @@ -487,7 +486,7 @@ impl RegistryClient { } if results.is_empty() { - if self.index_urls.simple_indexes_disabled() { + if self.index_urls.no_index() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } @@ -1757,11 +1756,9 @@ mod tests { use uv_torch::{TorchBackend, TorchSource, TorchStrategy}; use crate::{ - BaseClientBuilder, Connectivity, SimpleDetailMetadata, SimpleDetailMetadatum, - html::SimpleDetailHTML, + BaseClientBuilder, Connectivity, RegistryClient, RegistryClientBuilder, + SimpleDetailMetadata, SimpleDetailMetadatum, html::SimpleDetailHTML, }; - - use crate::RegistryClientBuilder; use uv_cache::Cache; use uv_distribution_types::{ FileLocation, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, @@ -1790,33 +1787,37 @@ mod tests { server } - #[tokio::test] - async fn no_index_disables_explicit_simple_index() -> Result<(), Error> { - let server = MockServer::start().await; - let explicit_index = IndexUrl::from_str(&format!("{}/simple", server.uri()))?; - let flat_index = Index::from_find_links(IndexUrl::from_str("https://example.com/flat")?); - let registry_client = + fn no_index_client(flat_indexes: Vec) -> Result { + Ok( RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) - .index_locations(IndexLocations::new(vec![], vec![flat_index], true)) - .build()?; + .index_locations(IndexLocations::new(vec![], flat_indexes, true)) + .build()?, + ) + } - let error = registry_client + async fn assert_no_index( + client: &RegistryClient, + package: &str, + index: Option>, + ) -> Result<(), Error> { + let error = client .simple_detail( - &PackageName::from_str("validation")?, - Some(IndexMetadataRef { - url: &explicit_index, - format: IndexFormat::Simple, - }), + &PackageName::from_str(package)?, + index, &IndexCapabilities::default(), &Semaphore::new(1), ) .await - .expect_err("explicit simple index should be disabled"); + .expect_err("index lookup should be disabled"); assert!(matches!( error.kind(), - crate::ErrorKind::NoIndex(package) if package == "validation" + crate::ErrorKind::NoIndex(error_package) if error_package == package )); + Ok(()) + } + + async fn assert_no_requests(server: &MockServer) { assert!( server .received_requests() @@ -1824,7 +1825,25 @@ mod tests { .expect("request recording should be enabled") .is_empty() ); + } + #[tokio::test] + async fn no_index_disables_explicit_simple_index() -> Result<(), Error> { + let server = MockServer::start().await; + let explicit_index = IndexUrl::from_str(&format!("{}/simple", server.uri()))?; + let flat_index = Index::from_find_links(IndexUrl::from_str("https://example.com/flat")?); + let registry_client = no_index_client(vec![flat_index])?; + + assert_no_index( + ®istry_client, + "validation", + Some(IndexMetadataRef { + url: &explicit_index, + format: IndexFormat::Simple, + }), + ) + .await?; + assert_no_requests(&server).await; Ok(()) } @@ -1832,36 +1851,18 @@ mod tests { async fn no_index_disables_explicit_flat_index() -> Result<(), Error> { let server = MockServer::start().await; let explicit_index = IndexUrl::from_str(&server.uri())?; - let registry_client = - RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) - .index_locations(IndexLocations::new(vec![], vec![], true)) - .build()?; - - let error = registry_client - .simple_detail( - &PackageName::from_str("validation")?, - Some(IndexMetadataRef { - url: &explicit_index, - format: IndexFormat::Flat, - }), - &IndexCapabilities::default(), - &Semaphore::new(1), - ) - .await - .expect_err("explicit flat index should be disabled"); - - assert!(matches!( - error.kind(), - crate::ErrorKind::NoIndex(package) if package == "validation" - )); - assert!( - server - .received_requests() - .await - .expect("request recording should be enabled") - .is_empty() - ); - + let registry_client = no_index_client(vec![])?; + + assert_no_index( + ®istry_client, + "validation", + Some(IndexMetadataRef { + url: &explicit_index, + format: IndexFormat::Flat, + }), + ) + .await?; + assert_no_requests(&server).await; Ok(()) } @@ -1883,21 +1884,7 @@ mod tests { })) .build()?; - let error = registry_client - .simple_detail( - &PackageName::from_str("torch")?, - None, - &IndexCapabilities::default(), - &Semaphore::new(1), - ) - .await - .expect_err("torch simple index should be disabled"); - - assert!(matches!( - error.kind(), - crate::ErrorKind::NoIndex(package) if package == "torch" - )); - + assert_no_index(®istry_client, "torch", None).await?; Ok(()) } @@ -1905,33 +1892,10 @@ mod tests { async fn simple_detail_does_not_fetch_legacy_find_links() -> Result<(), Error> { let server = MockServer::start().await; let flat_index = Index::from_find_links(IndexUrl::from_str(&server.uri())?); - let registry_client = - RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?) - .index_locations(IndexLocations::new(vec![], vec![flat_index], true)) - .build()?; - - let error = registry_client - .simple_detail( - &PackageName::from_str("validation")?, - None, - &IndexCapabilities::default(), - &Semaphore::new(1), - ) - .await - .expect_err("legacy find-links should not be fetched"); - - assert!(matches!( - error.kind(), - crate::ErrorKind::NoIndex(package) if package == "validation" - )); - assert!( - server - .received_requests() - .await - .expect("request recording should be enabled") - .is_empty() - ); + let registry_client = no_index_client(vec![flat_index])?; + assert_no_index(®istry_client, "validation", None).await?; + assert_no_requests(&server).await; Ok(()) } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index a1cc79d2f349d..ab55a43cce58f 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -256,16 +256,16 @@ impl Deref for IndexUrl { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { indexes: Vec, - flat_indexes: Vec, + flat_index: Vec, no_index: bool, } impl IndexLocations { /// Determine the index URLs to use for fetching packages. - pub fn new(indexes: Vec, flat_indexes: Vec, no_index: bool) -> Self { + pub fn new(indexes: Vec, flat_index: Vec, no_index: bool) -> Self { Self { indexes, - flat_indexes, + flat_index, no_index, } } @@ -277,10 +277,10 @@ impl IndexLocations { /// /// If the current index location has an `index` set, it will be preserved. #[must_use] - pub fn combine(self, indexes: Vec, flat_indexes: Vec, no_index: bool) -> Self { + pub fn combine(self, indexes: Vec, flat_index: Vec, no_index: bool) -> Self { Self { indexes: self.indexes.into_iter().chain(indexes).collect(), - flat_indexes: self.flat_indexes.into_iter().chain(flat_indexes).collect(), + flat_index: self.flat_index.into_iter().chain(flat_index).collect(), no_index: self.no_index || no_index, } } @@ -383,7 +383,7 @@ impl<'a> IndexLocations { /// Return an iterator over the [`FlatIndexLocation`] entries. pub fn flat_indexes(&'a self) -> impl Iterator + 'a { - self.flat_indexes.iter() + self.flat_index.iter() } /// Return the `--no-index` flag. @@ -395,7 +395,7 @@ impl<'a> IndexLocations { pub fn index_urls(&'a self) -> IndexUrls { IndexUrls { indexes: self.indexes.clone(), - flat_indexes: self.flat_indexes.clone(), + flat_indexes: self.flat_index.clone(), no_index: self.no_index, } } @@ -408,7 +408,7 @@ impl<'a> IndexLocations { /// that the last-defined index is the first item in the vector. pub fn allowed_indexes(&'a self) -> Vec<&'a Index> { if self.no_index { - self.flat_indexes.iter().rev().collect() + self.flat_index.iter().rev().collect() } else { let mut indexes = vec![]; @@ -417,7 +417,7 @@ impl<'a> IndexLocations { for index in { self.indexes .iter() - .chain(self.flat_indexes.iter()) + .chain(self.flat_index.iter()) .filter(move |index| index.name.as_ref().is_none_or(|name| seen.insert(name))) } { if index.default { @@ -447,11 +447,11 @@ impl<'a> IndexLocations { /// that the last-defined index is the first item in the vector. pub fn known_indexes(&'a self) -> impl Iterator { if self.no_index { - Either::Left(self.flat_indexes.iter().rev()) + Either::Left(self.flat_index.iter().rev()) } else { Either::Right( std::iter::once(&*DEFAULT_INDEX) - .chain(self.flat_indexes.iter().rev()) + .chain(self.flat_index.iter().rev()) .chain(self.indexes.iter().rev()), ) } @@ -518,10 +518,10 @@ pub struct IndexUrls { } impl<'a> IndexUrls { - pub fn from_indexes(indexes: Vec, flat_indexes: Vec) -> Self { + pub fn from_indexes(indexes: Vec) -> Self { Self { indexes, - flat_indexes, + flat_indexes: Vec::new(), no_index: false, } } @@ -578,7 +578,7 @@ impl<'a> IndexUrls { self.implicit_indexes() .chain(self.default_index()) .filter(|index| !index.explicit) - .filter(move |index| seen.insert(index.raw_url())) + .filter(move |index| seen.insert(index.raw_url())) // Filter out redundant raw URLs } /// Return an iterator over all user-defined [`Index`] entries in order. @@ -612,8 +612,8 @@ impl<'a> IndexUrls { Either::Right(non_default.into_iter().chain(default)) } - /// Returns `true` if simple indexes (e.g., PyPI or `--extra-index-url`) are disabled via `--no-index`. - pub fn simple_indexes_disabled(&self) -> bool { + /// Return the `--no-index` flag. + pub fn no_index(&self) -> bool { self.no_index } @@ -806,7 +806,7 @@ mod tests { }, ]; - let index_urls = IndexUrls::from_indexes(indexes, Vec::new()); + let index_urls = IndexUrls::from_indexes(indexes); let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap(); assert_eq!( @@ -844,7 +844,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); + let index_urls = IndexUrls::from_indexes(indexes.clone()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); @@ -891,7 +891,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); + let index_urls = IndexUrls::from_indexes(indexes.clone()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let pytorch_url = IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap(); @@ -934,7 +934,7 @@ mod tests { exclude_newer: None, }]; - let index_urls = IndexUrls::from_indexes(indexes.clone(), Vec::new()); + let index_urls = IndexUrls::from_indexes(indexes.clone()); let index_locations = IndexLocations::new(indexes, Vec::new(), false); let nvidia_url = IndexUrl::from_str("https://pypi.nvidia.com").unwrap(); diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index 7ca89798badc0..f6ba132776ac3 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -60,10 +60,8 @@ impl LatestClient<'_> { } // Unless explicitly allowed, skip pre-release artifacts. - if !filename.version().is_stable() { - if !matches!(self.prerelease, PrereleaseMode::Allow) { - return false; - } + if !filename.version().is_stable() && !matches!(self.prerelease, PrereleaseMode::Allow) { + return false; } // Avoid yanked or otherwise withdrawn files. @@ -88,13 +86,12 @@ impl LatestClient<'_> { } // Skip wheels that aren't compatible with the current platform. - if let DistFilename::WheelFilename(filename) = filename { - if self + if let DistFilename::WheelFilename(filename) = filename + && self .tags .is_some_and(|tags| !filename.compatibility(tags).is_compatible()) - { - return false; - } + { + return false; } true @@ -112,20 +109,14 @@ impl LatestClient<'_> { let mut latest: Option = None; let mut update_latest = |candidate: DistFilename| { - match latest.as_ref() { - Some(current) => { - // Prefer higher versions, and prefer wheels over sdists at parity. - if candidate.version() > current.version() - || (candidate.version() == current.version() - && matches!(candidate, DistFilename::WheelFilename(_)) - && matches!(current, DistFilename::SourceDistFilename(_))) - { - latest = Some(candidate); - } - } - None => { - latest = Some(candidate); - } + // Prefer higher versions, and prefer wheels over sdists at parity. + if latest.as_ref().is_none_or(|current| { + candidate.version() > current.version() + || (candidate.version() == current.version() + && matches!(candidate, DistFilename::WheelFilename(_)) + && matches!(current, DistFilename::SourceDistFilename(_))) + }) { + latest = Some(candidate); } }; @@ -160,27 +151,11 @@ impl LatestClient<'_> { rkyv::deserialize::(&datum.files) .expect("archived version files always deserializes"); - let mut best: Option = None; for (filename, file) in files.all() { - if !self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { - continue; - } - - match filename { - DistFilename::WheelFilename(_) => { - best = Some(filename); - break; - } - DistFilename::SourceDistFilename(_) if best.is_none() => { - best = Some(filename); - } - DistFilename::SourceDistFilename(_) => {} + if self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { + update_latest(filename); } } - - if let Some(best) = best { - update_latest(best); - } } } MetadataFormat::Flat(entries) => { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 0a04d8e12e814..1ebcb879d84d3 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -686,7 +686,7 @@ pub(crate) async fn add( // Add any indexes that were provided on the command-line, in priority order. if !raw { - let urls = IndexUrls::from_indexes(indexes, Vec::new()); + let urls = IndexUrls::from_indexes(indexes); let mut indexes = urls.defined_indexes().collect::>(); indexes.reverse(); for index in indexes { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 08cf0e1fc52bf..01a592ba1ce22 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -106,7 +106,7 @@ fn pip_compile_baseline() { settings: PipSettings { index_locations: IndexLocations { indexes: [], - flat_indexes: [], + flat_index: [], no_index: false, }, python: None, @@ -291,7 +291,7 @@ fn pip_install_baseline() { settings: PipSettings { index_locations: IndexLocations { indexes: [], - flat_indexes: [], + flat_index: [], no_index: false, }, python: None, @@ -488,7 +488,7 @@ fn lock_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_indexes: [], + flat_index: [], no_index: false, }, index_strategy: FirstIndex, @@ -617,7 +617,7 @@ fn version_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_indexes: [], + flat_index: [], no_index: false, }, index_strategy: FirstIndex, @@ -784,7 +784,7 @@ fn tool_install_baseline() { fork_strategy: RequiresPython, index_locations: IndexLocations { indexes: [], - flat_indexes: [], + flat_index: [], no_index: false, }, index_strategy: FirstIndex, @@ -902,7 +902,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -1058,7 +1058,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -1251,7 +1251,7 @@ fn resolve_index_url() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, "# @@ -1357,9 +1357,9 @@ fn resolve_find_links() -> anyhow::Result<()> { settings: PipSettings { index_locations: IndexLocations { indexes: [], - - flat_indexes: [], + - flat_index: [], - no_index: false, - + flat_indexes: [ + + flat_index: [ + Index { + name: None, + url: Url( @@ -1554,7 +1554,7 @@ fn resolve_top_level() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, "# @@ -1973,7 +1973,7 @@ fn resolve_both() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -2105,7 +2105,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, @@ -120,7 +158,7 @@ @@ -2318,7 +2318,7 @@ fn resolve_config_file() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, @@ -120,7 +156,7 @@ @@ -2656,7 +2656,7 @@ fn index_priority() -> anyhow::Result<()> { + exclude_newer: None, + }, + ], - flat_indexes: [], + flat_index: [], no_index: false, }, "# From 3d0365f8c9feba67f2cc167bc818700508cadb61 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Jun 2026 00:14:15 -0400 Subject: [PATCH 17/17] Separate find-links metadata fetching --- crates/uv-client/src/registry_client.rs | 89 ++++++++----------------- crates/uv/src/commands/pip/latest.rs | 33 ++++++--- 2 files changed, 51 insertions(+), 71 deletions(-) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index a341bd27aa0f2..28f4b11fb04bf 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -21,7 +21,7 @@ use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, Index, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, + BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_git::{GIT_LFS, GitError, GitHttpSettings, GitResolver, Reporter}; @@ -331,50 +331,17 @@ impl RegistryClient { capabilities: &IndexCapabilities, download_concurrency: &Semaphore, ) -> Result, Error> { - self.simple_detail_inner( - package_name, - index, - capabilities, - download_concurrency, - false, - ) - .await - } - - /// Fetch package metadata from an index and the configured legacy `--find-links` locations. - #[instrument(skip_all, fields(package = % package_name))] - pub async fn simple_detail_with_find_links<'index>( - &'index self, - package_name: &PackageName, - index: Option>, - capabilities: &IndexCapabilities, - download_concurrency: &Semaphore, - ) -> Result, Error> { - self.simple_detail_inner( - package_name, - index, - capabilities, - download_concurrency, - true, - ) - .await - } + // If `--no-index` is specified, avoid fetching regardless of whether the index is implicit, + // explicit, etc. + if self.index_urls.no_index() { + return Err(ErrorKind::NoIndex(package_name.to_string()).into()); + } - async fn simple_detail_inner<'index>( - &'index self, - package_name: &PackageName, - index: Option>, - capabilities: &IndexCapabilities, - download_concurrency: &Semaphore, - include_find_links: bool, - ) -> Result, Error> { - let has_explicit_index = index.is_some(); let indexes = if let Some(index) = index { Either::Left(std::iter::once(index)) } else { Either::Right(self.index_urls_for(package_name)) }; - let indexes = indexes.filter(|_| !self.index_urls.no_index()); let mut results = Vec::new(); @@ -465,31 +432,7 @@ impl RegistryClient { } } - if include_find_links && !has_explicit_index { - let flat_index = self.index_urls.flat_indexes().next().map(Index::url); - let flat_entries = futures::stream::iter(self.index_urls.flat_indexes()) - .map(async |index| { - let _permit = download_concurrency.acquire().await; - self.flat_single_index(package_name, index.url()).await - }) - .buffered(8) - .try_collect::>() - .await? - .into_iter() - .flatten() - .collect::>(); - if let Some(flat_index) = flat_index - && !flat_entries.is_empty() - { - results.push((flat_index, MetadataFormat::Flat(flat_entries))); - } - } - if results.is_empty() { - if self.index_urls.no_index() { - return Err(ErrorKind::NoIndex(package_name.to_string()).into()); - } - return match self.connectivity { Connectivity::Online => { Err(ErrorKind::RemotePackageNotFound(package_name.clone()).into()) @@ -501,6 +444,26 @@ impl RegistryClient { Ok(results) } + /// Fetch and combine entries for a package from the configured legacy `--find-links` locations. + #[instrument(skip_all, fields(package = % package_name))] + pub async fn find_links_entries( + &self, + package_name: &PackageName, + download_concurrency: &Semaphore, + ) -> Result, Error> { + Ok(futures::stream::iter(self.index_urls.flat_indexes()) + .map(async |index| { + let _permit = download_concurrency.acquire().await; + self.flat_single_index(package_name, index.url()).await + }) + .buffered(8) + .try_collect::>() + .await? + .into_iter() + .flatten() + .collect::>()) + } + /// Fetch the [`FlatIndexEntry`] entries for a given package from a single `--find-links` index. async fn flat_single_index( &self, diff --git a/crates/uv/src/commands/pip/latest.rs b/crates/uv/src/commands/pip/latest.rs index f6ba132776ac3..8768af37b72ae 100644 --- a/crates/uv/src/commands/pip/latest.rs +++ b/crates/uv/src/commands/pip/latest.rs @@ -122,7 +122,7 @@ impl LatestClient<'_> { let archives = match self .client - .simple_detail_with_find_links( + .simple_detail( package, index.map(IndexMetadataRef::from), self.capabilities, @@ -131,14 +131,17 @@ impl LatestClient<'_> { .await { Ok(archives) => archives, - Err(err) => { - return match err.kind() { - uv_client::ErrorKind::RemotePackageNotFound(_) => Ok(None), - uv_client::ErrorKind::NoIndex(_) => Ok(None), - uv_client::ErrorKind::Offline(_) => Ok(None), - _ => Err(err), - }; + Err(err) + if matches!( + err.kind(), + uv_client::ErrorKind::RemotePackageNotFound(_) + | uv_client::ErrorKind::NoIndex(_) + | uv_client::ErrorKind::Offline(_) + ) => + { + Vec::new() } + Err(err) => return Err(err), }; for (index, archive) in archives { @@ -169,6 +172,20 @@ impl LatestClient<'_> { } } + if index.is_none() { + for entry in self + .client + .find_links_entries(package, download_concurrency) + .await? + { + let (filename, file, index) = entry.into_parts(); + let exclude_newer = self.effective_exclude_newer(package, &index); + if self.consider_candidate(&filename, &file, exclude_newer.as_ref()) { + update_latest(filename); + } + } + } + Ok(latest) } }