1
- use pubgrub:: Range ;
1
+ use pubgrub:: Ranges ;
2
+ use smallvec:: SmallVec ;
3
+ use std:: ops:: Bound ;
4
+
2
5
use uv_pep440:: Version ;
3
6
use uv_pep508:: { CanonicalMarkerValueVersion , MarkerTree , MarkerTreeKind } ;
4
7
5
8
use crate :: requires_python:: { LowerBound , RequiresPythonRange , UpperBound } ;
6
9
7
10
/// Returns the bounding Python versions that can satisfy the [`MarkerTree`], if it's constrained.
8
11
pub ( crate ) fn requires_python ( tree : MarkerTree ) -> Option < RequiresPythonRange > {
9
- fn collect_python_markers ( tree : MarkerTree , markers : & mut Vec < Range < Version > > ) {
12
+ /// A small vector of Python version markers.
13
+ type Markers = SmallVec < [ Ranges < Version > ; 3 ] > ;
14
+
15
+ /// Collect the Python version markers from the tree.
16
+ ///
17
+ /// Specifically, performs a DFS to collect all Python requirements on the path to every
18
+ /// `MarkerTreeKind::True` node.
19
+ fn collect_python_markers ( tree : MarkerTree , markers : & mut Markers , range : & Ranges < Version > ) {
10
20
match tree. kind ( ) {
11
- MarkerTreeKind :: True | MarkerTreeKind :: False => { }
21
+ MarkerTreeKind :: True => {
22
+ markers. push ( range. clone ( ) ) ;
23
+ }
24
+ MarkerTreeKind :: False => { }
12
25
MarkerTreeKind :: Version ( marker) => match marker. key ( ) {
13
26
CanonicalMarkerValueVersion :: PythonFullVersion => {
14
27
for ( range, tree) in marker. edges ( ) {
15
- if !tree. is_false ( ) {
16
- markers. push ( range. clone ( ) ) ;
17
- }
28
+ collect_python_markers ( tree, markers, range) ;
18
29
}
19
30
}
20
31
CanonicalMarkerValueVersion :: ImplementationVersion => {
21
32
for ( _, tree) in marker. edges ( ) {
22
- collect_python_markers ( tree, markers) ;
33
+ collect_python_markers ( tree, markers, range ) ;
23
34
}
24
35
}
25
36
} ,
26
37
MarkerTreeKind :: String ( marker) => {
27
38
for ( _, tree) in marker. children ( ) {
28
- collect_python_markers ( tree, markers) ;
39
+ collect_python_markers ( tree, markers, range ) ;
29
40
}
30
41
}
31
42
MarkerTreeKind :: In ( marker) => {
32
43
for ( _, tree) in marker. children ( ) {
33
- collect_python_markers ( tree, markers) ;
44
+ collect_python_markers ( tree, markers, range ) ;
34
45
}
35
46
}
36
47
MarkerTreeKind :: Contains ( marker) => {
37
48
for ( _, tree) in marker. children ( ) {
38
- collect_python_markers ( tree, markers) ;
49
+ collect_python_markers ( tree, markers, range ) ;
39
50
}
40
51
}
41
52
MarkerTreeKind :: Extra ( marker) => {
42
53
for ( _, tree) in marker. children ( ) {
43
- collect_python_markers ( tree, markers) ;
54
+ collect_python_markers ( tree, markers, range ) ;
44
55
}
45
56
}
46
57
}
47
58
}
48
59
49
- let mut markers = Vec :: new ( ) ;
50
- collect_python_markers ( tree, & mut markers) ;
60
+ if tree. is_true ( ) || tree. is_false ( ) {
61
+ return None ;
62
+ }
63
+
64
+ let mut markers = Markers :: new ( ) ;
65
+ collect_python_markers ( tree, & mut markers, & Ranges :: full ( ) ) ;
51
66
52
- // Take the union of all Python version markers.
67
+ // If there are no Python version markers, return `None`.
68
+ if markers. iter ( ) . all ( |range| {
69
+ let Some ( ( lower, upper) ) = range. bounding_range ( ) else {
70
+ return true ;
71
+ } ;
72
+ matches ! ( ( lower, upper) , ( Bound :: Unbounded , Bound :: Unbounded ) )
73
+ } ) {
74
+ return None ;
75
+ }
76
+
77
+ // Take the union of the intersections of the Python version markers.
53
78
let range = markers
54
79
. into_iter ( )
55
- . fold ( None , |acc : Option < Range < Version > > , range| {
56
- Some ( match acc {
57
- Some ( acc) => acc. union ( & range) ,
58
- None => range. clone ( ) ,
59
- } )
60
- } ) ?;
80
+ . fold ( Ranges :: empty ( ) , |acc : Ranges < Version > , range| {
81
+ acc. union ( & range)
82
+ } ) ;
61
83
62
84
let ( lower, upper) = range. bounding_range ( ) ?;
63
85
@@ -66,3 +88,97 @@ pub(crate) fn requires_python(tree: MarkerTree) -> Option<RequiresPythonRange> {
66
88
UpperBound :: new ( upper. cloned ( ) ) ,
67
89
) )
68
90
}
91
+
92
+ #[ cfg( test) ]
93
+ mod tests {
94
+ use std:: ops:: Bound ;
95
+ use std:: str:: FromStr ;
96
+
97
+ use super :: * ;
98
+
99
+ #[ test]
100
+ fn test_requires_python ( ) {
101
+ // An exact version match.
102
+ let tree = MarkerTree :: from_str ( "python_full_version == '3.8.*'" ) . unwrap ( ) ;
103
+ let range = requires_python ( tree) . unwrap ( ) ;
104
+ assert_eq ! (
105
+ * range. lower( ) ,
106
+ LowerBound :: new( Bound :: Included ( Version :: from_str( "3.8" ) . unwrap( ) ) )
107
+ ) ;
108
+ assert_eq ! (
109
+ * range. upper( ) ,
110
+ UpperBound :: new( Bound :: Excluded ( Version :: from_str( "3.9" ) . unwrap( ) ) )
111
+ ) ;
112
+
113
+ // A version range with exclusive bounds.
114
+ let tree =
115
+ MarkerTree :: from_str ( "python_full_version > '3.8' and python_full_version < '3.9'" )
116
+ . unwrap ( ) ;
117
+ let range = requires_python ( tree) . unwrap ( ) ;
118
+ assert_eq ! (
119
+ * range. lower( ) ,
120
+ LowerBound :: new( Bound :: Excluded ( Version :: from_str( "3.8" ) . unwrap( ) ) )
121
+ ) ;
122
+ assert_eq ! (
123
+ * range. upper( ) ,
124
+ UpperBound :: new( Bound :: Excluded ( Version :: from_str( "3.9" ) . unwrap( ) ) )
125
+ ) ;
126
+
127
+ // A version range with inclusive bounds.
128
+ let tree =
129
+ MarkerTree :: from_str ( "python_full_version >= '3.8' and python_full_version <= '3.9'" )
130
+ . unwrap ( ) ;
131
+ let range = requires_python ( tree) . unwrap ( ) ;
132
+ assert_eq ! (
133
+ * range. lower( ) ,
134
+ LowerBound :: new( Bound :: Included ( Version :: from_str( "3.8" ) . unwrap( ) ) )
135
+ ) ;
136
+ assert_eq ! (
137
+ * range. upper( ) ,
138
+ UpperBound :: new( Bound :: Included ( Version :: from_str( "3.9" ) . unwrap( ) ) )
139
+ ) ;
140
+
141
+ // A version with a lower bound.
142
+ let tree = MarkerTree :: from_str ( "python_full_version >= '3.8'" ) . unwrap ( ) ;
143
+ let range = requires_python ( tree) . unwrap ( ) ;
144
+ assert_eq ! (
145
+ * range. lower( ) ,
146
+ LowerBound :: new( Bound :: Included ( Version :: from_str( "3.8" ) . unwrap( ) ) )
147
+ ) ;
148
+ assert_eq ! ( * range. upper( ) , UpperBound :: new( Bound :: Unbounded ) ) ;
149
+
150
+ // A version with an upper bound.
151
+ let tree = MarkerTree :: from_str ( "python_full_version < '3.9'" ) . unwrap ( ) ;
152
+ let range = requires_python ( tree) . unwrap ( ) ;
153
+ assert_eq ! ( * range. lower( ) , LowerBound :: new( Bound :: Unbounded ) ) ;
154
+ assert_eq ! (
155
+ * range. upper( ) ,
156
+ UpperBound :: new( Bound :: Excluded ( Version :: from_str( "3.9" ) . unwrap( ) ) )
157
+ ) ;
158
+
159
+ // A disjunction with a non-Python marker (i.e., an unbounded range).
160
+ let tree =
161
+ MarkerTree :: from_str ( "python_full_version > '3.8' or sys_platform == 'win32'" ) . unwrap ( ) ;
162
+ let range = requires_python ( tree) . unwrap ( ) ;
163
+ assert_eq ! ( * range. lower( ) , LowerBound :: new( Bound :: Unbounded ) ) ;
164
+ assert_eq ! ( * range. upper( ) , UpperBound :: new( Bound :: Unbounded ) ) ;
165
+
166
+ // A complex mix of conjunctions and disjunctions.
167
+ let tree = MarkerTree :: from_str ( "(python_full_version >= '3.8' and python_full_version < '3.9') or (python_full_version >= '3.10' and python_full_version < '3.11')" ) . unwrap ( ) ;
168
+ let range = requires_python ( tree) . unwrap ( ) ;
169
+ assert_eq ! (
170
+ * range. lower( ) ,
171
+ LowerBound :: new( Bound :: Included ( Version :: from_str( "3.8" ) . unwrap( ) ) )
172
+ ) ;
173
+ assert_eq ! (
174
+ * range. upper( ) ,
175
+ UpperBound :: new( Bound :: Excluded ( Version :: from_str( "3.11" ) . unwrap( ) ) )
176
+ ) ;
177
+
178
+ // An unbounded range across two specifiers.
179
+ let tree =
180
+ MarkerTree :: from_str ( "python_full_version > '3.8' or python_full_version <= '3.8'" )
181
+ . unwrap ( ) ;
182
+ assert_eq ! ( requires_python( tree) , None ) ;
183
+ }
184
+ }
0 commit comments