1
+ use petgraph:: {
2
+ algo:: toposort,
3
+ graph:: { DiGraph , NodeIndex } ,
4
+ } ;
5
+ use rustc_hash:: { FxHashMap , FxHashSet } ;
6
+ use std:: { collections:: BTreeSet , hash:: Hash , rc:: Rc } ;
1
7
use uv_normalize:: { ExtraName , GroupName , PackageName } ;
2
8
9
+ use crate :: dependency_groups:: { DependencyGroupSpecifier , DependencyGroups } ;
10
+
3
11
/// A list of conflicting sets of extras/groups pre-defined by an end user.
4
12
///
5
13
/// This is useful to force the resolver to fork according to extras that have
@@ -47,6 +55,153 @@ impl Conflicts {
47
55
pub fn append ( & mut self , other : & mut Conflicts ) {
48
56
self . 0 . append ( & mut other. 0 ) ;
49
57
}
58
+
59
+ /// Expand [`Conflicts`]s to include all [`ConflictSet`]s that can
60
+ /// be transitively inferred from group conflicts directly defined
61
+ /// in configuration.
62
+ ///
63
+ /// A directed acyclic graph (DAG) is created representing all
64
+ /// transitive group includes, with nodes corresponding to group conflict
65
+ /// items. For every conflict item directly mentioned in configuration,
66
+ /// its node starts with a set of canonical items with itself as the only
67
+ /// member.
68
+ ///
69
+ /// The graph is traversed one node at a time in topological order and
70
+ /// canonical items are propagated to each neighbor. We also update our
71
+ /// substitutions at each neighbor to reflect that this neighbor transitively
72
+ /// includes all canonical items visited so far to reach it.
73
+ ///
74
+ /// Finally, we apply the substitutions to the conflict sets that were
75
+ /// directly defined in configuration to generate all transitively inferable
76
+ /// [`ConflictSet`]s.
77
+ ///
78
+ /// There is an assumption that inclusion graphs will not be very large
79
+ /// or complex. This algorithm creates all combinations of substitutions.
80
+ /// Each resulting [`ConflictSet`] would also later correspond to a separate
81
+ /// resolver fork during resolution.
82
+ pub fn expand_transitive_group_includes (
83
+ & mut self ,
84
+ package : & PackageName ,
85
+ groups : & DependencyGroups ,
86
+ ) {
87
+ let mut graph = DiGraph :: new ( ) ;
88
+ let mut group_node_idxs: FxHashMap < GroupName , NodeIndex > = FxHashMap :: default ( ) ;
89
+ let mut node_conflict_items: FxHashMap < NodeIndex , Rc < ConflictItem > > = FxHashMap :: default ( ) ;
90
+ // Used for transitively deriving new conflict sets with substitutions.
91
+ // The keys are canonical items (mentioned directly in configured conflicts).
92
+ // The values correspond to groups that transitively include them.
93
+ let mut substitutions: FxHashMap < Rc < ConflictItem > , FxHashSet < Rc < ConflictItem > > > =
94
+ FxHashMap :: default ( ) ;
95
+
96
+ // Conflict sets that were directly defined in configuration.
97
+ let mut direct_conflict_sets: FxHashSet < ConflictSet > = FxHashSet :: default ( ) ;
98
+ // Conflict sets that we will transitively infer in this method.
99
+ let mut transitive_conflict_sets: FxHashSet < ConflictSet > = FxHashSet :: default ( ) ;
100
+
101
+ // Add groups in directly defined conflict sets to the graph.
102
+ let mut seen: std:: collections:: HashSet < & GroupName , rustc_hash:: FxBuildHasher > =
103
+ FxHashSet :: default ( ) ;
104
+ for set in & self . 0 {
105
+ direct_conflict_sets. insert ( set. clone ( ) ) ;
106
+ for item in set. iter ( ) {
107
+ let ConflictPackage :: Group ( group) = & item. conflict else {
108
+ // TODO: Do we also want to handle extras here?
109
+ continue ;
110
+ } ;
111
+ if !seen. contains ( group) {
112
+ let item = Rc :: new ( item. clone ( ) ) ;
113
+ let mut canonical_items = FxHashSet :: default ( ) ;
114
+ canonical_items. insert ( item. clone ( ) ) ;
115
+ let node_id = graph. add_node ( canonical_items) ;
116
+ group_node_idxs. insert ( group. clone ( ) , node_id) ;
117
+ seen. insert ( group) ;
118
+ node_conflict_items. insert ( node_id, item. clone ( ) ) ;
119
+ }
120
+ }
121
+ }
122
+
123
+ // Create conflict items for remaining groups and add them to the graph.
124
+ for group in groups. keys ( ) {
125
+ if !seen. contains ( group) {
126
+ seen. insert ( group) ;
127
+ let group_conflict_item = ConflictItem {
128
+ package : package. clone ( ) ,
129
+ conflict : ConflictPackage :: Group ( group. clone ( ) ) ,
130
+ } ;
131
+ let node_id = graph. add_node ( FxHashSet :: default ( ) ) ;
132
+ group_node_idxs. insert ( group. clone ( ) , node_id) ;
133
+ node_conflict_items. insert ( node_id, Rc :: new ( group_conflict_item) ) ;
134
+ }
135
+ }
136
+
137
+ // Create edges representing group inclusion (with edges reversed so that
138
+ // included groups point to including groups).
139
+ for ( group, specifiers) in groups {
140
+ let includer = group_node_idxs
141
+ . get ( group)
142
+ . expect ( "Group should have been added to graph" ) ;
143
+ for specifier in specifiers {
144
+ if let DependencyGroupSpecifier :: IncludeGroup { include_group } = specifier {
145
+ let included = group_node_idxs
146
+ . get ( include_group)
147
+ . expect ( "Group should have been added to graph" ) ;
148
+ graph. add_edge ( * included, * includer, ( ) ) ;
149
+ }
150
+ }
151
+ }
152
+
153
+ // Propagate canonical items through the graph and populate substitutions.
154
+ // FIXME: Have we already done cycle detection before this method was
155
+ // called or do we need to propagate error?
156
+ for node in toposort ( & graph, None ) . unwrap ( ) {
157
+ for neighbor_idx in graph. neighbors ( node) . collect :: < Vec < _ > > ( ) {
158
+ let mut neighbor_canonical_items = Vec :: new ( ) ;
159
+ if let Some ( canonical_items) = graph. node_weight ( node) {
160
+ let neighbor_item = node_conflict_items
161
+ . get ( & neighbor_idx)
162
+ . expect ( "ConflictItem should already be in graph" )
163
+ . clone ( ) ;
164
+ for canonical_item in canonical_items {
165
+ neighbor_canonical_items. push ( canonical_item. clone ( ) ) ;
166
+ substitutions
167
+ . entry ( canonical_item. clone ( ) )
168
+ . or_default ( )
169
+ . insert ( neighbor_item. clone ( ) ) ;
170
+ }
171
+ }
172
+ graph
173
+ . node_weight_mut ( neighbor_idx)
174
+ . expect ( "Graph node should have weight" )
175
+ . extend ( neighbor_canonical_items. into_iter ( ) ) ;
176
+ }
177
+ }
178
+
179
+ // Create new conflict sets for all possible replacements of canonical
180
+ // items by substitution items.
181
+ // Note that new sets are (potentially) added to transitive_conflict_sets
182
+ // at the end of each iteration.
183
+ for ( canonical_item, subs) in substitutions {
184
+ let mut new_conflict_sets = FxHashSet :: default ( ) ;
185
+ for conflict_set in direct_conflict_sets
186
+ . iter ( )
187
+ . chain ( transitive_conflict_sets. iter ( ) )
188
+ . filter ( |set| set. contains_item ( & canonical_item) )
189
+ {
190
+ for sub in & subs {
191
+ let mut new_set = conflict_set. replaced_item ( & canonical_item, ( * * sub) . clone ( ) ) . expect ( "`ConflictItem` should be in `ConflictSet`" ) ;
192
+ if !direct_conflict_sets. contains ( & new_set) {
193
+ new_set. set_as_inferred_conflict ( ) ;
194
+ if !transitive_conflict_sets. contains ( & new_set) {
195
+ new_conflict_sets. insert ( new_set) ;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ transitive_conflict_sets. extend ( new_conflict_sets. into_iter ( ) ) ;
201
+ }
202
+
203
+ self . 0 . extend ( transitive_conflict_sets) ;
204
+ }
50
205
}
51
206
52
207
/// A single set of package-extra pairs that conflict with one another.
@@ -58,23 +213,24 @@ impl Conflicts {
58
213
///
59
214
/// A `TryFrom<Vec<ConflictItem>>` impl may be used to build a set from a
60
215
/// sequence. Note though that at least 2 items are required.
61
- #[ derive( Debug , Default , Clone , Eq , PartialEq , serde:: Serialize ) ]
62
- pub struct ConflictSet ( Vec < ConflictItem > ) ;
216
+ #[ derive( Debug , Default , Clone , Hash , Eq , PartialEq , serde:: Serialize ) ]
217
+ pub struct ConflictSet {
218
+ set : BTreeSet < ConflictItem > ,
219
+ is_inferred_conflict : bool ,
220
+ }
63
221
64
222
impl ConflictSet {
65
223
/// Create a pair of items that conflict with one another.
66
224
pub fn pair ( item1 : ConflictItem , item2 : ConflictItem ) -> ConflictSet {
67
- ConflictSet ( vec ! [ item1, item2] )
68
- }
69
-
70
- /// Add a new conflicting item to this set.
71
- pub fn push ( & mut self , item : ConflictItem ) {
72
- self . 0 . push ( item) ;
225
+ ConflictSet {
226
+ set : BTreeSet :: from_iter ( vec ! [ item1, item2] ) ,
227
+ is_inferred_conflict : false ,
228
+ }
73
229
}
74
230
75
231
/// Returns an iterator over all conflicting items.
76
232
pub fn iter ( & self ) -> impl Iterator < Item = & ' _ ConflictItem > + Clone + ' _ {
77
- self . 0 . iter ( )
233
+ self . set . iter ( )
78
234
}
79
235
80
236
/// Returns true if this conflicting item contains the given package and
@@ -88,6 +244,39 @@ impl ConflictSet {
88
244
self . iter ( )
89
245
. any ( |set| set. package ( ) == package && * set. conflict ( ) == conflict)
90
246
}
247
+
248
+ /// Returns true if these conflicts contain any set that contains the given
249
+ /// [`ConflictItem`].
250
+ pub fn contains_item ( & self , conflict_item : & ConflictItem ) -> bool {
251
+ self . set . contains ( conflict_item)
252
+ }
253
+
254
+ /// This [`ConflictSet`] was inferred from directly defined conflicts.
255
+ pub fn is_inferred_conflict ( & self ) -> bool {
256
+ self . is_inferred_conflict
257
+ }
258
+
259
+ // FIXME: Error if old is not present
260
+ /// Replace an old [`ConflictItem`] with a new one.
261
+ #[ must_use]
262
+ pub fn replaced_item ( & self , old : & ConflictItem , new : ConflictItem ) -> Result < Self , ConflictError > {
263
+ let mut new_set = self . set . clone ( ) ;
264
+ if !new_set. contains ( old) {
265
+ return Err ( ConflictError :: ReplaceMissingConflictItem ) ;
266
+ }
267
+ new_set. remove ( old) ;
268
+ new_set. insert ( new) ;
269
+ Ok ( Self {
270
+ set : new_set,
271
+ is_inferred_conflict : false ,
272
+ } )
273
+ }
274
+
275
+ /// Mark this [`ConflictSet`] as being inferred from directly
276
+ /// defined conflicts.
277
+ pub fn set_as_inferred_conflict ( & mut self ) {
278
+ self . is_inferred_conflict = true ;
279
+ }
91
280
}
92
281
93
282
impl < ' de > serde:: Deserialize < ' de > for ConflictSet {
@@ -109,14 +298,17 @@ impl TryFrom<Vec<ConflictItem>> for ConflictSet {
109
298
1 => return Err ( ConflictError :: OneItem ) ,
110
299
_ => { }
111
300
}
112
- Ok ( ConflictSet ( items) )
301
+ Ok ( ConflictSet {
302
+ set : BTreeSet :: from_iter ( items) ,
303
+ is_inferred_conflict : false ,
304
+ } )
113
305
}
114
306
}
115
307
116
308
/// A single item in a conflicting set.
117
309
///
118
- /// Each item is a pair of a package and a corresponding extra name for that
119
- /// package.
310
+ /// Each item is a pair of a package and a corresponding extra or group name
311
+ /// for that package.
120
312
#[ derive(
121
313
Debug , Clone , Eq , Hash , PartialEq , PartialOrd , Ord , serde:: Deserialize , serde:: Serialize ,
122
314
) ]
@@ -364,6 +556,10 @@ pub enum ConflictError {
364
556
/// An error that occurs when both `extra` and `group` are present.
365
557
#[ error( "Expected one of `extra` or `group` in conflicting entry, but found both" ) ]
366
558
FoundExtraAndGroup ,
559
+ #[ error( "Cycle detected in transitive conflict inclusion" ) ]
560
+ ConflictInclusionCycle ,
561
+ #[ error( "Expected `ConflictSet` to contain `ConflictItem` to replace" ) ]
562
+ ReplaceMissingConflictItem
367
563
}
368
564
369
565
/// Like [`Conflicts`], but for deserialization in `pyproject.toml`.
0 commit comments