From e6f9d9aad52c29423807ecb93ebeb50f98fac1dd 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 | 31 ++- crates/uv-workspace/src/pyproject.rs | 42 +++- crates/uv-workspace/src/workspace.rs | 15 ++ crates/uv/src/lib.rs | 4 +- crates/uv/tests/lock.rs | 207 ++++++++++++++++-- crates/uv/tests/show_settings.rs | 24 ++ docs/reference/settings.md | 98 +++++++++ uv.schema.json | 43 ++++ 14 files changed, 578 insertions(+), 99 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 5aba8756ecdb..761b80ca94ea 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), } } @@ -401,7 +406,7 @@ impl<'a> IndexLocations { /// Return an iterator over all [`IndexUrl`] entries. pub fn indexes(&'a self) -> impl Iterator + 'a { - self.index().into_iter().chain(self.extra_index()) + self.sources().chain(self.extra_index()).chain(self.index()) } /// Return an iterator over the [`FlatIndexLocation`] entries. @@ -428,17 +433,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, @@ -446,25 +448,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), + ) } } @@ -479,6 +481,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), } } @@ -495,7 +498,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 982ba2f571fa..c18a5c6628c1 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -56,9 +56,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::*; @@ -74,9 +74,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 2f8efad3396b..83813154245d 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)); }; @@ -240,8 +241,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 8aca1d80b3c9..c7a204b65dc8 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -313,18 +313,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>, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 60eeaf4441f0..5e81eced7844 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -149,12 +149,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( @@ -165,6 +200,7 @@ pub struct ToolUv { "# )] pub managed: Option, + /// Whether the project should be considered a Python package, or a non-package ("virtual") /// project. /// @@ -183,6 +219,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( @@ -200,6 +237,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. @@ -224,6 +262,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 @@ -255,6 +294,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 0311f68e9785..fe01fd573105 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1634,6 +1634,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1685,6 +1686,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1767,6 +1769,7 @@ mod tests { "workspace": true } }, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -1785,6 +1788,7 @@ mod tests { "workspace": true } }, + "index": null, "workspace": { "members": [ "packages/*" @@ -1867,11 +1871,13 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": null, "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -1925,6 +1931,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2056,6 +2063,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2069,6 +2077,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -2154,6 +2163,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2167,6 +2177,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2266,6 +2277,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2279,6 +2291,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2352,6 +2365,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2365,6 +2379,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d7fc0e06e364..e44f87e799b9 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -884,7 +884,7 @@ async fn run(cli: Cli) -> Result { ) .collect::>(); - commands::tool_install( + Box::pin(commands::tool_install( args.package, args.editable, args.from, @@ -900,7 +900,7 @@ async fn run(cli: Cli) -> Result { globals.native_tls, cache, printer, - ) + )) .await } Commands::Tool(ToolNamespace { diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index c5bc235d5670..5d6ffba91f1c 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -11529,7 +11529,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 "#, )?; @@ -11613,42 +11614,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/show_settings.rs b/crates/uv/tests/show_settings.rs index 099e9052998a..deee4f534b34 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 { @@ -231,6 +232,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -371,6 +373,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -543,6 +546,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -684,6 +688,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -811,6 +816,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -975,6 +981,7 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Url( VerbatimUrl { @@ -1139,6 +1146,7 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Url( VerbatimUrl { @@ -1348,6 +1356,7 @@ fn resolve_find_links() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [ @@ -1511,6 +1520,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -1644,6 +1654,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [ Url( @@ -1805,6 +1816,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [ Url( @@ -1990,6 +2002,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2113,6 +2126,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2236,6 +2250,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2361,6 +2376,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2498,6 +2514,7 @@ fn resolve_tool() -> anyhow::Result<()> { ), ), options: ResolverInstallerOptions { + index: None, index_url: None, extra_index_url: None, no_index: None, @@ -2533,6 +2550,7 @@ fn resolve_tool() -> anyhow::Result<()> { }, settings: ResolverInstallerSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2658,6 +2676,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -2809,6 +2828,7 @@ fn resolve_both() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -2974,6 +2994,7 @@ fn resolve_config_file() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: Some( Pypi( VerbatimUrl { @@ -3215,6 +3236,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -3341,6 +3363,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { + sources: [], index: None, extra_index: [], flat_index: [], @@ -3475,6 +3498,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 146340a004be..6a3bf6581fad 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 @@ -546,6 +590,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 50d564251079..c5b2edd98e7b 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -138,6 +138,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": [ @@ -517,6 +527,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": [ {