use std::cmp::Ordering; use std::collections::Bound; use std::ops::Deref; use itertools::Itertools; use pubgrub::Range; use distribution_filename::WheelFilename; use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{MarkerExpression, MarkerTree, MarkerValueVersion}; #[derive(thiserror::Error, Debug)] pub enum RequiresPythonError { #[error(transparent)] PubGrub(#[from] crate::pubgrub::PubGrubSpecifierError), } /// The `Requires-Python` requirement specifier. /// /// 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. /// /// 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 union of all `requires-python` values in the workspace. If no /// bound was provided by the user, it's greater equal the current Python version. specifiers: VersionSpecifiers, /// The lower and upper bounds of `specifiers`. 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( RequiresPythonBound::new(Bound::Included(version.clone())), RequiresPythonBound::new(Bound::Unbounded), ), } } /// Returns a [`RequiresPython`] from a version specifier. pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result { let (lower_bound, upper_bound) = crate::pubgrub::PubGrubSpecifier::from_release_specifiers(specifiers)? .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Ok(Self { specifiers: specifiers.clone(), range: RequiresPythonRange( RequiresPythonBound(lower_bound), RequiresPythonBound(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, ) -> Result, RequiresPythonError> { // Convert to PubGrub range and perform an intersection. let range = specifiers .into_iter() .map(crate::pubgrub::PubGrubSpecifier::from_release_specifiers) .fold_ok(None, |range: Option>, requires_python| { if let Some(range) = range { Some(range.intersection(&requires_python.into())) } else { Some(requires_python.into()) } })?; let Some(range) = range else { return Ok(None); }; // Extract the bounds. let (lower_bound, upper_bound) = range .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); // Convert back to PEP 440 specifiers. let specifiers = range .iter() .flat_map(VersionSpecifier::from_release_only_bounds) .collect(); Ok(Some(Self { specifiers, range: RequiresPythonRange( RequiresPythonBound(lower_bound), RequiresPythonBound(upper_bound), ), })) } /// Narrow the [`RequiresPython`] to the given version, if it's stricter (i.e., greater) than /// the current target. pub fn narrow(&self, range: &RequiresPythonRange) -> Option { if *range == self.range { None } else if range.0 >= self.range.0 && range.1 <= self.range.1 { Some(Self { specifiers: self.specifiers.clone(), range: range.clone(), }) } else { None } } /// Returns the [`RequiresPython`] as a [`MarkerTree`]. pub fn markers(&self) -> MarkerTree { match (self.range.0.as_ref(), self.range.0.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. pub fn contains(&self, version: &Version) -> bool { let version = version.only_release(); self.specifiers.contains(&version) } /// Returns `true` if the `Requires-Python` is compatible with the given version specifiers. /// /// 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. pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool { let Ok(target) = crate::pubgrub::PubGrubSpecifier::from_release_specifiers(target) else { return false; }; let target = target .iter() .next() .map(|(lower, _)| lower) .unwrap_or(&Bound::Unbounded); // We want, e.g., `requires_python_lower` to be `>=3.8` and `version_lower` to be // `>=3.7`. // // That is: `version_lower` should be less than or equal to `requires_python_lower`. match (target, self.range.lower().as_ref()) { (Bound::Included(target_lower), Bound::Included(requires_python_lower)) => { target_lower <= requires_python_lower } (Bound::Excluded(target_lower), Bound::Included(requires_python_lower)) => { target_lower < requires_python_lower } (Bound::Included(target_lower), Bound::Excluded(requires_python_lower)) => { target_lower <= requires_python_lower } (Bound::Excluded(target_lower), Bound::Excluded(requires_python_lower)) => { target_lower < requires_python_lower } // If the dependency has no lower bound, then it supports all versions. (Bound::Unbounded, _) => true, // If we have no lower bound, then there must be versions we support that the // dependency does not. (_, Bound::Unbounded) => false, } } /// 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 the [`RequiresPythonBound`] truncated to the major and minor version. pub fn bound_major_minor(&self) -> RequiresPythonBound { match self.range.lower().as_ref() { // Ex) `>=3.10.1` -> `>=3.10` Bound::Included(version) => RequiresPythonBound(Bound::Included(Version::new( version.release().iter().take(2), ))), // Ex) `>3.10.1` -> `>=3.10` // This is unintuitive, but `>3.10.1` does indicate that _some_ version of Python 3.10 // is supported. Bound::Excluded(version) => RequiresPythonBound(Bound::Included(Version::new( version.release().iter().take(2), ))), Bound::Unbounded => RequiresPythonBound(Bound::Unbounded), } } /// Returns the [`Range`] bounding the `Requires-Python` specifier. pub fn range(&self) -> &RequiresPythonRange { &self.range } /// 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`. if python_tag.starts_with("py2") { return false; } // Remove (e.g.) `cp36-none-any` if the specifier is `==3.10.*`. let Some(minor) = python_tag .strip_prefix("cp3") .or_else(|| python_tag.strip_prefix("pp3")) .or_else(|| python_tag.strip_prefix("py3")) else { return true; }; 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 = RequiresPythonBound(Bound::Included(Version::new([3, minor]))); wheel_bound >= self.bound_major_minor() }) } 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; }; let wheel_bound = RequiresPythonBound(Bound::Included(Version::new([3, minor]))); wheel_bound >= self.bound_major_minor() } 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; }; let wheel_bound = RequiresPythonBound(Bound::Included(Version::new([3, minor]))); wheel_bound >= self.bound_major_minor() } else { // Unknown python tag -> 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 (lower_bound, upper_bound) = crate::pubgrub::PubGrubSpecifier::from_release_specifiers(&specifiers) .map_err(serde::de::Error::custom)? .bounding_range() .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned())) .unwrap_or((Bound::Unbounded, Bound::Unbounded)); Ok(Self { specifiers, range: RequiresPythonRange( RequiresPythonBound(lower_bound), RequiresPythonBound(upper_bound), ), }) } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPythonRange(RequiresPythonBound, RequiresPythonBound); impl RequiresPythonRange { /// Initialize a [`RequiresPythonRange`] with the given bounds. pub fn new(lower: RequiresPythonBound, upper: RequiresPythonBound) -> Self { Self(lower, upper) } /// Returns the lower bound. pub fn lower(&self) -> &RequiresPythonBound { &self.0 } /// Returns the upper bound. pub fn upper(&self) -> &RequiresPythonBound { &self.1 } } impl Default for RequiresPythonRange { fn default() -> Self { Self( RequiresPythonBound(Bound::Unbounded), RequiresPythonBound(Bound::Unbounded), ) } } impl From for Range { fn from(value: RequiresPythonRange) -> Self { Range::from_range_bounds::<(Bound, Bound), _>(( value.0.into(), value.1.into(), )) } } #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct RequiresPythonBound(Bound); impl Default for RequiresPythonBound { fn default() -> Self { Self(Bound::Unbounded) } } impl RequiresPythonBound { /// Initialize a [`RequiresPythonBound`] 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, }) } } impl Deref for RequiresPythonBound { type Target = Bound; fn deref(&self) -> &Self::Target { &self.0 } } impl From for Bound { fn from(bound: RequiresPythonBound) -> Self { bound.0 } } impl PartialOrd for RequiresPythonBound { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for RequiresPythonBound { fn cmp(&self, other: &Self) -> Ordering { match (self.as_ref(), other.as_ref()) { (Bound::Included(a), Bound::Included(b)) => a.cmp(b), (Bound::Included(a), Bound::Excluded(b)) => a.cmp(b).then(Ordering::Less), (Bound::Excluded(a), Bound::Included(b)) => a.cmp(b).then(Ordering::Greater), (Bound::Excluded(a), Bound::Excluded(b)) => a.cmp(b), (Bound::Unbounded, Bound::Unbounded) => Ordering::Equal, (Bound::Unbounded, _) => Ordering::Less, (_, Bound::Unbounded) => Ordering::Greater, } } } #[cfg(test)] mod tests { use std::cmp::Ordering; use std::collections::Bound; use std::str::FromStr; use distribution_filename::WheelFilename; use pep440_rs::{Version, VersionSpecifiers}; use crate::{RequiresPython, RequiresPythonBound}; #[test] fn requires_python_included() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); 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", "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", "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.3").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); 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}" ); } } #[test] fn requires_python_dropped() { let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); let wheel_names = &[ "PySocks-1.7.1-py27-none-any.whl", "black-24.4.2-cp39-cp39-win_amd64.whl", "psutil-6.0.0-cp36-cp36m-win32.whl", "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl", "torch-1.10.0-py36-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).unwrap(); 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 ordering() { let versions = &[ // No bound RequiresPythonBound::new(Bound::Unbounded), // >=3.8 RequiresPythonBound::new(Bound::Included(Version::new([3, 8]))), // >3.8 RequiresPythonBound::new(Bound::Excluded(Version::new([3, 8]))), // >=3.8.1 RequiresPythonBound::new(Bound::Included(Version::new([3, 8, 1]))), // >3.8.1 RequiresPythonBound::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:?}"); } } } }