diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index 2afb0d3f2..fe97710d7 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -32,7 +32,7 @@ mailparse = { workspace = true } regex = { workspace = true } rkyv = { workspace = true } schemars = { workspace = true, optional = true } -serde = { workspace = true } +serde = { workspace = true, optional = true } serde-untagged = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } diff --git a/crates/uv-pypi-types/src/dependency_groups.rs b/crates/uv-pypi-types/src/dependency_groups.rs new file mode 100644 index 000000000..4365c2cf2 --- /dev/null +++ b/crates/uv-pypi-types/src/dependency_groups.rs @@ -0,0 +1,148 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::BTreeMap; +use std::str::FromStr; + +use uv_normalize::GroupName; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct DependencyGroups(BTreeMap>); + +impl DependencyGroups { + /// Returns the names of the dependency groups. + pub fn keys(&self) -> impl Iterator { + self.0.keys() + } + + /// Returns the dependency group with the given name. + pub fn get(&self, group: &GroupName) -> Option<&Vec> { + self.0.get(group) + } + + /// Returns `true` if the dependency group is in the list. + pub fn contains_key(&self, group: &GroupName) -> bool { + self.0.contains_key(group) + } + + /// Returns an iterator over the dependency groups. + pub fn iter(&self) -> impl Iterator)> { + self.0.iter() + } +} + +impl<'a> IntoIterator for &'a DependencyGroups { + type Item = (&'a GroupName, &'a Vec); + type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +/// Ensure that all keys in the TOML table are unique. +impl<'de> serde::de::Deserialize<'de> for DependencyGroups { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct GroupVisitor; + + impl<'de> serde::de::Visitor<'de> for GroupVisitor { + type Value = DependencyGroups; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a table with unique dependency group names") + } + + fn visit_map(self, mut access: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut sources = BTreeMap::new(); + while let Some((key, value)) = + access.next_entry::>()? + { + match sources.entry(key) { + std::collections::btree_map::Entry::Occupied(entry) => { + return Err(serde::de::Error::custom(format!( + "duplicate dependency group: `{}`", + entry.key() + ))); + } + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(value); + } + } + } + Ok(DependencyGroups(sources)) + } + } + + deserializer.deserialize_map(GroupVisitor) + } +} + +/// A specifier item in a [PEP 735](https://peps.python.org/pep-0735/) Dependency Group. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum DependencyGroupSpecifier { + /// A PEP 508-compatible requirement string. + Requirement(String), + /// A reference to another dependency group. + IncludeGroup { + /// The name of the group to include. + include_group: GroupName, + }, + /// A Dependency Object Specifier. + Object(BTreeMap), +} + +impl<'de> Deserialize<'de> for DependencyGroupSpecifier { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = DependencyGroupSpecifier; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a map with the `include-group` key") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(DependencyGroupSpecifier::Requirement(value.to_owned())) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut map_data = BTreeMap::new(); + while let Some((key, value)) = map.next_entry()? { + map_data.insert(key, value); + } + + if map_data.is_empty() { + return Err(serde::de::Error::custom("missing field `include-group`")); + } + + if let Some(include_group) = map_data + .get("include-group") + .map(String::as_str) + .map(GroupName::from_str) + .transpose() + .map_err(serde::de::Error::custom)? + { + Ok(DependencyGroupSpecifier::IncludeGroup { include_group }) + } else { + Ok(DependencyGroupSpecifier::Object(map_data)) + } + } + } + + deserializer.deserialize_any(Visitor) + } +} diff --git a/crates/uv-pypi-types/src/lib.rs b/crates/uv-pypi-types/src/lib.rs index 8b7d88985..a3b2b6e05 100644 --- a/crates/uv-pypi-types/src/lib.rs +++ b/crates/uv-pypi-types/src/lib.rs @@ -1,5 +1,6 @@ pub use base_url::*; pub use conflicts::*; +pub use dependency_groups::*; pub use direct_url::*; pub use lenient_requirement::*; pub use marker_environment::*; @@ -12,6 +13,7 @@ pub use supported_environments::*; mod base_url; mod conflicts; +mod dependency_groups; mod direct_url; mod lenient_requirement; mod marker_environment; diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 947c76b63..43f4a53a9 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -25,7 +25,7 @@ uv-normalize = { workspace = true } uv-options-metadata = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } -uv-pypi-types = { workspace = true } +uv-pypi-types = { workspace = true, features = ["serde"] } uv-static = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv-workspace/src/dependency_groups.rs b/crates/uv-workspace/src/dependency_groups.rs index 3597a4049..edb4ef88e 100644 --- a/crates/uv-workspace/src/dependency_groups.rs +++ b/crates/uv-workspace/src/dependency_groups.rs @@ -7,9 +7,7 @@ use tracing::warn; use uv_normalize::{GroupName, DEV_DEPENDENCIES}; use uv_pep508::Pep508Error; -use uv_pypi_types::VerbatimParsedUrl; - -use crate::pyproject::DependencyGroupSpecifier; +use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl}; /// PEP 735 dependency groups, with any `include-group` entries resolved. #[derive(Debug, Default, Clone)] diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index f0d19db5b..41c674c9c 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -27,7 +27,8 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTree; use uv_pypi_types::{ - Conflicts, RequirementSource, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl, + Conflicts, DependencyGroups, RequirementSource, SchemaConflicts, SupportedEnvironments, + VerbatimParsedUrl, }; #[derive(Error, Debug)] @@ -143,73 +144,6 @@ impl AsRef<[u8]> for PyProjectToml { } } -/// A specifier item in a [PEP 735](https://peps.python.org/pep-0735/) Dependency Group. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(test, derive(Serialize))] -pub enum DependencyGroupSpecifier { - /// A PEP 508-compatible requirement string. - Requirement(String), - /// A reference to another dependency group. - IncludeGroup { - /// The name of the group to include. - include_group: GroupName, - }, - /// A Dependency Object Specifier. - Object(BTreeMap), -} - -impl<'de> Deserialize<'de> for DependencyGroupSpecifier { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct Visitor; - - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = DependencyGroupSpecifier; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or a map with the `include-group` key") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - Ok(DependencyGroupSpecifier::Requirement(value.to_owned())) - } - - fn visit_map(self, mut map: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let mut map_data = BTreeMap::new(); - while let Some((key, value)) = map.next_entry()? { - map_data.insert(key, value); - } - - if map_data.is_empty() { - return Err(serde::de::Error::custom("missing field `include-group`")); - } - - if let Some(include_group) = map_data - .get("include-group") - .map(String::as_str) - .map(GroupName::from_str) - .transpose() - .map_err(serde::de::Error::custom)? - { - Ok(DependencyGroupSpecifier::IncludeGroup { include_group }) - } else { - Ok(DependencyGroupSpecifier::Object(map_data)) - } - } - } - - deserializer.deserialize_any(Visitor) - } -} - /// PEP 621 project metadata (`project`). /// /// See . @@ -797,84 +731,6 @@ impl Deref for SerdePattern { } } -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr(test, derive(Serialize))] -pub struct DependencyGroups(BTreeMap>); - -impl DependencyGroups { - /// Returns the names of the dependency groups. - pub fn keys(&self) -> impl Iterator { - self.0.keys() - } - - /// Returns the dependency group with the given name. - pub fn get(&self, group: &GroupName) -> Option<&Vec> { - self.0.get(group) - } - - /// Returns `true` if the dependency group is in the list. - pub fn contains_key(&self, group: &GroupName) -> bool { - self.0.contains_key(group) - } - - /// Returns an iterator over the dependency groups. - pub fn iter(&self) -> impl Iterator)> { - self.0.iter() - } -} - -impl<'a> IntoIterator for &'a DependencyGroups { - type Item = (&'a GroupName, &'a Vec); - type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -/// Ensure that all keys in the TOML table are unique. -impl<'de> serde::de::Deserialize<'de> for DependencyGroups { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct GroupVisitor; - - impl<'de> serde::de::Visitor<'de> for GroupVisitor { - type Value = DependencyGroups; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a table with unique dependency group names") - } - - fn visit_map(self, mut access: M) -> Result - where - M: serde::de::MapAccess<'de>, - { - let mut sources = BTreeMap::new(); - while let Some((key, value)) = - access.next_entry::>()? - { - match sources.entry(key) { - std::collections::btree_map::Entry::Occupied(entry) => { - return Err(serde::de::Error::custom(format!( - "duplicate dependency group: `{}`", - entry.key() - ))); - } - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(value); - } - } - } - Ok(DependencyGroups(sources)) - } - } - - deserializer.deserialize_map(GroupVisitor) - } -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case", try_from = "SourcesWire")] diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index e48c0df88..aca1a593c 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1500,8 +1500,9 @@ mod tests { use insta::{assert_json_snapshot, assert_snapshot}; use uv_normalize::GroupName; + use uv_pypi_types::DependencyGroupSpecifier; - use crate::pyproject::{DependencyGroupSpecifier, PyProjectToml}; + use crate::pyproject::PyProjectToml; use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; use crate::WorkspaceError; diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs index 1db8ad2fe..236df5cd9 100644 --- a/crates/uv/src/commands/project/install_target.rs +++ b/crates/uv/src/commands/project/install_target.rs @@ -8,10 +8,10 @@ use rustc_hash::FxHashSet; use uv_configuration::{DependencyGroupsWithDefaults, ExtrasSpecification}; use uv_distribution_types::Index; use uv_normalize::PackageName; -use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl}; +use uv_pypi_types::{DependencyGroupSpecifier, LenientRequirement, VerbatimParsedUrl}; use uv_resolver::{Installable, Lock, Package}; use uv_scripts::Pep723Script; -use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; +use uv_workspace::pyproject::{Source, Sources, ToolUvSources}; use uv_workspace::Workspace; use crate::commands::project::ProjectError;