mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 02:22:19 +00:00
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:
parent
211fa91c2a
commit
083ec2f1bf
5 changed files with 154 additions and 13 deletions
|
@ -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 = {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
21
uv.schema.json
generated
|
@ -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": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue