diff --git a/crates/uv-pypi-types/src/conflicts.rs b/crates/uv-pypi-types/src/conflicts.rs index 2ce6f35e8..1961bb94d 100644 --- a/crates/uv-pypi-types/src/conflicts.rs +++ b/crates/uv-pypi-types/src/conflicts.rs @@ -95,16 +95,14 @@ impl Conflicts { let mut substitutions: FxHashMap, FxHashSet>> = FxHashMap::default(); - // Conflict sets that were directly defined in configuration. - let mut direct_conflict_sets: FxHashSet<&ConflictSet> = FxHashSet::default(); - // Conflict sets that we will transitively infer in this method. - let mut transitive_conflict_sets: FxHashSet = FxHashSet::default(); + // Track all existing conflict sets to avoid duplicates. + let mut conflict_sets: FxHashSet = FxHashSet::default(); // Add groups in directly defined conflict sets to the graph. let mut seen: FxHashSet<&GroupName> = FxHashSet::default(); for set in &self.0 { - direct_conflict_sets.insert(set); + conflict_sets.insert(set.clone()); for item in set.iter() { let ConflictKind::Group(group) = &item.kind else { // TODO(john): Do we also want to handle extras here? @@ -183,28 +181,30 @@ impl Conflicts { // at the end of each iteration. for (canonical_item, subs) in substitutions { let mut new_conflict_sets = FxHashSet::default(); - for conflict_set in direct_conflict_sets + for conflict_set in conflict_sets .iter() - .copied() - .chain(transitive_conflict_sets.iter()) .filter(|set| set.contains_item(&canonical_item)) + .cloned() + .collect::>() { for sub in &subs { - let mut new_set = conflict_set + let new_set = conflict_set .replaced_item(&canonical_item, (**sub).clone()) .expect("`ConflictItem` should be in `ConflictSet`"); - if !direct_conflict_sets.contains(&new_set) { - new_set = new_set.with_inferred_conflict(); - if !transitive_conflict_sets.contains(&new_set) { - new_conflict_sets.insert(new_set); - } + if !conflict_sets.contains(&new_set) { + new_conflict_sets.insert(new_set); } } } - transitive_conflict_sets.extend(new_conflict_sets.into_iter()); + conflict_sets.extend(new_conflict_sets.into_iter()); } - self.0.extend(transitive_conflict_sets); + // Add all newly discovered conflict sets (excluding the originals already in self.0) + for set in conflict_sets { + if !self.0.contains(&set) { + self.0.push(set); + } + } } } @@ -220,7 +220,6 @@ impl Conflicts { #[derive(Debug, Default, Clone, Hash, Eq, PartialEq)] pub struct ConflictSet { set: BTreeSet, - is_inferred_conflict: bool, } impl ConflictSet { @@ -228,7 +227,6 @@ impl ConflictSet { pub fn pair(item1: ConflictItem, item2: ConflictItem) -> Self { Self { set: BTreeSet::from_iter(vec![item1, item2]), - is_inferred_conflict: false, } } @@ -255,11 +253,6 @@ impl ConflictSet { self.set.contains(conflict_item) } - /// This [`ConflictSet`] was inferred from directly defined conflicts. - pub fn is_inferred_conflict(&self) -> bool { - self.is_inferred_conflict - } - /// Replace an old [`ConflictItem`] with a new one. pub fn replaced_item( &self, @@ -272,17 +265,7 @@ impl ConflictSet { } new_set.remove(old); new_set.insert(new); - Ok(Self { - set: new_set, - is_inferred_conflict: false, - }) - } - - /// Mark this [`ConflictSet`] as being inferred from directly - /// defined conflicts. - fn with_inferred_conflict(mut self) -> Self { - self.is_inferred_conflict = true; - self + Ok(Self { set: new_set }) } } @@ -307,7 +290,6 @@ impl TryFrom> for ConflictSet { } Ok(Self { set: BTreeSet::from_iter(items), - is_inferred_conflict: false, }) } } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index cc066454b..1dee92370 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -320,14 +320,9 @@ impl std::fmt::Display for ConflictError { .iter() .all(|conflict| matches!(conflict.kind(), ConflictKind::Group(..))) { - let conflict_source = if self.set.is_inferred_conflict() { - "transitively inferred" - } else { - "declared" - }; write!( f, - "Groups {} are incompatible with the {conflict_source} conflicts: {{{set}}}", + "Groups {} are incompatible with the conflicts: {{{set}}}", conjunction( self.conflicts .iter() diff --git a/crates/uv/tests/it/lock_conflict.rs b/crates/uv/tests/it/lock_conflict.rs index e313410a5..335b0b9fe 100644 --- a/crates/uv/tests/it/lock_conflict.rs +++ b/crates/uv/tests/it/lock_conflict.rs @@ -2072,7 +2072,7 @@ fn group_basic() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Groups `group1` and `group2` are incompatible with the declared conflicts: {`project:group1`, `project:group2`} + error: Groups `group1` and `group2` are incompatible with the conflicts: {`project:group1`, `project:group2`} "###); Ok(()) @@ -2217,7 +2217,7 @@ fn group_default() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Groups `group1` (enabled by default) and `group2` are incompatible with the declared conflicts: {`project:group1`, `project:group2`} + error: Groups `group1` (enabled by default) and `group2` are incompatible with the conflicts: {`project:group1`, `project:group2`} "###); // If the group is explicitly requested, we should still fail, but shouldn't mark it as @@ -2228,7 +2228,7 @@ fn group_default() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Groups `group1` and `group2` are incompatible with the declared conflicts: {`project:group1`, `project:group2`} + error: Groups `group1` and `group2` are incompatible with the conflicts: {`project:group1`, `project:group2`} "###); // If we install via `--all-groups`, we should also avoid marking the group as "enabled by @@ -2239,7 +2239,7 @@ fn group_default() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Groups `group1` and `group2` are incompatible with the declared conflicts: {`project:group1`, `project:group2`} + error: Groups `group1` and `group2` are incompatible with the conflicts: {`project:group1`, `project:group2`} "###); // Disabling the default group should succeed. diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index aa29d4652..7347d8fb6 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11038,7 +11038,7 @@ fn multiple_group_conflicts() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] - error: Groups `bar` and `foo` are incompatible with the declared conflicts: {`project:bar`, `project:foo`} + error: Groups `bar` and `foo` are incompatible with the conflicts: {`project:bar`, `project:foo`} "); Ok(()) @@ -11075,6 +11075,24 @@ fn transitive_group_conflicts_shallow() -> Result<()> { "#, )?; + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "); + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 @@ -11116,7 +11134,7 @@ fn transitive_group_conflicts_shallow() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] - error: Groups `magic` and `test` are incompatible with the declared conflicts: {`example:magic`, `example:test`} + error: Groups `magic` and `test` are incompatible with the conflicts: {`example:magic`, `example:test`} "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("magic"), @r" @@ -11126,7 +11144,7 @@ fn transitive_group_conflicts_shallow() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] - error: Groups `dev` and `magic` are incompatible with the transitively inferred conflicts: {`example:dev`, `example:magic`} + error: Groups `dev` and `magic` are incompatible with the conflicts: {`example:dev`, `example:magic`} "); Ok(()) @@ -11213,7 +11231,7 @@ fn transitive_group_conflicts_deep() -> Result<()> { ----- stderr ----- Resolved 7 packages in [TIME] - error: Groups `dev` and `magic` are incompatible with the transitively inferred conflicts: {`example:dev`, `example:magic`} + error: Groups `dev` and `magic` are incompatible with the conflicts: {`example:dev`, `example:magic`} "); uv_snapshot!(context.filters(), context.sync().arg("--no-dev").arg("--group").arg("intermediate").arg("--group").arg("magic"), @r" @@ -11223,7 +11241,7 @@ fn transitive_group_conflicts_deep() -> Result<()> { ----- stderr ----- Resolved 7 packages in [TIME] - error: Groups `intermediate` and `magic` are incompatible with the transitively inferred conflicts: {`example:intermediate`, `example:magic`} + error: Groups `intermediate` and `magic` are incompatible with the conflicts: {`example:intermediate`, `example:magic`} "); Ok(()) @@ -11307,7 +11325,7 @@ fn transitive_group_conflicts_siblings() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] - error: Groups `dev` (enabled by default) and `dev2` are incompatible with the transitively inferred conflicts: {`example:dev`, `example:dev2`} + error: Groups `dev` (enabled by default) and `dev2` are incompatible with the conflicts: {`example:dev`, `example:dev2`} "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--group").arg("dev2"), @r" @@ -11317,7 +11335,7 @@ fn transitive_group_conflicts_siblings() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] - error: Groups `dev` and `dev2` are incompatible with the transitively inferred conflicts: {`example:dev`, `example:dev2`} + error: Groups `dev` and `dev2` are incompatible with the conflicts: {`example:dev`, `example:dev2`} "); Ok(())