Move `DependencyGroups` to uv-pypi-types so it can be imported there (#12037)

This PR is in support of #12005, where we need to import
`DependencyGroups` in the `uv-pypi-types` crate without a circular
dependency on `uv-workspace`.
This commit is contained in:
John Mumm 2025-03-07 12:30:47 +01:00 committed by GitHub
parent 7a56aef7d1
commit 1ab1945dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 154 deletions

View File

@ -32,7 +32,7 @@ mailparse = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
rkyv = { workspace = true } rkyv = { workspace = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true, optional = true }
serde-untagged = { workspace = true } serde-untagged = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true } toml = { workspace = true }

View File

@ -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<GroupName, Vec<DependencyGroupSpecifier>>);
impl DependencyGroups {
/// Returns the names of the dependency groups.
pub fn keys(&self) -> impl Iterator<Item = &GroupName> {
self.0.keys()
}
/// Returns the dependency group with the given name.
pub fn get(&self, group: &GroupName) -> Option<&Vec<DependencyGroupSpecifier>> {
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<Item = (&GroupName, &Vec<DependencyGroupSpecifier>)> {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a DependencyGroups {
type Item = (&'a GroupName, &'a Vec<DependencyGroupSpecifier>);
type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec<DependencyGroupSpecifier>>;
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<D>(deserializer: D) -> Result<Self, D::Error>
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<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::<GroupName, Vec<DependencyGroupSpecifier>>()?
{
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<String, String>),
}
impl<'de> Deserialize<'de> for DependencyGroupSpecifier {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(DependencyGroupSpecifier::Requirement(value.to_owned()))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
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)
}
}

View File

@ -1,5 +1,6 @@
pub use base_url::*; pub use base_url::*;
pub use conflicts::*; pub use conflicts::*;
pub use dependency_groups::*;
pub use direct_url::*; pub use direct_url::*;
pub use lenient_requirement::*; pub use lenient_requirement::*;
pub use marker_environment::*; pub use marker_environment::*;
@ -12,6 +13,7 @@ pub use supported_environments::*;
mod base_url; mod base_url;
mod conflicts; mod conflicts;
mod dependency_groups;
mod direct_url; mod direct_url;
mod lenient_requirement; mod lenient_requirement;
mod marker_environment; mod marker_environment;

View File

@ -25,7 +25,7 @@ uv-normalize = { workspace = true }
uv-options-metadata = { workspace = true } uv-options-metadata = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true, features = ["serde"] }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }

View File

@ -7,9 +7,7 @@ use tracing::warn;
use uv_normalize::{GroupName, DEV_DEPENDENCIES}; use uv_normalize::{GroupName, DEV_DEPENDENCIES};
use uv_pep508::Pep508Error; use uv_pep508::Pep508Error;
use uv_pypi_types::VerbatimParsedUrl; use uv_pypi_types::{DependencyGroupSpecifier, VerbatimParsedUrl};
use crate::pyproject::DependencyGroupSpecifier;
/// PEP 735 dependency groups, with any `include-group` entries resolved. /// PEP 735 dependency groups, with any `include-group` entries resolved.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]

View File

@ -27,7 +27,8 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{Version, VersionSpecifiers}; use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::MarkerTree; use uv_pep508::MarkerTree;
use uv_pypi_types::{ use uv_pypi_types::{
Conflicts, RequirementSource, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl, Conflicts, DependencyGroups, RequirementSource, SchemaConflicts, SupportedEnvironments,
VerbatimParsedUrl,
}; };
#[derive(Error, Debug)] #[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<String, String>),
}
impl<'de> Deserialize<'de> for DependencyGroupSpecifier {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(DependencyGroupSpecifier::Requirement(value.to_owned()))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
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`). /// PEP 621 project metadata (`project`).
/// ///
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>. /// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
@ -797,84 +731,6 @@ impl Deref for SerdePattern {
} }
} }
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(Serialize))]
pub struct DependencyGroups(BTreeMap<GroupName, Vec<DependencyGroupSpecifier>>);
impl DependencyGroups {
/// Returns the names of the dependency groups.
pub fn keys(&self) -> impl Iterator<Item = &GroupName> {
self.0.keys()
}
/// Returns the dependency group with the given name.
pub fn get(&self, group: &GroupName) -> Option<&Vec<DependencyGroupSpecifier>> {
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<Item = (&GroupName, &Vec<DependencyGroupSpecifier>)> {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a DependencyGroups {
type Item = (&'a GroupName, &'a Vec<DependencyGroupSpecifier>);
type IntoIter = std::collections::btree_map::Iter<'a, GroupName, Vec<DependencyGroupSpecifier>>;
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<D>(deserializer: D) -> Result<Self, D::Error>
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<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::<GroupName, Vec<DependencyGroupSpecifier>>()?
{
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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", try_from = "SourcesWire")] #[serde(rename_all = "kebab-case", try_from = "SourcesWire")]

View File

@ -1500,8 +1500,9 @@ mod tests {
use insta::{assert_json_snapshot, assert_snapshot}; use insta::{assert_json_snapshot, assert_snapshot};
use uv_normalize::GroupName; 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::workspace::{DiscoveryOptions, ProjectWorkspace};
use crate::WorkspaceError; use crate::WorkspaceError;

View File

@ -8,10 +8,10 @@ use rustc_hash::FxHashSet;
use uv_configuration::{DependencyGroupsWithDefaults, ExtrasSpecification}; use uv_configuration::{DependencyGroupsWithDefaults, ExtrasSpecification};
use uv_distribution_types::Index; use uv_distribution_types::Index;
use uv_normalize::PackageName; 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_resolver::{Installable, Lock, Package};
use uv_scripts::Pep723Script; use uv_scripts::Pep723Script;
use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; use uv_workspace::pyproject::{Source, Sources, ToolUvSources};
use uv_workspace::Workspace; use uv_workspace::Workspace;
use crate::commands::project::ProjectError; use crate::commands::project::ProjectError;