From 6a40f9aa26ad051d6403e3748438a7e833fb4fe6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 17 Sep 2024 20:57:33 -0400 Subject: [PATCH] Add implicit indexes --- crates/distribution-types/src/index_source.rs | 70 ++++++ crates/distribution-types/src/index_url.rs | 84 +++---- crates/distribution-types/src/lib.rs | 4 +- crates/distribution-types/src/named_index.rs | 22 -- .../uv-distribution/src/metadata/lowering.rs | 19 +- crates/uv-resolver/src/resolver/indexes.rs | 14 +- crates/uv-settings/src/settings.rs | 36 ++- crates/uv-workspace/src/pyproject.rs | 42 +++- crates/uv-workspace/src/workspace.rs | 15 ++ crates/uv/tests/lock.rs | 207 ++++++++++++++++-- crates/uv/tests/pip_install.rs | 2 +- crates/uv/tests/show_settings.rs | 26 ++- docs/reference/settings.md | 98 +++++++++ uv.schema.json | 43 ++++ 14 files changed, 582 insertions(+), 100 deletions(-) create mode 100644 crates/distribution-types/src/index_source.rs delete mode 100644 crates/distribution-types/src/named_index.rs diff --git a/crates/distribution-types/src/index_source.rs b/crates/distribution-types/src/index_source.rs new file mode 100644 index 000000000000..e4eec40c785f --- /dev/null +++ b/crates/distribution-types/src/index_source.rs @@ -0,0 +1,70 @@ +use crate::IndexUrl; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct IndexSource { + /// 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 enabled 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, +// } diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index eb3b6231e18e..19fb35bff2f4 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -297,7 +297,7 @@ 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)] +#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { sources: Vec, @@ -307,19 +307,6 @@ pub struct IndexLocations { no_index: bool, } -impl Default for IndexLocations { - /// By default, use the `PyPI` index. - fn default() -> Self { - Self { - sources: Vec::new(), - 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( @@ -365,15 +352,32 @@ 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.sources.is_empty() - && self.index.is_none() - && self.extra_index.is_empty() - && self.flat_index.is_empty() - && !self.no_index + *self == Self::default() } } impl<'a> IndexLocations { + /// Return an iterator over the `tool.uv.index` sources, prioritizing the default index. + fn sources(&'a self) -> impl Iterator + 'a { + if self.no_index { + Either::Left(std::iter::empty()) + } else { + Either::Right( + self.sources + .iter() + .filter(|source| !source.explicit) + .filter(|source| source.default) + .chain( + self.sources + .iter() + .filter(|source| !source.explicit) + .filter(|source| !source.default), + ) + .map(|source| &source.url), + ) + } + } + /// Return the primary [`IndexUrl`] entry. /// /// If `--no-index` is set, return `None`. @@ -385,6 +389,7 @@ impl<'a> IndexLocations { } else { match self.index.as_ref() { Some(index) => Some(index), + None if self.sources.iter().any(|source| source.default) => None, None => Some(&DEFAULT_INDEX_URL), } } @@ -406,7 +411,7 @@ impl<'a> IndexLocations { /// 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()) + self.sources().chain(self.extra_index()).chain(self.index()) } /// Return an iterator over the [`FlatIndexLocation`] entries. @@ -433,17 +438,14 @@ impl<'a> IndexLocations { 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()), - })) + .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)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct IndexUrls { sources: Vec, index: Option, @@ -451,25 +453,25 @@ pub struct IndexUrls { no_index: bool, } -impl Default for IndexUrls { - /// By default, use the `PyPI` index. - fn default() -> Self { - Self { - sources: Vec::new(), - index: Some(DEFAULT_INDEX_URL.clone()), - extra_index: Vec::new(), - no_index: false, - } - } -} - impl<'a> IndexUrls { - /// Return an iterator over the `tool.uv.index` sources. + /// Return an iterator over the `tool.uv.index` sources, prioritizing the default index. fn sources(&'a self) -> impl Iterator + 'a { if self.no_index { Either::Left(std::iter::empty()) } else { - Either::Right(self.sources.iter().map(|source| &source.index)) + Either::Right( + self.sources + .iter() + .filter(|source| !source.explicit) + .filter(|source| source.default) + .chain( + self.sources + .iter() + .filter(|source| !source.explicit) + .filter(|source| !source.default), + ) + .map(|source| &source.url), + ) } } @@ -484,6 +486,7 @@ impl<'a> IndexUrls { } else { match self.index.as_ref() { Some(index) => Some(index), + None if self.sources.iter().any(|source| source.default) => None, None => Some(&DEFAULT_INDEX_URL), } } @@ -500,7 +503,8 @@ impl<'a> IndexUrls { /// 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. diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index aa4eb4b74b01..0d7c07cf716b 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -57,9 +57,9 @@ pub use crate::error::*; pub use crate::file::*; pub use crate::hash::*; pub use crate::id::*; +pub use crate::index_source::*; pub use crate::index_url::*; pub use crate::installed::*; -pub use crate::named_index::*; pub use crate::prioritized_distribution::*; pub use crate::resolution::*; pub use crate::resolved::*; @@ -76,9 +76,9 @@ mod error; mod file; mod hash; mod id; +mod index_source; mod index_url; mod installed; -mod named_index; mod prioritized_distribution; mod resolution; mod resolved; diff --git a/crates/distribution-types/src/named_index.rs b/crates/distribution-types/src/named_index.rs deleted file mode 100644 index 15d6870aa9b5..000000000000 --- a/crates/distribution-types/src/named_index.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::IndexUrl; - -#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct IndexSource { - pub name: String, - pub index: IndexUrl, - #[serde(default)] - pub kind: 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, -} diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 862d241c783e..ecbfc3eec2e4 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -115,14 +115,15 @@ impl LoweredRequirement { // in that order. let Some(index) = project_indexes .iter() - .find(|IndexSource { name, .. }| *name == index) + .find(|IndexSource { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) .or_else(|| { - workspace - .indexes() - .iter() - .find(|IndexSource { name, .. }| *name == index) + workspace.indexes().iter().find(|IndexSource { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) }) - .map(|IndexSource { index, .. }| index.clone()) + .map(|IndexSource { url: index, .. }| index.clone()) else { return Err(LoweringError::MissingIndex(requirement.name, index)); }; @@ -246,8 +247,10 @@ impl LoweredRequirement { Source::Registry { index } => { let Some(index) = indexes .iter() - .find(|IndexSource { name, .. }| *name == index) - .map(|IndexSource { index, .. }| index.clone()) + .find(|IndexSource { name, .. }| { + name.as_ref().is_some_and(|name| *name == index) + }) + .map(|IndexSource { url: index, .. }| index.clone()) else { return Err(LoweringError::MissingIndex(requirement.name, index)); }; diff --git a/crates/uv-resolver/src/resolver/indexes.rs b/crates/uv-resolver/src/resolver/indexes.rs index 61cf052fb93e..1a0b94c2b3ec 100644 --- a/crates/uv-resolver/src/resolver/indexes.rs +++ b/crates/uv-resolver/src/resolver/indexes.rs @@ -5,7 +5,19 @@ use pypi_types::RequirementSource; use rustc_hash::FxHashMap; use std::collections::hash_map::Entry; -/// A map of package names to their explicit index across all forks. +/// 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); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 6795c17b29bc..55c3fac3bc83 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; -use distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata, IndexSource}; +use distribution_types::{FlatIndexLocation, IndexSource, IndexUrl, StaticMetadata}; use install_wheel_rs::linker::LinkMode; use pep508_rs::Requirement; use pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; @@ -296,18 +296,39 @@ pub struct ResolverOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverInstallerOptions { - /// The URL of the Python package index (by default: ). + /// 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. /// - /// The index provided by this setting is given lower priority than any indexes specified via + /// 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). + /// + /// 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 front of the list of + /// the list of indexes, such that it is given the highest priority when resolving packages. + /// Additionally, marking an index as default will disable the PyPI default index. #[option( - default = "\"https://pypi.org/simple\"", - value_type = "str", + default = "\"[]\"", + value_type = "dict", example = r#" - index-url = "https://test.pypi.org/simple" + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" "# )] pub index: Option>, @@ -1497,6 +1518,7 @@ pub struct OptionsWire { // #[serde(flatten)] // top_level: ResolverInstallerOptions, + index: Option>, index_url: Option, extra_index_url: Option>, no_index: Option, @@ -1561,6 +1583,7 @@ impl From for Options { concurrent_downloads, concurrent_builds, concurrent_installs, + index, index_url, extra_index_url, no_index, @@ -1614,6 +1637,7 @@ impl From for Options { concurrent_installs, }, top_level: ResolverInstallerOptions { + index, index_url, extra_index_url, no_index, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index af15e4471f07..3ce3a92e9b9a 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -154,12 +154,47 @@ pub struct ToolUv { /// dependencies. pub sources: Option, - /// The indexes. + /// 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). + /// + /// 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 front of the list of + /// the list of indexes, such that it is given the highest 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( @@ -170,6 +205,7 @@ pub struct ToolUv { "# )] pub managed: Option, + /// Whether the project should be considered a Python package, or a non-package ("virtual") /// project. /// @@ -188,6 +224,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( @@ -205,6 +242,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. @@ -229,6 +267,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 @@ -264,6 +303,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 diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index a99bd16a880f..abbadc12abfa 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1637,6 +1637,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1688,6 +1689,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1770,6 +1772,7 @@ mod tests { "workspace": true } }, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -1788,6 +1791,7 @@ mod tests { "workspace": true } }, + "index": null, "workspace": { "members": [ "packages/*" @@ -1870,11 +1874,13 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": null, "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -1928,6 +1934,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2059,6 +2066,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2072,6 +2080,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -2157,6 +2166,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2170,6 +2180,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2269,6 +2280,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2282,6 +2294,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2355,6 +2368,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2368,6 +2382,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 7a6ecce0db46..802521d64d7a 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -11527,7 +11527,8 @@ fn lock_explicit_index() -> Result<()> { [[tool.uv.index]] name = "test" - index = "https://test.pypi.org/simple" + url = "https://test.pypi.org/simple" + explicit = true "#, )?; @@ -11611,42 +11612,212 @@ fn lock_explicit_index() -> Result<()> { ); }); - // Re-run with `--locked`. - uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + 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 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); - // Re-run with `--offline`. We shouldn't need a network connection to validate an - // already-correct lockfile with immutable metadata. - uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + 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 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); - // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" - success: true - exit_code: 0 + 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 ----- - Prepared 4 packages in [TIME] - Installed 4 packages in [TIME] - + anyio==3.7.0 - + idna==3.6 - + project==0.1.0 (from file://[TEMP_DIR]/) - + sniffio==1.3.1 + × 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(()) } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 1c1205c5b83f..d1fd79917b19 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -194,7 +194,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/show_settings.rs b/crates/uv/tests/show_settings.rs index dcad6fac7834..f87645cc07c4 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -92,6 +92,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -234,6 +235,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -377,6 +379,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -552,6 +555,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -696,6 +700,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -826,6 +831,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -993,6 +999,7 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Url( VerbatimUrl { @@ -1160,6 +1167,7 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Url( VerbatimUrl { @@ -1372,6 +1380,7 @@ fn resolve_find_links() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [ @@ -1538,6 +1547,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -1674,6 +1684,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [ Url( @@ -1838,6 +1849,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [ Url( @@ -2026,6 +2038,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2152,6 +2165,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2278,6 +2292,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2406,6 +2421,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2546,6 +2562,7 @@ fn resolve_tool() -> anyhow::Result<()> { ), ), options: ResolverInstallerOptions { + index: None, index_url: None, extra_index_url: None, no_index: None, @@ -2582,6 +2599,7 @@ fn resolve_tool() -> anyhow::Result<()> { }, settings: ResolverInstallerSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2710,6 +2728,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2864,6 +2883,7 @@ fn resolve_both() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -3033,6 +3053,7 @@ fn resolve_config_file() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -3150,7 +3171,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` "### ); @@ -3277,6 +3298,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -3406,6 +3428,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -3543,6 +3566,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 4e05ccb40aa9..7e37b47f5526 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -73,6 +73,50 @@ 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). + +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 front of the list of +the list of indexes, such that it is given the highest 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 @@ -591,6 +635,60 @@ formats described above. --- +### [`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). + +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 front of the list of +the list of indexes, such that it is given the highest 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. diff --git a/uv.schema.json b/uv.schema.json index 645ffc4582fb..18a5cfb7800f 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -147,6 +147,16 @@ "$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).\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 front of the list of the list of indexes, such that it is given the highest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/IndexSource" + } + }, "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.", "anyOf": [ @@ -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" }, + "IndexSource": { + "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 enabled 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": [ {