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 }
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 }

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 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;

View File

@ -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 }

View File

@ -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)]

View File

@ -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<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`).
///
/// 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)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]

View File

@ -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;

View File

@ -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;