From 06943ca870e7bd0539fcc8c5453c0a01fb30df07 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 13 Nov 2024 12:47:38 -0500 Subject: [PATCH] uv-pypi-types: make room for group names in addition to extras This adds support for providing conflicting group names in addition to extra names to `Conflicts`. This merely makes "room" for it in the types while keeping everything working. We'll add proper support for it in the next commit. Note that one interesting trick we do here is depend directly on `hashbrown` so that we can make use of its `Equivalent` trait. This in turn lets us use things like `ConflictItemRef` as a lookup key for a hashset that contains `ConflictItem`. This mirrors using a `&str` as a lookup key for a hashset that contains `String`, but works for arbitrary types. `std` doesn't support this, but `hashbrown` does. This trick in turn lets us simplify some of our data structures. This also rejiggers some of the serde-interaction with the conflicting types. We now use a wire type to represent our conflicting items for more flexibility. i.e., Support `extra` XOR `group` fields. --- Cargo.lock | 25 +- Cargo.toml | 1 + crates/uv-pypi-types/Cargo.toml | 1 + crates/uv-pypi-types/src/conflicts.rs | 264 ++++++++++++++++-- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/lib.rs | 7 + crates/uv-resolver/src/lock/mod.rs | 19 +- .../uv-resolver/src/resolver/environment.rs | 22 +- crates/uv-resolver/src/resolver/mod.rs | 24 +- crates/uv/src/commands/project/mod.rs | 9 +- crates/uv/src/commands/project/sync.rs | 5 +- 11 files changed, 315 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00f97da23..c2f109f23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" + [[package]] name = "anes" version = "0.1.6" @@ -1117,6 +1123,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -1386,9 +1398,14 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -1734,7 +1751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", "serde", ] @@ -5189,6 +5206,7 @@ name = "uv-pypi-types" version = "0.0.1" dependencies = [ "anyhow", + "hashbrown 0.15.1", "indexmap", "itertools 0.13.0", "jiff", @@ -5339,6 +5357,7 @@ dependencies = [ "dashmap", "either", "futures", + "hashbrown 0.15.1", "indexmap", "insta", "itertools 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index ff333acaa..82f490c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,7 @@ glob = { version = "0.3.1" } globset = { version = "0.4.15" } globwalk = { version = "0.9.1" } goblin = { version = "0.9.0", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } +hashbrown = { version = "0.15.1" } hex = { version = "0.4.3" } home = { version = "0.5.9" } html-escape = { version = "0.2.13" } diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index 6e4701174..6ee41883c 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -23,6 +23,7 @@ uv-normalize = { workspace = true, features = ["schemars"] } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } +hashbrown = { workspace = true } indexmap = { workspace = true, features = ["serde"] } itertools = { workspace = true } jiff = { workspace = true, features = ["serde"] } diff --git a/crates/uv-pypi-types/src/conflicts.rs b/crates/uv-pypi-types/src/conflicts.rs index 2fdd9e41c..e3c1942f5 100644 --- a/crates/uv-pypi-types/src/conflicts.rs +++ b/crates/uv-pypi-types/src/conflicts.rs @@ -1,4 +1,4 @@ -use uv_normalize::{ExtraName, PackageName}; +use uv_normalize::{ExtraName, GroupName, PackageName}; /// A list of conflicting sets of extras/groups pre-defined by an end user. /// @@ -78,7 +78,7 @@ impl ConflictSet { /// extra name pair. pub fn contains(&self, package: &PackageName, extra: &ExtraName) -> bool { self.iter() - .any(|set| set.package() == package && set.extra() == extra) + .any(|set| set.package() == package && set.extra() == Some(extra)) } } @@ -111,7 +111,6 @@ impl TryFrom> for ConflictSet { /// package. #[derive( Debug, - Default, Clone, Eq, Hash, @@ -122,9 +121,14 @@ impl TryFrom> for ConflictSet { serde::Serialize, schemars::JsonSchema, )] +#[serde( + deny_unknown_fields, + try_from = "ConflictItemWire", + into = "ConflictItemWire" +)] pub struct ConflictItem { package: PackageName, - extra: ExtraName, + conflict: ConflictPackage, } impl ConflictItem { @@ -133,23 +137,43 @@ impl ConflictItem { &self.package } + /// Returns the package-specific conflict. + /// + /// i.e., Either an extra or a group name. + pub fn conflict(&self) -> &ConflictPackage { + &self.conflict + } + /// Returns the extra name of this conflicting item. - pub fn extra(&self) -> &ExtraName { - &self.extra + pub fn extra(&self) -> Option<&ExtraName> { + self.conflict.extra() + } + + /// Returns the group name of this conflicting item. + pub fn group(&self) -> Option<&GroupName> { + self.conflict.group() } /// Returns this item as a new type with its fields borrowed. pub fn as_ref(&self) -> ConflictItemRef<'_> { ConflictItemRef { package: self.package(), - extra: self.extra(), + conflict: self.conflict.as_ref(), } } } impl From<(PackageName, ExtraName)> for ConflictItem { fn from((package, extra): (PackageName, ExtraName)) -> ConflictItem { - ConflictItem { package, extra } + let conflict = ConflictPackage::Extra(extra); + ConflictItem { package, conflict } + } +} + +impl From<(PackageName, GroupName)> for ConflictItem { + fn from((package, group): (PackageName, GroupName)) -> ConflictItem { + let conflict = ConflictPackage::Group(group); + ConflictItem { package, conflict } } } @@ -160,7 +184,7 @@ impl From<(PackageName, ExtraName)> for ConflictItem { #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ConflictItemRef<'a> { package: &'a PackageName, - extra: &'a ExtraName, + conflict: ConflictPackageRef<'a>, } impl<'a> ConflictItemRef<'a> { @@ -169,23 +193,129 @@ impl<'a> ConflictItemRef<'a> { self.package } + /// Returns the package-specific conflict. + /// + /// i.e., Either an extra or a group name. + pub fn conflict(&self) -> ConflictPackageRef<'a> { + self.conflict + } + /// Returns the extra name of this conflicting item. - pub fn extra(&self) -> &'a ExtraName { - self.extra + pub fn extra(&self) -> Option<&'a ExtraName> { + self.conflict.extra() + } + + /// Returns the group name of this conflicting item. + pub fn group(&self) -> Option<&'a GroupName> { + self.conflict.group() } /// Converts this borrowed conflicting item to its owned variant. pub fn to_owned(&self) -> ConflictItem { ConflictItem { package: self.package().clone(), - extra: self.extra().clone(), + conflict: self.conflict.to_owned(), } } } impl<'a> From<(&'a PackageName, &'a ExtraName)> for ConflictItemRef<'a> { fn from((package, extra): (&'a PackageName, &'a ExtraName)) -> ConflictItemRef<'a> { - ConflictItemRef { package, extra } + let conflict = ConflictPackageRef::Extra(extra); + ConflictItemRef { package, conflict } + } +} + +impl<'a> From<(&'a PackageName, &'a GroupName)> for ConflictItemRef<'a> { + fn from((package, group): (&'a PackageName, &'a GroupName)) -> ConflictItemRef<'a> { + let conflict = ConflictPackageRef::Group(group); + ConflictItemRef { package, conflict } + } +} + +impl<'a> hashbrown::Equivalent for ConflictItemRef<'a> { + fn equivalent(&self, key: &ConflictItem) -> bool { + key.as_ref() == *self + } +} + +/// The actual conflicting data for a package. +/// +/// That is, either an extra or a group name. +#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord, schemars::JsonSchema)] +pub enum ConflictPackage { + Extra(ExtraName), + Group(GroupName), +} + +impl ConflictPackage { + /// If this conflict corresponds to an extra, then return the + /// extra name. + pub fn extra(&self) -> Option<&ExtraName> { + match *self { + ConflictPackage::Extra(ref extra) => Some(extra), + ConflictPackage::Group(_) => None, + } + } + + /// If this conflict corresponds to a group, then return the + /// group name. + pub fn group(&self) -> Option<&GroupName> { + match *self { + ConflictPackage::Group(ref group) => Some(group), + ConflictPackage::Extra(_) => None, + } + } + + /// Returns this conflict as a new type with its fields borrowed. + pub fn as_ref(&self) -> ConflictPackageRef<'_> { + match *self { + ConflictPackage::Extra(ref extra) => ConflictPackageRef::Extra(extra), + ConflictPackage::Group(ref group) => ConflictPackageRef::Group(group), + } + } +} + +/// The actual conflicting data for a package, by reference. +/// +/// That is, either a borrowed extra name or a borrowed group name. +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum ConflictPackageRef<'a> { + Extra(&'a ExtraName), + Group(&'a GroupName), +} + +impl<'a> ConflictPackageRef<'a> { + /// If this conflict corresponds to an extra, then return the + /// extra name. + pub fn extra(&self) -> Option<&'a ExtraName> { + match *self { + ConflictPackageRef::Extra(extra) => Some(extra), + ConflictPackageRef::Group(_) => None, + } + } + + /// If this conflict corresponds to a group, then return the + /// group name. + pub fn group(&self) -> Option<&'a GroupName> { + match *self { + ConflictPackageRef::Group(group) => Some(group), + ConflictPackageRef::Extra(_) => None, + } + } + + /// Converts this borrowed conflict to its owned variant. + pub fn to_owned(&self) -> ConflictPackage { + match *self { + ConflictPackageRef::Extra(extra) => ConflictPackage::Extra(extra.clone()), + ConflictPackageRef::Group(group) => ConflictPackage::Group(group.clone()), + } + } +} + +impl<'a> hashbrown::Equivalent for ConflictPackageRef<'a> { + fn equivalent(&self, key: &ConflictPackage) -> bool { + key.as_ref() == *self } } @@ -198,6 +328,19 @@ pub enum ConflictError { /// An error for when there is one conflicting items. #[error("Each set of conflicts must have at least two entries, but found only one")] OneItem, + /// An error that occurs when the `package` field is missing. + /// + /// (This is only applicable when deserializing from the lock file. + /// When deserializing from `pyproject.toml`, the `package` field is + /// optional.) + #[error("Expected `package` field in conflicting entry")] + MissingPackage, + /// An error that occurs when both `extra` and `group` are missing. + #[error("Expected `extra` or `group` field in conflicting entry")] + MissingExtraAndGroup, + /// An error that occurs when both `extra` and `group` are present. + #[error("Expected one of `extra` or `group` in conflicting entry, but found both")] + FoundExtraAndGroup, } /// Like [`Conflicts`], but for deserialization in `pyproject.toml`. @@ -228,7 +371,10 @@ impl SchemaConflicts { let mut set = vec![]; for item in &tool_uv_set.0 { let package = item.package.clone().unwrap_or_else(|| package.clone()); - set.push(ConflictItem::from((package, item.extra.clone()))); + set.push(ConflictItem { + package: package.clone(), + conflict: item.conflict.clone(), + }); } // OK because we guarantee that // `SchemaConflictingGroupList` is valid and there aren't @@ -257,7 +403,6 @@ pub struct SchemaConflictSet(Vec); /// name. #[derive( Debug, - Default, Clone, Eq, Hash, @@ -268,11 +413,14 @@ pub struct SchemaConflictSet(Vec); serde::Serialize, schemars::JsonSchema, )] -#[serde(deny_unknown_fields)] +#[serde( + deny_unknown_fields, + try_from = "ConflictItemWire", + into = "ConflictItemWire" +)] pub struct SchemaConflictItem { - #[serde(default)] package: Option, - extra: ExtraName, + conflict: ConflictPackage, } impl<'de> serde::Deserialize<'de> for SchemaConflictSet { @@ -297,3 +445,83 @@ impl TryFrom> for SchemaConflictSet { Ok(SchemaConflictSet(items)) } } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ConflictItemWire { + #[serde(default)] + package: Option, + #[serde(default)] + extra: Option, + #[serde(default)] + group: Option, +} + +impl TryFrom for ConflictItem { + type Error = ConflictError; + + fn try_from(wire: ConflictItemWire) -> Result { + let Some(package) = wire.package else { + return Err(ConflictError::MissingPackage); + }; + match (wire.extra, wire.group) { + (None, None) => Err(ConflictError::MissingExtraAndGroup), + (Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup), + (Some(extra), None) => Ok(ConflictItem::from((package, extra))), + (None, Some(group)) => Ok(ConflictItem::from((package, group))), + } + } +} + +impl From for ConflictItemWire { + fn from(item: ConflictItem) -> ConflictItemWire { + match item.conflict { + ConflictPackage::Extra(extra) => ConflictItemWire { + package: Some(item.package), + extra: Some(extra), + group: None, + }, + ConflictPackage::Group(group) => ConflictItemWire { + package: Some(item.package), + extra: None, + group: Some(group), + }, + } + } +} + +impl TryFrom for SchemaConflictItem { + type Error = ConflictError; + + fn try_from(wire: ConflictItemWire) -> Result { + let package = wire.package; + match (wire.extra, wire.group) { + (None, None) => Err(ConflictError::MissingExtraAndGroup), + (Some(_), Some(_)) => Err(ConflictError::FoundExtraAndGroup), + (Some(extra), None) => Ok(SchemaConflictItem { + package, + conflict: ConflictPackage::Extra(extra), + }), + (None, Some(group)) => Ok(SchemaConflictItem { + package, + conflict: ConflictPackage::Group(group), + }), + } + } +} + +impl From for ConflictItemWire { + fn from(item: SchemaConflictItem) -> ConflictItemWire { + match item.conflict { + ConflictPackage::Extra(extra) => ConflictItemWire { + package: item.package, + extra: Some(extra), + group: None, + }, + ConflictPackage::Group(group) => ConflictItemWire { + package: item.package, + extra: None, + group: Some(group), + }, + } + } +} diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 6a881fa9b..6c9381e67 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -43,6 +43,7 @@ clap = { workspace = true, features = ["derive"], optional = true } dashmap = { workspace = true } either = { workspace = true } futures = { workspace = true } +hashbrown = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } jiff = { workspace = true, features = ["serde"] } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index b06cc74c9..d0f3d00c0 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -25,6 +25,13 @@ pub use resolver::{ pub use version_map::VersionMap; pub use yanks::AllowedYanks; +/// A custom `HashSet` using `hashbrown`. +/// +/// We use `hashbrown` instead of `std` to get access to its `Equivalent` +/// trait. This lets use store things like `ConflictItem`, but refer to it via +/// `ConflictItemRef`. i.e., We can avoid allocs on lookups. +type FxHashbrownSet = hashbrown::HashSet; + mod bare; mod candidate_selector; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 9bd33d96a..9dceeb6e9 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -40,8 +40,8 @@ use uv_pep440::Version; use uv_pep508::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; use uv_platform_tags::{TagCompatibility, TagPriority, Tags}; use uv_pypi_types::{ - redact_credentials, Conflicts, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, - RequirementSource, + redact_credentials, ConflictPackage, Conflicts, HashDigest, ParsedArchiveUrl, ParsedGitUrl, + Requirement, RequirementSource, }; use uv_types::{BuildContext, HashStrategy}; use uv_workspace::dependency_groups::DependencyGroupError; @@ -634,11 +634,18 @@ impl Lock { if !self.conflicts.is_empty() { let mut list = Array::new(); - for groups in self.conflicts.iter() { - list.push(each_element_on_its_line_array(groups.iter().map(|group| { + for set in self.conflicts.iter() { + list.push(each_element_on_its_line_array(set.iter().map(|item| { let mut table = InlineTable::new(); - table.insert("package", Value::from(group.package().to_string())); - table.insert("extra", Value::from(group.extra().to_string())); + table.insert("package", Value::from(item.package().to_string())); + match item.conflict() { + ConflictPackage::Extra(ref extra) => { + table.insert("extra", Value::from(extra.to_string())); + } + ConflictPackage::Group(ref group) => { + table.insert("group", Value::from(group.to_string())); + } + } table }))); } diff --git a/crates/uv-resolver/src/resolver/environment.rs b/crates/uv-resolver/src/resolver/environment.rs index 0c6fb0f71..91fa9d274 100644 --- a/crates/uv-resolver/src/resolver/environment.rs +++ b/crates/uv-resolver/src/resolver/environment.rs @@ -1,7 +1,5 @@ use std::sync::Arc; -use rustc_hash::{FxHashMap, FxHashSet}; -use uv_normalize::{ExtraName, PackageName}; use uv_pep508::{MarkerEnvironment, MarkerTree}; use uv_pypi_types::{ConflictItem, ConflictItemRef, ResolverMarkerEnvironment}; @@ -97,7 +95,7 @@ enum Kind { /// The markers associated with this resolver fork. markers: MarkerTree, /// Conflicting group exclusions. - exclude: Arc>>, + exclude: Arc>, }, } @@ -135,7 +133,7 @@ impl ResolverEnvironment { let kind = Kind::Universal { initial_forks: initial_forks.into(), markers: MarkerTree::TRUE, - exclude: Arc::new(FxHashMap::default()), + exclude: Arc::new(crate::FxHashbrownSet::default()), }; ResolverEnvironment { kind } } @@ -166,10 +164,7 @@ impl ResolverEnvironment { pub(crate) fn included_by_group(&self, group: ConflictItemRef<'_>) -> bool { match self.kind { Kind::Specific { .. } => true, - Kind::Universal { ref exclude, .. } => !exclude - .get(group.package()) - .map(|set| set.contains(group.extra())) - .unwrap_or(false), + Kind::Universal { ref exclude, .. } => !exclude.contains(&group), } } @@ -227,7 +222,7 @@ impl ResolverEnvironment { /// specific marker environment. i.e., "pip"-style resolution. pub(crate) fn exclude_by_group( &self, - groups: impl IntoIterator, + items: impl IntoIterator, ) -> ResolverEnvironment { match self.kind { Kind::Specific { .. } => { @@ -238,12 +233,9 @@ impl ResolverEnvironment { ref markers, ref exclude, } => { - let mut exclude: FxHashMap<_, _> = (**exclude).clone(); - for group in groups { - exclude - .entry(group.package().clone()) - .or_default() - .insert(group.extra().clone()); + let mut exclude: crate::FxHashbrownSet<_> = (**exclude).clone(); + for item in items { + exclude.insert(item); } let kind = Kind::Universal { initial_forks: Arc::clone(initial_forks), diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index b3b51d742..4a20dbcc6 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2950,7 +2950,7 @@ struct Fork { /// This exists to make some access patterns more efficient. Namely, /// it makes it easy to check whether there's a dependency with a /// particular conflicting group in this fork. - conflicts: FxHashMap>, + conflicts: crate::FxHashbrownSet, /// The resolver environment for this fork. /// /// Principally, this corresponds to the markers in this for. So in the @@ -2971,7 +2971,7 @@ impl Fork { fn new(env: ResolverEnvironment) -> Fork { Fork { dependencies: vec![], - conflicts: FxHashMap::default(), + conflicts: crate::FxHashbrownSet::default(), env, } } @@ -2979,10 +2979,7 @@ impl Fork { /// Add a dependency to this fork. fn add_dependency(&mut self, dep: PubGrubDependency) { if let Some(conflicting_item) = dep.package.conflicting_item() { - self.conflicts - .entry(conflicting_item.package().clone()) - .or_default() - .insert(conflicting_item.extra().clone()); + self.conflicts.insert(conflicting_item.to_owned()); } self.dependencies.push(dep); } @@ -3001,9 +2998,7 @@ impl Fork { return true; } if let Some(conflicting_item) = dep.package.conflicting_item() { - if let Some(set) = self.conflicts.get_mut(conflicting_item.package()) { - set.remove(conflicting_item.extra()); - } + self.conflicts.remove(&conflicting_item); } false }); @@ -3011,11 +3006,8 @@ impl Fork { /// Returns true if any of the dependencies in this fork contain a /// dependency with the given package and extra values. - fn contains_conflicting_item(&self, group: ConflictItemRef<'_>) -> bool { - self.conflicts - .get(group.package()) - .map(|set| set.contains(group.extra())) - .unwrap_or(false) + fn contains_conflicting_item(&self, item: ConflictItemRef<'_>) -> bool { + self.conflicts.contains(&item) } /// Exclude the given groups from this fork. @@ -3031,9 +3023,7 @@ impl Fork { return true; } if let Some(conflicting_item) = dep.package.conflicting_item() { - if let Some(set) = self.conflicts.get_mut(conflicting_item.package()) { - set.remove(conflicting_item.extra()); - } + self.conflicts.remove(&conflicting_item); } false }); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 6e309f8a6..201d6d26f 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -22,7 +22,7 @@ use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; -use uv_pypi_types::{ConflictSet, Conflicts, Requirement}; +use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts, Requirement}; use uv_python::{ EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, @@ -86,7 +86,12 @@ pub(crate) enum ProjectError { _1.iter().map(|extra| format!("`{extra}`")).collect::>().join(", "), _0 .iter() - .map(|group| format!("`{}[{}]`", group.package(), group.extra())) + .map(|item| { + match item.conflict() { + ConflictPackage::Extra(ref extra) => format!("`{}[{}]`", item.package(), extra), + ConflictPackage::Group(ref group) => format!("`{}:{}`", item.package(), group), + } + }) .collect::>() .join(", "), )] diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4eaa15e34..233ffdcfc 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -287,8 +287,9 @@ pub(super) async fn do_sync( for set in conflicts.iter() { let conflicting = set .iter() - .filter(|item| extras.contains(item.extra())) - .map(|item| item.extra().clone()) + .filter_map(|item| item.extra()) + .filter(|extra| extras.contains(extra)) + .map(|extra| extra.clone()) .collect::>(); if conflicting.len() >= 2 { return Err(ProjectError::ExtraIncompatibility(set.clone(), conflicting));