Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 140 additions & 4 deletions crates/uv-client/src/registry_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,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<Vec<FlatIndexEntry>, 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::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>())
}

/// Fetch the [`FlatIndexEntry`] entries for a given package from a single `--find-links` index.
async fn flat_single_index(
&self,
Expand Down Expand Up @@ -1691,18 +1711,22 @@ 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, RegistryClient, RegistryClientBuilder,
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};
Expand All @@ -1726,6 +1750,118 @@ mod tests {
server
}

fn no_index_client(flat_indexes: Vec<Index>) -> Result<RegistryClient, Error> {
Ok(
RegistryClientBuilder::new(BaseClientBuilder::default(), Cache::temp()?)
.index_locations(IndexLocations::new(vec![], flat_indexes, true))
.build()?,
)
}

async fn assert_no_index(
client: &RegistryClient,
package: &str,
index: Option<IndexMetadataRef<'_>>,
) -> Result<(), Error> {
let error = client
.simple_detail(
&PackageName::from_str(package)?,
index,
&IndexCapabilities::default(),
&Semaphore::new(1),
)
.await
.expect_err("index lookup should be disabled");

assert!(matches!(
error.kind(),
crate::ErrorKind::NoIndex(error_package) if error_package == package
));
Ok(())
}

async fn assert_no_requests(server: &MockServer) {
assert!(
server
.received_requests()
.await
.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(
&registry_client,
"validation",
Some(IndexMetadataRef {
url: &explicit_index,
format: IndexFormat::Simple,
}),
)
.await?;
assert_no_requests(&server).await;
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 = no_index_client(vec![])?;

assert_no_index(
&registry_client,
"validation",
Some(IndexMetadataRef {
url: &explicit_index,
format: IndexFormat::Flat,
}),
)
.await?;
assert_no_requests(&server).await;
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()?;

assert_no_index(&registry_client, "torch", None).await?;
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 = no_index_client(vec![flat_index])?;

assert_no_index(&registry_client, "validation", None).await?;
assert_no_requests(&server).await;
Ok(())
}

#[tokio::test]
async fn test_redirect_to_server_with_credentials() -> Result<(), Error> {
let username = "user";
Expand Down
8 changes: 8 additions & 0 deletions crates/uv-distribution-types/src/index_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ impl<'a> IndexLocations {
pub fn index_urls(&'a self) -> IndexUrls {
IndexUrls {
indexes: self.indexes.clone(),
flat_indexes: self.flat_index.clone(),
no_index: self.no_index,
}
}
Expand Down Expand Up @@ -512,17 +513,24 @@ impl From<&IndexLocations> for uv_auth::Indexes {
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct IndexUrls {
indexes: Vec<Index>,
flat_indexes: Vec<Index>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a reason why we did not want to include flat_indexes here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of any

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I really can't remember. I'm sure there's a reason for it. Probably because historically we didn't query flat indexes in the registry client? Something like that?

no_index: bool,
}

impl<'a> IndexUrls {
pub fn from_indexes(indexes: Vec<Index>) -> Self {
Self {
indexes,
flat_indexes: Vec::new(),
no_index: false,
}
}

/// Return an iterator over the configured flat-index locations.
pub fn flat_indexes(&'a self) -> impl Iterator<Item = &'a Index> + 'a {
self.flat_indexes.iter()
}

/// Return the default [`Index`] entry.
///
/// If `--no-index` is set, return `None`.
Expand Down
Loading