uv_distribution_types/
requires_python.rs

1use std::collections::Bound;
2
3use version_ranges::Ranges;
4
5use uv_distribution_filename::WheelFilename;
6use uv_pep440::{
7    LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
8    release_specifiers_to_ranges,
9};
10use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
11use uv_platform_tags::{AbiTag, LanguageTag};
12
13/// The `Requires-Python` requirement specifier.
14///
15/// See: <https://packaging.python.org/en/latest/guides/dropping-older-python-versions/>
16#[derive(Debug, Clone, Eq, PartialEq, Hash)]
17pub struct RequiresPython {
18    /// The supported Python versions as provides by the user, usually through the `requires-python`
19    /// field in `pyproject.toml`.
20    ///
21    /// For a workspace, it's the intersection of all `requires-python` values in the workspace. If
22    /// no bound was provided by the user, it's greater equal the current Python version.
23    ///
24    /// The specifiers remain static over the lifetime of the workspace, such that they
25    /// represent the initial Python version constraints.
26    specifiers: VersionSpecifiers,
27    /// The lower and upper bounds of the given specifiers.
28    ///
29    /// The range may be narrowed over the course of dependency resolution as the resolver
30    /// investigates environments with stricter Python version constraints.
31    range: RequiresPythonRange,
32}
33
34impl RequiresPython {
35    /// Returns a [`RequiresPython`] to express `>=` equality with the given version.
36    pub fn greater_than_equal_version(version: &Version) -> Self {
37        let version = version.only_release();
38        Self {
39            specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(
40                version.clone(),
41            )),
42            range: RequiresPythonRange(
43                LowerBound::new(Bound::Included(version.clone())),
44                UpperBound::new(Bound::Unbounded),
45            ),
46        }
47    }
48
49    /// Returns a [`RequiresPython`] from a version specifier.
50    pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Self {
51        let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
52            .bounding_range()
53            .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
54            .unwrap_or((Bound::Unbounded, Bound::Unbounded));
55        Self {
56            specifiers: specifiers.clone(),
57            range: RequiresPythonRange(LowerBound::new(lower_bound), UpperBound::new(upper_bound)),
58        }
59    }
60
61    /// Returns a [`RequiresPython`] to express the intersection of the given version specifiers.
62    ///
63    /// For example, given `>=3.8` and `>=3.9`, this would return `>=3.9`.
64    pub fn intersection<'a>(
65        specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
66    ) -> Option<Self> {
67        // Convert to PubGrub range and perform an intersection.
68        let range = specifiers
69            .map(|specs| release_specifiers_to_ranges(specs.clone()))
70            .reduce(|acc, r| acc.intersection(&r))?;
71
72        // If the intersection is empty, return `None`.
73        if range.is_empty() {
74            return None;
75        }
76
77        // Convert back to PEP 440 specifiers.
78        let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
79
80        // Extract the bounds.
81        let range = RequiresPythonRange::from_range(&range);
82
83        Some(Self { specifiers, range })
84    }
85
86    /// Split the [`RequiresPython`] at the given version.
87    ///
88    /// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then
89    /// the result will be `>=3.10 and <3.11` and `>=3.11`.
90    pub fn split(&self, bound: Bound<Version>) -> Option<(Self, Self)> {
91        let RequiresPythonRange(.., upper) = &self.range;
92
93        let upper = Ranges::from_range_bounds((bound, upper.clone().into()));
94        let lower = upper.complement();
95
96        // Intersect left and right with the existing range.
97        let lower = lower.intersection(&Ranges::from(self.range.clone()));
98        let upper = upper.intersection(&Ranges::from(self.range.clone()));
99
100        if lower.is_empty() || upper.is_empty() {
101            None
102        } else {
103            Some((
104                Self {
105                    specifiers: VersionSpecifiers::from_release_only_bounds(lower.iter()),
106                    range: RequiresPythonRange::from_range(&lower),
107                },
108                Self {
109                    specifiers: VersionSpecifiers::from_release_only_bounds(upper.iter()),
110                    range: RequiresPythonRange::from_range(&upper),
111                },
112            ))
113        }
114    }
115
116    /// Narrow the [`RequiresPython`] by computing the intersection with the given range.
117    ///
118    /// Returns `None` if the given range is not narrower than the current range.
119    pub fn narrow(&self, range: &RequiresPythonRange) -> Option<Self> {
120        if *range == self.range {
121            return None;
122        }
123        let lower = if range.0 >= self.range.0 {
124            Some(&range.0)
125        } else {
126            None
127        };
128        let upper = if range.1 <= self.range.1 {
129            Some(&range.1)
130        } else {
131            None
132        };
133        let range = match (lower, upper) {
134            (Some(lower), Some(upper)) => Some(RequiresPythonRange(lower.clone(), upper.clone())),
135            (Some(lower), None) => Some(RequiresPythonRange(lower.clone(), self.range.1.clone())),
136            (None, Some(upper)) => Some(RequiresPythonRange(self.range.0.clone(), upper.clone())),
137            (None, None) => None,
138        }?;
139        Some(Self {
140            specifiers: range.specifiers(),
141            range,
142        })
143    }
144
145    /// Returns this `Requires-Python` specifier as an equivalent
146    /// [`MarkerTree`] utilizing the `python_full_version` marker field.
147    ///
148    /// This is useful for comparing a `Requires-Python` specifier with
149    /// arbitrary marker expressions. For example, one can ask whether the
150    /// returned marker expression is disjoint with another marker expression.
151    /// If it is, then one can conclude that the `Requires-Python` specifier
152    /// excludes the dependency with that other marker expression.
153    ///
154    /// If this `Requires-Python` specifier has no constraints, then this
155    /// returns a marker tree that evaluates to `true` for all possible marker
156    /// environments.
157    pub fn to_marker_tree(&self) -> MarkerTree {
158        match (self.range.0.as_ref(), self.range.1.as_ref()) {
159            (Bound::Included(lower), Bound::Included(upper)) => {
160                let mut lower = MarkerTree::expression(MarkerExpression::Version {
161                    key: MarkerValueVersion::PythonFullVersion,
162                    specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
163                });
164                let upper = MarkerTree::expression(MarkerExpression::Version {
165                    key: MarkerValueVersion::PythonFullVersion,
166                    specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
167                });
168                lower.and(upper);
169                lower
170            }
171            (Bound::Included(lower), Bound::Excluded(upper)) => {
172                let mut lower = MarkerTree::expression(MarkerExpression::Version {
173                    key: MarkerValueVersion::PythonFullVersion,
174                    specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
175                });
176                let upper = MarkerTree::expression(MarkerExpression::Version {
177                    key: MarkerValueVersion::PythonFullVersion,
178                    specifier: VersionSpecifier::less_than_version(upper.clone()),
179                });
180                lower.and(upper);
181                lower
182            }
183            (Bound::Excluded(lower), Bound::Included(upper)) => {
184                let mut lower = MarkerTree::expression(MarkerExpression::Version {
185                    key: MarkerValueVersion::PythonFullVersion,
186                    specifier: VersionSpecifier::greater_than_version(lower.clone()),
187                });
188                let upper = MarkerTree::expression(MarkerExpression::Version {
189                    key: MarkerValueVersion::PythonFullVersion,
190                    specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
191                });
192                lower.and(upper);
193                lower
194            }
195            (Bound::Excluded(lower), Bound::Excluded(upper)) => {
196                let mut lower = MarkerTree::expression(MarkerExpression::Version {
197                    key: MarkerValueVersion::PythonFullVersion,
198                    specifier: VersionSpecifier::greater_than_version(lower.clone()),
199                });
200                let upper = MarkerTree::expression(MarkerExpression::Version {
201                    key: MarkerValueVersion::PythonFullVersion,
202                    specifier: VersionSpecifier::less_than_version(upper.clone()),
203                });
204                lower.and(upper);
205                lower
206            }
207            (Bound::Unbounded, Bound::Unbounded) => MarkerTree::TRUE,
208            (Bound::Unbounded, Bound::Included(upper)) => {
209                MarkerTree::expression(MarkerExpression::Version {
210                    key: MarkerValueVersion::PythonFullVersion,
211                    specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
212                })
213            }
214            (Bound::Unbounded, Bound::Excluded(upper)) => {
215                MarkerTree::expression(MarkerExpression::Version {
216                    key: MarkerValueVersion::PythonFullVersion,
217                    specifier: VersionSpecifier::less_than_version(upper.clone()),
218                })
219            }
220            (Bound::Included(lower), Bound::Unbounded) => {
221                MarkerTree::expression(MarkerExpression::Version {
222                    key: MarkerValueVersion::PythonFullVersion,
223                    specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
224                })
225            }
226            (Bound::Excluded(lower), Bound::Unbounded) => {
227                MarkerTree::expression(MarkerExpression::Version {
228                    key: MarkerValueVersion::PythonFullVersion,
229                    specifier: VersionSpecifier::greater_than_version(lower.clone()),
230                })
231            }
232        }
233    }
234
235    /// Returns `true` if the `Requires-Python` is compatible with the given version.
236    ///
237    /// N.B. This operation should primarily be used when evaluating compatibility of Python
238    /// versions against the user's own project. For example, if the user defines a
239    /// `requires-python` in a `pyproject.toml`, this operation could be used to determine whether
240    /// a given Python interpreter is compatible with the user's project.
241    pub fn contains(&self, version: &Version) -> bool {
242        let version = version.only_release();
243        self.specifiers.contains(&version)
244    }
245
246    /// Returns `true` if the `Requires-Python` is contained by the given version specifiers.
247    ///
248    /// In this context, we treat `Requires-Python` as a lower bound. For example, if the
249    /// requirement expresses `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was
250    /// intended to enable packages to drop support for older versions of Python without breaking
251    /// installations on those versions, and packages cannot know whether they are compatible with
252    /// future, unreleased versions of Python.
253    ///
254    /// The specifiers are considered to "contain" the `Requires-Python` if the specifiers are
255    /// compatible with all versions in the `Requires-Python` range (i.e., have a _lower_ lower
256    /// bound).
257    ///
258    /// For example, if the `Requires-Python` is `>=3.8`, then `>=3.7` would be considered
259    /// compatible, since all versions in the `Requires-Python` range are also covered by the
260    /// provided range. However, `>=3.9` would not be considered compatible, as the
261    /// `Requires-Python` includes Python 3.8, but `>=3.9` does not.
262    ///
263    /// N.B. This operation should primarily be used when evaluating the compatibility of a
264    /// project's `Requires-Python` specifier against a dependency's `Requires-Python` specifier.
265    pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
266        let target = release_specifiers_to_ranges(target.clone())
267            .bounding_range()
268            .map(|bounding_range| bounding_range.0.cloned())
269            .unwrap_or(Bound::Unbounded);
270
271        // We want, e.g., `self.range.lower()` to be `>=3.8` and `target` to be `>=3.7`.
272        //
273        // That is: `target` should be less than or equal to `self.range.lower()`.
274        *self.range.lower() >= LowerBound(target.clone())
275    }
276
277    /// Returns the [`VersionSpecifiers`] for the `Requires-Python` specifier.
278    pub fn specifiers(&self) -> &VersionSpecifiers {
279        &self.specifiers
280    }
281
282    /// Returns `true` if the `Requires-Python` specifier is unbounded.
283    pub fn is_unbounded(&self) -> bool {
284        self.range.lower().as_ref() == Bound::Unbounded
285    }
286
287    /// Returns `true` if the `Requires-Python` specifier is set to an exact version
288    /// without specifying a patch version. (e.g. `==3.10`)
289    pub fn is_exact_without_patch(&self) -> bool {
290        match self.range.lower().as_ref() {
291            Bound::Included(version) => {
292                version.release().len() == 2
293                    && self.range.upper().as_ref() == Bound::Included(version)
294            }
295            _ => false,
296        }
297    }
298
299    /// Returns the [`Range`] bounding the `Requires-Python` specifier.
300    pub fn range(&self) -> &RequiresPythonRange {
301        &self.range
302    }
303
304    /// Returns a wheel tag that's compatible with the `Requires-Python` specifier.
305    pub fn abi_tag(&self) -> Option<AbiTag> {
306        match self.range.lower().as_ref() {
307            Bound::Included(version) | Bound::Excluded(version) => {
308                let major = version.release().first().copied()?;
309                let major = u8::try_from(major).ok()?;
310                let minor = version.release().get(1).copied()?;
311                let minor = u8::try_from(minor).ok()?;
312                Some(AbiTag::CPython {
313                    gil_disabled: false,
314                    python_version: (major, minor),
315                })
316            }
317            Bound::Unbounded => None,
318        }
319    }
320
321    /// Simplifies the given markers in such a way as to assume that
322    /// the Python version is constrained by this Python version bound.
323    ///
324    /// For example, with `requires-python = '>=3.8'`, a marker like this:
325    ///
326    /// ```text
327    /// python_full_version >= '3.8' and python_full_version < '3.12'
328    /// ```
329    ///
330    /// Will be simplified to:
331    ///
332    /// ```text
333    /// python_full_version < '3.12'
334    /// ```
335    ///
336    /// That is, `python_full_version >= '3.8'` is assumed to be true by virtue
337    /// of `requires-python`, and is thus not needed in the marker.
338    ///
339    /// This should be used in contexts in which this assumption is valid to
340    /// make. Generally, this means it should not be used inside the resolver,
341    /// but instead near the boundaries of the system (like formatting error
342    /// messages and writing the lock file). The reason for this is that
343    /// this simplification fundamentally changes the meaning of the marker,
344    /// and the *only* correct way to interpret it is in a context in which
345    /// `requires-python` is known to be true. For example, when markers from
346    /// a lock file are deserialized and turned into a `ResolutionGraph`, the
347    /// markers are "complexified" to put the `requires-python` assumption back
348    /// into the marker explicitly.
349    pub fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
350        let (lower, upper) = (self.range().lower(), self.range().upper());
351        marker.simplify_python_versions(lower.as_ref(), upper.as_ref())
352    }
353
354    /// The inverse of `simplify_markers`.
355    ///
356    /// This should be applied near the boundaries of uv when markers are
357    /// deserialized from a context where `requires-python` is assumed. For
358    /// example, with `requires-python = '>=3.8'` and a marker like:
359    ///
360    /// ```text
361    /// python_full_version < '3.12'
362    /// ```
363    ///
364    /// It will be "complexified" to:
365    ///
366    /// ```text
367    /// python_full_version >= '3.8' and python_full_version < '3.12'
368    /// ```
369    pub fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree {
370        let (lower, upper) = (self.range().lower(), self.range().upper());
371        marker.complexify_python_versions(lower.as_ref(), upper.as_ref())
372    }
373
374    /// Returns `false` if the wheel's tags state it can't be used in the given Python version
375    /// range.
376    ///
377    /// It is meant to filter out clearly unusable wheels with perfect specificity and acceptable
378    /// sensitivity, we return `true` if the tags are unknown.
379    pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool {
380        wheel.abi_tags().iter().any(|abi_tag| {
381            if *abi_tag == AbiTag::Abi3 {
382                // Universal tags are allowed.
383                true
384            } else if *abi_tag == AbiTag::None {
385                wheel.python_tags().iter().any(|python_tag| {
386                    // Remove `py2-none-any` and `py27-none-any` and analogous `cp` and `pp` tags.
387                    if matches!(
388                        python_tag,
389                        LanguageTag::Python { major: 2, .. }
390                            | LanguageTag::CPython {
391                                python_version: (2, ..)
392                            }
393                            | LanguageTag::PyPy {
394                                python_version: (2, ..)
395                            }
396                            | LanguageTag::GraalPy {
397                                python_version: (2, ..)
398                            }
399                            | LanguageTag::Pyston {
400                                python_version: (2, ..)
401                            }
402                    ) {
403                        return false;
404                    }
405
406                    // Remove (e.g.) `py312-none-any` if the specifier is `==3.10.*`. However,
407                    // `py37-none-any` would be fine, since the `3.7` represents a lower bound.
408                    if let LanguageTag::Python {
409                        major: 3,
410                        minor: Some(minor),
411                    } = python_tag
412                    {
413                        // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
414                        let wheel_bound =
415                            UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
416                        if wheel_bound > self.range.upper().major_minor() {
417                            return false;
418                        }
419
420                        return true;
421                    }
422
423                    // Remove (e.g.) `cp36-none-any` or `cp312-none-any` if the specifier is
424                    // `==3.10.*`, since these tags require an exact match.
425                    if let LanguageTag::CPython {
426                        python_version: (3, minor),
427                    }
428                    | LanguageTag::PyPy {
429                        python_version: (3, minor),
430                    }
431                    | LanguageTag::GraalPy {
432                        python_version: (3, minor),
433                    }
434                    | LanguageTag::Pyston {
435                        python_version: (3, minor),
436                    } = python_tag
437                    {
438                        // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`.
439                        let wheel_bound =
440                            LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
441                        if wheel_bound < self.range.lower().major_minor() {
442                            return false;
443                        }
444
445                        // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
446                        let wheel_bound =
447                            UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
448                        if wheel_bound > self.range.upper().major_minor() {
449                            return false;
450                        }
451
452                        return true;
453                    }
454
455                    // Unknown tags are allowed.
456                    true
457                })
458            } else if matches!(
459                abi_tag,
460                AbiTag::CPython {
461                    python_version: (2, ..),
462                    ..
463                } | AbiTag::PyPy {
464                    python_version: None | Some((2, ..)),
465                    ..
466                } | AbiTag::GraalPy {
467                    python_version: (2, ..),
468                    ..
469                }
470            ) {
471                // Python 2 is never allowed.
472                false
473            } else if let AbiTag::CPython {
474                python_version: (3, minor),
475                ..
476            }
477            | AbiTag::PyPy {
478                python_version: Some((3, minor)),
479                ..
480            }
481            | AbiTag::GraalPy {
482                python_version: (3, minor),
483                ..
484            } = abi_tag
485            {
486                // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`.
487                let wheel_bound = LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
488                if wheel_bound < self.range.lower().major_minor() {
489                    return false;
490                }
491
492                // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`.
493                let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
494                if wheel_bound > self.range.upper().major_minor() {
495                    return false;
496                }
497
498                true
499            } else {
500                // Unknown tags are allowed.
501                true
502            }
503        })
504    }
505}
506
507impl std::fmt::Display for RequiresPython {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        std::fmt::Display::fmt(&self.specifiers, f)
510    }
511}
512
513impl serde::Serialize for RequiresPython {
514    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
515        self.specifiers.serialize(serializer)
516    }
517}
518
519impl<'de> serde::Deserialize<'de> for RequiresPython {
520    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
521        let specifiers = VersionSpecifiers::deserialize(deserializer)?;
522        let range = release_specifiers_to_ranges(specifiers.clone());
523        let range = RequiresPythonRange::from_range(&range);
524        Ok(Self { specifiers, range })
525    }
526}
527
528#[derive(Debug, Clone, Eq, PartialEq, Hash)]
529pub struct RequiresPythonRange(LowerBound, UpperBound);
530
531impl RequiresPythonRange {
532    /// Initialize a [`RequiresPythonRange`] from a [`Range`].
533    pub fn from_range(range: &Ranges<Version>) -> Self {
534        let (lower, upper) = range
535            .bounding_range()
536            .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
537            .unwrap_or((Bound::Unbounded, Bound::Unbounded));
538        Self(LowerBound(lower), UpperBound(upper))
539    }
540
541    /// Initialize a [`RequiresPythonRange`] with the given bounds.
542    pub fn new(lower: LowerBound, upper: UpperBound) -> Self {
543        Self(lower, upper)
544    }
545
546    /// Returns the lower bound.
547    pub fn lower(&self) -> &LowerBound {
548        &self.0
549    }
550
551    /// Returns the upper bound.
552    pub fn upper(&self) -> &UpperBound {
553        &self.1
554    }
555
556    /// Returns the [`VersionSpecifiers`] for the range.
557    pub fn specifiers(&self) -> VersionSpecifiers {
558        [self.0.specifier(), self.1.specifier()]
559            .into_iter()
560            .flatten()
561            .collect()
562    }
563}
564
565impl Default for RequiresPythonRange {
566    fn default() -> Self {
567        Self(LowerBound(Bound::Unbounded), UpperBound(Bound::Unbounded))
568    }
569}
570
571impl From<RequiresPythonRange> for Ranges<Version> {
572    fn from(value: RequiresPythonRange) -> Self {
573        Self::from_range_bounds::<(Bound<Version>, Bound<Version>), _>((
574            value.0.into(),
575            value.1.into(),
576        ))
577    }
578}
579
580/// A simplified marker is just like a normal marker, except it has possibly
581/// been simplified by `requires-python`.
582///
583/// A simplified marker should only exist in contexts where a `requires-python`
584/// setting can be assumed. In order to get a "normal" marker out of
585/// a simplified marker, one must re-contextualize it by adding the
586/// `requires-python` constraint back to the marker.
587#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
588pub struct SimplifiedMarkerTree(MarkerTree);
589
590impl SimplifiedMarkerTree {
591    /// Simplifies the given markers by assuming the given `requires-python`
592    /// bound is true.
593    pub fn new(requires_python: &RequiresPython, marker: MarkerTree) -> Self {
594        Self(requires_python.simplify_markers(marker))
595    }
596
597    /// Complexifies the given markers by adding the given `requires-python` as
598    /// a constraint to these simplified markers.
599    pub fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree {
600        requires_python.complexify_markers(self.0)
601    }
602
603    /// Attempts to convert this simplified marker to a string.
604    ///
605    /// This only returns `None` when the underlying marker is always true,
606    /// i.e., it matches all possible marker environments.
607    pub fn try_to_string(self) -> Option<String> {
608        self.0.try_to_string()
609    }
610
611    /// Returns the underlying marker tree without re-complexifying them.
612    pub fn as_simplified_marker_tree(self) -> MarkerTree {
613        self.0
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use std::cmp::Ordering;
620    use std::collections::Bound;
621    use std::str::FromStr;
622
623    use uv_distribution_filename::WheelFilename;
624    use uv_pep440::{LowerBound, UpperBound, Version, VersionSpecifiers};
625
626    use crate::RequiresPython;
627
628    #[test]
629    fn requires_python_included() {
630        let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
631        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
632        let wheel_names = &[
633            "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl",
634            "black-24.4.2-cp310-cp310-win_amd64.whl",
635            "black-24.4.2-cp310-none-win_amd64.whl",
636            "cbor2-5.6.4-py3-none-any.whl",
637            "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl",
638            "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl",
639            "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl",
640            "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",
641        ];
642        for wheel_name in wheel_names {
643            assert!(
644                requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
645                "{wheel_name}"
646            );
647        }
648
649        let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
650        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
651        let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"];
652        for wheel_name in wheel_names {
653            assert!(
654                requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
655                "{wheel_name}"
656            );
657        }
658
659        let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap();
660        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
661        let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"];
662        for wheel_name in wheel_names {
663            assert!(
664                requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
665                "{wheel_name}"
666            );
667        }
668
669        let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap();
670        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
671        let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"];
672        for wheel_name in wheel_names {
673            assert!(
674                requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
675                "{wheel_name}"
676            );
677        }
678    }
679
680    #[test]
681    fn requires_python_dropped() {
682        let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
683        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
684        let wheel_names = &[
685            "PySocks-1.7.1-py27-none-any.whl",
686            "black-24.4.2-cp39-cp39-win_amd64.whl",
687            "dearpygui-1.11.1-cp312-cp312-win_amd64.whl",
688            "psutil-6.0.0-cp27-none-win32.whl",
689            "psutil-6.0.0-cp36-cp36m-win32.whl",
690            "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl",
691            "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl",
692            "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl",
693            "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl",
694        ];
695        for wheel_name in wheel_names {
696            assert!(
697                !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
698                "{wheel_name}"
699            );
700        }
701
702        let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
703        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
704        let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"];
705        for wheel_name in wheel_names {
706            assert!(
707                !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
708                "{wheel_name}"
709            );
710        }
711    }
712
713    #[test]
714    fn lower_bound_ordering() {
715        let versions = &[
716            // No bound
717            LowerBound::new(Bound::Unbounded),
718            // >=3.8
719            LowerBound::new(Bound::Included(Version::new([3, 8]))),
720            // >3.8
721            LowerBound::new(Bound::Excluded(Version::new([3, 8]))),
722            // >=3.8.1
723            LowerBound::new(Bound::Included(Version::new([3, 8, 1]))),
724            // >3.8.1
725            LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
726        ];
727        for (i, v1) in versions.iter().enumerate() {
728            for v2 in &versions[i + 1..] {
729                assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
730            }
731        }
732    }
733
734    #[test]
735    fn upper_bound_ordering() {
736        let versions = &[
737            // <3.8
738            UpperBound::new(Bound::Excluded(Version::new([3, 8]))),
739            // <=3.8
740            UpperBound::new(Bound::Included(Version::new([3, 8]))),
741            // <3.8.1
742            UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
743            // <=3.8.1
744            UpperBound::new(Bound::Included(Version::new([3, 8, 1]))),
745            // No bound
746            UpperBound::new(Bound::Unbounded),
747        ];
748        for (i, v1) in versions.iter().enumerate() {
749            for v2 in &versions[i + 1..] {
750                assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
751            }
752        }
753    }
754
755    #[test]
756    fn is_exact_without_patch() {
757        let test_cases = [
758            ("==3.12", true),
759            ("==3.10, <3.11", true),
760            ("==3.10, <=3.11", true),
761            ("==3.12.1", false),
762            ("==3.12.*", false),
763            ("==3.*", false),
764            (">=3.10", false),
765            (">3.9", false),
766            ("<4.0", false),
767            (">=3.10, <3.11", false),
768            ("", false),
769        ];
770        for (version, expected) in test_cases {
771            let version_specifiers = VersionSpecifiers::from_str(version).unwrap();
772            let requires_python = RequiresPython::from_specifiers(&version_specifiers);
773            assert_eq!(requires_python.is_exact_without_patch(), expected);
774        }
775    }
776
777    #[test]
778    fn split_version() {
779        // Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`.
780        let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
781        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
782        let (lower, upper) = requires_python
783            .split(Bound::Excluded(Version::new([3, 12])))
784            .unwrap();
785        assert_eq!(
786            lower,
787            RequiresPython::from_specifiers(
788                &VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap()
789            )
790        );
791        assert_eq!(
792            upper,
793            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap())
794        );
795
796        // Splitting `>=3.10` on `>=3.12` should result in `>=3.10, <3.12` and `>=3.12`.
797        let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
798        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
799        let (lower, upper) = requires_python
800            .split(Bound::Included(Version::new([3, 12])))
801            .unwrap();
802        assert_eq!(
803            lower,
804            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap())
805        );
806        assert_eq!(
807            upper,
808            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap())
809        );
810
811        // Splitting `>=3.10` on `>=3.9` should return `None`.
812        let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
813        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
814        assert!(
815            requires_python
816                .split(Bound::Included(Version::new([3, 9])))
817                .is_none()
818        );
819
820        // Splitting `>=3.10` on `>=3.10` should return `None`.
821        let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
822        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
823        assert!(
824            requires_python
825                .split(Bound::Included(Version::new([3, 10])))
826                .is_none()
827        );
828
829        // Splitting `>=3.9, <3.13` on `>=3.11` should result in `>=3.9, <3.11` and `>=3.11, <3.13`.
830        let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap();
831        let requires_python = RequiresPython::from_specifiers(&version_specifiers);
832        let (lower, upper) = requires_python
833            .split(Bound::Included(Version::new([3, 11])))
834            .unwrap();
835        assert_eq!(
836            lower,
837            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap())
838        );
839        assert_eq!(
840            upper,
841            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap())
842        );
843    }
844}