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,155 @@ 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
192
+ . replaced_item ( & canonical_item, ( * * sub) . clone ( ) )
193
+ . expect ( "`ConflictItem` should be in `ConflictSet`" ) ;
194
+ if !direct_conflict_sets. contains ( & new_set) {
195
+ new_set. set_as_inferred_conflict ( ) ;
196
+ if !transitive_conflict_sets. contains ( & new_set) {
197
+ new_conflict_sets. insert ( new_set) ;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ transitive_conflict_sets. extend ( new_conflict_sets. into_iter ( ) ) ;
203
+ }
204
+
205
+ self . 0 . extend ( transitive_conflict_sets) ;
206
+ }
50
207
}
51
208
52
209
/// A single set of package-extra pairs that conflict with one another.
@@ -58,23 +215,24 @@ impl Conflicts {
58
215
///
59
216
/// A `TryFrom<Vec<ConflictItem>>` impl may be used to build a set from a
60
217
/// 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 > ) ;
218
+ #[ derive( Debug , Default , Clone , Hash , Eq , PartialEq , serde:: Serialize ) ]
219
+ pub struct ConflictSet {
220
+ set : BTreeSet < ConflictItem > ,
221
+ is_inferred_conflict : bool ,
222
+ }
63
223
64
224
impl ConflictSet {
65
225
/// Create a pair of items that conflict with one another.
66
226
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) ;
227
+ ConflictSet {
228
+ set : BTreeSet :: from_iter ( vec ! [ item1, item2] ) ,
229
+ is_inferred_conflict : false ,
230
+ }
73
231
}
74
232
75
233
/// Returns an iterator over all conflicting items.
76
234
pub fn iter ( & self ) -> impl Iterator < Item = & ' _ ConflictItem > + Clone + ' _ {
77
- self . 0 . iter ( )
235
+ self . set . iter ( )
78
236
}
79
237
80
238
/// Returns true if this conflicting item contains the given package and
@@ -88,6 +246,42 @@ impl ConflictSet {
88
246
self . iter ( )
89
247
. any ( |set| set. package ( ) == package && * set. conflict ( ) == conflict)
90
248
}
249
+
250
+ /// Returns true if these conflicts contain any set that contains the given
251
+ /// [`ConflictItem`].
252
+ pub fn contains_item ( & self , conflict_item : & ConflictItem ) -> bool {
253
+ self . set . contains ( conflict_item)
254
+ }
255
+
256
+ /// This [`ConflictSet`] was inferred from directly defined conflicts.
257
+ pub fn is_inferred_conflict ( & self ) -> bool {
258
+ self . is_inferred_conflict
259
+ }
260
+
261
+ // FIXME: Error if old is not present
262
+ /// Replace an old [`ConflictItem`] with a new one.
263
+ pub fn replaced_item (
264
+ & self ,
265
+ old : & ConflictItem ,
266
+ new : ConflictItem ,
267
+ ) -> Result < Self , ConflictError > {
268
+ let mut new_set = self . set . clone ( ) ;
269
+ if !new_set. contains ( old) {
270
+ return Err ( ConflictError :: ReplaceMissingConflictItem ) ;
271
+ }
272
+ new_set. remove ( old) ;
273
+ new_set. insert ( new) ;
274
+ Ok ( Self {
275
+ set : new_set,
276
+ is_inferred_conflict : false ,
277
+ } )
278
+ }
279
+
280
+ /// Mark this [`ConflictSet`] as being inferred from directly
281
+ /// defined conflicts.
282
+ pub fn set_as_inferred_conflict ( & mut self ) {
283
+ self . is_inferred_conflict = true ;
284
+ }
91
285
}
92
286
93
287
impl < ' de > serde:: Deserialize < ' de > for ConflictSet {
@@ -109,14 +303,17 @@ impl TryFrom<Vec<ConflictItem>> for ConflictSet {
109
303
1 => return Err ( ConflictError :: OneItem ) ,
110
304
_ => { }
111
305
}
112
- Ok ( ConflictSet ( items) )
306
+ Ok ( ConflictSet {
307
+ set : BTreeSet :: from_iter ( items) ,
308
+ is_inferred_conflict : false ,
309
+ } )
113
310
}
114
311
}
115
312
116
313
/// A single item in a conflicting set.
117
314
///
118
- /// Each item is a pair of a package and a corresponding extra name for that
119
- /// package.
315
+ /// Each item is a pair of a package and a corresponding extra or group name
316
+ /// for that package.
120
317
#[ derive(
121
318
Debug , Clone , Eq , Hash , PartialEq , PartialOrd , Ord , serde:: Deserialize , serde:: Serialize ,
122
319
) ]
@@ -364,6 +561,10 @@ pub enum ConflictError {
364
561
/// An error that occurs when both `extra` and `group` are present.
365
562
#[ error( "Expected one of `extra` or `group` in conflicting entry, but found both" ) ]
366
563
FoundExtraAndGroup ,
564
+ #[ error( "Cycle detected in transitive conflict inclusion" ) ]
565
+ ConflictInclusionCycle ,
566
+ #[ error( "Expected `ConflictSet` to contain `ConflictItem` to replace" ) ]
567
+ ReplaceMissingConflictItem ,
367
568
}
368
569
369
570
/// Like [`Conflicts`], but for deserialization in `pyproject.toml`.
0 commit comments