diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 9caf24ec021af..0de4751330013 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -35,6 +35,7 @@ use uv_pypi_types::{ Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl, }; use uv_redacted::DisplaySafeUrl; +use uv_warnings::warn_user_once; #[derive(Error, Debug)] pub enum PyprojectTomlError { @@ -276,10 +277,11 @@ pub struct Tool { pub uv: Option, } -/// Validates that index names in the `tool.uv.index` field are unique. +/// Validates the `tool.uv.index` field. /// -/// This custom deserializer function checks for duplicate index names -/// and returns an error if any duplicates are found. +/// This custom deserializer function checks for: +/// - Duplicate index names +/// - Multiple indexes marked as default fn deserialize_index_vec<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, @@ -287,6 +289,7 @@ where let indexes = Option::>::deserialize(deserializer)?; if let Some(indexes) = indexes.as_ref() { let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher); + let mut seen_default = false; for index in indexes { if let Some(name) = index.name.as_ref() { if !seen_names.insert(name) { @@ -295,6 +298,16 @@ where ))); } } + if index.default { + if seen_default { + warn_user_once!( + "Found multiple indexes with `default = true`; \ + only one index may be marked as default. \ + This will become an error in the future.", + ); + } + seen_default = true; + } } } Ok(indexes) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 8fff3c72588e4..aa46e8c1adadb 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -19250,6 +19250,45 @@ fn lock_repeat_named_index() -> Result<()> { Ok(()) } +/// If multiple indexes are marked as default within a single file, we should raise an error. +#[test] +fn lock_multiple_default_indexes() -> 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 = "first" + url = "https://pypi.org/simple" + default = true + + [[tool.uv.index]] + name = "second" + url = "https://example.com" + default = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Found multiple indexes with `default = true`; only one index may be marked as default. This will become an error in the future. + Resolved 2 packages in [TIME] + "); + + Ok(()) +} + /// If a name is defined in both the workspace root and the member, prefer the index in the member. #[test] fn lock_repeat_named_index_member() -> Result<()> { @@ -33274,6 +33313,7 @@ fn lock_check_multiple_default_indexes_explicit_assignment_dependency_group() -> ----- stdout ----- ----- stderr ----- + warning: Found multiple indexes with `default = true`; only one index may be marked as default. This will become an error in the future. Resolved 2 packages in [TIME] "); @@ -33324,6 +33364,7 @@ fn lock_check_multiple_default_indexes_explicit_assignment_dependency_group() -> ----- stdout ----- ----- stderr ----- + warning: Found multiple indexes with `default = true`; only one index may be marked as default. This will become an error in the future. Resolved 2 packages in [TIME] ");