Support --find-links-style "flat" indexes in [[tool.uv.index]] (#12407)
Some checks are pending
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

## Summary

This PR extends `[[tool.uv.index]]` to support `--find-links`-style
"flat" indexes, so that users can point to such indexes without using
`--find-links` _and_ get access to the full functionality of
`[[tool.uv.index]]` (e.g., they can now pin packages to
`--find-links`-style indexes).

Note that, at present, `--find-links` indexes actually have some quirky
behavior, in that we combine them into a single entity and then merge
the discovered distributions into each Simple API-style index. The
motivation here, IIRC, was to match pip's behavior quite closely. I'm
interested in _removing_ that behavior, but it'd be breaking (and may
also be inconvenient for some use-cases). So, the behavior for indexes
passed in via `--find-links` remains completely unchanged. However,
`[[tool.uv.index]]` entries with `format = "flat"` are now treated
identically to those defined with `format = "simple"` (the default), in
that we stop after we find the first-matching index, etc.

Closes https://github.com/astral-sh/uv/issues/11634.
This commit is contained in:
Charlie Marsh 2025-03-25 21:14:44 -04:00 committed by GitHub
parent f2a2d982b8
commit bd9c365b92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 826 additions and 122 deletions

1
Cargo.lock generated
View file

@ -4876,6 +4876,7 @@ dependencies = [
"reqwest-retry",
"rkyv",
"rmp-serde",
"rustc-hash",
"serde",
"serde_json",
"sys-info",

View file

@ -46,6 +46,7 @@ reqwest-middleware = { workspace = true }
reqwest-retry = { workspace = true }
rkyv = { workspace = true }
rmp-serde = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sys-info = { workspace = true }

View file

@ -8,8 +8,8 @@ use url::Url;
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
use uv_normalize::PackageName;
use crate::html;
use crate::middleware::OfflineError;
use crate::{html, FlatIndexError};
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
@ -155,6 +155,9 @@ pub enum ErrorKind {
#[error(transparent)]
JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError),
#[error(transparent)]
Flat(#[from] FlatIndexError),
#[error("Expected a file URL, but received: {0}")]
NonFileUrl(Url),

View file

@ -4,11 +4,11 @@ pub use base_client::{
};
pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
pub use error::{Error, ErrorKind, WrappedReqwestError};
pub use flat_index::{FlatIndexClient, FlatIndexEntries, FlatIndexError};
pub use flat_index::{FlatIndexClient, FlatIndexEntries, FlatIndexEntry, FlatIndexError};
pub use linehaul::LineHaul;
pub use registry_client::{
Connectivity, RegistryClient, RegistryClientBuilder, SimpleMetadata, SimpleMetadatum,
VersionFiles,
Connectivity, MetadataFormat, RegistryClient, RegistryClientBuilder, SimpleMetadata,
SimpleMetadatum, VersionFiles,
};
pub use rkyvutil::{Deserializer, OwnedArchive, Serializer, Validator};

View file

@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use std::fmt::Debug;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use async_http_range_reader::AsyncHttpRangeReader;
@ -10,7 +11,8 @@ use http::HeaderMap;
use itertools::Either;
use reqwest::{Proxy, Response, StatusCode};
use reqwest_middleware::ClientWithMiddleware;
use tokio::sync::Semaphore;
use rustc_hash::FxHashMap;
use tokio::sync::{Mutex, Semaphore};
use tracing::{info_span, instrument, trace, warn, Instrument};
use url::Url;
@ -20,7 +22,8 @@ use uv_configuration::KeyringProviderType;
use uv_configuration::{IndexStrategy, TrustedHost};
use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use uv_distribution_types::{
BuiltDist, File, FileLocation, IndexCapabilities, IndexMetadataRef, IndexUrl, IndexUrls, Name,
BuiltDist, File, FileLocation, IndexCapabilities, IndexFormat, IndexMetadataRef, IndexUrl,
IndexUrls, Name,
};
use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream};
use uv_normalize::PackageName;
@ -33,10 +36,14 @@ use uv_torch::TorchStrategy;
use crate::base_client::{BaseClientBuilder, ExtraMiddleware};
use crate::cached_client::CacheControl;
use crate::flat_index::FlatIndexEntry;
use crate::html::SimpleHtml;
use crate::remote_metadata::wheel_metadata_from_remote_zip;
use crate::rkyvutil::OwnedArchive;
use crate::{BaseClient, CachedClient, CachedClientError, Error, ErrorKind};
use crate::{
BaseClient, CachedClient, CachedClientError, Error, ErrorKind, FlatIndexClient,
FlatIndexEntries,
};
/// A builder for an [`RegistryClient`].
#[derive(Debug, Clone)]
@ -169,6 +176,7 @@ impl<'a> RegistryClientBuilder<'a> {
connectivity,
client,
timeout,
flat_indexes: Arc::default(),
}
}
@ -191,6 +199,7 @@ impl<'a> RegistryClientBuilder<'a> {
connectivity,
client,
timeout,
flat_indexes: Arc::default(),
}
}
}
@ -226,6 +235,17 @@ pub struct RegistryClient {
connectivity: Connectivity,
/// Configured client timeout, in seconds.
timeout: Duration,
/// The flat index entries for each `--find-links`-style index URL.
flat_indexes: Arc<Mutex<FlatIndexCache>>,
}
/// The format of the package metadata returned by querying an index.
#[derive(Debug)]
pub enum MetadataFormat {
/// The metadata adheres to the Simple Repository API format.
Simple(OwnedArchive<SimpleMetadata>),
/// The metadata consists of a list of distributions from a "flat" index.
Flat(Vec<FlatIndexEntry>),
}
impl RegistryClient {
@ -280,19 +300,21 @@ impl RegistryClient {
.unwrap_or(self.index_strategy)
}
/// Fetch a package from the `PyPI` simple API.
/// Fetch package metadata from an index.
///
/// "simple" here refers to [PEP 503 Simple Repository API](https://peps.python.org/pep-0503/)
/// Supports both the "Simple" API and `--find-links`-style flat indexes.
///
/// "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 approximately implements.
#[instrument("simple_api", skip_all, fields(package = % package_name))]
pub async fn simple<'index>(
/// which the PyPI JSON API implements.
#[instrument(skip_all, fields(package = % package_name))]
pub async fn package_metadata<'index>(
&'index self,
package_name: &PackageName,
index: Option<IndexMetadataRef<'index>>,
capabilities: &IndexCapabilities,
download_concurrency: &Semaphore,
) -> Result<Vec<(IndexMetadataRef<'index>, OwnedArchive<SimpleMetadata>)>, Error> {
) -> Result<Vec<(&'index IndexUrl, MetadataFormat)>, Error> {
// If `--no-index` is specified, avoid fetching regardless of whether the index is implicit,
// explicit, etc.
if self.index_urls.no_index() {
@ -312,14 +334,25 @@ impl RegistryClient {
IndexStrategy::FirstIndex => {
for index in indexes {
let _permit = download_concurrency.acquire().await;
match index.format {
IndexFormat::Simple => {
if let Some(metadata) = self
.simple_single_index(package_name, index.url(), capabilities)
.simple_single_index(package_name, index.url, capabilities)
.await?
{
results.push((index, metadata));
results.push((index.url, MetadataFormat::Simple(metadata)));
break;
}
}
IndexFormat::Flat => {
let entries = self.flat_single_index(package_name, index.url).await?;
if !entries.is_empty() {
results.push((index.url, MetadataFormat::Flat(entries)));
break;
}
}
}
}
}
// Otherwise, fetch concurrently.
@ -327,10 +360,19 @@ impl RegistryClient {
results = futures::stream::iter(indexes)
.map(|index| async move {
let _permit = download_concurrency.acquire().await;
match index.format {
IndexFormat::Simple => {
let metadata = self
.simple_single_index(package_name, index.url(), capabilities)
.simple_single_index(package_name, index.url, capabilities)
.await?;
Ok((index, metadata))
Ok((index.url, metadata.map(MetadataFormat::Simple)))
}
IndexFormat::Flat => {
let entries =
self.flat_single_index(package_name, index.url).await?;
Ok((index.url, Some(MetadataFormat::Flat(entries))))
}
}
})
.buffered(8)
.filter_map(|result: Result<_, Error>| async move {
@ -357,6 +399,46 @@ impl RegistryClient {
Ok(results)
}
/// Fetch the [`FlatIndexEntry`] entries for a given package from a single `--find-links` index.
async fn flat_single_index(
&self,
package_name: &PackageName,
index: &IndexUrl,
) -> Result<Vec<FlatIndexEntry>, Error> {
// Store the flat index entries in a cache, to avoid redundant fetches. A flat index will
// typically contain entries for multiple packages; as such, it's more efficient to cache
// the entire index rather than re-fetching it for each package.
let mut cache = self.flat_indexes.lock().await;
if let Some(entries) = cache.get(index) {
return Ok(entries.get(package_name).cloned().unwrap_or_default());
}
let client = FlatIndexClient::new(self.cached_client(), self.connectivity, &self.cache);
// Fetch the entries for the index.
let FlatIndexEntries { entries, .. } =
client.fetch_index(index).await.map_err(ErrorKind::Flat)?;
// Index by package name.
let mut entries_by_package: FxHashMap<PackageName, Vec<FlatIndexEntry>> =
FxHashMap::default();
for entry in entries {
entries_by_package
.entry(entry.filename.name().clone())
.or_default()
.push(entry);
}
let package_entries = entries_by_package
.get(package_name)
.cloned()
.unwrap_or_default();
// Write to the cache.
cache.insert(index.clone(), entries_by_package);
Ok(package_entries)
}
/// Fetch the [`SimpleMetadata`] from a single index for a given package.
///
/// The index can either be a PEP 503-compatible remote repository, or a local directory laid
@ -883,6 +965,27 @@ impl RegistryClient {
}
}
/// A map from [`IndexUrl`] to [`FlatIndexEntry`] entries found at the given URL, indexed by
/// [`PackageName`].
#[derive(Default, Debug, Clone)]
struct FlatIndexCache(FxHashMap<IndexUrl, FxHashMap<PackageName, Vec<FlatIndexEntry>>>);
impl FlatIndexCache {
/// Get the entries for a given index URL.
fn get(&self, index: &IndexUrl) -> Option<&FxHashMap<PackageName, Vec<FlatIndexEntry>>> {
self.0.get(index)
}
/// Insert the entries for a given index URL.
fn insert(
&mut self,
index: IndexUrl,
entries: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
) -> Option<FxHashMap<PackageName, Vec<FlatIndexEntry>>> {
self.0.insert(index, entries)
}
}
#[derive(Default, Debug, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
#[rkyv(derive(Debug))]
pub struct VersionFiles {

View file

@ -64,13 +64,13 @@ pub struct Index {
/// The origin of the index (e.g., a CLI flag, a user-level configuration file, etc.).
#[serde(skip)]
pub origin: Option<Origin>,
// /// The type of the index.
// ///
// /// Indexes can either be PEP 503-compliant (i.e., a registry implementing the Simple API) or
// /// structured as a flat list of distributions (e.g., `--find-links`). In both cases, indexes
// /// can point to either local or remote resources.
// #[serde(default)]
// pub r#type: IndexKind,
/// The format used by the index.
///
/// Indexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple
/// API) or structured as a flat list of distributions (e.g., `--find-links`). In both cases,
/// indexes can point to either local or remote resources.
#[serde(default)]
pub format: IndexFormat,
/// The URL of the upload endpoint.
///
/// When using `uv publish --index <name>`, this URL is used for publishing.
@ -96,17 +96,28 @@ pub struct Index {
pub authenticate: AuthPolicy,
}
// #[derive(
// Default, Debug, Copy, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize,
// )]
// #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
// pub enum IndexKind {
// /// A PEP 503 and/or PEP 691-compliant index.
// #[default]
// Simple,
// /// An index containing a list of links to distributions (e.g., `--find-links`).
// Flat,
// }
#[derive(
Default,
Debug,
Copy,
Clone,
Hash,
Eq,
PartialEq,
Ord,
PartialOrd,
serde::Serialize,
serde::Deserialize,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum IndexFormat {
/// A PyPI-style index implementing the Simple Repository API.
#[default]
Simple,
/// A `--find-links`-style index containing a flat list of wheels and source distributions.
Flat,
}
impl Index {
/// Initialize an [`Index`] from a pip-style `--index-url`.
@ -117,6 +128,7 @@ impl Index {
explicit: false,
default: true,
origin: None,
format: IndexFormat::Simple,
publish_url: None,
authenticate: AuthPolicy::default(),
}
@ -130,6 +142,7 @@ impl Index {
explicit: false,
default: false,
origin: None,
format: IndexFormat::Simple,
publish_url: None,
authenticate: AuthPolicy::default(),
}
@ -143,6 +156,7 @@ impl Index {
explicit: false,
default: false,
origin: None,
format: IndexFormat::Flat,
publish_url: None,
authenticate: AuthPolicy::default(),
}
@ -210,6 +224,7 @@ impl From<IndexUrl> for Index {
explicit: false,
default: false,
origin: None,
format: IndexFormat::Simple,
publish_url: None,
authenticate: AuthPolicy::default(),
}
@ -231,6 +246,7 @@ impl FromStr for Index {
explicit: false,
default: false,
origin: None,
format: IndexFormat::Simple,
publish_url: None,
authenticate: AuthPolicy::default(),
});
@ -245,6 +261,7 @@ impl FromStr for Index {
explicit: false,
default: false,
origin: None,
format: IndexFormat::Simple,
publish_url: None,
authenticate: AuthPolicy::default(),
})
@ -254,21 +271,17 @@ impl FromStr for Index {
/// An [`IndexUrl`] along with the metadata necessary to query the index.
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct IndexMetadata {
/// The URL of the index.
pub url: IndexUrl,
// /// The type of the index.
// ///
// /// Indexes can either be PEP 503-compliant (i.e., a registry implementing the Simple API) or
// /// structured as a flat list of distributions (e.g., `--find-links`). In both cases, indexes
// /// can point to either local or remote resources.
// #[serde(default)]
// pub r#type: IndexKind,
/// The format used by the index.
pub format: IndexFormat,
}
impl IndexMetadata {
/// Return a reference to the [`IndexMetadata`].
pub fn as_ref(&self) -> IndexMetadataRef<'_> {
let Self { url } = self;
IndexMetadataRef { url }
let Self { url, format: kind } = self;
IndexMetadataRef { url, format: *kind }
}
/// Consume the [`IndexMetadata`] and return the [`IndexUrl`].
@ -280,7 +293,10 @@ impl IndexMetadata {
/// A reference to an [`IndexMetadata`].
#[derive(Debug, Copy, Clone)]
pub struct IndexMetadataRef<'a> {
/// The URL of the index.
pub url: &'a IndexUrl,
/// The format used by the index.
pub format: IndexFormat,
}
impl IndexMetadata {
@ -297,27 +313,39 @@ impl IndexMetadataRef<'_> {
}
}
impl<'a> From<&'a IndexMetadata> for IndexMetadataRef<'a> {
fn from(value: &'a IndexMetadata) -> Self {
Self { url: &value.url }
}
}
impl<'a> From<&'a IndexUrl> for IndexMetadataRef<'a> {
fn from(value: &'a IndexUrl) -> Self {
Self { url: value }
}
}
impl<'a> From<&'a Index> for IndexMetadataRef<'a> {
fn from(value: &'a Index) -> Self {
Self { url: &value.url }
Self {
url: &value.url,
format: value.format,
}
}
}
impl<'a> From<&'a IndexMetadata> for IndexMetadataRef<'a> {
fn from(value: &'a IndexMetadata) -> Self {
Self {
url: &value.url,
format: value.format,
}
}
}
impl From<IndexUrl> for IndexMetadata {
fn from(value: IndexUrl) -> Self {
Self { url: value }
Self {
url: value,
format: IndexFormat::Simple,
}
}
}
impl<'a> From<&'a IndexUrl> for IndexMetadataRef<'a> {
fn from(value: &'a IndexUrl) -> Self {
Self {
url: value,
format: IndexFormat::Simple,
}
}
}

View file

@ -98,7 +98,7 @@ impl schemars::JsonSchema for IndexUrl {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
metadata: Some(Box::new(schemars::schema::Metadata {
description: Some("The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`).".to_string()),
description: Some("The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path.".to_string()),
..schemars::schema::Metadata::default()
})),
..schemars::schema::SchemaObject::default()

View file

@ -222,9 +222,14 @@ impl LoweredRequirement {
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
.map(|index| IndexMetadata {
url: index.url.clone(),
})
.map(
|Index {
url, format: kind, ..
}| IndexMetadata {
url: url.clone(),
format: *kind,
},
)
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),
@ -447,9 +452,14 @@ impl LoweredRequirement {
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
.map(|index| IndexMetadata {
url: index.url.clone(),
})
.map(
|Index {
url, format: kind, ..
}| IndexMetadata {
url: url.clone(),
format: *kind,
},
)
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),

View file

@ -28,7 +28,8 @@ use trusted_publishing::TrustedPublishingToken;
use url::Url;
use uv_cache::{Cache, Refresh};
use uv_client::{
BaseClient, OwnedArchive, RegistryClientBuilder, UvRetryableStrategy, DEFAULT_RETRIES,
BaseClient, MetadataFormat, OwnedArchive, RegistryClientBuilder, UvRetryableStrategy,
DEFAULT_RETRIES,
};
use uv_configuration::{KeyringProviderType, TrustedPublishing};
use uv_distribution_filename::{DistFilename, SourceDistExtension, SourceDistFilename};
@ -477,7 +478,7 @@ pub async fn check_url(
debug!("Checking for {filename} in the registry");
let response = match registry_client
.simple(
.package_metadata(
filename.name(),
Some(index_url.into()),
index_capabilities,
@ -499,7 +500,7 @@ pub async fn check_url(
};
}
};
let [(_, simple_metadata)] = response.as_slice() else {
let [(_, MetadataFormat::Simple(simple_metadata))] = response.as_slice() else {
unreachable!("We queried a single index, we must get a single response");
};
let simple_metadata = OwnedArchive::deserialize(simple_metadata);

View file

@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use rustc_hash::FxHashMap;
use tracing::instrument;
use uv_client::FlatIndexEntries;
use uv_client::{FlatIndexEntries, FlatIndexEntry};
use uv_configuration::BuildOptions;
use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use uv_distribution_types::{
@ -39,11 +39,10 @@ impl FlatIndex {
build_options: &BuildOptions,
) -> Self {
// Collect compatible distributions.
let mut index = FxHashMap::default();
let mut index = FxHashMap::<PackageName, FlatDistributions>::default();
for entry in entries.entries {
let distributions = index.entry(entry.filename.name().clone()).or_default();
Self::add_file(
distributions,
distributions.add_file(
entry.file,
entry.filename,
tags,
@ -59,8 +58,59 @@ impl FlatIndex {
Self { index, offline }
}
/// Get the [`FlatDistributions`] for the given package name.
pub fn get(&self, package_name: &PackageName) -> Option<&FlatDistributions> {
self.index.get(package_name)
}
/// Whether any `--find-links` entries could not be resolved due to a lack of network
/// connectivity.
pub fn offline(&self) -> bool {
self.offline
}
}
/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed
/// by [`Version`].
#[derive(Debug, Clone, Default)]
pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
impl FlatDistributions {
/// Collect all files from a `--find-links` target into a [`FlatIndex`].
#[instrument(skip_all)]
pub fn from_entries(
entries: Vec<FlatIndexEntry>,
tags: Option<&Tags>,
hasher: &HashStrategy,
build_options: &BuildOptions,
) -> Self {
let mut distributions = Self::default();
for entry in entries {
distributions.add_file(
entry.file,
entry.filename,
tags,
hasher,
build_options,
entry.index,
);
}
distributions
}
/// Returns an [`Iterator`] over the distributions.
pub fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
self.0.iter()
}
/// Removes the [`PrioritizedDist`] for the given version.
pub fn remove(&mut self, version: &Version) -> Option<PrioritizedDist> {
self.0.remove(version)
}
/// Add the given [`File`] to the [`FlatDistributions`] for the given package.
fn add_file(
distributions: &mut FlatDistributions,
&mut self,
file: File,
filename: DistFilename,
tags: Option<&Tags>,
@ -86,7 +136,7 @@ impl FlatIndex {
file: Box::new(file),
index,
};
match distributions.0.entry(version) {
match self.0.entry(version) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_built(dist, vec![], compatibility);
}
@ -110,7 +160,7 @@ impl FlatIndex {
index,
wheels: vec![],
};
match distributions.0.entry(filename.version) {
match self.0.entry(filename.version) {
Entry::Occupied(mut entry) => {
entry.get_mut().insert_source(dist, vec![], compatibility);
}
@ -194,31 +244,6 @@ impl FlatIndex {
WheelCompatibility::Compatible(hash, priority, build_tag)
}
/// Get the [`FlatDistributions`] for the given package name.
pub fn get(&self, package_name: &PackageName) -> Option<&FlatDistributions> {
self.index.get(package_name)
}
/// Returns `true` if there are any offline `--find-links` entries.
pub fn offline(&self) -> bool {
self.offline
}
}
/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed
/// by [`Version`].
#[derive(Debug, Clone, Default)]
pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
impl FlatDistributions {
pub fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
self.0.iter()
}
pub fn remove(&mut self, version: &Version) -> Option<PrioritizedDist> {
self.0.remove(version)
}
}
impl IntoIterator for FlatDistributions {

View file

@ -45,9 +45,7 @@ impl Indexes {
else {
continue;
};
let index = IndexMetadata {
url: index.url.clone(),
};
let index = index.clone();
let conflict = conflict.clone();
indexes.add(&requirement, Entry { index, conflict });
}

View file

@ -1,6 +1,6 @@
use std::future::Future;
use std::sync::Arc;
use uv_client::MetadataFormat;
use uv_configuration::BuildOptions;
use uv_distribution::{ArchiveMetadata, DistributionDatabase, Reporter};
use uv_distribution_types::{
@ -158,7 +158,7 @@ impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Con
.fetcher
.client()
.manual(|client, semaphore| {
client.simple(
client.package_metadata(
package_name,
index.map(IndexMetadataRef::from),
self.capabilities,
@ -174,11 +174,11 @@ impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Con
Ok(results) => Ok(VersionsResponse::Found(
results
.into_iter()
.map(|(index, metadata)| {
VersionMap::from_metadata(
.map(|(index, metadata)| match metadata {
MetadataFormat::Simple(metadata) => VersionMap::from_simple_metadata(
metadata,
package_name,
index.url(),
index,
self.tags.as_ref(),
&self.requires_python,
&self.allowed_yanks,
@ -188,7 +188,13 @@ impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Con
.and_then(|flat_index| flat_index.get(package_name))
.cloned(),
self.build_options,
)
),
MetadataFormat::Flat(metadata) => VersionMap::from_flat_metadata(
metadata,
self.tags.as_ref(),
&self.hasher,
self.build_options,
),
})
.collect(),
)),

View file

@ -6,7 +6,7 @@ use std::sync::OnceLock;
use pubgrub::Ranges;
use tracing::instrument;
use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles};
use uv_client::{FlatIndexEntry, OwnedArchive, SimpleMetadata, VersionFiles};
use uv_configuration::BuildOptions;
use uv_distribution_filename::{DistFilename, WheelFilename};
use uv_distribution_types::{
@ -41,7 +41,7 @@ impl VersionMap {
///
/// PEP 592: <https://peps.python.org/pep-0592/#warehouse-pypi-implementation-notes>
#[instrument(skip_all, fields(package_name))]
pub(crate) fn from_metadata(
pub(crate) fn from_simple_metadata(
simple_metadata: OwnedArchive<SimpleMetadata>,
package_name: &PackageName,
index: &IndexUrl,
@ -116,6 +116,30 @@ impl VersionMap {
}
}
#[instrument(skip_all, fields(package_name))]
pub(crate) fn from_flat_metadata(
flat_metadata: Vec<FlatIndexEntry>,
tags: Option<&Tags>,
hasher: &HashStrategy,
build_options: &BuildOptions,
) -> Self {
let mut stable = false;
let mut local = false;
let mut map = BTreeMap::new();
for (version, prioritized_dist) in
FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options)
{
stable |= version.is_stable();
local |= version.is_local();
map.insert(version, prioritized_dist);
}
Self {
inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }),
}
}
/// Return the [`DistFile`] for the given version, if any.
pub(crate) fn get(&self, version: &Version) -> Option<&PrioritizedDist> {
match self.inner {

View file

@ -1,6 +1,7 @@
use tokio::sync::Semaphore;
use tracing::debug;
use uv_client::{RegistryClient, VersionFiles};
use uv_client::{MetadataFormat, RegistryClient, VersionFiles};
use uv_distribution_filename::DistFilename;
use uv_distribution_types::{IndexCapabilities, IndexMetadataRef, IndexUrl};
use uv_normalize::PackageName;
@ -34,7 +35,7 @@ impl LatestClient<'_> {
let archives = match self
.client
.simple(
.package_metadata(
package,
index.map(IndexMetadataRef::from),
self.capabilities,
@ -55,6 +56,10 @@ impl LatestClient<'_> {
let mut latest: Option<DistFilename> = None;
for (_, archive) in archives {
let MetadataFormat::Simple(archive) = archive else {
continue;
};
for datum in archive.iter().rev() {
// Find the first compatible distribution.
let files = rkyv::deserialize::<VersionFiles, rkyv::rancor::Error>(&datum.files)

View file

@ -172,6 +172,7 @@ pub(crate) async fn tree(
.packages()
.iter()
.filter_map(|package| {
// TODO(charlie): We would need to know the format here.
let index = match package.index(target.install_path()) {
Ok(Some(index)) => index,
Ok(None) => return None,
@ -232,6 +233,7 @@ pub(crate) async fn tree(
let download_concurrency = &download_concurrency;
let mut fetches = futures::stream::iter(packages)
.map(|(package, index)| async move {
// This probably already doesn't work for `--find-links`?
let Some(filename) = client
.find_latest(package.name(), Some(&index), download_concurrency)
.await?

View file

@ -9551,7 +9551,7 @@ fn lock_find_links_local_wheel() -> Result<()> {
/// Prefer an explicit index over any `--find-links` entries.
#[test]
fn lock_find_links_explicit_index() -> Result<()> {
fn lock_find_links_ignore_explicit_index() -> Result<()> {
let context = TestContext::new("3.12");
// Populate the `--find-links` entries.
@ -9669,6 +9669,123 @@ fn lock_find_links_explicit_index() -> Result<()> {
Ok(())
}
/// Ensure that `[[tool.uv.index]]` entries with `format = "flat"` can use relative paths in the
/// `url` field.
#[test]
fn lock_find_links_relative_url() -> Result<()> {
let context = TestContext::new("3.12");
// Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm"]
[[tool.uv.index]]
name = "local"
format = "flat"
url = "./links"
explicit = true
"#,
)?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
");
let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [{ name = "tqdm" }]
[[package]]
name = "tqdm"
version = "4.66.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/85/3ce0f9f7d3f596e7ea57f4e5ce8c18cb44e4a9daa58ddb46ee0d13d6bff8/tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531", size = 169462 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/14/e75e52d521442e2fcc9f1df3c5e456aead034203d4797867980de558ab34/tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9", size = 78296 },
]
"#
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
");
Ok(())
}
/// Lock a local source distribution via `--find-links`.
#[test]
fn lock_find_links_local_sdist() -> Result<()> {
@ -9963,6 +10080,311 @@ fn lock_find_links_http_sdist() -> Result<()> {
Ok(())
}
/// Use an explicit `--find-links` index.
#[test]
fn lock_find_links_explicit_index() -> Result<()> {
let context = TestContext::new("3.12");
// Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm"]
[[tool.uv.index]]
name = "local"
format = "flat"
url = "{}"
explicit = true
[tool.uv.sources]
tqdm = {{ index = "local" }}
"#,
Url::from_file_path(context.temp_dir.join("links/")).unwrap()
})?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
");
let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [{ name = "tqdm", index = "file://[TEMP_DIR]/links" }]
[[package]]
name = "tqdm"
version = "1000.0.0"
source = { registry = "../links" }
wheels = [
{ path = "tqdm-1000.0.0-py3-none-any.whl" },
]
"#
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
");
Ok(())
}
/// Use the same index priority rules, interchangeably, for `--find-links` and Simple API indexes.
#[test]
fn lock_find_links_higher_priority_index() -> Result<()> {
let context = TestContext::new("3.12");
// Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm"]
[[tool.uv.index]]
name = "local"
format = "flat"
url = "{}"
"#,
Url::from_file_path(context.temp_dir.join("links/")).unwrap()
})?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
");
let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [{ name = "tqdm" }]
[[package]]
name = "tqdm"
version = "1000.0.0"
source = { registry = "../links" }
wheels = [
{ path = "tqdm-1000.0.0-py3-none-any.whl" },
]
"#
);
});
Ok(())
}
/// Use the same index priority rules, interchangeably, for `--find-links` and Simple API indexes.
#[test]
fn lock_find_links_lower_priority_index() -> Result<()> {
let context = TestContext::new("3.12");
// Populate the `--find-links` entries.
fs_err::create_dir_all(context.temp_dir.join("links"))?;
for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|file_name| file_name.to_str())
.is_some_and(|file_name| file_name.starts_with("tqdm-"))
{
let dest = context
.temp_dir
.join("links")
.join(path.file_name().unwrap());
fs_err::copy(&path, &dest)?;
}
}
let workspace = context.temp_dir.child("workspace");
let pyproject_toml = workspace.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm"]
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
[[tool.uv.index]]
name = "local"
format = "flat"
url = "{}"
"#,
Url::from_file_path(context.temp_dir.join("links/")).unwrap()
})?;
uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 3 packages in [TIME]
");
let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap();
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "tqdm" },
]
[package.metadata]
requires-dist = [{ name = "tqdm" }]
[[package]]
name = "tqdm"
version = "4.66.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/85/3ce0f9f7d3f596e7ea57f4e5ce8c18cb44e4a9daa58ddb46ee0d13d6bff8/tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531", size = 169462 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/14/e75e52d521442e2fcc9f1df3c5e456aead034203d4797867980de558ab34/tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9", size = 78296 },
]
"#
);
});
Ok(())
}
/// Lock against a local directory laid out as a PEP 503-compatible index.
#[test]
fn lock_local_index() -> Result<()> {
@ -15331,7 +15753,7 @@ fn lock_explicit_default_index() -> Result<()> {
DEBUG No workspace root found, using project root
DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0`
Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }}
Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }) }), conflict: None }, origin: None }}
Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }}
DEBUG Solving with installed Python version: 3.12.[X]
DEBUG Solving with target Python version: >=3.12
DEBUG Adding direct dependency: project*

View file

@ -134,6 +134,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -293,6 +294,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -453,6 +455,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -645,6 +648,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -945,6 +949,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1129,6 +1134,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1159,6 +1165,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1322,6 +1329,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1352,6 +1360,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1382,6 +1391,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1567,6 +1577,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Flat,
publish_url: None,
authenticate: Auto,
},
@ -1894,6 +1905,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -1924,6 +1936,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -2083,6 +2096,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -2113,6 +2127,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -3184,6 +3199,7 @@ fn resolve_both() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -3468,6 +3484,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4174,6 +4191,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4204,6 +4222,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4365,6 +4384,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4395,6 +4415,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: false,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4562,6 +4583,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4592,6 +4614,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4754,6 +4777,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4784,6 +4808,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4953,6 +4978,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -4983,6 +5009,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -5145,6 +5172,7 @@ fn index_priority() -> anyhow::Result<()> {
origin: Some(
Cli,
),
format: Simple,
publish_url: None,
authenticate: Auto,
},
@ -5175,6 +5203,7 @@ fn index_priority() -> anyhow::Result<()> {
explicit: false,
default: true,
origin: None,
format: Simple,
publish_url: None,
authenticate: Auto,
},

View file

@ -223,6 +223,25 @@ authenticate = "never"
When `authenticate` is set to `never`, uv will never search for credentials for the given index and
will error if credentials are provided directly.
## "Flat" indexes
By default, `[[tool.uv.index]]` entries are assumed to be PyPI-style registries that implement the
[PEP 503](https://peps.python.org/pep-0503/) Simple Repository API. However, uv also supports "flat"
indexes, which are local directories or HTML pages that contain flat lists of wheels and source
distributions. In pip, such indexes are specified using the `--find-links` option.
To define a flat index in your `pyproject.toml`, use the `kind = "flat"` option:
```toml
[[tool.uv.index]]
name = "example"
url = "/path/to/directory"
kind = "flat"
```
Flat indexes support the same feature set as Simple Repository API indexes (e.g.,
`explicit = true`); you can also pin a package to a flat index using `tool.uv.sources`.
## `--index-url` and `--extra-index-url`
In addition to the `[[tool.uv.index]]` configuration option, uv supports pip-style `--index-url` and

29
uv.schema.json generated
View file

@ -796,6 +796,15 @@
"default": false,
"type": "boolean"
},
"format": {
"description": "The format used by the index.\n\nIndexes can either be PEP 503-compliant (i.e., a PyPI-style registry implementing the Simple API) or structured as a flat list of distributions (e.g., `--find-links`). In both cases, indexes can point to either local or remote resources.",
"default": "simple",
"allOf": [
{
"$ref": "#/definitions/IndexFormat"
}
]
},
"name": {
"description": "The name of the index.\n\nIndex names can be used to reference indexes elsewhere in the configuration. For example, you can pin a package to a specific index by name:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\"\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```",
"anyOf": [
@ -825,6 +834,24 @@
}
}
},
"IndexFormat": {
"oneOf": [
{
"description": "A PyPI-style index implementing the Simple Repository API.",
"type": "string",
"enum": [
"simple"
]
},
{
"description": "A `--find-links`-style index containing a flat list of wheels and source distributions.",
"type": "string",
"enum": [
"flat"
]
}
]
},
"IndexName": {
"description": "The normalized name of an index.\n\nIndex names may contain letters, digits, hyphens, underscores, and periods, and must be ASCII.",
"type": "string"
@ -855,7 +882,7 @@
]
},
"IndexUrl": {
"description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`).",
"description": "The URL of an index to use for fetching packages (e.g., `https://pypi.org/simple`), or a local path.",
"type": "string"
},
"KeyringProviderType": {