From 5b391770df9c0cb845d73212c396a44834467d2b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 15 Oct 2024 15:24:23 -0700 Subject: [PATCH] Add support for named and explicit indexes (#7481) ## Summary This PR adds a first-class API for defining registry indexes, beyond our existing `--index-url` and `--extra-index-url` setup. Specifically, you now define indexes like so in a `uv.toml` or `pyproject.toml` file: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" ``` You can also provide indexes via `--index` and `UV_INDEX`, and override the default index with `--default-index` and `UV_DEFAULT_INDEX`. ### Index priority Indexes are prioritized in the order in which they're defined, such that the first-defined index has highest priority. Indexes are also inherited from parent configuration (e.g., the user-level `uv.toml`), but are placed after any indexes in the current project, matching our semantics for other array-based configuration values. You can mix `--index` and `--default-index` with the legacy `--index-url` and `--extra-index-url` settings; the latter two are merely treated as unnamed `[[tool.uv.index]]` entries. ### Index pinning If an index includes a name (which is optional), it can then be referenced via `tool.uv.sources`: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" [tool.uv.sources] torch = { index = "pytorch" } ``` If an index is marked as `explicit = true`, it can _only_ be used via such references, and will never be searched implicitly: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" explicit = true [tool.uv.sources] torch = { index = "pytorch" } ``` Indexes defined outside of the current project (e.g., in the user-level `uv.toml`) can _not_ be explicitly selected. (As of now, we only support using a single index for a given `tool.uv.sources` definition.) ### Default index By default, we include PyPI as the default index. This remains true even if the user defines a `[[tool.uv.index]]` -- PyPI is still used as a fallback. You can mark an index as `default = true` to (1) disable the use of PyPI, and (2) bump it to the bottom of the prioritized list, such that it's used only if a package does not exist on a prior index: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" default = true ``` ### Name reuse If a name is reused, the higher-priority index with that name is used, while the lower-priority indexes are ignored entirely. For example, given: ```toml [[tool.uv.index]] name = "pytorch" url = "https://download.pytorch.org/whl/cu121" [[tool.uv.index]] name = "pytorch" url = "https://test.pypi.org/simple" ``` The `https://test.pypi.org/simple` index would be ignored entirely, since it's lower-priority than `https://download.pytorch.org/whl/cu121` but shares the same name. Closes #171. ## Future work - Users should be able to provide authentication for named indexes via environment variables. - `uv add` should automatically write `--index` entries to the `pyproject.toml` file. - Users should be able to provide multiple indexes for a given package, stratified by platform: ```toml [tool.uv.sources] torch = [ { index = "cpu", markers = "sys_platform == 'darwin'" }, { index = "gpu", markers = "sys_platform != 'darwin'" }, ] ``` - Users should be able to specify a proxy URL for a given index, to avoid writing user-specific URLs to a lockfile: ```toml [[tool.uv.index]] name = "test" url = "https://private.org/simple" proxy = "http:///pypi/simple" ``` --- Cargo.lock | 2 + crates/uv-cli/src/lib.rs | 74 +- crates/uv-cli/src/options.rs | 28 +- crates/uv-client/src/registry_client.rs | 12 +- crates/uv-distribution-types/src/index.rs | 146 ++ crates/uv-distribution-types/src/index_url.rs | 203 +- crates/uv-distribution-types/src/lib.rs | 2 + .../uv-distribution-types/src/resolution.rs | 4 +- .../src/index/registry_wheel_index.rs | 22 +- .../uv-distribution/src/metadata/lowering.rs | 42 +- .../src/metadata/requires_dist.rs | 16 + crates/uv-installer/src/plan.rs | 6 + crates/uv-pep508/src/verbatim_url.rs | 5 + crates/uv-pypi-types/src/requirement.rs | 6 +- crates/uv-resolver/src/error.rs | 3 + crates/uv-resolver/src/lock/mod.rs | 14 +- crates/uv-resolver/src/pubgrub/report.rs | 3 +- crates/uv-resolver/src/resolver/indexes.rs | 67 + crates/uv-resolver/src/resolver/mod.rs | 33 +- crates/uv-resolver/src/resolver/provider.rs | 6 +- crates/uv-scripts/Cargo.toml | 1 + crates/uv-scripts/src/lib.rs | 8 +- crates/uv-settings/src/settings.rs | 69 +- crates/uv-workspace/Cargo.toml | 1 + crates/uv-workspace/src/pyproject.rs | 50 +- crates/uv-workspace/src/workspace.rs | 33 +- crates/uv-workspace/src/workspace/tests.rs | 51 +- crates/uv/src/commands/build_frontend.rs | 2 +- crates/uv/src/commands/pip/compile.rs | 23 +- crates/uv/src/commands/pip/install.rs | 15 +- crates/uv/src/commands/pip/sync.rs | 15 +- crates/uv/src/commands/project/add.rs | 2 +- crates/uv/src/commands/project/lock.rs | 2 +- crates/uv/src/commands/project/mod.rs | 16 +- crates/uv/src/commands/project/run.rs | 15 +- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/src/commands/venv.rs | 4 +- crates/uv/src/settings.rs | 58 +- crates/uv/tests/it/lock.rs | 589 +++++- crates/uv/tests/it/pip_install.rs | 2 +- crates/uv/tests/it/show_settings.rs | 1851 +++++++++++++---- crates/uv/tests/it/sync.rs | 58 + docs/concepts/dependencies.md | 24 + docs/configuration/environment.md | 8 +- docs/configuration/index.md | 1 + docs/configuration/indexes.md | 114 + docs/pip/compatibility.md | 7 +- docs/reference/cli.md | 300 ++- docs/reference/settings.md | 117 +- mkdocs.template.yml | 1 + uv.schema.json | 51 +- 51 files changed, 3526 insertions(+), 658 deletions(-) create mode 100644 crates/uv-distribution-types/src/index.rs create mode 100644 crates/uv-resolver/src/resolver/indexes.rs create mode 100644 docs/configuration/indexes.md diff --git a/Cargo.lock b/Cargo.lock index 9ee6e4b0889a..0b71c519c5f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5121,6 +5121,7 @@ dependencies = [ "serde", "thiserror", "toml", + "uv-distribution-types", "uv-pep440", "uv-pep508", "uv-pypi-types", @@ -5285,6 +5286,7 @@ dependencies = [ "toml_edit", "tracing", "url", + "uv-distribution-types", "uv-fs", "uv-git", "uv-macros", diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8cdbde832203..c173e4cef2e6 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -7,13 +7,14 @@ use anyhow::{anyhow, Result}; use clap::builder::styling::{AnsiColor, Effects, Style}; use clap::builder::Styles; use clap::{Args, Parser, Subcommand}; + use url::Url; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; -use uv_distribution_types::{FlatIndexLocation, IndexUrl}; +use uv_distribution_types::{FlatIndexLocation, Index, IndexUrl}; use uv_normalize::{ExtraName, PackageName}; use uv_pep508::Requirement; use uv_pypi_types::VerbatimParsedUrl; @@ -793,6 +794,36 @@ fn parse_flat_index(input: &str) -> Result, String> { } } +/// Parse a string into an [`Index`], mapping the empty string to `None`. +fn parse_index_source(input: &str) -> Result, String> { + if input.is_empty() { + Ok(Maybe::None) + } else { + match Index::from_str(input) { + Ok(index) => Ok(Maybe::Some(Index { + default: false, + ..index + })), + Err(err) => Err(err.to_string()), + } + } +} + +/// Parse a string into an [`Index`], mapping the empty string to `None`. +fn parse_default_index_source(input: &str) -> Result, String> { + if input.is_empty() { + Ok(Maybe::None) + } else { + match Index::from_str(input) { + Ok(index) => Ok(Maybe::Some(Index { + default: true, + ..index + })), + Err(err) => Err(err.to_string()), + } + } +} + /// Parse a string into an [`Url`], mapping the empty string to `None`. fn parse_insecure_host(input: &str) -> Result, String> { if input.is_empty() { @@ -2282,8 +2313,8 @@ pub struct VenvArgs { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[arg(long, value_enum, env = EnvVars::UV_INDEX_STRATEGY)] pub index_strategy: Option, @@ -3808,7 +3839,28 @@ pub struct GenerateShellCompletionArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct IndexArgs { - /// The URL of the Python package index (by default: ). + /// The URLs to use when resolving dependencies, in addition to the default index. + /// + /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local + /// directory laid out in the same format. + /// + /// All indexes provided via this flag take priority over the index specified by + /// `--default-index` (which defaults to PyPI). When multiple `--index` flags are + /// provided, earlier values take priority. + #[arg(long, env = "UV_INDEX", value_delimiter = ' ', value_parser = parse_index_source, help_heading = "Index options")] + pub index: Option>>, + + /// The URL of the default package index (by default: ). + /// + /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local + /// directory laid out in the same format. + /// + /// The index given by this flag is given lower priority than all other indexes specified via + /// the `--index` flag. + #[arg(long, env = "UV_DEFAULT_INDEX", value_parser = parse_default_index_source, help_heading = "Index options")] + pub default_index: Option>, + + /// (Deprecated: use `--default-index` instead) The URL of the Python package index (by default: ). /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. @@ -3818,7 +3870,7 @@ pub struct IndexArgs { #[arg(long, short, env = EnvVars::UV_INDEX_URL, value_parser = parse_index_url, help_heading = "Index options")] pub index_url: Option>, - /// Extra URLs of package indexes to use, in addition to `--index-url`. + /// (Deprecated: use `--index` instead) Extra URLs of package indexes to use, in addition to `--index-url`. /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local /// directory laid out in the same format. @@ -3955,8 +4007,8 @@ pub struct InstallerArgs { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[arg( long, value_enum, @@ -4117,8 +4169,8 @@ pub struct ResolverArgs { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[arg( long, value_enum, @@ -4309,8 +4361,8 @@ pub struct ResolverInstallerArgs { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[arg( long, value_enum, diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 3fe5d0e77ea4..499a2109a80d 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,7 +1,7 @@ use uv_cache::Refresh; use uv_configuration::ConfigSettings; use uv_resolver::PrereleaseMode; -use uv_settings::{PipOptions, ResolverInstallerOptions, ResolverOptions}; +use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; use crate::{ BuildOptionsArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, @@ -186,6 +186,8 @@ impl From for PipOptions { impl From for PipOptions { fn from(args: IndexArgs) -> Self { let IndexArgs { + default_index, + index, index_url, extra_index_url, no_index, @@ -193,6 +195,12 @@ impl From for PipOptions { } = args; Self { + index: default_index + .and_then(Maybe::into_option) + .map(|default_index| vec![default_index]) + .combine( + index.map(|index| index.into_iter().filter_map(Maybe::into_option).collect()), + ), index_url: index_url.and_then(Maybe::into_option), extra_index_url: extra_index_url.map(|extra_index_url| { extra_index_url @@ -247,6 +255,15 @@ pub fn resolver_options( } = build_args; ResolverOptions { + index: index_args + .default_index + .and_then(Maybe::into_option) + .map(|default_index| vec![default_index]) + .combine( + index_args + .index + .map(|index| index.into_iter().filter_map(Maybe::into_option).collect()), + ), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url @@ -335,7 +352,16 @@ pub fn resolver_installer_options( no_binary_package, } = build_args; + let default_index = index_args + .default_index + .and_then(Maybe::into_option) + .map(|default_index| vec![default_index]); + let index = index_args + .index + .map(|index| index.into_iter().filter_map(Maybe::into_option).collect()); + ResolverInstallerOptions { + index: default_index.combine(index), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 923fa8a9a184..aee7ba7f8c64 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1,6 +1,7 @@ use async_http_range_reader::AsyncHttpRangeReader; use futures::{FutureExt, TryStreamExt}; use http::HeaderMap; +use itertools::Either; use reqwest::{Client, Response, StatusCode}; use reqwest_middleware::ClientWithMiddleware; use std::collections::BTreeMap; @@ -16,7 +17,7 @@ use uv_configuration::KeyringProviderType; use uv_configuration::{IndexStrategy, TrustedHost}; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, FileLocation, IndexCapabilities, IndexUrl, IndexUrls, Name, + BuiltDist, File, FileLocation, Index, IndexCapabilities, IndexUrl, IndexUrls, Name, }; use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream}; use uv_normalize::PackageName; @@ -204,8 +205,15 @@ impl RegistryClient { pub async fn simple( &self, package_name: &PackageName, + index: Option<&IndexUrl>, ) -> Result)>, Error> { - let mut it = self.index_urls.indexes().peekable(); + let indexes = if let Some(index) = index { + Either::Left(std::iter::once(index)) + } else { + Either::Right(self.index_urls.indexes().map(Index::url)) + }; + + let mut it = indexes.peekable(); if it.peek().is_none() { return Err(ErrorKind::NoIndex(package_name.to_string()).into()); } diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs new file mode 100644 index 000000000000..baa7d8175730 --- /dev/null +++ b/crates/uv-distribution-types/src/index.rs @@ -0,0 +1,146 @@ +use crate::{IndexUrl, IndexUrlError}; +use std::str::FromStr; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Index { + /// The name of the index. + /// + /// Index names can be used to reference indexes elsewhere in the configuration. For example, + /// you can pin a package to a specific index by name: + /// + /// ```toml + /// [[tool.uv.index]] + /// name = "pytorch" + /// url = "https://download.pytorch.org/whl/cu121" + /// + /// [tool.uv.sources] + /// torch = { index = "pytorch" } + /// ``` + pub name: Option, + /// The URL of the index. + /// + /// Expects to receive a URL (e.g., `https://pypi.org/simple`) or a local path. + pub url: IndexUrl, + /// Mark the index as explicit. + /// + /// Explicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]` + /// definition, as in: + /// + /// ```toml + /// [[tool.uv.index]] + /// name = "pytorch" + /// url = "https://download.pytorch.org/whl/cu121" + /// explicit = true + /// + /// [tool.uv.sources] + /// torch = { index = "pytorch" } + /// ``` + #[serde(default)] + pub explicit: bool, + /// Mark the index as the default index. + /// + /// By default, uv uses PyPI as the default index, such that even if additional indexes are + /// defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that + /// aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one + /// other index. + /// + /// Marking an index as default will move it to the front of the list of indexes, such that it + /// is given the highest priority when resolving packages. + #[serde(default)] + pub default: bool, + // /// 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, +} + +// #[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, +// } + +impl Index { + /// Initialize an [`Index`] from a pip-style `--index-url`. + pub fn from_index_url(url: IndexUrl) -> Self { + Self { + url, + name: None, + explicit: false, + default: true, + } + } + + /// Initialize an [`Index`] from a pip-style `--extra-index-url`. + pub fn from_extra_index_url(url: IndexUrl) -> Self { + Self { + url, + name: None, + explicit: false, + default: false, + } + } + + /// Return the [`IndexUrl`] of the index. + pub fn url(&self) -> &IndexUrl { + &self.url + } + + /// Return the raw [`URL`] of the index. + pub fn raw_url(&self) -> &Url { + self.url.url() + } +} + +impl FromStr for Index { + type Err = IndexSourceError; + + fn from_str(s: &str) -> Result { + // Determine whether the source is prefixed with a name, as in `name=https://pypi.org/simple`. + if let Some((name, url)) = s.split_once('=') { + if name.is_empty() { + return Err(IndexSourceError::EmptyName); + } + + if name.chars().all(char::is_alphanumeric) { + let url = IndexUrl::from_str(url)?; + return Ok(Self { + name: Some(name.to_string()), + url, + explicit: false, + default: false, + }); + } + } + + // Otherwise, assume the source is a URL. + let url = IndexUrl::from_str(s)?; + Ok(Self { + name: None, + url, + explicit: false, + default: false, + }) + } +} + +/// An error that can occur when parsing an [`Index`]. +#[derive(Error, Debug)] +pub enum IndexSourceError { + #[error(transparent)] + Url(#[from] IndexUrlError), + #[error("Index included a name, but the name was empty")] + EmptyName, +} diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index cd3eb8ad146a..f868f8c83528 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -11,12 +11,13 @@ use url::{ParseError, Url}; use uv_pep508::{VerbatimUrl, VerbatimUrlError}; -use crate::Verbatim; +use crate::{Index, Verbatim}; static PYPI_URL: LazyLock = LazyLock::new(|| Url::parse("https://pypi.org/simple").unwrap()); -static DEFAULT_INDEX_URL: LazyLock = - LazyLock::new(|| IndexUrl::Pypi(VerbatimUrl::from_url(PYPI_URL.clone()))); +static DEFAULT_INDEX_URL: LazyLock = LazyLock::new(|| { + Index::from_index_url(IndexUrl::Pypi(VerbatimUrl::from_url(PYPI_URL.clone()))) +}); /// The URL of an index to use for fetching packages (e.g., PyPI). #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -55,6 +56,15 @@ impl IndexUrl { } } + /// Convert the index URL into a [`Url`]. + pub fn into_url(self) -> Url { + match self { + Self::Pypi(url) => url.into_url(), + Self::Url(url) => url.into_url(), + Self::Path(url) => url.into_url(), + } + } + /// Return the redacted URL for the index, omitting any sensitive credentials. pub fn redacted(&self) -> Cow<'_, Url> { let url = self.url(); @@ -288,39 +298,21 @@ impl From for FlatIndexLocation { /// The index locations to use for fetching packages. By default, uses the PyPI index. /// -/// From a pip perspective, this type merges `--index-url`, `--extra-index-url`, and `--find-links`. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +/// This type merges the legacy `--index-url`, `--extra-index-url`, and `--find-links` options, +/// along with the uv-specific `--index` and `--default-index`. +#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { - index: Option, - extra_index: Vec, + indexes: Vec, flat_index: Vec, no_index: bool, } -impl Default for IndexLocations { - /// By default, use the `PyPI` index. - fn default() -> Self { - Self { - index: Some(DEFAULT_INDEX_URL.clone()), - extra_index: Vec::new(), - flat_index: Vec::new(), - no_index: false, - } - } -} - impl IndexLocations { /// Determine the index URLs to use for fetching packages. - pub fn new( - index: Option, - extra_index: Vec, - flat_index: Vec, - no_index: bool, - ) -> Self { + pub fn new(indexes: Vec, flat_index: Vec, no_index: bool) -> Self { Self { - index, - extra_index, + indexes, flat_index, no_index, } @@ -335,14 +327,12 @@ impl IndexLocations { #[must_use] pub fn combine( self, - index: Option, - extra_index: Vec, + indexes: Vec, flat_index: Vec, no_index: bool, ) -> Self { Self { - index: self.index.or(index), - extra_index: self.extra_index.into_iter().chain(extra_index).collect(), + indexes: self.indexes.into_iter().chain(indexes).collect(), flat_index: self.flat_index.into_iter().chain(flat_index).collect(), no_index: self.no_index || no_index, } @@ -351,47 +341,69 @@ impl IndexLocations { /// Returns `true` if no index configuration is set, i.e., the [`IndexLocations`] matches the /// default configuration. pub fn is_none(&self) -> bool { - self.index.is_none() - && self.extra_index.is_empty() - && self.flat_index.is_empty() - && !self.no_index + *self == Self::default() } } impl<'a> IndexLocations { - /// Return the primary [`IndexUrl`] entry. + /// Return the default [`Index`] entry. /// /// If `--no-index` is set, return `None`. /// /// If no index is provided, use the `PyPI` index. - pub fn index(&'a self) -> Option<&'a IndexUrl> { + pub fn default_index(&'a self) -> Option<&'a Index> { if self.no_index { None } else { - match self.index.as_ref() { - Some(index) => Some(index), - None => Some(&DEFAULT_INDEX_URL), - } + let mut seen = FxHashSet::default(); + self.indexes + .iter() + .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name))) + .find(|index| index.default && !index.explicit) + .or_else(|| Some(&DEFAULT_INDEX_URL)) } } - /// Return an iterator over the extra [`IndexUrl`] entries. - pub fn extra_index(&'a self) -> impl Iterator + 'a { + /// Return an iterator over the implicit [`Index`] entries. + pub fn implicit_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { - Either::Right(self.extra_index.iter()) + let mut seen = FxHashSet::default(); + Either::Right( + self.indexes + .iter() + .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name))) + .filter(|index| !(index.default || index.explicit)), + ) } } - /// Return an iterator over all [`IndexUrl`] entries in order. + /// Return an iterator over the explicit [`Index`] entries. + pub fn explicit_indexes(&'a self) -> impl Iterator + 'a { + if self.no_index { + Either::Left(std::iter::empty()) + } else { + let mut seen = FxHashSet::default(); + Either::Right( + self.indexes + .iter() + .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name))) + .filter(|index| index.explicit), + ) + } + } + + /// Return an iterator over all [`Index`] entries in order. + /// + /// Explicit indexes are excluded. /// - /// Prioritizes the extra indexes over the main index. + /// Prioritizes the extra indexes over the default index. /// /// If `no_index` was enabled, then this always returns an empty /// iterator. - pub fn indexes(&'a self) -> impl Iterator + 'a { - self.extra_index().chain(self.index()) + pub fn indexes(&'a self) -> impl Iterator + 'a { + self.implicit_indexes().chain(self.default_index()) } /// Return an iterator over the [`FlatIndexLocation`] entries. @@ -407,88 +419,89 @@ impl<'a> IndexLocations { /// Clone the index locations into a [`IndexUrls`] instance. pub fn index_urls(&'a self) -> IndexUrls { IndexUrls { - index: self.index.clone(), - extra_index: self.extra_index.clone(), + indexes: self.indexes.clone(), no_index: self.no_index, } } - /// Return an iterator over all [`Url`] entries. - pub fn urls(&'a self) -> impl Iterator + 'a { - self.indexes() - .map(IndexUrl::url) - .chain(self.flat_index.iter().filter_map(|index| match index { - FlatIndexLocation::Path(_) => None, - FlatIndexLocation::Url(url) => Some(url.raw()), - })) + /// Return an iterator over all allowed [`IndexUrl`] entries. + /// + /// This includes both explicit and implicit indexes, as well as the default index. + /// + /// If `no_index` was enabled, then this always returns an empty + /// iterator. + pub fn allowed_indexes(&'a self) -> impl Iterator + 'a { + self.explicit_indexes() + .chain(self.implicit_indexes()) + .chain(self.default_index()) + } + + /// Return an iterator over all allowed [`Url`] entries. + /// + /// This includes both explicit and implicit index URLs, as well as the default index. + /// + /// If `no_index` was enabled, then this always returns an empty + /// iterator. + pub fn allowed_urls(&'a self) -> impl Iterator + 'a { + self.allowed_indexes() + .map(Index::raw_url) + .chain(self.flat_index().map(FlatIndexLocation::url)) } } /// The index URLs to use for fetching packages. /// -/// From a pip perspective, this type merges `--index-url` and `--extra-index-url`. -#[derive(Debug, Clone)] +/// This type merges the legacy `--index-url` and `--extra-index-url` options, along with the +/// uv-specific `--index` and `--default-index`. +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct IndexUrls { - index: Option, - extra_index: Vec, + indexes: Vec, no_index: bool, } -impl Default for IndexUrls { - /// By default, use the `PyPI` index. - fn default() -> Self { - Self { - index: Some(DEFAULT_INDEX_URL.clone()), - extra_index: Vec::new(), - no_index: false, - } - } -} - impl<'a> IndexUrls { - /// Return the fallback [`IndexUrl`] entry. + /// Return the default [`Index`] entry. /// /// If `--no-index` is set, return `None`. /// /// If no index is provided, use the `PyPI` index. - fn index(&'a self) -> Option<&'a IndexUrl> { + fn default_index(&'a self) -> Option<&'a Index> { if self.no_index { None } else { - match self.index.as_ref() { - Some(index) => Some(index), - None => Some(&DEFAULT_INDEX_URL), - } + let mut seen = FxHashSet::default(); + self.indexes + .iter() + .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name))) + .find(|index| index.default && !index.explicit) + .or_else(|| Some(&DEFAULT_INDEX_URL)) } } - /// Return an iterator over the extra [`IndexUrl`] entries. - fn extra_index(&'a self) -> impl Iterator + 'a { + /// Return an iterator over the implicit [`Index`] entries. + fn implicit_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { - Either::Right(self.extra_index.iter()) + let mut seen = FxHashSet::default(); + Either::Right( + self.indexes + .iter() + .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name))) + .filter(|index| !(index.default || index.explicit)), + ) } } /// Return an iterator over all [`IndexUrl`] entries in order. /// - /// Prioritizes the extra indexes over the main index. + /// 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. - pub fn indexes(&'a self) -> impl Iterator + 'a { - self.extra_index().chain(self.index()) - } -} - -impl From for IndexUrls { - fn from(locations: IndexLocations) -> Self { - Self { - index: locations.index, - extra_index: locations.extra_index, - no_index: locations.no_index, - } + pub fn indexes(&'a self) -> impl Iterator + 'a { + self.implicit_indexes().chain(self.default_index()) } } diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index a77d0518544e..3758284a19e6 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -57,6 +57,7 @@ pub use crate::error::*; pub use crate::file::*; pub use crate::hash::*; pub use crate::id::*; +pub use crate::index::*; pub use crate::index_url::*; pub use crate::installed::*; pub use crate::prioritized_distribution::*; @@ -75,6 +76,7 @@ mod error; mod file; mod hash; mod id; +mod index; mod index_url; mod installed; mod prioritized_distribution; diff --git a/crates/uv-distribution-types/src/resolution.rs b/crates/uv-distribution-types/src/resolution.rs index f8b51b160e6e..6ec6edce3be0 100644 --- a/crates/uv-distribution-types/src/resolution.rs +++ b/crates/uv-distribution-types/src/resolution.rs @@ -190,7 +190,7 @@ impl From<&ResolvedDist> for Requirement { wheels.best_wheel().filename.version.clone(), ), ), - index: None, + index: Some(wheels.best_wheel().index.url().clone()), }, Dist::Built(BuiltDist::DirectUrl(wheel)) => { let mut location = wheel.url.to_url(); @@ -211,7 +211,7 @@ impl From<&ResolvedDist> for Requirement { specifier: uv_pep440::VersionSpecifiers::from( uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()), ), - index: None, + index: Some(sdist.index.url().clone()), }, Dist::Source(SourceDist::DirectUrl(sdist)) => { let mut location = sdist.url.to_url(); diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index f751a3fea403..5646e91e3e1f 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -3,7 +3,7 @@ use std::collections::hash_map::Entry; use rustc_hash::FxHashMap; use uv_cache::{Cache, CacheBucket, WheelCache}; -use uv_distribution_types::{CachedRegistryDist, Hashed, IndexLocations, IndexUrl}; +use uv_distribution_types::{CachedRegistryDist, Hashed, Index, IndexLocations, IndexUrl}; use uv_fs::{directories, files, symlinks}; use uv_normalize::PackageName; use uv_platform_tags::Tags; @@ -17,6 +17,8 @@ use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LO pub struct IndexEntry { /// The cached distribution. pub dist: CachedRegistryDist, + /// The index from which the wheel was downloaded. + pub index: Index, /// Whether the wheel was built from source (true), or downloaded from the registry directly (false). pub built: bool, } @@ -80,23 +82,22 @@ impl<'a> RegistryWheelIndex<'a> { ) -> Vec { let mut entries = vec![]; - // Collect into owned `IndexUrl`. - let flat_index_urls: Vec = index_locations + let flat_index_urls: Vec = index_locations .flat_index() - .map(|flat_index| IndexUrl::from(flat_index.clone())) + .map(|flat_index| Index::from_extra_index_url(IndexUrl::from(flat_index.clone()))) .collect(); - for index_url in index_locations.indexes().chain(flat_index_urls.iter()) { + for index in index_locations.indexes().chain(flat_index_urls.iter()) { // Index all the wheels that were downloaded directly from the registry. let wheel_dir = cache.shard( CacheBucket::Wheels, - WheelCache::Index(index_url).wheel_dir(package.to_string()), + WheelCache::Index(index.url()).wheel_dir(package.to_string()), ); // For registry wheels, the cache structure is: `//.http` // or `///.rev`. for file in files(&wheel_dir) { - match index_url { + match index.url() { // Add files from remote registries. IndexUrl::Pypi(_) | IndexUrl::Url(_) => { if file @@ -116,6 +117,7 @@ impl<'a> RegistryWheelIndex<'a> { ) { entries.push(IndexEntry { dist: wheel.into_registry_dist(), + index: index.clone(), built: false, }); } @@ -142,6 +144,7 @@ impl<'a> RegistryWheelIndex<'a> { ) { entries.push(IndexEntry { dist: wheel.into_registry_dist(), + index: index.clone(), built: false, }); } @@ -156,7 +159,7 @@ impl<'a> RegistryWheelIndex<'a> { // from the registry. let cache_shard = cache.shard( CacheBucket::SourceDistributions, - WheelCache::Index(index_url).wheel_dir(package.to_string()), + WheelCache::Index(index.url()).wheel_dir(package.to_string()), ); // For registry wheels, the cache structure is: `///`. @@ -165,7 +168,7 @@ impl<'a> RegistryWheelIndex<'a> { let cache_shard = cache_shard.shard(shard); // Read the revision from the cache. - let revision = match index_url { + let revision = match index.url() { // Add files from remote registries. IndexUrl::Pypi(_) | IndexUrl::Url(_) => { let revision_entry = cache_shard.entry(HTTP_REVISION); @@ -197,6 +200,7 @@ impl<'a> RegistryWheelIndex<'a> { ) { entries.push(IndexEntry { dist: wheel.into_registry_dist(), + index: index.clone(), built: true, }); } diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 9ad9207f80fb..5f3480eab6a6 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -6,6 +6,7 @@ use thiserror::Error; use url::Url; use uv_distribution_filename::DistExtension; +use uv_distribution_types::Index; use uv_git::GitReference; use uv_normalize::PackageName; use uv_pep440::VersionSpecifiers; @@ -33,6 +34,7 @@ impl LoweredRequirement { project_name: &'data PackageName, project_dir: &'data Path, project_sources: &'data BTreeMap, + project_indexes: &'data [Index], workspace: &'data Workspace, lower_bound: LowerBound, ) -> impl Iterator> + 'data { @@ -151,7 +153,26 @@ impl LoweredRequirement { (source, marker) } Source::Registry { index, marker } => { - let source = registry_source(&requirement, index)?; + // Identify the named index from either the project indexes or the workspace indexes, + // in that order. + let Some(index) = project_indexes + .iter() + .find(|Index { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) + .or_else(|| { + workspace.indexes().iter().find(|Index { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) + }) + .map(|Index { url: index, .. }| index.clone()) + else { + return Err(LoweringError::MissingIndex( + requirement.name.clone(), + index, + )); + }; + let source = registry_source(&requirement, index.into_url())?; (source, marker) } Source::Workspace { @@ -238,6 +259,7 @@ impl LoweredRequirement { requirement: uv_pep508::Requirement, dir: &'data Path, sources: &'data BTreeMap, + indexes: &'data [Index], ) -> impl Iterator> + 'data { let source = sources.get(&requirement.name).cloned(); @@ -318,7 +340,19 @@ impl LoweredRequirement { (source, marker) } Source::Registry { index, marker } => { - let source = registry_source(&requirement, index)?; + let Some(index) = indexes + .iter() + .find(|Index { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) + .map(|Index { url: index, .. }| index.clone()) + else { + return Err(LoweringError::MissingIndex( + requirement.name.clone(), + index, + )); + }; + let source = registry_source(&requirement, index.into_url())?; (source, marker) } Source::Workspace { .. } => { @@ -358,6 +392,8 @@ pub enum LoweringError { UndeclaredWorkspacePackage, #[error("Can only specify one of: `rev`, `tag`, or `branch`")] MoreThanOneGitRef, + #[error("Package `{0}` references an undeclared index: `{1}`")] + MissingIndex(PackageName, String), #[error("Workspace members are not allowed in non-workspace contexts")] WorkspaceMember, #[error(transparent)] @@ -448,7 +484,7 @@ fn url_source(url: Url, subdirectory: Option) -> Result, - index: String, + index: Url, ) -> Result { match &requirement.version_or_url { None => { diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index 5e67c846e934..2df77967a237 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -59,6 +59,20 @@ impl RequiresDist { source_strategy: SourceStrategy, lower_bound: LowerBound, ) -> Result { + // Collect any `tool.uv.index` entries. + let empty = vec![]; + let indexes = match source_strategy { + SourceStrategy::Enabled => project_workspace + .current_project() + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.index.as_deref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; + // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`. let empty = BTreeMap::default(); let sources = match source_strategy { @@ -94,6 +108,7 @@ impl RequiresDist { &metadata.name, project_workspace.project_root(), sources, + indexes, project_workspace.workspace(), lower_bound, ) @@ -127,6 +142,7 @@ impl RequiresDist { &metadata.name, project_workspace.project_root(), sources, + indexes, project_workspace.workspace(), lower_bound, ) diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index b1241355d8a1..cd654206ce30 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -121,6 +121,9 @@ impl<'a> Planner<'a> { match installable { Dist::Built(BuiltDist::Registry(wheel)) => { if let Some(distribution) = registry_index.get(wheel.name()).find_map(|entry| { + if *entry.index.url() != wheel.best_wheel().index { + return None; + } if entry.dist.filename.version != wheel.best_wheel().filename.version { return None; }; @@ -231,6 +234,9 @@ impl<'a> Planner<'a> { } Dist::Source(SourceDist::Registry(sdist)) => { if let Some(distribution) = registry_index.get(sdist.name()).find_map(|entry| { + if *entry.index.url() != sdist.index { + return None; + } if entry.dist.filename.version != sdist.version { return None; }; diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index e3224c99e9fc..f75539d2ed2b 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -134,6 +134,11 @@ impl VerbatimUrl { self.url.clone() } + /// Convert a [`VerbatimUrl`] into a [`Url`]. + pub fn into_url(self) -> Url { + self.url + } + /// Return the underlying [`Path`], if the URL is a file URL. pub fn as_path(&self) -> Result { self.url diff --git a/crates/uv-pypi-types/src/requirement.rs b/crates/uv-pypi-types/src/requirement.rs index ee930c573353..101b943f36b5 100644 --- a/crates/uv-pypi-types/src/requirement.rs +++ b/crates/uv-pypi-types/src/requirement.rs @@ -318,8 +318,8 @@ pub enum RequirementSource { /// The requirement has a version specifier, such as `foo >1,<2`. Registry { specifier: VersionSpecifiers, - /// Choose a version from the index with this name. - index: Option, + /// Choose a version from the index at the given URL. + index: Option, }, // TODO(konsti): Track and verify version specifier from `project.dependencies` matches the // version in remote location. @@ -607,7 +607,7 @@ enum RequirementSourceWire { Registry { #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] specifier: VersionSpecifiers, - index: Option, + index: Option, }, } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index e16caabcdf03..786e3b6e3c4e 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -53,6 +53,9 @@ pub enum ResolveError { fork_markers: MarkerTree, }, + #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")] + ConflictingIndexes(PackageName, String, String), + #[error("Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file.")] DisallowedUrl(PackageName, String), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 1c4de1cd09d0..2e9d18801123 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1058,10 +1058,10 @@ impl Lock { // Collect the set of available indexes (both `--index-url` and `--find-links` entries). let remotes = indexes.map(|locations| { locations - .indexes() - .filter_map(|index_url| match index_url { + .allowed_indexes() + .filter_map(|index| match index.url() { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { - Some(UrlString::from(index_url.redacted())) + Some(UrlString::from(index.url().redacted())) } IndexUrl::Path(_) => None, }) @@ -1080,11 +1080,11 @@ impl Lock { let locals = indexes.map(|locations| { locations - .indexes() - .filter_map(|index_url| match index_url { + .allowed_indexes() + .filter_map(|index| match index.url() { IndexUrl::Pypi(_) | IndexUrl::Url(_) => None, - IndexUrl::Path(index_url) => { - let path = index_url.to_file_path().ok()?; + IndexUrl::Path(url) => { + let path = url.to_file_path().ok()?; let path = relative_to(&path, workspace.install_path()) .or_else(|_| std::path::absolute(path)) .ok()?; diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 688206318bbe..150b7c653b2b 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -9,7 +9,7 @@ use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Te use rustc_hash::FxHashMap; use uv_configuration::IndexStrategy; -use uv_distribution_types::{IndexLocations, IndexUrl}; +use uv_distribution_types::{Index, IndexLocations, IndexUrl}; use uv_normalize::PackageName; use uv_pep440::Version; @@ -708,6 +708,7 @@ impl PubGrubReportFormatter<'_> { // indexes were not queried, and could contain a compatible version. if let Some(next_index) = index_locations .indexes() + .map(Index::url) .skip_while(|url| *url != found_index) .nth(1) { diff --git a/crates/uv-resolver/src/resolver/indexes.rs b/crates/uv-resolver/src/resolver/indexes.rs new file mode 100644 index 000000000000..d925cd786274 --- /dev/null +++ b/crates/uv-resolver/src/resolver/indexes.rs @@ -0,0 +1,67 @@ +use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers}; + +use rustc_hash::FxHashMap; +use std::collections::hash_map::Entry; +use uv_distribution_types::IndexUrl; +use uv_normalize::PackageName; +use uv_pep508::VerbatimUrl; +use uv_pypi_types::RequirementSource; + +/// A map of package names to their explicit index. +/// +/// For example, given: +/// ```toml +/// [[tool.uv.index]] +/// name = "pytorch" +/// url = "https://download.pytorch.org/whl/cu121" +/// +/// [tool.uv.sources] +/// torch = { index = "pytorch" } +/// ``` +/// +/// [`Indexes`] would contain a single entry mapping `torch` to `https://download.pytorch.org/whl/cu121`. +#[derive(Debug, Default, Clone)] +pub(crate) struct Indexes(FxHashMap); + +impl Indexes { + /// Determine the set of explicit, pinned indexes in the [`Manifest`]. + pub(crate) fn from_manifest( + manifest: &Manifest, + markers: &ResolverMarkers, + dependencies: DependencyMode, + ) -> Result { + let mut indexes = FxHashMap::::default(); + + for requirement in manifest.requirements(markers, dependencies) { + let RequirementSource::Registry { + index: Some(index), .. + } = &requirement.source + else { + continue; + }; + let index = IndexUrl::from(VerbatimUrl::from_url(index.clone())); + match indexes.entry(requirement.name.clone()) { + Entry::Occupied(entry) => { + let existing = entry.get(); + if *existing != index { + return Err(ResolveError::ConflictingIndexes( + requirement.name.clone(), + existing.to_string(), + index.to_string(), + )); + } + } + Entry::Vacant(entry) => { + entry.insert(index); + } + } + } + + Ok(Self(indexes)) + } + + /// Return the explicit index for a given [`PackageName`]. + pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> { + self.0.get(package_name) + } +} diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 9050d2b9ae8b..1bc06f5c5668 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -28,8 +28,9 @@ use uv_configuration::{Constraints, Overrides}; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_distribution_types::{ BuiltDist, CompatibleDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, - IncompatibleWheel, IndexCapabilities, IndexLocations, InstalledDist, PythonRequirementKind, - RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef, + IncompatibleWheel, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist, + PythonRequirementKind, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, + VersionOrUrlRef, }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -60,6 +61,7 @@ pub(crate) use crate::resolver::availability::{ use crate::resolver::batch_prefetch::BatchPrefetcher; use crate::resolver::groups::Groups; pub use crate::resolver::index::InMemoryIndex; +use crate::resolver::indexes::Indexes; pub use crate::resolver::provider::{ DefaultResolverProvider, MetadataResponse, PackageVersionsResult, ResolverProvider, VersionsResponse, WheelMetadataResult, @@ -74,6 +76,7 @@ mod batch_prefetch; mod fork_map; mod groups; mod index; +mod indexes; mod locals; mod provider; mod reporter; @@ -100,6 +103,7 @@ struct ResolverState { exclusions: Exclusions, urls: Urls, locals: Locals, + indexes: Indexes, dependency_mode: DependencyMode, hasher: HashStrategy, markers: ResolverMarkers, @@ -204,6 +208,7 @@ impl dependency_mode: options.dependency_mode, urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?, locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode), + indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode)?, groups: Groups::from_manifest(&manifest, &markers), project: manifest.project, workspace_members: manifest.workspace_members, @@ -377,7 +382,9 @@ impl ResolverState ResolverState ResolverState ResolverState ResolverState, + index: Option<&IndexUrl>, request_sink: &Sender, ) -> Result<(), ResolveError> { // Ignore unresolved URL packages. @@ -732,13 +742,14 @@ impl ResolverState, + index: Option<&IndexUrl>, request_sink: &Sender, ) -> Result<(), ResolveError> { // Only request real package @@ -760,7 +771,7 @@ impl ResolverState ResolverState Result, ResolveError> { match request { // Fetch package metadata from the registry. - Request::Package(package_name) => { + Request::Package(package_name, index) => { let package_versions = provider - .get_package_versions(&package_name) + .get_package_versions(&package_name, index.as_ref()) .boxed_local() .await .map_err(ResolveError::Client)?; @@ -2505,7 +2516,7 @@ impl ResolutionPackage { #[allow(clippy::large_enum_variant)] pub(crate) enum Request { /// A request to fetch the metadata for a package. - Package(PackageName), + Package(PackageName, Option), /// A request to fetch the metadata for a built or source distribution. Dist(Dist), /// A request to fetch the metadata from an already-installed distribution. @@ -2554,7 +2565,7 @@ impl<'a> From> for Request { impl Display for Request { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::Package(package_name) => { + Self::Package(package_name, _) => { write!(f, "Versions {package_name}") } Self::Dist(dist) => { diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 7823dffa1469..3bf4735013c8 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -2,7 +2,7 @@ use std::future::Future; use uv_configuration::BuildOptions; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; -use uv_distribution_types::Dist; +use uv_distribution_types::{Dist, IndexUrl}; use uv_normalize::PackageName; use uv_platform_tags::Tags; use uv_types::{BuildContext, HashStrategy}; @@ -49,6 +49,7 @@ pub trait ResolverProvider { fn get_package_versions<'io>( &'io self, package_name: &'io PackageName, + index: Option<&'io IndexUrl>, ) -> impl Future + 'io; /// Get the metadata for a distribution. @@ -111,11 +112,12 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, async fn get_package_versions<'io>( &'io self, package_name: &'io PackageName, + index: Option<&'io IndexUrl>, ) -> PackageVersionsResult { let result = self .fetcher .client() - .managed(|client| client.simple(package_name)) + .managed(|client| client.simple(package_name, index)) .await; match result { diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index a92848d4d83c..90d0bef4f800 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -11,6 +11,7 @@ doctest = false workspace = true [dependencies] +uv-distribution-types = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 59ce3685f587..689227475b39 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -1,13 +1,12 @@ +use memchr::memmem::Finder; +use serde::Deserialize; use std::collections::BTreeMap; use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::LazyLock; - -use memchr::memmem::Finder; -use serde::Deserialize; use thiserror::Error; - +use uv_distribution_types::Index; use uv_pep440::VersionSpecifiers; use uv_pep508::PackageName; use uv_pypi_types::VerbatimParsedUrl; @@ -263,6 +262,7 @@ pub struct ToolUv { #[serde(flatten)] pub top_level: ResolverInstallerOptions, pub sources: Option>, + pub indexes: Option>, } #[derive(Debug, Error)] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 7ce90d10a7cb..f2c31fc4d8f2 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -7,7 +7,7 @@ use uv_configuration::{ ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, TrustedHost, TrustedPublishing, }; -use uv_distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata}; +use uv_distribution_types::{FlatIndexLocation, Index, IndexUrl, StaticMetadata}; use uv_install_wheel::linker::LinkMode; use uv_macros::{CombineOptions, OptionsMetadata}; use uv_normalize::{ExtraName, PackageName}; @@ -230,6 +230,7 @@ pub struct GlobalOptions { /// Settings relevant to all installer operations. #[derive(Debug, Clone, Default, CombineOptions)] pub struct InstallerOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -254,6 +255,7 @@ pub struct InstallerOptions { /// Settings relevant to all resolver operations. #[derive(Debug, Clone, Default, CombineOptions)] pub struct ResolverOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -284,13 +286,52 @@ pub struct ResolverOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverInstallerOptions { + /// The package indexes to use when resolving dependencies. + /// + /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) + /// (the simple repository API), or a local directory laid out in the same format. + /// + /// Indexes are considered in the order in which they're defined, such that the first-defined + /// index has the highest priority. Further, the indexes provided by this setting are given + /// higher priority than any indexes specified via [`index_url`](#index-url) or + /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains + /// a given package, unless an alternative [index strategy](#index-strategy) is specified. + /// + /// If an index is marked as `explicit = true`, it will be used exclusively for those + /// dependencies that select it explicitly via `[tool.uv.sources]`, as in: + /// + /// ```toml + /// [[tool.uv.index]] + /// name = "pytorch" + /// url = "https://download.pytorch.org/whl/cu121" + /// explicit = true + /// + /// [tool.uv.sources] + /// torch = { index = "pytorch" } + /// ``` + /// + /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is + /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the + /// PyPI default index. + #[option( + default = "\"[]\"", + value_type = "dict", + example = r#" + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "# + )] + pub index: Option>, /// The URL of the Python package index (by default: ). /// /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) /// (the simple repository API), or a local directory laid out in the same format. /// /// The index provided by this setting is given lower priority than any indexes specified via - /// [`extra_index_url`](#extra-index-url). + /// [`extra_index_url`](#extra-index-url) or [`index`](#index). + /// + /// (Deprecated: use `index` instead.) #[option( default = "\"https://pypi.org/simple\"", value_type = "str", @@ -305,10 +346,13 @@ pub struct ResolverInstallerOptions { /// (the simple repository API), or a local directory laid out in the same format. /// /// All indexes provided via this flag take priority over the index specified by - /// [`index_url`](#index-url). When multiple indexes are provided, earlier values take priority. + /// [`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes + /// are provided, earlier values take priority. /// /// To control uv's resolution strategy when multiple indexes are present, see /// [`index_strategy`](#index-strategy). + /// + /// (Deprecated: use `index` instead.) #[option( default = "[]", value_type = "list[str]", @@ -347,8 +391,8 @@ pub struct ResolverInstallerOptions { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[option( default = "\"first-index\"", value_type = "str", @@ -693,6 +737,9 @@ pub struct PipOptions { "# )] pub prefix: Option, + #[serde(skip)] + #[cfg_attr(feature = "schemars", schemars(skip))] + pub index: Option>, /// The URL of the Python package index (by default: ). /// /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) @@ -756,8 +803,8 @@ pub struct PipOptions { /// /// By default, uv will stop at the first index on which a given package is available, and /// limit resolutions to those present on that first index (`first-match`). This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary. + /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the + /// same name to an alternate index. #[option( default = "\"first-index\"", value_type = "str", @@ -1299,6 +1346,7 @@ pub struct PipOptions { impl From for ResolverOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1328,6 +1376,7 @@ impl From for ResolverOptions { impl From for InstallerOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1361,6 +1410,7 @@ impl From for InstallerOptions { #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -1387,6 +1437,7 @@ pub struct ToolOptions { impl From for ToolOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1415,6 +1466,7 @@ impl From for ToolOptions { impl From for ResolverInstallerOptions { fn from(value: ToolOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1464,6 +1516,7 @@ pub struct OptionsWire { // #[serde(flatten)] // top_level: ResolverInstallerOptions, + index: Option>, index_url: Option, extra_index_url: Option>, no_index: Option, @@ -1528,6 +1581,7 @@ impl From for Options { concurrent_downloads, concurrent_builds, concurrent_installs, + index, index_url, extra_index_url, no_index, @@ -1581,6 +1635,7 @@ impl From for Options { concurrent_installs, }, top_level: ResolverInstallerOptions { + index, index_url, extra_index_url, no_index, diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 5c8be2c233ad..040f9fe068ba 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] +uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio", "schemars"] } uv-git = { workspace = true } uv-macros = { workspace = true } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 9420934cf07b..95bbe4ac1c27 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -15,7 +15,7 @@ use std::str::FromStr; use std::{collections::BTreeMap, mem}; use thiserror::Error; use url::Url; - +use uv_distribution_types::Index; use uv_fs::{relative_to, PortablePathBuf}; use uv_git::GitReference; use uv_macros::OptionsMetadata; @@ -154,9 +154,49 @@ pub struct ToolUv { /// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving /// dependencies. pub sources: Option, + + /// The indexes to use when resolving dependencies. + /// + /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) + /// (the simple repository API), or a local directory laid out in the same format. + /// + /// Indexes are considered in the order in which they're defined, such that the first-defined + /// index has the highest priority. Further, the indexes provided by this setting are given + /// higher priority than any indexes specified via [`index_url`](#index-url) or + /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains + /// a given package, unless an alternative [index strategy](#index-strategy) is specified. + /// + /// If an index is marked as `explicit = true`, it will be used exclusively for those + /// dependencies that select it explicitly via `[tool.uv.sources]`, as in: + /// + /// ```toml + /// [[tool.uv.index]] + /// name = "pytorch" + /// url = "https://download.pytorch.org/whl/cu121" + /// explicit = true + /// + /// [tool.uv.sources] + /// torch = { index = "pytorch" } + /// ``` + /// + /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is + /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the + /// PyPI default index. + #[option( + default = "\"[]\"", + value_type = "dict", + example = r#" + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "# + )] + pub index: Option>, + /// The workspace definition for the project, if any. #[option_group] pub workspace: Option, + /// Whether the project is managed by uv. If `false`, uv will ignore the project when /// `uv run` is invoked. #[option( @@ -167,6 +207,7 @@ pub struct ToolUv { "# )] pub managed: Option, + /// Whether the project should be considered a Python package, or a non-package ("virtual") /// project. /// @@ -185,6 +226,7 @@ pub struct ToolUv { "# )] pub package: Option, + /// The project's development dependencies. Development dependencies will be installed by /// default in `uv run` and `uv sync`, but will not appear in the project's published metadata. #[cfg_attr( @@ -202,6 +244,7 @@ pub struct ToolUv { "# )] pub dev_dependencies: Option>>, + /// A list of supported environments against which to resolve dependencies. /// /// By default, uv will resolve for all possible environments during a `uv lock` operation. @@ -226,6 +269,7 @@ pub struct ToolUv { "# )] pub environments: Option, + /// Overrides to apply when resolving the project's dependencies. /// /// Overrides are used to force selection of a specific version of a package, regardless of the @@ -261,6 +305,7 @@ pub struct ToolUv { "# )] pub override_dependencies: Option>>, + /// Constraints to apply when resolving the project's dependencies. /// /// Constraints are used to restrict the versions of dependencies that are selected during @@ -315,7 +360,7 @@ impl ToolUvSources { impl<'de> serde::de::Deserialize<'de> for ToolUvSources { fn deserialize(deserializer: D) -> Result where - D: serde::de::Deserializer<'de>, + D: Deserializer<'de>, { struct SourcesVisitor; @@ -581,7 +626,6 @@ pub enum Source { }, /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`. Registry { - // TODO(konstin): The string is more-or-less a placeholder index: String, #[serde( skip_serializing_if = "uv_pep508::marker::ser::is_empty", diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 24fd85a04291..ece22fafa533 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashSet; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use tracing::{debug, trace, warn}; - +use uv_distribution_types::Index; use uv_fs::{Simplified, CWD}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_pep508::{MarkerTree, RequirementOrigin, VerbatimUrl}; @@ -15,7 +15,7 @@ use uv_static::EnvVars; use uv_warnings::{warn_user, warn_user_once}; use crate::pyproject::{ - Project, PyProjectToml, PyprojectTomlError, Source, Sources, ToolUvSources, ToolUvWorkspace, + Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace, }; #[derive(thiserror::Error, Debug)] @@ -80,6 +80,10 @@ pub struct Workspace { /// /// This table is overridden by the project sources. sources: BTreeMap, + /// The index table from the workspace `pyproject.toml`. + /// + /// This table is overridden by the project indexes. + indexes: Vec, /// The `pyproject.toml` of the workspace root. pyproject_toml: PyProjectToml, } @@ -489,20 +493,9 @@ impl Workspace { &self.sources } - /// Returns an iterator over all sources in the workspace. - pub fn iter_sources(&self) -> impl Iterator { - self.packages - .values() - .filter_map(|member| { - member.pyproject_toml().tool.as_ref().and_then(|tool| { - tool.uv - .as_ref() - .and_then(|uv| uv.sources.as_ref()) - .map(ToolUvSources::inner) - .map(|sources| sources.values().flat_map(Sources::iter)) - }) - }) - .flatten() + /// The index table from the workspace `pyproject.toml`. + pub fn indexes(&self) -> &[Index] { + &self.indexes } /// The `pyproject.toml` of the workspace. @@ -719,11 +712,18 @@ impl Workspace { .and_then(|uv| uv.sources) .map(ToolUvSources::into_inner) .unwrap_or_default(); + let workspace_indexes = workspace_pyproject_toml + .tool + .clone() + .and_then(|tool| tool.uv) + .and_then(|uv| uv.index) + .unwrap_or_default(); Ok(Workspace { install_path: workspace_root, packages: workspace_members, sources: workspace_sources, + indexes: workspace_indexes, pyproject_toml: workspace_pyproject_toml, }) } @@ -1025,6 +1025,7 @@ impl ProjectWorkspace { // There may be package sources, but we don't need to duplicate them into the // workspace sources. sources: BTreeMap::default(), + indexes: Vec::default(), pyproject_toml: project_pyproject_toml.clone(), }, }); diff --git a/crates/uv-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs index f12d130ab100..82978abdea7b 100644 --- a/crates/uv-workspace/src/workspace/tests.rs +++ b/crates/uv-workspace/src/workspace/tests.rs @@ -43,7 +43,7 @@ async fn albatross_in_example() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-in-example/examples/bird-feeder", "project_name": "bird-feeder", @@ -65,6 +65,7 @@ async fn albatross_in_example() { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -79,7 +80,7 @@ async fn albatross_in_example() { } } } - "#); + "###); }); } @@ -94,7 +95,7 @@ async fn albatross_project_in_excluded() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", "project_name": "bird-feeder", @@ -116,6 +117,7 @@ async fn albatross_project_in_excluded() { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -130,7 +132,7 @@ async fn albatross_project_in_excluded() { } } } - "#); + "###); }); } @@ -144,7 +146,7 @@ async fn albatross_root_workspace() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-root-workspace", "project_name": "albatross", @@ -200,6 +202,7 @@ async fn albatross_root_workspace() { } ] }, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -220,6 +223,7 @@ async fn albatross_root_workspace() { } ] }, + "index": null, "workspace": { "members": [ "packages/*" @@ -237,7 +241,7 @@ async fn albatross_root_workspace() { } } } - "#); + "###); }); } @@ -252,7 +256,7 @@ async fn albatross_virtual_workspace() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-virtual-workspace/packages/albatross", "project_name": "albatross", @@ -302,11 +306,13 @@ async fn albatross_virtual_workspace() { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": null, "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -324,7 +330,7 @@ async fn albatross_virtual_workspace() { } } } - "#); + "###); }); } @@ -338,7 +344,7 @@ async fn albatross_just_project() { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]/albatross-just-project", "project_name": "albatross", @@ -360,6 +366,7 @@ async fn albatross_just_project() { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -374,7 +381,7 @@ async fn albatross_just_project() { } } } - "#); + "###); }); } #[tokio::test] @@ -456,7 +463,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -491,6 +498,7 @@ async fn exclude_package() -> Result<()> { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -504,6 +512,7 @@ async fn exclude_package() -> Result<()> { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -523,7 +532,7 @@ async fn exclude_package() -> Result<()> { } } } - "#); + "###); }); // Rewrite the members to both include and exclude `bird-feeder` by name. @@ -554,7 +563,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -589,6 +598,7 @@ async fn exclude_package() -> Result<()> { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -602,6 +612,7 @@ async fn exclude_package() -> Result<()> { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -622,7 +633,7 @@ async fn exclude_package() -> Result<()> { } } } - "#); + "###); }); // Rewrite the exclusion to use the top-level directory (`packages`). @@ -653,7 +664,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -701,6 +712,7 @@ async fn exclude_package() -> Result<()> { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -714,6 +726,7 @@ async fn exclude_package() -> Result<()> { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -734,7 +747,7 @@ async fn exclude_package() -> Result<()> { } } } - "#); + "###); }); // Rewrite the exclusion to use the top-level directory with a glob (`packages/*`). @@ -765,7 +778,7 @@ async fn exclude_package() -> Result<()> { { ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" }, - @r#" + @r###" { "project_root": "[ROOT]", "project_name": "albatross", @@ -787,6 +800,7 @@ async fn exclude_package() -> Result<()> { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -800,6 +814,7 @@ async fn exclude_package() -> Result<()> { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -820,7 +835,7 @@ async fn exclude_package() -> Result<()> { } } } - "#); + "###); }); Ok(()) diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index b24dfdd9f4a6..dd56532d1795 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -400,7 +400,7 @@ async fn build_package( .into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index cbeaa2311184..f5f2a87123e0 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -16,7 +16,7 @@ use uv_configuration::{ use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DependencyMetadata, IndexCapabilities, IndexLocations, NameRequirementSpecification, + DependencyMetadata, Index, IndexCapabilities, IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification, Verbatim, }; use uv_fs::Simplified; @@ -273,11 +273,18 @@ pub(crate) async fn pip_compile( let dev = Vec::default(); // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -446,12 +453,12 @@ pub(crate) async fn pip_compile( // If necessary, include the `--index-url` and `--extra-index-url` locations. if include_index_url { - if let Some(index) = index_locations.index() { - writeln!(writer, "--index-url {}", index.verbatim())?; + if let Some(index) = index_locations.default_index() { + writeln!(writer, "--index-url {}", index.url().verbatim())?; wrote_preamble = true; } - for extra_index in index_locations.extra_index() { - writeln!(writer, "--extra-index-url {}", extra_index.verbatim())?; + for extra_index in index_locations.implicit_indexes() { + writeln!(writer, "--extra-index-url {}", extra_index.url().verbatim())?; wrote_preamble = true; } } diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index db08f8a4f907..a5872a7cb374 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -14,7 +14,7 @@ use uv_configuration::{ use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DependencyMetadata, IndexLocations, NameRequirementSpecification, Resolution, + DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, Resolution, UnresolvedRequirementSpecification, }; use uv_fs::Simplified; @@ -275,11 +275,18 @@ pub(crate) async fn pip_install( let dev = Vec::default(); // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 3d49dbf7f053..6d2abbc6aa3d 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -13,7 +13,7 @@ use uv_configuration::{ }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::BuildDispatch; -use uv_distribution_types::{DependencyMetadata, IndexLocations, Resolution}; +use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, Resolution}; use uv_fs::Simplified; use uv_install_wheel::linker::LinkMode; use uv_installer::SitePackages; @@ -211,11 +211,18 @@ pub(crate) async fn pip_sync( }; // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index e69916d5bf1c..721a7d2f3953 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -246,7 +246,7 @@ pub(crate) async fn add( resolution_environment(python_version, python_platform, target.interpreter())?; // Add all authenticated sources to the cache. - for url in settings.index_locations.urls() { + for url in settings.index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 056908b3008f..a1f1cd00ea36 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -364,7 +364,7 @@ async fn do_lock( PythonRequirement::from_requires_python(interpreter, requires_python.clone()); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 33fd30e4dff8..1678f20d8c16 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -12,7 +12,7 @@ use uv_configuration::{Concurrency, Constraints, ExtrasSpecification, Reinstall, use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; use uv_distribution_types::{ - Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification, + Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_fs::Simplified; use uv_git::ResolvedRepositoryReference; @@ -647,7 +647,7 @@ pub(crate) async fn resolve_names( } = settings; // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -794,7 +794,7 @@ pub(crate) async fn resolve_environment<'a>( let python_requirement = PythonRequirement::from_interpreter(interpreter); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -952,7 +952,7 @@ pub(crate) async fn sync_environment( let tags = venv.interpreter().tags()?; // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -1140,7 +1140,7 @@ pub(crate) async fn update_environment( } // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -1349,7 +1349,7 @@ fn warn_on_requirements_txt_setting( warn_user_once!("Ignoring `--no-index` from requirements file. Instead, use the `--no-index` command-line argument, or set `no-index` in a `uv.toml` or `pyproject.toml` file."); } else { if let Some(index_url) = index_url { - if settings.index_locations.index() != Some(index_url) { + if settings.index_locations.default_index().map(Index::url) != Some(index_url) { warn_user_once!( "Ignoring `--index-url` from requirements file: `{index_url}`. Instead, use the `--index-url` command-line argument, or set `index-url` in a `uv.toml` or `pyproject.toml` file." ); @@ -1358,8 +1358,8 @@ fn warn_on_requirements_txt_setting( for extra_index_url in extra_index_urls { if !settings .index_locations - .extra_index() - .contains(extra_index_url) + .implicit_indexes() + .any(|index| index.url() == extra_index_url) { warn_user_once!( "Ignoring `--extra-index-url` from requirements file: `{extra_index_url}`. Instead, use the `--extra-index-url` command-line argument, or set `extra-index-url` in a `uv.toml` or `pyproject.toml` file.`" diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8bea0ac04ae8..1a55f11797c8 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -215,7 +215,19 @@ pub(crate) async fn run( // Install the script requirements, if necessary. Otherwise, use an isolated environment. if let Some(dependencies) = script.dependencies { - // // Collect any `tool.uv.sources` from the script. + // Collect any `tool.uv.index` from the script. + let empty = Vec::default(); + let script_indexes = match settings.sources { + SourceStrategy::Enabled => script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.indexes.as_deref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; + + // Collect any `tool.uv.sources` from the script. let empty = BTreeMap::default(); let script_sources = match settings.sources { SourceStrategy::Enabled => script @@ -234,6 +246,7 @@ pub(crate) async fn run( requirement, script_dir.as_ref(), script_sources, + script_indexes, ) .map_ok(LoweredRequirement::into_inner) }) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index b80f5b0efbbc..db0285441d0d 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -276,7 +276,7 @@ pub(super) async fn do_sync( let resolution = apply_editable_mode(resolution, editable); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { uv_auth::store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 425c0b5a343c..5f872a477823 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -229,7 +229,7 @@ async fn venv_impl( let interpreter = python.into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -278,7 +278,7 @@ async fn venv_impl( let interpreter = venv.interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 80f274b7d0b1..d0d75071c1f8 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -24,7 +24,7 @@ use uv_configuration::{ NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; -use uv_distribution_types::{DependencyMetadata, IndexLocations}; +use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; use uv_install_wheel::linker::LinkMode; use uv_normalize::PackageName; use uv_pep508::{ExtraName, RequirementOrigin}; @@ -1921,8 +1921,19 @@ impl From for ResolverSettings { fn from(value: ResolverOptions) -> Self { Self { index_locations: IndexLocations::new( - value.index_url, - value.extra_index_url.unwrap_or_default(), + value + .index + .into_iter() + .flatten() + .chain( + value + .extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(value.index_url.into_iter().map(Index::from_index_url)) + .collect(), value.find_links.unwrap_or_default(), value.no_index.unwrap_or_default(), ), @@ -2048,8 +2059,19 @@ impl From for ResolverInstallerSettings { fn from(value: ResolverInstallerOptions) -> Self { Self { index_locations: IndexLocations::new( - value.index_url, - value.extra_index_url.unwrap_or_default(), + value + .index + .into_iter() + .flatten() + .chain( + value + .extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(value.index_url.into_iter().map(Index::from_index_url)) + .collect(), value.find_links.unwrap_or_default(), value.no_index.unwrap_or_default(), ), @@ -2155,6 +2177,7 @@ impl PipSettings { break_system_packages, target, prefix, + index, index_url, extra_index_url, no_index, @@ -2206,6 +2229,7 @@ impl PipSettings { } = pip.unwrap_or_default(); let ResolverInstallerOptions { + index: top_level_index, index_url: top_level_index_url, extra_index_url: top_level_extra_index_url, no_index: top_level_no_index, @@ -2237,6 +2261,7 @@ impl PipSettings { // preferring the latter. // // For example, prefer `tool.uv.pip.index-url` over `tool.uv.index-url`. + let index = index.combine(top_level_index); let index_url = index_url.combine(top_level_index_url); let extra_index_url = extra_index_url.combine(top_level_extra_index_url); let no_index = no_index.combine(top_level_no_index); @@ -2262,10 +2287,25 @@ impl PipSettings { Self { index_locations: IndexLocations::new( - args.index_url.combine(index_url), - args.extra_index_url - .combine(extra_index_url) - .unwrap_or_default(), + args.index + .into_iter() + .flatten() + .chain( + args.extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(args.index_url.into_iter().map(Index::from_index_url)) + .chain(index.into_iter().flatten()) + .chain( + extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(index_url.into_iter().map(Index::from_index_url)) + .collect(), args.find_links.combine(find_links).unwrap_or_default(), args.no_index.combine(no_index).unwrap_or_default(), ), diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 87ad9d749396..8b76b5c072dd 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7751,7 +7751,7 @@ fn lock_local_index() -> Result<()> { [tool.uv] extra-index-url = ["{}"] "#, - Url::from_directory_path(&root).unwrap().as_str() + Url::from_file_path(&root).unwrap().as_str() })?; uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" @@ -7765,7 +7765,7 @@ fn lock_local_index() -> Result<()> { let lock = context.read("uv.lock"); - let index = Url::from_directory_path(&root).unwrap().to_string(); + let index = Url::from_file_path(&root).unwrap().to_string(); let filters = [(index.as_str(), "file://[TMP]")] .into_iter() .chain(context.filters()) @@ -11883,6 +11883,591 @@ fn lock_trailing_slash() -> Result<()> { Ok(()) } +#[test] +fn lock_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "iniconfig==2.0.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + iniconfig = { index = "test" } + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://test.pypi.org/simple" } + sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio", specifier = "==3.7.0" }, + { name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_named_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + explicit = true + + [[tool.uv.index]] + name = "heron" + url = "https://pypi-proxy.fly.dev/simple" + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_default_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // If an index is included, PyPI will still be used as the default index. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + // Unless that index is marked as the default. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + default = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_named_index_cli() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "jinja2"] + + [tool.uv.sources] + jinja2 = { index = "pytorch" } + "#, + )?; + + // The package references a non-existent index. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry for: `jinja2` + Caused by: Package `jinja2` references an undeclared index: `pytorch` + "###); + + // This also isn't supported right now; you need to specify the index in the `pyproject.toml`. + uv_snapshot!(context.filters(), context.lock().arg("--index").arg("pytorch=https://download.pytorch.org/whl/cu121"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry for: `jinja2` + Caused by: Package `jinja2` references an undeclared index: `pytorch` + "###); + + Ok(()) +} + +/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. +/// In other words, the lower-priority index should be ignored entirely during implicit resolution. +/// +/// In this test, we should use PyPI (the default index) and ignore `https://example.com` entirely. +/// (Querying `https://example.com` would fail with a 500.) +#[test] +fn lock_repeat_named_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + + [[tool.uv.index]] + name = "pytorch" + url = "https://example.com" + "#, + )?; + + // Fall back to PyPI, since `iniconfig` doesn't exist on the PyTorch index. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + Ok(()) +} + +/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. +/// This includes names passed in via the CLI. +#[test] +fn lock_repeat_named_index_cli() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["jinja2==3.1.2"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "#, + )?; + + // Resolve to the PyTorch index. + uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [[package]] + name = "jinja2" + version = "3.1.2" + source = { registry = "https://download.pytorch.org/whl/cu121" } + dependencies = [ + { name = "markupsafe" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu121" } + wheels = [ + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2" }, + ] + + [package.metadata] + requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + "### + ); + }); + + // Resolve to PyPI, since the PyTorch index is replaced by the Packse index, which doesn't + // include `jinja2`. + uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())).env_remove("UV_EXCLUDE_NEWER"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [[package]] + name = "jinja2" + version = "3.1.2" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markupsafe" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", size = 268239 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", size = 133101 }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2" }, + ] + + [package.metadata] + requires-dist = [{ name = "jinja2", specifier = "==3.1.2" }] + "### + ); + }); + + Ok(()) +} + /// Lock a project with `package = false`, making it a virtual project. #[test] fn lock_explicit_virtual_project() -> Result<()> { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index aa21a5b5e27e..454c36a34405 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -191,7 +191,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` + unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index d7969ca9284a..295c25fde952 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -88,31 +88,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -230,31 +234,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -373,31 +381,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -548,31 +560,35 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -692,8 +708,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -822,31 +837,35 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -989,53 +1008,61 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - ), - extra_index: [ - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + 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: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: true, + }, ], flat_index: [], no_index: false, @@ -1156,75 +1183,88 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + indexes: [ + Index { + name: None, + 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: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - ), - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + 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: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: true, + }, ], flat_index: [], no_index: false, @@ -1368,8 +1408,7 @@ fn resolve_find_links() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [ Url( VerbatimUrl { @@ -1534,8 +1573,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -1670,52 +1708,61 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), ), + port: None, + path: "/whl", + query: None, + fragment: None, + }, + given: Some( + "https://download.pytorch.org/whl", ), - port: None, - path: "/whl", - query: None, - fragment: None, }, - given: Some( - "https://download.pytorch.org/whl", - ), - }, - ), - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + 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: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: false, + }, ], flat_index: [], no_index: false, @@ -1834,52 +1881,61 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), ), + port: None, + path: "/whl", + query: None, + fragment: None, + }, + given: Some( + "https://download.pytorch.org/whl", ), - port: None, - path: "/whl", - query: None, - fragment: None, }, - given: Some( - "https://download.pytorch.org/whl", - ), - }, - ), - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + 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: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: false, + }, ], flat_index: [], no_index: false, @@ -2022,8 +2078,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2148,8 +2203,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2274,8 +2328,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2402,8 +2455,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2542,6 +2594,7 @@ fn resolve_tool() -> anyhow::Result<()> { ), ), options: ResolverInstallerOptions { + index: None, index_url: None, extra_index_url: None, no_index: None, @@ -2578,8 +2631,7 @@ fn resolve_tool() -> anyhow::Result<()> { }, settings: ResolverInstallerSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2706,8 +2758,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2860,31 +2911,35 @@ fn resolve_both() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -3029,31 +3084,35 @@ fn resolve_config_file() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -3146,7 +3205,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` + unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` "### ); @@ -3273,8 +3332,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3402,8 +3460,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3539,8 +3596,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3618,3 +3674,1078 @@ fn allow_insecure_host() -> anyhow::Result<()> { Ok(()) } + +/// Prioritize indexes defined across multiple configuration sources. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn index_priority() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [[index]] + url = "https://file.pypi.org/simple" + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--default-index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + index-url = "https://file.pypi.org/simple" + "#})?; + + // Prefer the `--default-index` from the CLI, and treat it as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--default-index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + // Prefer the `--index` from the CLI, but treat the index from the file as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [[index]] + url = "https://file.pypi.org/simple" + default = true + "#})?; + + // Prefer the `--index-url` from the CLI, and treat it as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + // Prefer the `--extra-index-url` from the CLI, but not as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--extra-index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 2a50c3ea78e0..ef264d3aa1b6 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -3075,3 +3075,61 @@ fn sync_no_sources_missing_member() -> Result<()> { Ok(()) } + +#[test] +fn sync_explicit() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "root" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "idna>2", + ] + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + explicit = true + + [tool.uv.sources] + idna = { index = "test" } + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==2.7 + "###); + + // Clear the environment. + fs_err::remove_dir_all(&context.venv)?; + + // The package should be drawn from the cache. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==2.7 + "###); + + Ok(()) +} diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index 642a31fd71d1..e8fa89388bc1 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -70,6 +70,7 @@ standards-compliant `project.dependencies` table. During development, a project may rely on a package that isn't available on PyPI. The following additional sources are supported by uv: +- Index: A package resolved from a specific package index. - Git: A Git repository. - URL: A remote wheel or source distribution. - Path: A local wheel, source distribution, or project directory. @@ -91,6 +92,29 @@ $ uv lock --no-sources The use of `--no-sources` will also prevent uv from discovering any [workspace members](#workspace-member) that could satisfy a given dependency. +### Index + +To pin a Python package to a specific index, add a named index to the `pyproject.toml`: + +```toml title="pyproject.toml" +[project] +dependencies = [ + "torch", +] + +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true +``` + +The `explicit` flag is optional and indicates that the index should _only_ be used for packages that +explicitly specify it in `tool.uv.sources`. If `explicit` is not set, other packages may be resolved +from the index, if not found elsewhere. + ### Git To add a Git dependency source, prefix a Git-compatible URL to clone with `git+`. diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 31bf07f885ff..0b20bdaca570 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -2,10 +2,16 @@ uv accepts the following command-line arguments as environment variables: +- `UV_INDEX`: Equivalent to the `--index` command-line argument. If set, uv will use this + space-separated list of URLs as additional indexes when searching for packages. +- `UV_DEFAULT_INDEX`: Equivalent to the `--default-index` command-line argument. If set, uv will use + this URL as the default index when searching for packages. - `UV_INDEX_URL`: Equivalent to the `--index-url` command-line argument. If set, uv will use this - URL as the base index for searching for packages. + URL as the default index when searching for packages. (Deprecated: use `UV_DEFAULT_INDEX` + instead.) - `UV_EXTRA_INDEX_URL`: Equivalent to the `--extra-index-url` command-line argument. If set, uv will use this space-separated list of URLs as additional indexes when searching for packages. + (Deprecated: use `UV_INDEX` instead.) - `UV_FIND_LINKS`: Equivalent to the `--find-links` command-line argument. If set, uv will use this comma-separated list of additional locations to search for packages. - `UV_CACHE_DIR`: Equivalent to the `--cache-dir` command-line argument. If set, uv will use this diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 8f0bc1f3edce..38c68875fc1e 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -5,6 +5,7 @@ Read about the various ways to configure uv: - [Using configuration files](./files.md) - [Using environment variables](./environment.md) - [Configuring authentication](./authentication.md) +- [Configuring package indexes](./indexes.md) Or, jump to the [settings reference](../reference/settings.md) which enumerates the available configuration options. diff --git a/docs/configuration/indexes.md b/docs/configuration/indexes.md new file mode 100644 index 000000000000..889b5726e632 --- /dev/null +++ b/docs/configuration/indexes.md @@ -0,0 +1,114 @@ +# Package indexes + +By default, uv uses the [Python Package Index (PyPI)](https://pypi.org) for dependency resolution +and package installation. However, uv can be configured to use other package indexes, including +private indexes, via the `[[tool.uv.index]]` configuration option (and `--index`, the analogous +command-line option). + +## Defining an index + +To include an additional index when resolving dependencies, add a `[[tool.uv.index]]` entry to your +`pyproject.toml`: + +```toml +[[tool.uv.index]] +# Optional name for the index. +name = "pytorch" +# Required URL for the index. +url = "https://download.pytorch.org/whl/cpu" +``` + +Indexes are prioritized in the order in which they’re defined, such that the first index listed in +the configuration file is the first index consulted when resolving dependencies, with indexes +provided via the command line taking precedence over those in the configuration file. + +By default, uv includes the Python Package Index (PyPI) as the "default" index, i.e., the index used +when a package is not found on any other index. To exclude PyPI from the list of indexes, set +`default = true` on another index entry (or use the `--default-index` command-line option): + +```toml +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +default = true +``` + +The default index is always treated as lowest priority, regardless of its position in the list of +indexes. + +## Pinning a package to an index + +A package can be pinned to a specific index by specifying the index in its `tool.uv.sources` entry. +For example, to ensure that `torch` is _always_ installed from the `pytorch` index, add the +following to your `pyproject.toml`: + +```toml +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +``` + +An index can be marked as `explicit = true` to prevent packages from being installed from that index +unless explicitly pinned to it. For example, to ensure that `torch` is installed from the `pytorch` +index, but all other packages are installed from PyPI, add the following to your `pyproject.toml`: + +```toml +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true +``` + +Named indexes referenced via `tool.uv.sources` must be defined within the project's `pyproject.toml` +file; indexes provided via the command-line, environment variables, or user-level configuration will +not be recognized. + +## Searching across multiple indexes + +By default, uv will stop at the first index on which a given package is available, and limit +resolutions to those present on that first index (`first-match`). + +For example, if an internal index is specified via `[[tool.uv.index]]`, uv's behavior is such that +if a package exists on that internal index, it will _always_ be installed from that internal index, +and never from PyPI. The intent is to prevent "dependency confusion" attacks, in which an attacker +publishes a malicious package on PyPI with the same name as an internal package, thus causing the +malicious package to be installed instead of the internal package. See, for example, +[the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/) from +December 2022. + +Users can opt in to alternate index behaviors via the`--index-strategy` command-line option, or the +`UV_INDEX_STRATEGY` environment variable, which supports the following values: + +- `first-match` (default): Search for each package across all indexes, limiting the candidate + versions to those present in the first index that contains the package. +- `unsafe-first-match`: Search for each package across all indexes, but prefer the first index with + a compatible version, even if newer versions are available on other indexes. +- `unsafe-best-match`: Search for each package across all indexes, and select the best version from + the combined set of candidate versions. + +While `unsafe-best-match` is the closest to pip's behavior, it exposes users to the risk of +"dependency confusion" attacks. + +## `--index-url` and `--extra-index-url` + +In addition to the `[[tool.uv.index]]` configuration option, uv supports pip-style `--index-url` and +`--extra-index-url` command-line options for compatibility, where `--index-url` defines the default +index and `--extra-index-url` defines additional indexes. + +These options can be used in conjunction with the `[[tool.uv.index]]` configuration option, and use +the same prioritization rules: + +- The default index is always treated as lowest priority, whether defined via the legacy + `--index-url` argument, the recommended `--default-index` argument, or a `[[tool.uv.index]]` entry + with `default = true`. +- Indexes are consulted in the order in which they’re defined, either via the legacy + `--extra-index-url` argument, the recommended `--index` argument, or `[[tool.uv.index]]` entries. + +In effect, `--index-url` and `--extra-index-url` can be thought of as unnamed `[[tool.uv.index]]` +entries, with `default = true` enabled for the former. diff --git a/docs/pip/compatibility.md b/docs/pip/compatibility.md index 754ebe8689a0..1be873ba3760 100644 --- a/docs/pip/compatibility.md +++ b/docs/pip/compatibility.md @@ -148,10 +148,9 @@ supports the following values: While `unsafe-best-match` is the closest to `pip`'s behavior, it exposes users to the risk of "dependency confusion" attacks. -In the future, uv will support pinning packages to dedicated indexes (see: -[#171](https://github.com/astral-sh/uv/issues/171)). Additionally, -[PEP 708](https://peps.python.org/pep-0708/) is a provisional standard that aims to address the -"dependency confusion" issue across package registries and installers. +uv also supports pinning packages to dedicated indexes (see: +[_Indexes_](../configuration/indexes.md#pinning-a-package-to-an-index)), such that a given package +is _always_ installed from a specific index. ## PEP 517 build isolation diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5add2f5ddb46..0794efbd07f1 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -116,6 +116,13 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-index

The URL of the default package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

+ +

May also be set with the UV_DEFAULT_INDEX environment variable.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -135,7 +142,7 @@ uv run [OPTIONS] [COMMAND]

This option is only available when running in a project.

-
--extra-index-url extra-index-url

Extra URLs of package indexes to use, in addition to --index-url.

+
--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -155,9 +162,16 @@ uv run [OPTIONS] [COMMAND]
--help, -h

Display the concise help for this command

+
--index index

The URLs to use when resolving dependencies, in addition to the default index.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+ +

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

-

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

+

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -169,7 +183,7 @@ uv run [OPTIONS] [COMMAND]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -672,6 +686,13 @@ uv add [OPTIONS] >

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --dev

    Add the requirements as development dependencies

    --directory directory

    Change to the given directory prior to running the command.

    @@ -693,7 +714,7 @@ uv add [OPTIONS] >

    To add this dependency to an optional group in the current project instead, see --optional.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -713,9 +734,16 @@ uv add [OPTIONS] >
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -727,7 +755,7 @@ uv add [OPTIONS] >
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -998,6 +1026,13 @@ uv remove [OPTIONS] ...

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --dev

    Remove the packages from the development dependencies

    --directory directory

    Change to the given directory prior to running the command.

    @@ -1011,7 +1046,7 @@ uv remove [OPTIONS] ...

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1031,9 +1066,16 @@ uv remove [OPTIONS] ...
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1045,7 +1087,7 @@ uv remove [OPTIONS] ...
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1300,6 +1342,13 @@ uv sync [OPTIONS]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -1317,7 +1366,7 @@ uv sync [OPTIONS]

    Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1337,9 +1386,16 @@ uv sync [OPTIONS]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1351,7 +1407,7 @@ uv sync [OPTIONS]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1610,6 +1666,13 @@ uv lock [OPTIONS]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -1621,7 +1684,7 @@ uv lock [OPTIONS]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1639,9 +1702,16 @@ uv lock [OPTIONS]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1653,7 +1723,7 @@ uv lock [OPTIONS]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1886,6 +1956,13 @@ uv export [OPTIONS]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -1901,7 +1978,7 @@ uv export [OPTIONS]

    May be provided more than once.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -1931,9 +2008,16 @@ uv export [OPTIONS]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -1945,7 +2029,7 @@ uv export [OPTIONS]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2198,6 +2282,13 @@ uv tree [OPTIONS]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --depth, -d depth

    Maximum display depth of the dependency tree

    [default: 255]

    @@ -2212,7 +2303,7 @@ uv tree [OPTIONS]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2232,9 +2323,16 @@ uv tree [OPTIONS]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -2246,7 +2344,7 @@ uv tree [OPTIONS]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2575,6 +2673,13 @@ uv tool run [OPTIONS] [COMMAND]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -2586,7 +2691,7 @@ uv tool run [OPTIONS] [COMMAND]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2606,9 +2711,16 @@ uv tool run [OPTIONS] [COMMAND]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -2620,7 +2732,7 @@ uv tool run [OPTIONS] [COMMAND]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2864,6 +2976,13 @@ uv tool install [OPTIONS]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -2875,7 +2994,7 @@ uv tool install [OPTIONS]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -2895,9 +3014,16 @@ uv tool install [OPTIONS]
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -2909,7 +3035,7 @@ uv tool install [OPTIONS]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -3149,6 +3275,13 @@ uv tool upgrade [OPTIONS] ...

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -3160,7 +3293,7 @@ uv tool upgrade [OPTIONS] ...

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -3176,9 +3309,16 @@ uv tool upgrade [OPTIONS] ...

    May also be set with the UV_FIND_LINKS environment variable.

    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -3190,7 +3330,7 @@ uv tool upgrade [OPTIONS] ...
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -4702,6 +4842,13 @@ uv pip compile [OPTIONS] ...

    Used to reflect custom build scripts and commands that wrap uv pip compile.

    May also be set with the UV_CUSTOM_COMPILE_COMMAND environment variable.

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -4725,7 +4872,7 @@ uv pip compile [OPTIONS] ...

    Only applies to pyproject.toml, setup.py, and setup.cfg sources.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -4743,9 +4890,16 @@ uv pip compile [OPTIONS] ...
    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -4757,7 +4911,7 @@ uv pip compile [OPTIONS] ...
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -5101,6 +5255,13 @@ uv pip sync [OPTIONS] ...

    This is equivalent to pip’s --constraint option.

    May also be set with the UV_CONSTRAINT environment variable.

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -5114,7 +5275,7 @@ uv pip sync [OPTIONS] ...

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -5130,9 +5291,16 @@ uv pip sync [OPTIONS] ...

    May also be set with the UV_FIND_LINKS environment variable.

    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -5144,7 +5312,7 @@ uv pip sync [OPTIONS] ...
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -5438,6 +5606,13 @@ uv pip install [OPTIONS] |--editable This is equivalent to pip’s --constraint option.

    May also be set with the UV_CONSTRAINT environment variable.

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -5461,7 +5636,7 @@ uv pip install [OPTIONS] |--editable Only applies to pyproject.toml, setup.py, and setup.cfg sources.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -5477,9 +5652,16 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_FIND_LINKS environment variable.

    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -5491,7 +5673,7 @@ uv pip install [OPTIONS] |--editable unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -6620,6 +6802,13 @@ uv venv [OPTIONS] [PATH]

    While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

    May also be set with the UV_CONFIG_FILE environment variable.

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -6631,7 +6820,7 @@ uv venv [OPTIONS] [PATH]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -6647,9 +6836,16 @@ uv venv [OPTIONS] [PATH]

    May also be set with the UV_FIND_LINKS environment variable.

    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -6661,7 +6857,7 @@ uv venv [OPTIONS] [PATH]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -6868,6 +7064,13 @@ uv build [OPTIONS] [SRC]

    May also be set with the UV_CONFIG_FILE environment variable.

    --config-setting, -C config-setting

    Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

    +
    --default-index default-index

    The URL of the default package index (by default: <https://pypi.org/simple>).

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    The index given by this flag is given lower priority than all other indexes specified via the --index flag.

    + +

    May also be set with the UV_DEFAULT_INDEX environment variable.

    --directory directory

    Change to the given directory prior to running the command.

    Relative paths are resolved with the given directory as the base.

    @@ -6879,7 +7082,7 @@ uv build [OPTIONS] [SRC]

    Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

    May also be set with the UV_EXCLUDE_NEWER environment variable.

    -
    --extra-index-url extra-index-url

    Extra URLs of package indexes to use, in addition to --index-url.

    +
    --extra-index-url extra-index-url

    (Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    @@ -6895,9 +7098,16 @@ uv build [OPTIONS] [SRC]

    May also be set with the UV_FIND_LINKS environment variable.

    --help, -h

    Display the concise help for this command

    +
    --index index

    The URLs to use when resolving dependencies, in addition to the default index.

    + +

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    + +

    All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

    + +

    May also be set with the UV_INDEX environment variable.

    --index-strategy index-strategy

    The strategy to use when resolving against multiple index URLs.

    -

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

    +

    By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

    May also be set with the UV_INDEX_STRATEGY environment variable.

    Possible values:

    @@ -6909,7 +7119,7 @@ uv build [OPTIONS] [SRC]
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • -
    --index-url, -i index-url

    The URL of the Python package index (by default: <https://pypi.org/simple>).

    +
    --index-url, -i index-url

    (Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

    Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

    diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 4e05ccb40aa9..ff89d4e8be8f 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -73,6 +73,51 @@ environments = ["sys_platform == 'darwin'"] --- +### [`index`](#index) {: #index } + +The indexes to use when resolving dependencies. + +Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) +(the simple repository API), or a local directory laid out in the same format. + +Indexes are considered in the order in which they're defined, such that the first-defined +index has the highest priority. Further, the indexes provided by this setting are given +higher priority than any indexes specified via [`index_url`](#index-url) or +[`extra_index_url`](#extra-index-url). uv will only consider the first index that contains +a given package, unless an alternative [index strategy](#index-strategy) is specified. + +If an index is marked as `explicit = true`, it will be used exclusively for those +dependencies that select it explicitly via `[tool.uv.sources]`, as in: + +```toml +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cu121" +explicit = true + +[tool.uv.sources] +torch = { index = "pytorch" } +``` + +If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is +given the lowest priority when resolving packages. Additionally, marking an index as default will disable the +PyPI default index. + +**Default value**: `"[]"` + +**Type**: `dict` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cu121" +``` + +--- + ### [`managed`](#managed) {: #managed } Whether the project is managed by uv. If `false`, uv will ignore the project when @@ -533,11 +578,14 @@ Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep (the simple repository API), or a local directory laid out in the same format. All indexes provided via this flag take priority over the index specified by -[`index_url`](#index-url). When multiple indexes are provided, earlier values take priority. +[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes +are provided, earlier values take priority. To control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy). +(Deprecated: use `index` instead.) + **Default value**: `[]` **Type**: `list[str]` @@ -591,14 +639,69 @@ formats described above. --- +### [`index`](#index) {: #index } + +The package indexes to use when resolving dependencies. + +Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) +(the simple repository API), or a local directory laid out in the same format. + +Indexes are considered in the order in which they're defined, such that the first-defined +index has the highest priority. Further, the indexes provided by this setting are given +higher priority than any indexes specified via [`index_url`](#index-url) or +[`extra_index_url`](#extra-index-url). uv will only consider the first index that contains +a given package, unless an alternative [index strategy](#index-strategy) is specified. + +If an index is marked as `explicit = true`, it will be used exclusively for those +dependencies that select it explicitly via `[tool.uv.sources]`, as in: + +```toml +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cu121" +explicit = true + +[tool.uv.sources] +torch = { index = "pytorch" } +``` + +If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is +given the lowest priority when resolving packages. Additionally, marking an index as default will disable the +PyPI default index. + +**Default value**: `"[]"` + +**Type**: `dict` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + ``` +=== "uv.toml" + + ```toml + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + ``` + +--- + ### [`index-strategy`](#index-strategy) {: #index-strategy } The strategy to use when resolving against multiple index URLs. By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents -"dependency confusion" attacks, whereby an attack can upload a malicious package under the -same name to a secondary. +"dependency confusion" attacks, whereby an attacker can upload a malicious package under the +same name to an alternate index. **Default value**: `"first-index"` @@ -633,7 +736,9 @@ Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep (the simple repository API), or a local directory laid out in the same format. The index provided by this setting is given lower priority than any indexes specified via -[`extra_index_url`](#extra-index-url). +[`extra_index_url`](#extra-index-url) or [`index`](#index). + +(Deprecated: use `index` instead.) **Default value**: `"https://pypi.org/simple"` @@ -1911,8 +2016,8 @@ The strategy to use when resolving against multiple index URLs. By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents -"dependency confusion" attacks, whereby an attack can upload a malicious package under the -same name to a secondary. +"dependency confusion" attacks, whereby an attacker can upload a malicious package under the +same name to an alternate index. **Default value**: `"first-index"` diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 158ce2cd6970..eff7a4058937 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -106,6 +106,7 @@ nav: - Configuration files: configuration/files.md - Environment variables: configuration/environment.md - Authentication: configuration/authentication.md + - Package indexes: configuration/indexes.md - Integration guides: - guides/integration/index.md - Docker: guides/integration/docker.md diff --git a/uv.schema.json b/uv.schema.json index ced2bb33b3f2..f6ab2f8c57b4 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -128,7 +128,7 @@ ] }, "extra-index-url": { - "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).", + "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)", "type": [ "array", "null" @@ -147,8 +147,18 @@ "$ref": "#/definitions/FlatIndexLocation" } }, + "index": { + "description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined index has the highest priority. Further, the indexes provided by this setting are given higher priority than any indexes specified via [`index_url`](#index-url) or [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains a given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for those dependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is given the lowest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Index" + } + }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy" @@ -159,7 +169,7 @@ ] }, "index-url": { - "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url).", + "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url) or [`index`](#index).\n\n(Deprecated: use `index` instead.)", "anyOf": [ { "$ref": "#/definitions/IndexUrl" @@ -545,6 +555,39 @@ "description": "The path to a directory of distributions, or a URL to an HTML file with a flat listing of distributions.", "type": "string" }, + "Index": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "default": { + "description": "Mark the index as the default index.\n\nBy default, uv uses PyPI as the default index, such that even if additional indexes are defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one other index.\n\nMarking an index as default will move it to the front of the list of indexes, such that it is given the highest priority when resolving packages.", + "default": false, + "type": "boolean" + }, + "explicit": { + "description": "Mark the index as explicit.\n\nExplicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]` definition, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```", + "default": false, + "type": "boolean" + }, + "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\" } ```", + "type": [ + "string", + "null" + ] + }, + "url": { + "description": "The URL of the index.\n\nExpects to receive a URL (e.g., `https://pypi.org/simple`) or a local path.", + "allOf": [ + { + "$ref": "#/definitions/IndexUrl" + } + ] + } + } + }, "IndexStrategy": { "oneOf": [ { @@ -803,7 +846,7 @@ ] }, "index-strategy": { - "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.", + "description": "The strategy to use when resolving against multiple index URLs.\n\nBy default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.", "anyOf": [ { "$ref": "#/definitions/IndexStrategy"