@@ -5,7 +5,9 @@ use petgraph::visit::EdgeRef;
55use petgraph:: Direction ;
66use rustc_hash:: { FxBuildHasher , FxHashMap } ;
77
8- use uv_distribution_types:: { DistributionMetadata , Name , SourceAnnotation , SourceAnnotations } ;
8+ use uv_distribution_types:: {
9+ DistributionMetadata , Name , SourceAnnotation , SourceAnnotations , VersionId ,
10+ } ;
911use uv_normalize:: PackageName ;
1012use uv_pep508:: MarkerTree ;
1113
@@ -44,15 +46,6 @@ enum DisplayResolutionGraphNode<'dist> {
4446 Dist ( RequirementsTxtDist < ' dist > ) ,
4547}
4648
47- impl DisplayResolutionGraphNode < ' _ > {
48- fn markers ( & self ) -> & MarkerTree {
49- match self {
50- DisplayResolutionGraphNode :: Root => & MarkerTree :: TRUE ,
51- DisplayResolutionGraphNode :: Dist ( dist) => dist. markers ,
52- }
53- }
54- }
55-
5649impl < ' a > DisplayResolutionGraph < ' a > {
5750 /// Create a new [`DisplayResolutionGraph`] for the given graph.
5851 #[ allow( clippy:: fn_params_excessive_bools) ]
@@ -156,9 +149,12 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
156149 |_index, _edge| ( ) ,
157150 ) ;
158151
159- // Reduce the graph, such that all nodes for a single package are combined, regardless of
160- // the extras.
161- let petgraph = combine_extras ( & petgraph) ;
152+ // Reduce the graph, removing or combining extras for a given package.
153+ let petgraph = if self . include_extras {
154+ combine_extras ( & petgraph)
155+ } else {
156+ strip_extras ( & petgraph)
157+ } ;
162158
163159 // Collect all packages.
164160 let mut nodes = petgraph
@@ -181,11 +177,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
181177 for ( index, node) in nodes {
182178 // Display the node itself.
183179 let mut line = node
184- . to_requirements_txt (
185- & self . resolution . requires_python ,
186- self . include_extras ,
187- self . include_markers ,
188- )
180+ . to_requirements_txt ( & self . resolution . requires_python , self . include_markers )
189181 . to_string ( ) ;
190182
191183 // Display the distribution hashes, if any.
@@ -320,13 +312,22 @@ type RequirementsTxtGraph<'dist> =
320312 petgraph:: graph:: Graph < RequirementsTxtDist < ' dist > , ( ) , petgraph:: Directed > ;
321313
322314/// Reduce the graph, such that all nodes for a single package are combined, regardless of
323- /// the extras.
315+ /// the extras, as long as they have the same version and markers .
324316///
325317/// For example, `flask` and `flask[dotenv]` should be reduced into a single `flask[dotenv]`
326318/// node.
327319///
320+ /// If the extras have different markers, they'll be treated as separate nodes. For example,
321+ /// `flask[dotenv] ; sys_platform == "win32"` and `flask[async] ; sys_platform == "linux"`
322+ /// would _not_ be combined.
323+ ///
328324/// We also remove the root node, to simplify the graph structure.
329325fn combine_extras < ' dist > ( graph : & IntermediatePetGraph < ' dist > ) -> RequirementsTxtGraph < ' dist > {
326+ /// Return the key for a node.
327+ fn version_marker ( dist : & RequirementsTxtDist ) -> ( VersionId , MarkerTree ) {
328+ ( dist. version_id ( ) , dist. markers . clone ( ) )
329+ }
330+
330331 let mut next = RequirementsTxtGraph :: with_capacity ( graph. node_count ( ) , graph. edge_count ( ) ) ;
331332 let mut inverse = FxHashMap :: with_capacity_and_hasher ( graph. node_count ( ) , FxBuildHasher ) ;
332333
@@ -338,40 +339,68 @@ fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxt
338339
339340 // In the `requirements.txt` output, we want a flat installation list, so we need to use
340341 // the reachability markers instead of the edge markers.
341- // We use the markers of the base package: We know that each virtual extra package has an
342- // edge to the base package, so we know that base package markers are more general than the
343- // extra package markers (the extra package markers are a subset of the base package
344- // markers).
345- if let Some ( index) = inverse. get ( & dist. version_id ( ) ) {
346- let node: & mut RequirementsTxtDist = & mut next[ * index] ;
347- node. extras . extend ( dist. extras . iter ( ) . cloned ( ) ) ;
348- node. extras . sort_unstable ( ) ;
349- node. extras . dedup ( ) ;
350- } else {
351- let version_id = dist. version_id ( ) ;
352- let dist = dist. clone ( ) ;
353- let index = next. add_node ( dist) ;
354- inverse. insert ( version_id, index) ;
342+ match inverse. entry ( version_marker ( dist) ) {
343+ std:: collections:: hash_map:: Entry :: Occupied ( entry) => {
344+ let index = * entry. get ( ) ;
345+ let node: & mut RequirementsTxtDist = & mut next[ index] ;
346+ node. extras . extend ( dist. extras . iter ( ) . cloned ( ) ) ;
347+ node. extras . sort_unstable ( ) ;
348+ node. extras . dedup ( ) ;
349+ }
350+ std:: collections:: hash_map:: Entry :: Vacant ( entry) => {
351+ let index = next. add_node ( dist. clone ( ) ) ;
352+ entry. insert ( index) ;
353+ }
355354 }
356355 }
357356
358- // Verify that the package markers are more general than the extra markers.
359- if cfg ! ( debug_assertions) {
360- for index in graph. node_indices ( ) {
361- let DisplayResolutionGraphNode :: Dist ( dist) = & graph[ index] else {
362- continue ;
363- } ;
364- let combined_markers = next[ inverse[ & dist. version_id ( ) ] ] . markers . clone ( ) ;
365- let mut package_markers = combined_markers. clone ( ) ;
366- package_markers. or ( graph[ index] . markers ( ) . clone ( ) ) ;
367- assert_eq ! (
368- package_markers,
369- combined_markers,
370- "{} {:?} {:?}" ,
371- dist. version_id( ) ,
372- dist. extras,
373- dist. markers. try_to_string( )
374- ) ;
357+ // Re-add the edges to the reduced graph.
358+ for edge in graph. edge_indices ( ) {
359+ let ( source, target) = graph. edge_endpoints ( edge) . unwrap ( ) ;
360+ let DisplayResolutionGraphNode :: Dist ( source_node) = & graph[ source] else {
361+ continue ;
362+ } ;
363+ let DisplayResolutionGraphNode :: Dist ( target_node) = & graph[ target] else {
364+ continue ;
365+ } ;
366+ let source = inverse[ & version_marker ( source_node) ] ;
367+ let target = inverse[ & version_marker ( target_node) ] ;
368+
369+ next. update_edge ( source, target, ( ) ) ;
370+ }
371+
372+ next
373+ }
374+
375+ /// Reduce the graph, such that all nodes for a single package are combined, with extras
376+ /// removed.
377+ ///
378+ /// For example, `flask`, `flask[async]`, and `flask[dotenv]` should be reduced into a single
379+ /// `flask` node, with a conjunction of their markers.
380+ ///
381+ /// We also remove the root node, to simplify the graph structure.
382+ fn strip_extras < ' dist > ( graph : & IntermediatePetGraph < ' dist > ) -> RequirementsTxtGraph < ' dist > {
383+ let mut next = RequirementsTxtGraph :: with_capacity ( graph. node_count ( ) , graph. edge_count ( ) ) ;
384+ let mut inverse = FxHashMap :: with_capacity_and_hasher ( graph. node_count ( ) , FxBuildHasher ) ;
385+
386+ // Re-add the nodes to the reduced graph.
387+ for index in graph. node_indices ( ) {
388+ let DisplayResolutionGraphNode :: Dist ( dist) = & graph[ index] else {
389+ continue ;
390+ } ;
391+
392+ // In the `requirements.txt` output, we want a flat installation list, so we need to use
393+ // the reachability markers instead of the edge markers.
394+ match inverse. entry ( dist. version_id ( ) ) {
395+ std:: collections:: hash_map:: Entry :: Occupied ( entry) => {
396+ let index = * entry. get ( ) ;
397+ let node: & mut RequirementsTxtDist = & mut next[ index] ;
398+ node. extras . clear ( ) ;
399+ }
400+ std:: collections:: hash_map:: Entry :: Vacant ( entry) => {
401+ let index = next. add_node ( dist. clone ( ) ) ;
402+ entry. insert ( index) ;
403+ }
375404 }
376405 }
377406
0 commit comments