Move `LowerBound` and `UpperBound` structs in `uv-pep440` (#11950)

## Summary

I want to use these in `uv-python` and there's nothing specific to the
resolver or even to Python in these structs.
This commit is contained in:
Charlie Marsh 2025-03-04 09:35:16 -08:00 committed by GitHub
parent 6132d252d6
commit 8f8c0e8918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 295 additions and 287 deletions

View File

@ -24,7 +24,9 @@
#![warn(missing_docs)]
#[cfg(feature = "version-ranges")]
pub use version_ranges::{release_specifier_to_range, release_specifiers_to_ranges};
pub use version_ranges::{
release_specifier_to_range, release_specifiers_to_ranges, LowerBound, UpperBound,
};
pub use {
version::{
LocalSegment, LocalVersion, LocalVersionSlice, Operator, OperatorParseError, Prerelease,

View File

@ -1,5 +1,8 @@
//! Convert [`VersionSpecifiers`] to [`Ranges`].
use std::cmp::Ordering;
use std::collections::Bound;
use std::ops::Deref;
use version_ranges::Ranges;
use crate::{
@ -209,3 +212,277 @@ pub fn release_specifier_to_range(specifier: VersionSpecifier) -> Ranges<Version
}
}
}
/// A lower bound for a version range.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LowerBound(pub Bound<Version>);
impl LowerBound {
/// Initialize a [`LowerBound`] with the given bound.
///
/// These bounds use release-only semantics when comparing versions.
pub fn new(bound: Bound<Version>) -> 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.
#[must_use]
pub 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,
}
}
/// Returns the [`VersionSpecifier`] for the lower bound.
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => Some(VersionSpecifier::greater_than_equal_version(
version.clone(),
)),
Bound::Excluded(version) => {
Some(VersionSpecifier::greater_than_version(version.clone()))
}
Bound::Unbounded => None,
}
}
}
impl PartialOrd for LowerBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// See: <https://github.com/pubgrub-rs/pubgrub/blob/4b4b44481c5f93f3233221dc736dd23e67e00992/src/range.rs#L324>
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<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<LowerBound> for Bound<Version> {
fn from(bound: LowerBound) -> Self {
bound.0
}
}
/// An upper bound for a version range.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct UpperBound(pub Bound<Version>);
impl UpperBound {
/// Initialize a [`UpperBound`] with the given bound.
///
/// These bounds use release-only semantics when comparing versions.
pub fn new(bound: Bound<Version>) -> 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.
#[must_use]
pub 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,
}
}
/// Returns the [`VersionSpecifier`] for the upper bound.
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => {
Some(VersionSpecifier::less_than_equal_version(version.clone()))
}
Bound::Excluded(version) => Some(VersionSpecifier::less_than_version(version.clone())),
Bound::Unbounded => None,
}
}
}
impl PartialOrd for UpperBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// See: <https://github.com/pubgrub-rs/pubgrub/blob/4b4b44481c5f93f3233221dc736dd23e67e00992/src/range.rs#L324>
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<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<UpperBound> for Bound<Version> {
fn from(bound: UpperBound) -> Self {
bound.0
}
}

View File

@ -13,7 +13,7 @@ use uv_distribution_types::{
DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
};
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
use uv_pep440::{LocalVersionSlice, Version};
use uv_pep440::{LocalVersionSlice, LowerBound, Version};
use uv_platform_tags::Tags;
use uv_static::EnvVars;
@ -24,7 +24,6 @@ use crate::fork_urls::ForkUrls;
use crate::prerelease::AllowPrerelease;
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter};
use crate::python_requirement::PythonRequirement;
use crate::requires_python::LowerBound;
use crate::resolution::ConflictingDistributionError;
use crate::resolver::{
MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,

View File

@ -2,10 +2,10 @@ use pubgrub::Ranges;
use smallvec::SmallVec;
use std::ops::Bound;
use uv_pep440::Version;
use uv_pep440::{LowerBound, UpperBound, Version};
use uv_pep508::{CanonicalMarkerValueVersion, MarkerTree, MarkerTreeKind};
use crate::requires_python::{LowerBound, RequiresPythonRange, UpperBound};
use crate::requires_python::RequiresPythonRange;
/// Returns the bounding Python versions that can satisfy the [`MarkerTree`], if it's constrained.
pub(crate) fn requires_python(tree: MarkerTree) -> Option<RequiresPythonRange> {
@ -91,10 +91,10 @@ pub(crate) fn requires_python(tree: MarkerTree) -> Option<RequiresPythonRange> {
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Bound;
use std::str::FromStr;
use super::*;
use uv_pep440::UpperBound;
#[test]
fn test_requires_python() {

View File

@ -1,11 +1,12 @@
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_pep440::{
release_specifiers_to_ranges, LowerBound, UpperBound, Version, VersionSpecifier,
VersionSpecifiers,
};
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
use uv_platform_tags::{AbiTag, LanguageTag};
@ -53,7 +54,7 @@ impl RequiresPython {
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
Self {
specifiers: specifiers.clone(),
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
range: RequiresPythonRange(LowerBound::new(lower_bound), UpperBound::new(upper_bound)),
}
}
@ -354,7 +355,7 @@ impl RequiresPython {
/// 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())
marker.simplify_python_versions(lower.as_ref(), upper.as_ref())
}
/// The inverse of `simplify_markers`.
@ -374,7 +375,7 @@ impl RequiresPython {
/// ```
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())
marker.complexify_python_versions(lower.as_ref(), upper.as_ref())
}
/// Returns `false` if the wheel's tags state it can't be used in the given Python version
@ -623,276 +624,6 @@ impl SimplifiedMarkerTree {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LowerBound(Bound<Version>);
impl LowerBound {
/// Initialize a [`LowerBound`] with the given bound.
///
/// These bounds use release-only semantics when comparing versions.
pub fn new(bound: Bound<Version>) -> 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,
}
}
/// Returns the [`VersionSpecifier`] for the lower bound.
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => Some(VersionSpecifier::greater_than_equal_version(
version.clone(),
)),
Bound::Excluded(version) => {
Some(VersionSpecifier::greater_than_version(version.clone()))
}
Bound::Unbounded => None,
}
}
}
impl PartialOrd for LowerBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// See: <https://github.com/pubgrub-rs/pubgrub/blob/4b4b44481c5f93f3233221dc736dd23e67e00992/src/range.rs#L324>
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<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<LowerBound> for Bound<Version> {
fn from(bound: LowerBound) -> Self {
bound.0
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct UpperBound(Bound<Version>);
impl UpperBound {
/// Initialize a [`UpperBound`] with the given bound.
///
/// These bounds use release-only semantics when comparing versions.
pub fn new(bound: Bound<Version>) -> 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,
}
}
/// Returns the [`VersionSpecifier`] for the upper bound.
pub fn specifier(&self) -> Option<VersionSpecifier> {
match &self.0 {
Bound::Included(version) => {
Some(VersionSpecifier::less_than_equal_version(version.clone()))
}
Bound::Excluded(version) => Some(VersionSpecifier::less_than_version(version.clone())),
Bound::Unbounded => None,
}
}
}
impl PartialOrd for UpperBound {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// See: <https://github.com/pubgrub-rs/pubgrub/blob/4b4b44481c5f93f3233221dc736dd23e67e00992/src/range.rs#L324>
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<Version>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<UpperBound> for Bound<Version> {
fn from(bound: UpperBound) -> Self {
bound.0
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
@ -900,9 +631,8 @@ mod tests {
use std::str::FromStr;
use uv_distribution_filename::WheelFilename;
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep440::{LowerBound, UpperBound, Version, VersionSpecifiers};
use crate::requires_python::{LowerBound, UpperBound};
use crate::RequiresPython;
#[test]

View File

@ -617,10 +617,10 @@ mod tests {
use std::ops::Bound;
use std::sync::LazyLock;
use uv_pep440::Version;
use uv_pep440::{LowerBound, UpperBound, Version};
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
use crate::requires_python::{LowerBound, RequiresPython, RequiresPythonRange, UpperBound};
use crate::requires_python::{RequiresPython, RequiresPythonRange};
use super::*;