Remove tracking of inferred dependency conflicts (#15909)

Alternative to #15884 (see commentary there)

Closes https://github.com/astral-sh/uv/issues/15869
This commit is contained in:
Zanie Blue 2025-10-01 10:03:42 -05:00 committed by GitHub
parent ab2f394019
commit 8b86bd530e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 47 additions and 52 deletions

View File

@ -95,16 +95,14 @@ impl Conflicts {
let mut substitutions: FxHashMap<Rc<ConflictItem>, FxHashSet<Rc<ConflictItem>>> = let mut substitutions: FxHashMap<Rc<ConflictItem>, FxHashSet<Rc<ConflictItem>>> =
FxHashMap::default(); FxHashMap::default();
// Conflict sets that were directly defined in configuration. // Track all existing conflict sets to avoid duplicates.
let mut direct_conflict_sets: FxHashSet<&ConflictSet> = FxHashSet::default(); let mut conflict_sets: FxHashSet<ConflictSet> = FxHashSet::default();
// Conflict sets that we will transitively infer in this method.
let mut transitive_conflict_sets: FxHashSet<ConflictSet> = FxHashSet::default();
// Add groups in directly defined conflict sets to the graph. // Add groups in directly defined conflict sets to the graph.
let mut seen: FxHashSet<&GroupName> = FxHashSet::default(); let mut seen: FxHashSet<&GroupName> = FxHashSet::default();
for set in &self.0 { for set in &self.0 {
direct_conflict_sets.insert(set); conflict_sets.insert(set.clone());
for item in set.iter() { for item in set.iter() {
let ConflictKind::Group(group) = &item.kind else { let ConflictKind::Group(group) = &item.kind else {
// TODO(john): Do we also want to handle extras here? // TODO(john): Do we also want to handle extras here?
@ -183,28 +181,30 @@ impl Conflicts {
// at the end of each iteration. // at the end of each iteration.
for (canonical_item, subs) in substitutions { for (canonical_item, subs) in substitutions {
let mut new_conflict_sets = FxHashSet::default(); let mut new_conflict_sets = FxHashSet::default();
for conflict_set in direct_conflict_sets for conflict_set in conflict_sets
.iter() .iter()
.copied()
.chain(transitive_conflict_sets.iter())
.filter(|set| set.contains_item(&canonical_item)) .filter(|set| set.contains_item(&canonical_item))
.cloned()
.collect::<Vec<_>>()
{ {
for sub in &subs { for sub in &subs {
let mut new_set = conflict_set let new_set = conflict_set
.replaced_item(&canonical_item, (**sub).clone()) .replaced_item(&canonical_item, (**sub).clone())
.expect("`ConflictItem` should be in `ConflictSet`"); .expect("`ConflictItem` should be in `ConflictSet`");
if !direct_conflict_sets.contains(&new_set) { if !conflict_sets.contains(&new_set) {
new_set = new_set.with_inferred_conflict(); new_conflict_sets.insert(new_set);
if !transitive_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)] #[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
pub struct ConflictSet { pub struct ConflictSet {
set: BTreeSet<ConflictItem>, set: BTreeSet<ConflictItem>,
is_inferred_conflict: bool,
} }
impl ConflictSet { impl ConflictSet {
@ -228,7 +227,6 @@ impl ConflictSet {
pub fn pair(item1: ConflictItem, item2: ConflictItem) -> Self { pub fn pair(item1: ConflictItem, item2: ConflictItem) -> Self {
Self { Self {
set: BTreeSet::from_iter(vec![item1, item2]), set: BTreeSet::from_iter(vec![item1, item2]),
is_inferred_conflict: false,
} }
} }
@ -255,11 +253,6 @@ impl ConflictSet {
self.set.contains(conflict_item) 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. /// Replace an old [`ConflictItem`] with a new one.
pub fn replaced_item( pub fn replaced_item(
&self, &self,
@ -272,17 +265,7 @@ impl ConflictSet {
} }
new_set.remove(old); new_set.remove(old);
new_set.insert(new); new_set.insert(new);
Ok(Self { Ok(Self { set: new_set })
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
} }
} }
@ -307,7 +290,6 @@ impl TryFrom<Vec<ConflictItem>> for ConflictSet {
} }
Ok(Self { Ok(Self {
set: BTreeSet::from_iter(items), set: BTreeSet::from_iter(items),
is_inferred_conflict: false,
}) })
} }
} }

View File

@ -320,14 +320,9 @@ impl std::fmt::Display for ConflictError {
.iter() .iter()
.all(|conflict| matches!(conflict.kind(), ConflictKind::Group(..))) .all(|conflict| matches!(conflict.kind(), ConflictKind::Group(..)))
{ {
let conflict_source = if self.set.is_inferred_conflict() {
"transitively inferred"
} else {
"declared"
};
write!( write!(
f, f,
"Groups {} are incompatible with the {conflict_source} conflicts: {{{set}}}", "Groups {} are incompatible with the conflicts: {{{set}}}",
conjunction( conjunction(
self.conflicts self.conflicts
.iter() .iter()

View File

@ -2072,7 +2072,7 @@ fn group_basic() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- 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(()) Ok(())
@ -2217,7 +2217,7 @@ fn group_default() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- 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 // 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 ----- ----- stdout -----
----- stderr ----- ----- 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 // 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 ----- ----- stdout -----
----- stderr ----- ----- 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. // Disabling the default group should succeed.

View File

@ -11038,7 +11038,7 @@ fn multiple_group_conflicts() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 3 packages in [TIME] 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(()) 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" uv_snapshot!(context.filters(), context.sync(), @r"
success: true success: true
exit_code: 0 exit_code: 0
@ -11116,7 +11134,7 @@ fn transitive_group_conflicts_shallow() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 5 packages in [TIME] 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" 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 ----- ----- stderr -----
Resolved 5 packages in [TIME] 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(()) Ok(())
@ -11213,7 +11231,7 @@ fn transitive_group_conflicts_deep() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 7 packages in [TIME] 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" 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 ----- ----- stderr -----
Resolved 7 packages in [TIME] 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(()) Ok(())
@ -11307,7 +11325,7 @@ fn transitive_group_conflicts_siblings() -> Result<()> {
----- stderr ----- ----- stderr -----
Resolved 5 packages in [TIME] 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" 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 ----- ----- stderr -----
Resolved 5 packages in [TIME] 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(()) Ok(())