Error when tool.uv.sources contains duplicate package names (#7383)

## Summary

Closes https://github.com/astral-sh/uv/issues/7229.
This commit is contained in:
Charlie Marsh 2024-09-13 23:37:23 -04:00 committed by GitHub
parent 211fa91c2a
commit 083ec2f1bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 154 additions and 13 deletions

View file

@ -1,12 +1,12 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use uv_configuration::SourceStrategy;
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::metadata::{LoweredRequirement, MetadataError}; use crate::metadata::{LoweredRequirement, MetadataError};
use crate::Metadata; use crate::Metadata;
use uv_configuration::SourceStrategy;
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
use uv_workspace::pyproject::ToolUvSources;
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RequiresDist { pub struct RequiresDist {
@ -71,6 +71,7 @@ impl RequiresDist {
.as_ref() .as_ref()
.and_then(|tool| tool.uv.as_ref()) .and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref()) .and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner)
.unwrap_or(&empty); .unwrap_or(&empty);
let dev_dependencies = { let dev_dependencies = {

View file

@ -109,7 +109,7 @@ pub struct Tool {
pub struct ToolUv { pub struct ToolUv {
/// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving /// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving
/// dependencies. /// dependencies.
pub sources: Option<BTreeMap<PackageName, Source>>, pub sources: Option<ToolUvSources>,
/// The workspace definition for the project, if any. /// The workspace definition for the project, if any.
#[option_group] #[option_group]
pub workspace: Option<ToolUvWorkspace>, pub workspace: Option<ToolUvWorkspace>,
@ -245,6 +245,65 @@ pub struct ToolUv {
pub constraint_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>, pub constraint_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
} }
#[derive(Serialize, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct ToolUvSources(BTreeMap<PackageName, Source>);
impl ToolUvSources {
/// Returns the underlying `BTreeMap` of package names to sources.
pub fn inner(&self) -> &BTreeMap<PackageName, Source> {
&self.0
}
/// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
#[must_use]
pub fn into_inner(self) -> BTreeMap<PackageName, Source> {
self.0
}
}
/// Ensure that all keys in the TOML table are unique.
impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct SourcesVisitor;
impl<'de> serde::de::Visitor<'de> for SourcesVisitor {
type Value = ToolUvSources;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map with unique keys")
}
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut sources = BTreeMap::new();
while let Some((key, value)) = access.next_entry::<PackageName, Source>()? {
match sources.entry(key) {
std::collections::btree_map::Entry::Occupied(entry) => {
return Err(serde::de::Error::custom(format!(
"duplicate sources for package `{}`",
entry.key()
)));
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(ToolUvSources(sources))
}
}
deserializer.deserialize_map(SourcesVisitor)
}
}
#[derive(Serialize, Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]

View file

@ -14,7 +14,7 @@ use uv_fs::{Simplified, CWD};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace}; use crate::pyproject::{Project, PyProjectToml, Source, ToolUvSources, ToolUvWorkspace};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum WorkspaceError { pub enum WorkspaceError {
@ -234,6 +234,7 @@ impl Workspace {
.clone() .clone()
.and_then(|tool| tool.uv) .and_then(|tool| tool.uv)
.and_then(|uv| uv.sources) .and_then(|uv| uv.sources)
.map(ToolUvSources::into_inner)
.unwrap_or_default(); .unwrap_or_default();
// Set the `pyproject.toml` for the member. // Set the `pyproject.toml` for the member.
@ -741,6 +742,7 @@ impl Workspace {
.clone() .clone()
.and_then(|tool| tool.uv) .and_then(|tool| tool.uv)
.and_then(|uv| uv.sources) .and_then(|uv| uv.sources)
.map(ToolUvSources::into_inner)
.unwrap_or_default(); .unwrap_or_default();
Ok(Workspace { Ok(Workspace {

View file

@ -12628,3 +12628,75 @@ fn lock_request_requires_python() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn lock_duplicate_sources() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "projeect"
version = "0.1.0"
dependencies = ["python-multipart"]
[tool.uv.sources]
python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" }
python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" }
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: Failed to parse `pyproject.toml` during settings discovery:
TOML parse error at line 9, column 9
|
9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" }
| ^
duplicate key `python-multipart` in table `tool.uv.sources`
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 9, column 9
|
9 | python-multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" }
| ^
duplicate key `python-multipart` in table `tool.uv.sources`
"###);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
dependencies = ["python-multipart"]
[tool.uv.sources]
python-multipart = { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl" }
python_multipart = { url = "https://files.pythonhosted.org/packages/c0/3e/9fbfd74e7f5b54f653f7ca99d44ceb56e718846920162165061c4c22b71a/python_multipart-0.0.8-py3-none-any.whl" }
"#,
)?;
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 7, column 9
|
7 | [tool.uv.sources]
| ^^^^^^^^^^^^^^^^^
duplicate sources for package `python-multipart`
"###);
Ok(())
}

21
uv.schema.json generated
View file

@ -372,13 +372,14 @@
}, },
"sources": { "sources": {
"description": "The sources to use (e.g., workspace members, Git repositories, local paths) when resolving dependencies.", "description": "The sources to use (e.g., workspace members, Git repositories, local paths) when resolving dependencies.",
"type": [ "anyOf": [
"object", {
"null" "$ref": "#/definitions/ToolUvSources"
], },
"additionalProperties": { {
"$ref": "#/definitions/Source" "type": "null"
} }
]
}, },
"upgrade": { "upgrade": {
"description": "Allow package upgrades, ignoring pinned versions in any existing output file.", "description": "Allow package upgrades, ignoring pinned versions in any existing output file.",
@ -1473,6 +1474,12 @@
} }
] ]
}, },
"ToolUvSources": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Source"
}
},
"ToolUvWorkspace": { "ToolUvWorkspace": {
"type": "object", "type": "object",
"properties": { "properties": {