From deeaf3cfba62b5cfd3fcc10e42bcfe64d6253737 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Dec 2025 21:04:22 -0500 Subject: [PATCH] Error if multiple indexes include default = true --- crates/uv-workspace/src/pyproject.rs | 16 +++++++++-- crates/uv/tests/it/lock.rs | 43 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 438e57816..6beb95282 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -275,10 +275,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>, @@ -286,6 +287,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) { @@ -294,6 +296,14 @@ where ))); } } + if index.default { + if seen_default { + return Err(serde::de::Error::custom( + "found multiple indexes with `default = true`; only one index may be marked as default", + )); + } + seen_default = true; + } } } Ok(indexes) diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 1aff6c3e7..9c5414dd7 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -18384,6 +18384,49 @@ 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(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 8, column 9 + | + 8 | [[tool.uv.index]] + | ^^^^^^^^^^^^^^^^^ + found multiple indexes with `default = true`; only one index may be marked as default + "###); + + 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<()> {