use std::cmp::Ordering; use std::collections::Bound; use std::ops::Deref; use pubgrub::Range; use uv_distribution_filename::WheelFilename; use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion}; use uv_platform_tags::AbiTag; /// The `Requires-Python` requirement specifier. /// /// See: #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPython { /// The supported Python versions as provides by the user, usually through the `requires-python` /// field in `pyproject.toml`. /// /// For a workspace, it's the intersection of all `requires-python` values in the workspace. If /// no bound was provided by the user, it's greater equal the current Python version. /// /// The specifiers remain static over the lifetime of the workspace, such that they /// represent the initial Python version constraints. specifiers: VersionSpecifiers, /// The lower and upper bounds of the given specifiers. /// /// The range may be narrowed over the course of dependency resolution as the resolver /// investigates environments with stricter Python version constraints. range: RequiresPythonRange, } impl RequiresPython { /// Returns a [`RequiresPython`] to express `>=` equality with the given version. pub fn greater_than_equal_version(version: &Version) -> Self { let version = version.only_release(); Self { specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version( version.clone(), )), range: RequiresPythonRange( LowerBound::new(Bound::Included(version.clone())), UpperBound::new(Bound::Unbounded), ), } } /// Returns a [`RequiresPython`] from a version specifier. pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Self { let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone()) .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Self { specifiers: specifiers.clone(), range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)), } } /// Returns a [`RequiresPython`] to express the intersection of the given version specifiers. /// /// For example, given `>=3.8` and `>=3.9`, this would return `>=3.9`. pub fn intersection<'a>( specifiers: impl Iterator, ) -> Option { // Convert to PubGrub range and perform an intersection. let range = specifiers .into_iter() .map(|specifier| release_specifiers_to_ranges(specifier.clone())) .fold(None, |range: Option>, requires_python| { if let Some(range) = range { Some(range.intersection(&requires_python)) } else { Some(requires_python) } })?; // Convert back to PEP 440 specifiers. let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter()); // Extract the bounds. let range = RequiresPythonRange::from_range(&range); Some(Self { specifiers, range }) } /// Split the [`RequiresPython`] at the given version. /// /// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then /// the result will be `>=3.10 and <3.11` and `>=3.11`. pub fn split(&self, bound: Bound) -> Option<(Self, Self)> { let RequiresPythonRange(.., upper) = &self.range; let upper = Range::from_range_bounds((bound, upper.clone().into())); let lower = upper.complement(); // Intersect left and right with the existing range. let lower = lower.intersection(&Range::from(self.range.clone())); let upper = upper.intersection(&Range::from(self.range.clone())); if lower.is_empty() || upper.is_empty() { None } else { Some(( Self { specifiers: VersionSpecifiers::from_release_only_bounds(lower.iter()), range: RequiresPythonRange::from_range(&lower), }, Self { specifiers: VersionSpecifiers::from_release_only_bounds(upper.iter()), range: RequiresPythonRange::from_range(&upper), }, )) } } /// Narrow the [`RequiresPython`] by computing the intersection with the given range. pub fn narrow(&self, range: &RequiresPythonRange) -> Option { let lower = if range.0 >= self.range.0 { Some(&range.0) } else { None }; let upper = if range.1 <= self.range.1 { Some(&range.1) } else { None }; // TODO(charlie): Consider re-computing the specifiers (or removing them entirely in favor // of tracking the range). After narrowing, the specifiers and range may be out of sync. match (lower, upper) { (Some(lower), Some(upper)) => Some(Self { specifiers: self.specifiers.clone(), range: RequiresPythonRange(lower.clone(), upper.clone()), }), (Some(lower), None) => Some(Self { specifiers: self.specifiers.clone(), range: RequiresPythonRange(lower.clone(), self.range.1.clone()), }), (None, Some(upper)) => Some(Self { specifiers: self.specifiers.clone(), range: RequiresPythonRange(self.range.0.clone(), upper.clone()), }), (None, None) => None, } } /// Returns this `Requires-Python` specifier as an equivalent /// [`MarkerTree`] utilizing the `python_full_version` marker field. /// /// This is useful for comparing a `Requires-Python` specifier with /// arbitrary marker expressions. For example, one can ask whether the /// returned marker expression is disjoint with another marker expression. /// If it is, then one can conclude that the `Requires-Python` specifier /// excludes the dependency with that other marker expression. /// /// If this `Requires-Python` specifier has no constraints, then this /// returns a marker tree that evaluates to `true` for all possible marker /// environments. pub fn to_marker_tree(&self) -> MarkerTree { match (self.range.0.as_ref(), self.range.1.as_ref()) { (Bound::Included(lower), Bound::Included(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }); lower.and(upper); lower } (Bound::Included(lower), Bound::Excluded(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }); lower.and(upper); lower } (Bound::Excluded(lower), Bound::Included(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }); lower.and(upper); lower } (Bound::Excluded(lower), Bound::Excluded(upper)) => { let mut lower = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }); let upper = MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }); lower.and(upper); lower } (Bound::Unbounded, Bound::Unbounded) => MarkerTree::TRUE, (Bound::Unbounded, Bound::Included(upper)) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_equal_version(upper.clone()), }) } (Bound::Unbounded, Bound::Excluded(upper)) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::less_than_version(upper.clone()), }) } (Bound::Included(lower), Bound::Unbounded) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_equal_version(lower.clone()), }) } (Bound::Excluded(lower), Bound::Unbounded) => { MarkerTree::expression(MarkerExpression::Version { key: MarkerValueVersion::PythonFullVersion, specifier: VersionSpecifier::greater_than_version(lower.clone()), }) } } } /// Returns `true` if the `Requires-Python` is compatible with the given version. /// /// N.B. This operation should primarily be used when evaluating compatibility of Python /// versions against the user's own project. For example, if the user defines a /// `requires-python` in a `pyproject.toml`, this operation could be used to determine whether /// a given Python interpreter is compatible with the user's project. pub fn contains(&self, version: &Version) -> bool { let version = version.only_release(); self.specifiers.contains(&version) } /// Returns `true` if the `Requires-Python` is contained by the given version specifiers. /// /// In this context, we treat `Requires-Python` as a lower bound. For example, if the /// requirement expresses `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was /// intended to enable packages to drop support for older versions of Python without breaking /// installations on those versions, and packages cannot know whether they are compatible with /// future, unreleased versions of Python. /// /// The specifiers are considered to "contain" the `Requires-Python` if the specifiers are /// compatible with all versions in the `Requires-Python` range (i.e., have a _lower_ lower /// bound). /// /// For example, if the `Requires-Python` is `>=3.8`, then `>=3.7` would be considered /// compatible, since all versions in the `Requires-Python` range are also covered by the /// provided range. However, `>=3.9` would not be considered compatible, as the /// `Requires-Python` includes Python 3.8, but `>=3.9` does not. /// /// N.B. This operation should primarily be used when evaluating the compatibility of a /// project's `Requires-Python` specifier against a dependency's `Requires-Python` specifier. pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool { let target = release_specifiers_to_ranges(target.clone()) .bounding_range() .map(|bounding_range| bounding_range.0.cloned()) .unwrap_or(Bound::Unbounded); // We want, e.g., `self.range.lower()` to be `>=3.8` and `target` to be `>=3.7`. // // That is: `target` should be less than or equal to `self.range.lower()`. *self.range.lower() >= LowerBound(target.clone()) } /// Returns the [`VersionSpecifiers`] for the `Requires-Python` specifier. pub fn specifiers(&self) -> &VersionSpecifiers { &self.specifiers } /// Returns `true` if the `Requires-Python` specifier is unbounded. pub fn is_unbounded(&self) -> bool { self.range.lower().as_ref() == Bound::Unbounded } /// Returns `true` if the `Requires-Python` specifier is set to an exact version /// without specifying a patch version. (e.g. `==3.10`) pub fn is_exact_without_patch(&self) -> bool { match self.range.lower().as_ref() { Bound::Included(version) => { version.release().len() == 2 && self.range.upper().as_ref() == Bound::Included(version) } _ => false, } } /// Returns the [`Range`] bounding the `Requires-Python` specifier. pub fn range(&self) -> &RequiresPythonRange { &self.range } /// Returns a wheel tag that's compatible with the `Requires-Python` specifier. pub fn abi_tag(&self) -> Option { match self.range.lower().as_ref() { Bound::Included(version) | Bound::Excluded(version) => { let major = version.release().first().copied()?; let major = u8::try_from(major).ok()?; let minor = version.release().get(1).copied()?; let minor = u8::try_from(minor).ok()?; Some(AbiTag::CPython { gil_disabled: false, python_version: (major, minor), }) } Bound::Unbounded => None, } } /// Simplifies the given markers in such a way as to assume that /// the Python version is constrained by this Python version bound. /// /// For example, with `requires-python = '>=3.8'`, a marker like this: /// /// ```text /// python_full_version >= '3.8' and python_full_version < '3.12' /// ``` /// /// Will be simplified to: /// /// ```text /// python_full_version < '3.12' /// ``` /// /// That is, `python_full_version >= '3.8'` is assumed to be true by virtue /// of `requires-python`, and is thus not needed in the marker. /// /// This should be used in contexts in which this assumption is valid to /// make. Generally, this means it should not be used inside the resolver, /// but instead near the boundaries of the system (like formatting error /// messages and writing the lock file). The reason for this is that /// this simplification fundamentally changes the meaning of the marker, /// and the *only* correct way to interpret it is in a context in which /// `requires-python` is known to be true. For example, when markers from /// a lock file are deserialized and turned into a `ResolutionGraph`, the /// markers are "complexified" to put the `requires-python` assumption back /// into the marker explicitly. pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.simplify_python_versions(lower.0.as_ref(), upper.0.as_ref()) } /// The inverse of `simplify_markers`. /// /// This should be applied near the boundaries of uv when markers are /// deserialized from a context where `requires-python` is assumed. For /// example, with `requires-python = '>=3.8'` and a marker like: /// /// ```text /// python_full_version < '3.12' /// ``` /// /// It will be "complexified" to: /// /// ```text /// python_full_version >= '3.8' and python_full_version < '3.12' /// ``` pub(crate) fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree { let (lower, upper) = (self.range().lower(), self.range().upper()); marker.complexify_python_versions(lower.0.as_ref(), upper.0.as_ref()) } /// Returns `false` if the wheel's tags state it can't be used in the given Python version /// range. /// /// It is meant to filter out clearly unusable wheels with perfect specificity and acceptable /// sensitivity, we return `true` if the tags are unknown. pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool { wheel.abi_tag.iter().any(|abi_tag| { if abi_tag == "abi3" { // Universal tags are allowed. true } else if abi_tag == "none" { wheel.python_tag.iter().any(|python_tag| { // Remove `py2-none-any` and `py27-none-any` and analogous `cp` and `pp` tags. if python_tag.starts_with("py2") || python_tag.starts_with("cp2") || python_tag.starts_with("pp2") { return false; } // Remove (e.g.) `py312-none-any` if the specifier is `==3.10.*`. However, // `py37-none-any` would be fine, since the `3.7` represents a lower bound. if let Some(minor) = python_tag.strip_prefix("py3") { let Ok(minor) = minor.parse::() else { return true; }; // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor]))); if wheel_bound > self.range.upper().major_minor() { return false; } return true; }; // Remove (e.g.) `cp36-none-any` or `cp312-none-any` if the specifier is // `==3.10.*`, since these tags require an exact match. if let Some(minor) = python_tag .strip_prefix("cp3") .or_else(|| python_tag.strip_prefix("pp3")) { let Ok(minor) = minor.parse::() else { return true; }; // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor]))); if wheel_bound < self.range.lower().major_minor() { return false; } // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor]))); if wheel_bound > self.range.upper().major_minor() { return false; } return true; } // Unknown tags are allowed. true }) } else if abi_tag.starts_with("cp2") || abi_tag.starts_with("pypy2") { // Python 2 is never allowed. false } else if let Some(minor_no_dot_abi) = abi_tag.strip_prefix("cp3") { // Remove ABI tags, both old (dmu) and future (t, and all other letters). let minor_not_dot = minor_no_dot_abi.trim_matches(char::is_alphabetic); let Ok(minor) = minor_not_dot.parse::() else { // Unknown version pattern are allowed. return true; }; // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor]))); if wheel_bound < self.range.lower().major_minor() { return false; } // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor]))); if wheel_bound > self.range.upper().major_minor() { return false; } true } else if let Some(minor_no_dot_abi) = abi_tag.strip_prefix("pypy3") { // Given `pypy39_pp73`, we just removed `pypy3`, now we remove `_pp73` ... let Some((minor_not_dot, _)) = minor_no_dot_abi.split_once('_') else { // Unknown version pattern are allowed. return true; }; // ... and get `9`. let Ok(minor) = minor_not_dot.parse::() else { // Unknown version pattern are allowed. return true; }; // Ex) If the wheel bound is `3.6`, then it doesn't match `>=3.10`. let wheel_bound = LowerBound(Bound::Included(Version::new([3, minor]))); if wheel_bound < self.range.lower().major_minor() { return false; } // Ex) If the wheel bound is `3.12`, then it doesn't match `<=3.10.`. let wheel_bound = UpperBound(Bound::Included(Version::new([3, minor]))); if wheel_bound > self.range.upper().major_minor() { return false; } true } else { // Unknown tags are allowed. true } }) } } impl std::fmt::Display for RequiresPython { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.specifiers, f) } } impl serde::Serialize for RequiresPython { fn serialize(&self, serializer: S) -> Result { self.specifiers.serialize(serializer) } } impl<'de> serde::Deserialize<'de> for RequiresPython { fn deserialize>(deserializer: D) -> Result { let specifiers = VersionSpecifiers::deserialize(deserializer)?; let range = release_specifiers_to_ranges(specifiers.clone()); let range = RequiresPythonRange::from_range(&range); Ok(Self { specifiers, range }) } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPythonRange(LowerBound, UpperBound); impl RequiresPythonRange { /// Initialize a [`RequiresPythonRange`] from a [`Range`]. pub fn from_range(range: &Range) -> Self { let (lower, upper) = range .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Self(LowerBound(lower), UpperBound(upper)) } /// Initialize a [`RequiresPythonRange`] with the given bounds. pub fn new(lower: LowerBound, upper: UpperBound) -> Self { Self(lower, upper) } /// Returns the lower bound. pub fn lower(&self) -> &LowerBound { &self.0 } /// Returns the upper bound. pub fn upper(&self) -> &UpperBound { &self.1 } } impl Default for RequiresPythonRange { fn default() -> Self { Self(LowerBound(Bound::Unbounded), UpperBound(Bound::Unbounded)) } } impl From for Range { fn from(value: RequiresPythonRange) -> Self { Range::from_range_bounds::<(Bound, Bound), _>(( value.0.into(), value.1.into(), )) } } /// A simplified marker is just like a normal marker, except it has possibly /// been simplified by `requires-python`. /// /// A simplified marker should only exist in contexts where a `requires-python` /// setting can be assumed. In order to get a "normal" marker out of /// a simplified marker, one must re-contextualize it by adding the /// `requires-python` constraint back to the marker. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)] pub(crate) struct SimplifiedMarkerTree(MarkerTree); impl SimplifiedMarkerTree { /// Simplifies the given markers by assuming the given `requires-python` /// bound is true. pub(crate) fn new( requires_python: &RequiresPython, marker: MarkerTree, ) -> SimplifiedMarkerTree { SimplifiedMarkerTree(requires_python.simplify_markers(marker)) } /// Complexifies the given markers by adding the given `requires-python` as /// a constraint to these simplified markers. pub(crate) fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree { requires_python.complexify_markers(self.0) } /// Attempts to convert this simplified marker to a string. /// /// This only returns `None` when the underlying marker is always true, /// i.e., it matches all possible marker environments. pub(crate) fn try_to_string(self) -> Option { self.0.try_to_string() } /// Returns the underlying marker tree without re-complexifying them. pub(crate) fn as_simplified_marker_tree(self) -> MarkerTree { self.0 } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct LowerBound(Bound); impl LowerBound { /// Initialize a [`LowerBound`] with the given bound. /// /// These bounds use release-only semantics when comparing versions. pub fn new(bound: Bound) -> Self { Self(match bound { Bound::Included(version) => Bound::Included(version.only_release()), Bound::Excluded(version) => Bound::Excluded(version.only_release()), Bound::Unbounded => Bound::Unbounded, }) } /// Return the [`LowerBound`] truncated to the major and minor version. fn major_minor(&self) -> Self { match &self.0 { // Ex) `>=3.10.1` -> `>=3.10` Bound::Included(version) => Self(Bound::Included(Version::new( version.release().iter().take(2), ))), // Ex) `>3.10.1` -> `>=3.10`. Bound::Excluded(version) => Self(Bound::Included(Version::new( version.release().iter().take(2), ))), Bound::Unbounded => Self(Bound::Unbounded), } } /// Returns `true` if the lower bound contains the given version. pub fn contains(&self, version: &Version) -> bool { match self.0 { Bound::Included(ref bound) => bound <= version, Bound::Excluded(ref bound) => bound < version, Bound::Unbounded => true, } } } impl PartialOrd for LowerBound { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } /// See: impl Ord for LowerBound { fn cmp(&self, other: &Self) -> Ordering { let left = self.0.as_ref(); let right = other.0.as_ref(); match (left, right) { // left: ∞----- // right: ∞----- (Bound::Unbounded, Bound::Unbounded) => Ordering::Equal, // left: [--- // right: ∞----- (Bound::Included(_left), Bound::Unbounded) => Ordering::Greater, // left: ]--- // right: ∞----- (Bound::Excluded(_left), Bound::Unbounded) => Ordering::Greater, // left: ∞----- // right: [--- (Bound::Unbounded, Bound::Included(_right)) => Ordering::Less, // left: [----- OR [----- OR [----- // right: [--- OR [----- OR [--- (Bound::Included(left), Bound::Included(right)) => left.cmp(right), (Bound::Excluded(left), Bound::Included(right)) => match left.cmp(right) { // left: ]----- // right: [--- Ordering::Less => Ordering::Less, // left: ]----- // right: [--- Ordering::Equal => Ordering::Greater, // left: ]--- // right: [----- Ordering::Greater => Ordering::Greater, }, // left: ∞----- // right: ]--- (Bound::Unbounded, Bound::Excluded(_right)) => Ordering::Less, (Bound::Included(left), Bound::Excluded(right)) => match left.cmp(right) { // left: [----- // right: ]--- Ordering::Less => Ordering::Less, // left: [----- // right: ]--- Ordering::Equal => Ordering::Less, // left: [--- // right: ]----- Ordering::Greater => Ordering::Greater, }, // left: ]----- OR ]----- OR ]--- // right: ]--- OR ]----- OR ]----- (Bound::Excluded(left), Bound::Excluded(right)) => left.cmp(right), } } } impl Default for LowerBound { fn default() -> Self { Self(Bound::Unbounded) } } impl Deref for LowerBound { type Target = Bound; fn deref(&self) -> &Self::Target { &self.0 } } impl From for Bound { fn from(bound: LowerBound) -> Self { bound.0 } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct UpperBound(Bound); impl UpperBound { /// Initialize a [`UpperBound`] with the given bound. /// /// These bounds use release-only semantics when comparing versions. pub fn new(bound: Bound) -> Self { Self(match bound { Bound::Included(version) => Bound::Included(version.only_release()), Bound::Excluded(version) => Bound::Excluded(version.only_release()), Bound::Unbounded => Bound::Unbounded, }) } /// Return the [`UpperBound`] truncated to the major and minor version. fn major_minor(&self) -> Self { match &self.0 { // Ex) `<=3.10.1` -> `<=3.10` Bound::Included(version) => Self(Bound::Included(Version::new( version.release().iter().take(2), ))), // Ex) `<3.10.1` -> `<=3.10` (but `<3.10.0` is `<3.10`) Bound::Excluded(version) => { if version.release().get(2).is_some_and(|patch| *patch > 0) { Self(Bound::Included(Version::new( version.release().iter().take(2), ))) } else { Self(Bound::Excluded(Version::new( version.release().iter().take(2), ))) } } Bound::Unbounded => Self(Bound::Unbounded), } } /// Returns `true` if the upper bound contains the given version. pub fn contains(&self, version: &Version) -> bool { match self.0 { Bound::Included(ref bound) => bound >= version, Bound::Excluded(ref bound) => bound > version, Bound::Unbounded => true, } } } impl PartialOrd for UpperBound { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } /// See: impl Ord for UpperBound { fn cmp(&self, other: &Self) -> Ordering { let left = self.0.as_ref(); let right = other.0.as_ref(); match (left, right) { // left: -----∞ // right: -----∞ (Bound::Unbounded, Bound::Unbounded) => Ordering::Equal, // left: ---] // right: -----∞ (Bound::Included(_left), Bound::Unbounded) => Ordering::Less, // left: ---[ // right: -----∞ (Bound::Excluded(_left), Bound::Unbounded) => Ordering::Less, // left: -----∞ // right: ---] (Bound::Unbounded, Bound::Included(_right)) => Ordering::Greater, // left: -----] OR -----] OR ---] // right: ---] OR -----] OR -----] (Bound::Included(left), Bound::Included(right)) => left.cmp(right), (Bound::Excluded(left), Bound::Included(right)) => match left.cmp(right) { // left: ---[ // right: -----] Ordering::Less => Ordering::Less, // left: -----[ // right: -----] Ordering::Equal => Ordering::Less, // left: -----[ // right: ---] Ordering::Greater => Ordering::Greater, }, (Bound::Unbounded, Bound::Excluded(_right)) => Ordering::Greater, (Bound::Included(left), Bound::Excluded(right)) => match left.cmp(right) { // left: ---] // right: -----[ Ordering::Less => Ordering::Less, // left: -----] // right: -----[ Ordering::Equal => Ordering::Greater, // left: -----] // right: ---[ Ordering::Greater => Ordering::Greater, }, // left: -----[ OR -----[ OR ---[ // right: ---[ OR -----[ OR -----[ (Bound::Excluded(left), Bound::Excluded(right)) => left.cmp(right), } } } impl Default for UpperBound { fn default() -> Self { Self(Bound::Unbounded) } } impl Deref for UpperBound { type Target = Bound; fn deref(&self) -> &Self::Target { &self.0 } } impl From for Bound { fn from(bound: UpperBound) -> Self { bound.0 } } #[cfg(test)] mod tests { use std::cmp::Ordering; use std::collections::Bound; use std::str::FromStr; use uv_distribution_filename::WheelFilename; use uv_pep440::{Version, VersionSpecifiers}; use crate::requires_python::{LowerBound, UpperBound}; use crate::RequiresPython; #[test] fn requires_python_included() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", "black-24.4.2-cp310-cp310-win_amd64.whl", "black-24.4.2-cp310-none-win_amd64.whl", "cbor2-5.6.4-py3-none-any.whl", "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl", "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl", "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl", "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", ]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"]; for wheel_name in wheel_names { assert!( requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } } #[test] fn requires_python_dropped() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &[ "PySocks-1.7.1-py27-none-any.whl", "black-24.4.2-cp39-cp39-win_amd64.whl", "dearpygui-1.11.1-cp312-cp312-win_amd64.whl", "psutil-6.0.0-cp27-none-win32.whl", "psutil-6.0.0-cp36-cp36m-win32.whl", "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl", "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl", "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl", ]; for wheel_name in wheel_names { assert!( !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"]; for wheel_name in wheel_names { assert!( !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), "{wheel_name}" ); } } #[test] fn lower_bound_ordering() { let versions = &[ // No bound LowerBound::new(Bound::Unbounded), // >=3.8 LowerBound::new(Bound::Included(Version::new([3, 8]))), // >3.8 LowerBound::new(Bound::Excluded(Version::new([3, 8]))), // >=3.8.1 LowerBound::new(Bound::Included(Version::new([3, 8, 1]))), // >3.8.1 LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))), ]; for (i, v1) in versions.iter().enumerate() { for v2 in &versions[i + 1..] { assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); } } } #[test] fn upper_bound_ordering() { let versions = &[ // <3.8 UpperBound::new(Bound::Excluded(Version::new([3, 8]))), // <=3.8 UpperBound::new(Bound::Included(Version::new([3, 8]))), // <3.8.1 UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))), // <=3.8.1 UpperBound::new(Bound::Included(Version::new([3, 8, 1]))), // No bound UpperBound::new(Bound::Unbounded), ]; for (i, v1) in versions.iter().enumerate() { for v2 in &versions[i + 1..] { assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); } } } #[test] fn is_exact_without_patch() { let test_cases = [ ("==3.12", true), ("==3.10, <3.11", true), ("==3.10, <=3.11", true), ("==3.12.1", false), ("==3.12.*", false), ("==3.*", false), (">=3.10", false), (">3.9", false), ("<4.0", false), (">=3.10, <3.11", false), ("", false), ]; for (version, expected) in test_cases { let version_specifiers = VersionSpecifiers::from_str(version).unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert_eq!(requires_python.is_exact_without_patch(), expected); } } #[test] fn split_version() { // Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Excluded(Version::new([3, 12]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers( &VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap() ) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap()) ); // Splitting `>=3.10` on `>=3.12` should result in `>=3.10, <3.12` and `>=3.12`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Included(Version::new([3, 12]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap()) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap()) ); // Splitting `>=3.10` on `>=3.9` should return `None`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert!(requires_python .split(Bound::Included(Version::new([3, 9]))) .is_none()); // Splitting `>=3.10` on `>=3.10` should return `None`. let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); assert!(requires_python .split(Bound::Included(Version::new([3, 10]))) .is_none()); // Splitting `>=3.9, <3.13` on `>=3.11` should result in `>=3.9, <3.11` and `>=3.11, <3.13`. let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers); let (lower, upper) = requires_python .split(Bound::Included(Version::new([3, 11]))) .unwrap(); assert_eq!( lower, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap()) ); assert_eq!( upper, RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap()) ); } }