Skip to content

Commit a6ab42f

Browse files
committed
Fork version selection based on requires-python requirements
1 parent f80ddf1 commit a6ab42f

File tree

10 files changed

+937
-200
lines changed

10 files changed

+937
-200
lines changed

crates/uv-distribution-types/src/prioritized_distribution.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ pub enum CompatibleDist<'a> {
6060
},
6161
}
6262

63+
impl CompatibleDist<'_> {
64+
/// Return the `requires-python` specifier for the distribution, if any.
65+
pub fn requires_python(&self) -> Option<&VersionSpecifiers> {
66+
match self {
67+
CompatibleDist::InstalledDist(_) => None,
68+
CompatibleDist::SourceDist { sdist, .. } => sdist.file.requires_python.as_ref(),
69+
CompatibleDist::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_ref(),
70+
CompatibleDist::IncompatibleWheel { sdist, .. } => sdist.file.requires_python.as_ref(),
71+
}
72+
}
73+
}
74+
6375
#[derive(Debug, PartialEq, Eq, Clone)]
6476
pub enum IncompatibleDist {
6577
/// An incompatible wheel is available.

crates/uv-resolver/src/python_requirement.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::Bound;
2+
13
use uv_pep440::Version;
24
use uv_pep508::{MarkerEnvironment, MarkerTree};
35
use uv_python::{Interpreter, PythonVersion};
@@ -84,6 +86,28 @@ impl PythonRequirement {
8486
})
8587
}
8688

89+
/// Split the [`PythonRequirement`] at the given version.
90+
///
91+
/// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then
92+
/// the result will be `>=3.10 and <3.11` and `>=3.11`.
93+
pub fn split(&self, at: Bound<Version>) -> Option<(Self, Self)> {
94+
let (lower, upper) = self.target.split(at)?;
95+
Some((
96+
Self {
97+
exact: self.exact.clone(),
98+
installed: self.installed.clone(),
99+
target: lower,
100+
source: self.source,
101+
},
102+
Self {
103+
exact: self.exact.clone(),
104+
installed: self.installed.clone(),
105+
target: upper,
106+
source: self.source,
107+
},
108+
))
109+
}
110+
87111
/// Returns `true` if the minimum version of Python required by the target is greater than the
88112
/// installed version.
89113
pub fn raises(&self, target: &RequiresPythonRange) -> bool {

crates/uv-resolver/src/requires_python.rs

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use pubgrub::Range;
21
use std::cmp::Ordering;
32
use std::collections::Bound;
43
use std::ops::Deref;
54

5+
use pubgrub::Range;
6+
67
use uv_distribution_filename::WheelFilename;
78
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers};
89
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
@@ -73,24 +74,43 @@ impl RequiresPython {
7374
}
7475
})?;
7576

76-
// Extract the bounds.
77-
let (lower_bound, upper_bound) = range
78-
.bounding_range()
79-
.map(|(lower_bound, upper_bound)| {
80-
(
81-
LowerBound(lower_bound.cloned()),
82-
UpperBound(upper_bound.cloned()),
83-
)
84-
})
85-
.unwrap_or((LowerBound::default(), UpperBound::default()));
86-
8777
// Convert back to PEP 440 specifiers.
8878
let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
8979

90-
Some(Self {
91-
specifiers,
92-
range: RequiresPythonRange(lower_bound, upper_bound),
93-
})
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 = Range::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(&Range::from(self.range.clone()));
98+
let upper = upper.intersection(&Range::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+
}
94114
}
95115

96116
/// Narrow the [`RequiresPython`] by computing the intersection with the given range.
@@ -489,21 +509,25 @@ impl serde::Serialize for RequiresPython {
489509
impl<'de> serde::Deserialize<'de> for RequiresPython {
490510
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
491511
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
492-
let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
493-
.bounding_range()
494-
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
495-
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
496-
Ok(Self {
497-
specifiers,
498-
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
499-
})
512+
let range = release_specifiers_to_ranges(specifiers.clone());
513+
let range = RequiresPythonRange::from_range(&range);
514+
Ok(Self { specifiers, range })
500515
}
501516
}
502517

503518
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
504519
pub struct RequiresPythonRange(LowerBound, UpperBound);
505520

506521
impl RequiresPythonRange {
522+
/// Initialize a [`RequiresPythonRange`] from a [`Range`].
523+
pub fn from_range(range: &Range<Version>) -> Self {
524+
let (lower, upper) = range
525+
.bounding_range()
526+
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
527+
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
528+
Self(LowerBound(lower), UpperBound(upper))
529+
}
530+
507531
/// Initialize a [`RequiresPythonRange`] with the given bounds.
508532
pub fn new(lower: LowerBound, upper: UpperBound) -> Self {
509533
Self(lower, upper)
@@ -967,4 +991,68 @@ mod tests {
967991
assert_eq!(requires_python.is_exact_without_patch(), expected);
968992
}
969993
}
994+
995+
#[test]
996+
fn split_version() {
997+
// Splitting `>=3.10` on `>3.12` should result in `>=3.10, <=3.12` and `>3.12`.
998+
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
999+
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
1000+
let (lower, upper) = requires_python
1001+
.split(Bound::Excluded(Version::new([3, 12])))
1002+
.unwrap();
1003+
assert_eq!(
1004+
lower,
1005+
RequiresPython::from_specifiers(
1006+
&VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap()
1007+
)
1008+
);
1009+
assert_eq!(
1010+
upper,
1011+
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap())
1012+
);
1013+
1014+
// Splitting `>=3.10` on `>=3.12` should result in `>=3.10, <3.12` and `>=3.12`.
1015+
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
1016+
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
1017+
let (lower, upper) = requires_python
1018+
.split(Bound::Included(Version::new([3, 12])))
1019+
.unwrap();
1020+
assert_eq!(
1021+
lower,
1022+
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap())
1023+
);
1024+
assert_eq!(
1025+
upper,
1026+
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap())
1027+
);
1028+
1029+
// Splitting `>=3.10` on `>=3.9` should return `None`.
1030+
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
1031+
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
1032+
assert!(requires_python
1033+
.split(Bound::Included(Version::new([3, 9])))
1034+
.is_none());
1035+
1036+
// Splitting `>=3.10` on `>=3.10` should return `None`.
1037+
let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
1038+
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
1039+
assert!(requires_python
1040+
.split(Bound::Included(Version::new([3, 10])))
1041+
.is_none());
1042+
1043+
// Splitting `>=3.9, <3.13` on `>=3.11` should result in `>=3.9, <3.11` and `>=3.11, <3.13`.
1044+
let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap();
1045+
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
1046+
let (lower, upper) = requires_python
1047+
.split(Bound::Included(Version::new([3, 11])))
1048+
.unwrap();
1049+
assert_eq!(
1050+
lower,
1051+
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap())
1052+
);
1053+
assert_eq!(
1054+
upper,
1055+
RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap())
1056+
);
1057+
}
9701058
}

crates/uv-resolver/src/resolver/availability.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::fmt::{Display, Formatter};
22

3-
use crate::resolver::MetadataUnavailable;
43
use uv_distribution_types::IncompatibleDist;
54
use uv_pep440::{Version, VersionSpecifiers};
65

6+
use crate::resolver::MetadataUnavailable;
7+
use crate::ResolverEnvironment;
8+
79
/// The reason why a package or a version cannot be used.
810
#[derive(Debug, Clone, Eq, PartialEq)]
911
pub(crate) enum UnavailableReason {
@@ -164,8 +166,10 @@ impl From<&MetadataUnavailable> for UnavailablePackage {
164166

165167
#[derive(Debug, Clone)]
166168
pub(crate) enum ResolverVersion {
167-
/// A usable version
168-
Available(Version),
169169
/// A version that is not usable for some reason
170170
Unavailable(Version, UnavailableVersion),
171+
/// A usable version
172+
Unforked(Version),
173+
/// A set of forks.
174+
Forked(Vec<ResolverEnvironment>),
171175
}

crates/uv-resolver/src/resolver/environment.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use std::sync::Arc;
2-
2+
use tracing::trace;
3+
use uv_pep440::VersionSpecifiers;
34
use uv_pep508::{MarkerEnvironment, MarkerTree};
45
use uv_pypi_types::{ConflictItem, ConflictItemRef, ResolverMarkerEnvironment};
56

67
use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
78
use crate::requires_python::RequiresPythonRange;
89
use crate::resolver::ForkState;
910
use crate::universal_marker::{ConflictMarker, UniversalMarker};
10-
use crate::PythonRequirement;
11+
use crate::{PythonRequirement, RequiresPython};
1112

1213
/// Represents one or more marker environments for a resolution.
1314
///
@@ -510,6 +511,50 @@ impl<'d> Forker<'d> {
510511
}
511512
}
512513

514+
/// Fork the resolver based on a `Requires-Python` specifier.
515+
pub(crate) fn fork_python_requirement(
516+
requires_python: &VersionSpecifiers,
517+
python_requirement: &PythonRequirement,
518+
env: &ResolverEnvironment,
519+
) -> Vec<ResolverEnvironment> {
520+
let requires_python = RequiresPython::from_specifiers(requires_python);
521+
let lower = requires_python.range().lower().clone();
522+
523+
// Attempt to split the current Python requirement based on the `requires-python` specifier.
524+
//
525+
// For example, if the current requirement is `>=3.10`, and the split point is `>=3.11`, then
526+
// the result will be `>=3.10 and <3.11` and `>=3.11`.
527+
//
528+
// However, if the current requirement is `>=3.10`, and the split point is `>=3.9`, then the
529+
// lower segment will be empty, so we should return an empty list.
530+
let Some((lower, upper)) = python_requirement.split(lower.into()) else {
531+
trace!(
532+
"Unable to split Python requirement `{}` via `Requires-Python` specifier `{}`",
533+
python_requirement.target(),
534+
requires_python,
535+
);
536+
return vec![];
537+
};
538+
539+
let Kind::Universal {
540+
markers: ref env_marker,
541+
..
542+
} = env.kind
543+
else {
544+
panic!("resolver must be in universal mode for forking")
545+
};
546+
547+
let mut envs = vec![];
548+
if !env_marker.is_disjoint(lower.to_marker_tree()) {
549+
envs.push(env.narrow_environment(lower.to_marker_tree()));
550+
}
551+
if !env_marker.is_disjoint(upper.to_marker_tree()) {
552+
envs.push(env.narrow_environment(upper.to_marker_tree()));
553+
}
554+
debug_assert!(!envs.is_empty(), "at least one fork should be produced");
555+
envs
556+
}
557+
513558
#[cfg(test)]
514559
mod tests {
515560
use std::ops::Bound;

0 commit comments

Comments
 (0)