mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Narrow requires-python
requirement in resolver forks (#4707)
## Summary Given: ```text numpy >=1.26 ; python_version >= '3.9' numpy <1.26 ; python_version < '3.9' ``` When resolving for Python 3.8, we need to narrow the `requires-python` requirement in the top branch of the fork, because `numpy >=1.26` all require Python 3.9 or later -- but we know (in that branch) that we only need to _solve_ for Python 3.9 or later. Closes https://github.com/astral-sh/uv/issues/4669.
This commit is contained in:
parent
89b3324ae1
commit
d9f389a58d
9 changed files with 251 additions and 58 deletions
|
@ -436,7 +436,7 @@ impl VersionSpecifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a version specifier representing the given lower bound.
|
/// Returns a version specifier representing the given lower bound.
|
||||||
fn from_lower_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
pub fn from_lower_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
||||||
match bound {
|
match bound {
|
||||||
Bound::Included(version) => Some(
|
Bound::Included(version) => Some(
|
||||||
VersionSpecifier::from_version(Operator::GreaterThanEqual, version.clone())
|
VersionSpecifier::from_version(Operator::GreaterThanEqual, version.clone())
|
||||||
|
@ -450,7 +450,7 @@ impl VersionSpecifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a version specifier representing the given upper bound.
|
/// Returns a version specifier representing the given upper bound.
|
||||||
fn from_upper_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
pub fn from_upper_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
||||||
match bound {
|
match bound {
|
||||||
Bound::Included(version) => Some(
|
Bound::Included(version) => Some(
|
||||||
VersionSpecifier::from_version(Operator::LessThanEqual, version.clone()).unwrap(),
|
VersionSpecifier::from_version(Operator::LessThanEqual, version.clone()).unwrap(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub use preferences::{Preference, PreferenceError, Preferences};
|
||||||
pub use prerelease_mode::PreReleaseMode;
|
pub use prerelease_mode::PreReleaseMode;
|
||||||
pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
||||||
pub use python_requirement::PythonRequirement;
|
pub use python_requirement::PythonRequirement;
|
||||||
pub use requires_python::{RequiresPython, RequiresPythonError};
|
pub use requires_python::{RequiresPython, RequiresPythonBound, RequiresPythonError};
|
||||||
pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
|
pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
|
||||||
pub use resolution_mode::ResolutionMode;
|
pub use resolution_mode::ResolutionMode;
|
||||||
pub use resolver::{
|
pub use resolver::{
|
||||||
|
|
|
@ -4,6 +4,8 @@ use std::collections::HashMap;
|
||||||
use std::ops::Bound::{self, *};
|
use std::ops::Bound::{self, *};
|
||||||
use std::ops::RangeBounds;
|
use std::ops::RangeBounds;
|
||||||
|
|
||||||
|
use pubgrub::range::Range as PubGrubRange;
|
||||||
|
|
||||||
use pep440_rs::{Operator, Version, VersionSpecifier};
|
use pep440_rs::{Operator, Version, VersionSpecifier};
|
||||||
use pep508_rs::{
|
use pep508_rs::{
|
||||||
ExtraName, ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
|
ExtraName, ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
|
||||||
|
@ -11,7 +13,7 @@ use pep508_rs::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::pubgrub::PubGrubSpecifier;
|
use crate::pubgrub::PubGrubSpecifier;
|
||||||
use pubgrub::range::Range as PubGrubRange;
|
use crate::RequiresPythonBound;
|
||||||
|
|
||||||
/// Returns `true` if there is no environment in which both marker trees can both apply, i.e.
|
/// Returns `true` if there is no environment in which both marker trees can both apply, i.e.
|
||||||
/// the expression `first and second` is always false.
|
/// the expression `first and second` is always false.
|
||||||
|
@ -80,6 +82,42 @@ fn string_is_disjoint(this: &MarkerExpression, other: &MarkerExpression) -> bool
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum Python version that can satisfy the [`MarkerTree`], if it's constrained.
|
||||||
|
pub(crate) fn requires_python_marker(tree: &MarkerTree) -> Option<RequiresPythonBound> {
|
||||||
|
match tree {
|
||||||
|
MarkerTree::Expression(MarkerExpression::Version {
|
||||||
|
key: MarkerValueVersion::PythonFullVersion | MarkerValueVersion::PythonVersion,
|
||||||
|
specifier,
|
||||||
|
}) => {
|
||||||
|
let specifier = PubGrubSpecifier::try_from(specifier).ok()?;
|
||||||
|
|
||||||
|
// Convert to PubGrub range and perform a union.
|
||||||
|
let range = PubGrubRange::from(specifier);
|
||||||
|
let (lower, _) = range.iter().next()?;
|
||||||
|
|
||||||
|
// Extract the lower bound.
|
||||||
|
Some(RequiresPythonBound::new(lower.clone()))
|
||||||
|
}
|
||||||
|
MarkerTree::And(trees) => {
|
||||||
|
// Take the maximum of any nested expressions.
|
||||||
|
trees.iter().filter_map(requires_python_marker).max()
|
||||||
|
}
|
||||||
|
MarkerTree::Or(trees) => {
|
||||||
|
// If all subtrees have a bound, take the minimum.
|
||||||
|
let mut min_version = None;
|
||||||
|
for tree in trees {
|
||||||
|
let version = requires_python_marker(tree)?;
|
||||||
|
min_version = match min_version {
|
||||||
|
Some(min_version) => Some(std::cmp::min(min_version, version)),
|
||||||
|
None => Some(version),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
min_version
|
||||||
|
}
|
||||||
|
MarkerTree::Expression(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Normalizes this marker tree.
|
/// Normalizes this marker tree.
|
||||||
///
|
///
|
||||||
/// This function does a number of operations to normalize a marker tree recursively:
|
/// This function does a number of operations to normalize a marker tree recursively:
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -17,9 +16,9 @@ impl Deref for PubGrubPackage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PubGrubPackage {
|
impl std::fmt::Display for PubGrubPackage {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Display::fmt(&self.0, f)
|
std::fmt::Display::fmt(&self.0, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::{MarkerTree, StringVersion};
|
use pep508_rs::{MarkerTree, StringVersion};
|
||||||
use uv_toolchain::{Interpreter, PythonVersion};
|
use uv_toolchain::{Interpreter, PythonVersion};
|
||||||
|
|
||||||
use crate::RequiresPython;
|
use crate::{RequiresPython, RequiresPythonBound};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct PythonRequirement {
|
pub struct PythonRequirement {
|
||||||
|
@ -49,6 +49,19 @@ impl PythonRequirement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Narrow the [`PythonRequirement`] to the given version, if it's stricter (i.e., greater)
|
||||||
|
/// than the current `Requires-Python` minimum.
|
||||||
|
pub fn narrow(&self, target: &RequiresPythonBound) -> Option<Self> {
|
||||||
|
let Some(PythonTarget::RequiresPython(requires_python)) = self.target.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let requires_python = requires_python.narrow(target)?;
|
||||||
|
Some(Self {
|
||||||
|
installed: self.installed.clone(),
|
||||||
|
target: Some(PythonTarget::RequiresPython(requires_python)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the installed version of Python.
|
/// Return the installed version of Python.
|
||||||
pub fn installed(&self) -> &StringVersion {
|
pub fn installed(&self) -> &StringVersion {
|
||||||
&self.installed
|
&self.installed
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::Bound;
|
use std::collections::Bound;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use pubgrub::range::Range;
|
use pubgrub::range::Range;
|
||||||
|
@ -24,7 +26,7 @@ pub enum RequiresPythonError {
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct RequiresPython {
|
pub struct RequiresPython {
|
||||||
specifiers: VersionSpecifiers,
|
specifiers: VersionSpecifiers,
|
||||||
bound: Bound<Version>,
|
bound: RequiresPythonBound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiresPython {
|
impl RequiresPython {
|
||||||
|
@ -34,7 +36,7 @@ impl RequiresPython {
|
||||||
specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(
|
specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(
|
||||||
version.clone(),
|
version.clone(),
|
||||||
)),
|
)),
|
||||||
bound: Bound::Included(version),
|
bound: RequiresPythonBound(Bound::Included(version)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,11 +63,13 @@ impl RequiresPython {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract the lower bound.
|
// Extract the lower bound.
|
||||||
let bound = range
|
let bound = RequiresPythonBound(
|
||||||
|
range
|
||||||
.iter()
|
.iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|(lower, _)| lower.clone())
|
.map(|(lower, _)| lower.clone())
|
||||||
.unwrap_or(Bound::Unbounded);
|
.unwrap_or(Bound::Unbounded),
|
||||||
|
);
|
||||||
|
|
||||||
// Convert back to PEP 440 specifiers.
|
// Convert back to PEP 440 specifiers.
|
||||||
let specifiers = range
|
let specifiers = range
|
||||||
|
@ -76,6 +80,16 @@ impl RequiresPython {
|
||||||
Ok(Some(Self { specifiers, bound }))
|
Ok(Some(Self { specifiers, bound }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Narrow the [`RequiresPython`] to the given version, if it's stricter (i.e., greater) than
|
||||||
|
/// the current target.
|
||||||
|
pub fn narrow(&self, target: &RequiresPythonBound) -> Option<Self> {
|
||||||
|
let target = VersionSpecifiers::from(VersionSpecifier::from_lower_bound(target)?);
|
||||||
|
Self::union(std::iter::once(&target))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.filter(|next| next.bound > self.bound)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if the `Requires-Python` is compatible with the given version.
|
/// Returns `true` if the `Requires-Python` is compatible with the given version.
|
||||||
pub fn contains(&self, version: &Version) -> bool {
|
pub fn contains(&self, version: &Version) -> bool {
|
||||||
self.specifiers.contains(version)
|
self.specifiers.contains(version)
|
||||||
|
@ -140,7 +154,7 @@ impl RequiresPython {
|
||||||
// Alternatively, we could vary the semantics depending on whether or not the user included
|
// Alternatively, we could vary the semantics depending on whether or not the user included
|
||||||
// a pre-release in their specifier, enforcing pre-release compatibility only if the user
|
// a pre-release in their specifier, enforcing pre-release compatibility only if the user
|
||||||
// explicitly requested it.
|
// explicitly requested it.
|
||||||
match (target, &self.bound) {
|
match (target, self.bound.as_ref()) {
|
||||||
(Bound::Included(target_lower), Bound::Included(requires_python_lower)) => {
|
(Bound::Included(target_lower), Bound::Included(requires_python_lower)) => {
|
||||||
target_lower.release() <= requires_python_lower.release()
|
target_lower.release() <= requires_python_lower.release()
|
||||||
}
|
}
|
||||||
|
@ -166,9 +180,9 @@ impl RequiresPython {
|
||||||
&self.specifiers
|
&self.specifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the lower [`Bound`] for the `Requires-Python` specifier.
|
/// Returns `true` if the `Requires-Python` specifier is unbounded.
|
||||||
pub fn bound(&self) -> &Bound<Version> {
|
pub fn is_unbounded(&self) -> bool {
|
||||||
&self.bound
|
self.bound.as_ref() == Bound::Unbounded
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns this `Requires-Python` specifier as an equivalent marker
|
/// Returns this `Requires-Python` specifier as an equivalent marker
|
||||||
|
@ -184,16 +198,14 @@ impl RequiresPython {
|
||||||
/// returns a marker tree that evaluates to `true` for all possible marker
|
/// returns a marker tree that evaluates to `true` for all possible marker
|
||||||
/// environments.
|
/// environments.
|
||||||
pub fn to_marker_tree(&self) -> MarkerTree {
|
pub fn to_marker_tree(&self) -> MarkerTree {
|
||||||
let (op, version) = match self.bound {
|
let (op, version) = match self.bound.as_ref() {
|
||||||
// If we see this anywhere, then it implies the marker
|
// If we see this anywhere, then it implies the marker
|
||||||
// tree we would generate would always evaluate to
|
// tree we would generate would always evaluate to
|
||||||
// `true` because every possible Python version would
|
// `true` because every possible Python version would
|
||||||
// satisfy it.
|
// satisfy it.
|
||||||
Bound::Unbounded => return MarkerTree::And(vec![]),
|
Bound::Unbounded => return MarkerTree::And(vec![]),
|
||||||
Bound::Excluded(ref version) => {
|
Bound::Excluded(version) => (Operator::GreaterThan, version.clone().without_local()),
|
||||||
(Operator::GreaterThan, version.clone().without_local())
|
Bound::Included(version) => {
|
||||||
}
|
|
||||||
Bound::Included(ref version) => {
|
|
||||||
(Operator::GreaterThanEqual, version.clone().without_local())
|
(Operator::GreaterThanEqual, version.clone().without_local())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -247,12 +259,50 @@ impl serde::Serialize for RequiresPython {
|
||||||
impl<'de> serde::Deserialize<'de> for RequiresPython {
|
impl<'de> serde::Deserialize<'de> for RequiresPython {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
|
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
|
||||||
let bound = crate::pubgrub::PubGrubSpecifier::try_from(&specifiers)
|
let bound = RequiresPythonBound(
|
||||||
|
crate::pubgrub::PubGrubSpecifier::try_from(&specifiers)
|
||||||
.map_err(serde::de::Error::custom)?
|
.map_err(serde::de::Error::custom)?
|
||||||
.iter()
|
.iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|(lower, _)| lower.clone())
|
.map(|(lower, _)| lower.clone())
|
||||||
.unwrap_or(Bound::Unbounded);
|
.unwrap_or(Bound::Unbounded),
|
||||||
|
);
|
||||||
Ok(Self { specifiers, bound })
|
Ok(Self { specifiers, bound })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct RequiresPythonBound(Bound<Version>);
|
||||||
|
|
||||||
|
impl RequiresPythonBound {
|
||||||
|
pub fn new(bound: Bound<Version>) -> Self {
|
||||||
|
Self(bound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for RequiresPythonBound {
|
||||||
|
type Target = Bound<Version>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for RequiresPythonBound {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
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(_), Bound::Excluded(_)) => Ordering::Less,
|
||||||
|
(Bound::Excluded(_), Bound::Included(_)) => Ordering::Greater,
|
||||||
|
(Bound::Excluded(a), Bound::Excluded(b)) => a.cmp(b),
|
||||||
|
(Bound::Unbounded, _) => Ordering::Less,
|
||||||
|
(_, Bound::Unbounded) => Ordering::Greater,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ use crate::dependency_provider::UvDependencyProvider;
|
||||||
use crate::error::ResolveError;
|
use crate::error::ResolveError;
|
||||||
use crate::fork_urls::ForkUrls;
|
use crate::fork_urls::ForkUrls;
|
||||||
use crate::manifest::Manifest;
|
use crate::manifest::Manifest;
|
||||||
use crate::marker::normalize;
|
use crate::marker::{normalize, requires_python_marker};
|
||||||
use crate::pins::FilePins;
|
use crate::pins::FilePins;
|
||||||
use crate::preferences::Preferences;
|
use crate::preferences::Preferences;
|
||||||
use crate::pubgrub::{
|
use crate::pubgrub::{
|
||||||
|
@ -312,6 +312,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
visited: &mut FxHashSet<PackageName>,
|
visited: &mut FxHashSet<PackageName>,
|
||||||
request_sink: Sender<Request>,
|
request_sink: Sender<Request>,
|
||||||
) -> Result<ResolutionGraph, ResolveError> {
|
) -> Result<ResolutionGraph, ResolveError> {
|
||||||
|
debug!(
|
||||||
|
"Solving with installed Python version: {}",
|
||||||
|
self.python_requirement.installed()
|
||||||
|
);
|
||||||
|
if let Some(target) = self.python_requirement.target() {
|
||||||
|
debug!("Solving with target Python version: {}", target);
|
||||||
|
}
|
||||||
|
|
||||||
let root = PubGrubPackage::from(PubGrubPackageInner::Root(self.project.clone()));
|
let root = PubGrubPackage::from(PubGrubPackageInner::Root(self.project.clone()));
|
||||||
let mut prefetcher = BatchPrefetcher::default();
|
let mut prefetcher = BatchPrefetcher::default();
|
||||||
let state = ForkState {
|
let state = ForkState {
|
||||||
|
@ -322,23 +330,21 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
priorities: PubGrubPriorities::default(),
|
priorities: PubGrubPriorities::default(),
|
||||||
added_dependencies: FxHashMap::default(),
|
added_dependencies: FxHashMap::default(),
|
||||||
markers: MarkerTree::And(vec![]),
|
markers: MarkerTree::And(vec![]),
|
||||||
|
python_requirement: self.python_requirement.clone(),
|
||||||
|
requires_python: self.requires_python.clone(),
|
||||||
};
|
};
|
||||||
let mut preferences = self.preferences.clone();
|
let mut preferences = self.preferences.clone();
|
||||||
let mut forked_states = vec![state];
|
let mut forked_states = vec![state];
|
||||||
let mut resolutions = vec![];
|
let mut resolutions = vec![];
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Solving with installed Python version: {}",
|
|
||||||
self.python_requirement.installed()
|
|
||||||
);
|
|
||||||
if let Some(target) = self.python_requirement.target() {
|
|
||||||
debug!("Solving with target Python version: {}", target);
|
|
||||||
}
|
|
||||||
|
|
||||||
'FORK: while let Some(mut state) = forked_states.pop() {
|
'FORK: while let Some(mut state) = forked_states.pop() {
|
||||||
if !state.markers.is_universal() {
|
if !state.markers.is_universal() {
|
||||||
|
if let Some(requires_python) = state.requires_python.as_ref() {
|
||||||
|
debug!("Solving split {} ({})", state.markers, requires_python);
|
||||||
|
} else {
|
||||||
debug!("Solving split {}", state.markers);
|
debug!("Solving split {}", state.markers);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
// Run unit propagation.
|
// Run unit propagation.
|
||||||
|
@ -424,6 +430,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
&mut state.pins,
|
&mut state.pins,
|
||||||
&preferences,
|
&preferences,
|
||||||
&state.fork_urls,
|
&state.fork_urls,
|
||||||
|
&state.python_requirement,
|
||||||
visited,
|
visited,
|
||||||
&request_sink,
|
&request_sink,
|
||||||
)?;
|
)?;
|
||||||
|
@ -502,6 +509,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
&version,
|
&version,
|
||||||
&state.fork_urls,
|
&state.fork_urls,
|
||||||
&state.markers,
|
&state.markers,
|
||||||
|
state.requires_python.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
match forked_deps {
|
match forked_deps {
|
||||||
ForkedDependencies::Unavailable(reason) => {
|
ForkedDependencies::Unavailable(reason) => {
|
||||||
|
@ -562,10 +570,29 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
if !is_last {
|
if !is_last {
|
||||||
cur_state = Some(forked_state.clone());
|
cur_state = Some(forked_state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
forked_state.markers.and(fork.markers);
|
forked_state.markers.and(fork.markers);
|
||||||
forked_state.markers = normalize(forked_state.markers)
|
forked_state.markers = normalize(forked_state.markers)
|
||||||
.unwrap_or(MarkerTree::And(Vec::new()));
|
.unwrap_or(MarkerTree::And(Vec::new()));
|
||||||
|
|
||||||
|
// If the fork contains a narrowed Python requirement, apply it.
|
||||||
|
let python_requirement = requires_python_marker(
|
||||||
|
&forked_state.markers,
|
||||||
|
)
|
||||||
|
.and_then(|marker| forked_state.python_requirement.narrow(&marker));
|
||||||
|
if let Some(python_requirement) = python_requirement {
|
||||||
|
if let Some(target) = python_requirement.target() {
|
||||||
|
debug!("Narrowed `requires-python` bound to: {target}");
|
||||||
|
}
|
||||||
|
forked_state.requires_python =
|
||||||
|
if forked_state.requires_python.is_some() {
|
||||||
|
python_requirement.to_marker_tree()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
forked_state.python_requirement = python_requirement;
|
||||||
|
}
|
||||||
|
|
||||||
forked_state.add_package_version_dependencies(
|
forked_state.add_package_version_dependencies(
|
||||||
for_package.as_deref(),
|
for_package.as_deref(),
|
||||||
&version,
|
&version,
|
||||||
|
@ -726,6 +753,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
pins: &mut FilePins,
|
pins: &mut FilePins,
|
||||||
preferences: &Preferences,
|
preferences: &Preferences,
|
||||||
fork_urls: &ForkUrls,
|
fork_urls: &ForkUrls,
|
||||||
|
python_requirement: &PythonRequirement,
|
||||||
visited: &mut FxHashSet<PackageName>,
|
visited: &mut FxHashSet<PackageName>,
|
||||||
request_sink: &Sender<Request>,
|
request_sink: &Sender<Request>,
|
||||||
) -> Result<Option<ResolverVersion>, ResolveError> {
|
) -> Result<Option<ResolverVersion>, ResolveError> {
|
||||||
|
@ -746,13 +774,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
| PubGrubPackageInner::Dev { name, .. }
|
| PubGrubPackageInner::Dev { name, .. }
|
||||||
| PubGrubPackageInner::Package { name, .. } => {
|
| PubGrubPackageInner::Package { name, .. } => {
|
||||||
if let Some(url) = package.name().and_then(|name| fork_urls.get(name)) {
|
if let Some(url) = package.name().and_then(|name| fork_urls.get(name)) {
|
||||||
self.choose_version_url(name, range, url)
|
self.choose_version_url(name, range, url, python_requirement)
|
||||||
} else {
|
} else {
|
||||||
self.choose_version_registry(
|
self.choose_version_registry(
|
||||||
name,
|
name,
|
||||||
range,
|
range,
|
||||||
package,
|
package,
|
||||||
preferences,
|
preferences,
|
||||||
|
python_requirement,
|
||||||
pins,
|
pins,
|
||||||
visited,
|
visited,
|
||||||
request_sink,
|
request_sink,
|
||||||
|
@ -769,6 +798,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
name: &PackageName,
|
name: &PackageName,
|
||||||
range: &Range<Version>,
|
range: &Range<Version>,
|
||||||
url: &VerbatimParsedUrl,
|
url: &VerbatimParsedUrl,
|
||||||
|
python_requirement: &PythonRequirement,
|
||||||
) -> Result<Option<ResolverVersion>, ResolveError> {
|
) -> Result<Option<ResolverVersion>, ResolveError> {
|
||||||
debug!(
|
debug!(
|
||||||
"Searching for a compatible version of {name} @ {} ({range})",
|
"Searching for a compatible version of {name} @ {} ({range})",
|
||||||
|
@ -826,8 +856,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
}
|
}
|
||||||
|
|
||||||
// The version is incompatible due to its Python requirement.
|
// The version is incompatible due to its Python requirement.
|
||||||
|
// STOPSHIP(charlie): Merge markers into `python_requirement`.
|
||||||
if let Some(requires_python) = metadata.requires_python.as_ref() {
|
if let Some(requires_python) = metadata.requires_python.as_ref() {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
if let Some(target) = python_requirement.target() {
|
||||||
if !target.is_compatible_with(requires_python) {
|
if !target.is_compatible_with(requires_python) {
|
||||||
return Ok(Some(ResolverVersion::Unavailable(
|
return Ok(Some(ResolverVersion::Unavailable(
|
||||||
version.clone(),
|
version.clone(),
|
||||||
|
@ -840,7 +871,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !requires_python.contains(self.python_requirement.installed()) {
|
if !requires_python.contains(python_requirement.installed()) {
|
||||||
return Ok(Some(ResolverVersion::Unavailable(
|
return Ok(Some(ResolverVersion::Unavailable(
|
||||||
version.clone(),
|
version.clone(),
|
||||||
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
||||||
|
@ -864,6 +895,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
range: &Range<Version>,
|
range: &Range<Version>,
|
||||||
package: &PubGrubPackage,
|
package: &PubGrubPackage,
|
||||||
preferences: &Preferences,
|
preferences: &Preferences,
|
||||||
|
python_requirement: &PythonRequirement,
|
||||||
pins: &mut FilePins,
|
pins: &mut FilePins,
|
||||||
visited: &mut FxHashSet<PackageName>,
|
visited: &mut FxHashSet<PackageName>,
|
||||||
request_sink: &Sender<Request>,
|
request_sink: &Sender<Request>,
|
||||||
|
@ -932,7 +964,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.requires_python
|
.requires_python
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|requires_python| {
|
.and_then(|requires_python| {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
if let Some(target) = python_requirement.target() {
|
||||||
if !target.is_compatible_with(requires_python) {
|
if !target.is_compatible_with(requires_python) {
|
||||||
return Some(IncompatibleDist::Source(
|
return Some(IncompatibleDist::Source(
|
||||||
IncompatibleSource::RequiresPython(
|
IncompatibleSource::RequiresPython(
|
||||||
|
@ -942,7 +974,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !requires_python.contains(self.python_requirement.installed()) {
|
if !requires_python.contains(python_requirement.installed()) {
|
||||||
return Some(IncompatibleDist::Source(
|
return Some(IncompatibleDist::Source(
|
||||||
IncompatibleSource::RequiresPython(
|
IncompatibleSource::RequiresPython(
|
||||||
requires_python.clone(),
|
requires_python.clone(),
|
||||||
|
@ -960,7 +992,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.requires_python
|
.requires_python
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|requires_python| {
|
.and_then(|requires_python| {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
if let Some(target) = python_requirement.target() {
|
||||||
if !target.is_compatible_with(requires_python) {
|
if !target.is_compatible_with(requires_python) {
|
||||||
return Some(IncompatibleDist::Wheel(
|
return Some(IncompatibleDist::Wheel(
|
||||||
IncompatibleWheel::RequiresPython(
|
IncompatibleWheel::RequiresPython(
|
||||||
|
@ -970,7 +1002,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !requires_python.contains(self.python_requirement.installed()) {
|
if !requires_python.contains(python_requirement.installed()) {
|
||||||
return Some(IncompatibleDist::Wheel(
|
return Some(IncompatibleDist::Wheel(
|
||||||
IncompatibleWheel::RequiresPython(
|
IncompatibleWheel::RequiresPython(
|
||||||
requires_python.clone(),
|
requires_python.clone(),
|
||||||
|
@ -1034,8 +1066,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
version: &Version,
|
version: &Version,
|
||||||
fork_urls: &ForkUrls,
|
fork_urls: &ForkUrls,
|
||||||
markers: &MarkerTree,
|
markers: &MarkerTree,
|
||||||
|
requires_python: Option<&MarkerTree>,
|
||||||
) -> Result<ForkedDependencies, ResolveError> {
|
) -> Result<ForkedDependencies, ResolveError> {
|
||||||
let result = self.get_dependencies(package, version, fork_urls, markers);
|
let result = self.get_dependencies(package, version, fork_urls, markers, requires_python);
|
||||||
if self.markers.is_some() {
|
if self.markers.is_some() {
|
||||||
return result.map(|deps| match deps {
|
return result.map(|deps| match deps {
|
||||||
Dependencies::Available(deps) => ForkedDependencies::Unforked(deps),
|
Dependencies::Available(deps) => ForkedDependencies::Unforked(deps),
|
||||||
|
@ -1053,6 +1086,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
version: &Version,
|
version: &Version,
|
||||||
fork_urls: &ForkUrls,
|
fork_urls: &ForkUrls,
|
||||||
markers: &MarkerTree,
|
markers: &MarkerTree,
|
||||||
|
requires_python: Option<&MarkerTree>,
|
||||||
) -> Result<Dependencies, ResolveError> {
|
) -> Result<Dependencies, ResolveError> {
|
||||||
let url = package.name().and_then(|name| fork_urls.get(name));
|
let url = package.name().and_then(|name| fork_urls.get(name));
|
||||||
let dependencies = match &**package {
|
let dependencies = match &**package {
|
||||||
|
@ -1065,6 +1099,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
markers,
|
markers,
|
||||||
|
requires_python,
|
||||||
);
|
);
|
||||||
|
|
||||||
requirements
|
requirements
|
||||||
|
@ -1193,6 +1228,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
dev.as_ref(),
|
dev.as_ref(),
|
||||||
Some(name),
|
Some(name),
|
||||||
markers,
|
markers,
|
||||||
|
requires_python,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut dependencies = requirements
|
let mut dependencies = requirements
|
||||||
|
@ -1313,6 +1349,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
dev: Option<&'a GroupName>,
|
dev: Option<&'a GroupName>,
|
||||||
name: Option<&PackageName>,
|
name: Option<&PackageName>,
|
||||||
markers: &'a MarkerTree,
|
markers: &'a MarkerTree,
|
||||||
|
requires_python: Option<&'a MarkerTree>,
|
||||||
) -> Vec<Cow<'a, Requirement>> {
|
) -> Vec<Cow<'a, Requirement>> {
|
||||||
// Start with the requirements for the current extra of the package (for an extra
|
// Start with the requirements for the current extra of the package (for an extra
|
||||||
// requirement) or the non-extra (regular) dependencies (if extra is None), plus
|
// requirement) or the non-extra (regular) dependencies (if extra is None), plus
|
||||||
|
@ -1323,7 +1360,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
Either::Right(dependencies.iter())
|
Either::Right(dependencies.iter())
|
||||||
};
|
};
|
||||||
let mut requirements = self
|
let mut requirements = self
|
||||||
.requirements_for_extra(regular_and_dev_dependencies, extra, markers)
|
.requirements_for_extra(
|
||||||
|
regular_and_dev_dependencies,
|
||||||
|
extra,
|
||||||
|
markers,
|
||||||
|
requires_python,
|
||||||
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Check if there are recursive self inclusions and we need to go into the expensive branch.
|
// Check if there are recursive self inclusions and we need to go into the expensive branch.
|
||||||
|
@ -1346,7 +1388,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
if !seen.insert(extra.clone()) {
|
if !seen.insert(extra.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for requirement in self.requirements_for_extra(dependencies, Some(&extra), markers) {
|
for requirement in
|
||||||
|
self.requirements_for_extra(dependencies, Some(&extra), markers, requires_python)
|
||||||
|
{
|
||||||
if name == Some(&requirement.name) {
|
if name == Some(&requirement.name) {
|
||||||
// Add each transitively included extra.
|
// Add each transitively included extra.
|
||||||
queue.extend(requirement.extras.iter().cloned());
|
queue.extend(requirement.extras.iter().cloned());
|
||||||
|
@ -1370,6 +1414,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
|
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
|
||||||
extra: Option<&'parameters ExtraName>,
|
extra: Option<&'parameters ExtraName>,
|
||||||
markers: &'parameters MarkerTree,
|
markers: &'parameters MarkerTree,
|
||||||
|
requires_python: Option<&'parameters MarkerTree>,
|
||||||
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
|
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
|
||||||
where
|
where
|
||||||
'data: 'parameters,
|
'data: 'parameters,
|
||||||
|
@ -1379,12 +1424,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.filter(move |requirement| {
|
.filter(move |requirement| {
|
||||||
// If the requirement would not be selected with any Python version
|
// If the requirement would not be selected with any Python version
|
||||||
// supported by the root, skip it.
|
// supported by the root, skip it.
|
||||||
if !satisfies_requires_python(self.requires_python.as_ref(), requirement) {
|
if !satisfies_requires_python(requires_python, requirement) {
|
||||||
trace!(
|
trace!(
|
||||||
"skipping {requirement} because of Requires-Python {requires_python}",
|
"skipping {requirement} because of Requires-Python {requires_python}",
|
||||||
// OK because this filter only applies when there is a present
|
// OK because this filter only applies when there is a present
|
||||||
// Requires-Python specifier.
|
// Requires-Python specifier.
|
||||||
requires_python = self.requires_python.as_ref().unwrap()
|
requires_python = requires_python.unwrap()
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1428,10 +1473,10 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter(move |constraint| {
|
.filter(move |constraint| {
|
||||||
if !satisfies_requires_python(self.requires_python.as_ref(), constraint) {
|
if !satisfies_requires_python(requires_python, constraint) {
|
||||||
trace!(
|
trace!(
|
||||||
"skipping {constraint} because of Requires-Python {requires_python}",
|
"skipping {constraint} because of Requires-Python {requires_python}",
|
||||||
requires_python = self.requires_python.as_ref().unwrap()
|
requires_python = requires_python.unwrap()
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1793,6 +1838,21 @@ struct ForkState {
|
||||||
/// that the marker expression that provoked the fork is true), then that
|
/// that the marker expression that provoked the fork is true), then that
|
||||||
/// dependency is completely ignored.
|
/// dependency is completely ignored.
|
||||||
markers: MarkerTree,
|
markers: MarkerTree,
|
||||||
|
/// The Python requirement for this fork. Defaults to the Python requirement for
|
||||||
|
/// the resolution, but may be narrowed if a `python_version` marker is present
|
||||||
|
/// in a given fork.
|
||||||
|
///
|
||||||
|
/// For example, in:
|
||||||
|
/// ```text
|
||||||
|
/// numpy >=1.26 ; python_version >= "3.9"
|
||||||
|
/// numpy <1.26 ; python_version < "3.9"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The top fork has a narrower Python compatibility range, and thus can find a
|
||||||
|
/// solution that omits Python 3.8 support.
|
||||||
|
python_requirement: PythonRequirement,
|
||||||
|
/// The [`MarkerTree`] corresponding to the [`PythonRequirement`].
|
||||||
|
requires_python: Option<MarkerTree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForkState {
|
impl ForkState {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::collections::Bound;
|
|
||||||
|
|
||||||
use anstream::eprint;
|
use anstream::eprint;
|
||||||
|
|
||||||
use distribution_types::UnresolvedRequirementSpecification;
|
use distribution_types::UnresolvedRequirementSpecification;
|
||||||
|
@ -129,7 +127,7 @@ pub(super) async fn do_lock(
|
||||||
let requires_python = find_requires_python(workspace)?;
|
let requires_python = find_requires_python(workspace)?;
|
||||||
|
|
||||||
let requires_python = if let Some(requires_python) = requires_python {
|
let requires_python = if let Some(requires_python) = requires_python {
|
||||||
if matches!(requires_python.bound(), Bound::Unbounded) {
|
if requires_python.is_unbounded() {
|
||||||
let default =
|
let default =
|
||||||
RequiresPython::greater_than_equal_version(interpreter.python_minor_version());
|
RequiresPython::greater_than_equal_version(interpreter.python_minor_version());
|
||||||
warn_user!("The workspace `requires-python` field does not contain a lower bound: `{requires_python}`. Set a lower bound to indicate the minimum compatible Python version (e.g., `{default}`).");
|
warn_user!("The workspace `requires-python` field does not contain a lower bound: `{requires_python}`. Set a lower bound to indicate the minimum compatible Python version (e.g., `{default}`).");
|
||||||
|
|
|
@ -6564,6 +6564,41 @@ fn universal_multi_version() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform a universal resolution that requires narrowing the supported Python range in one of the
|
||||||
|
/// fork branches.
|
||||||
|
#[test]
|
||||||
|
fn universal_requires_python() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str(indoc::indoc! {r"
|
||||||
|
numpy >=1.26 ; python_version >= '3.9'
|
||||||
|
numpy <1.26 ; python_version < '3.9'
|
||||||
|
"})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("-p")
|
||||||
|
.arg("3.8")
|
||||||
|
.arg("--universal"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -p 3.8 --universal
|
||||||
|
numpy==1.24.4 ; python_version < '3.9'
|
||||||
|
# via -r requirements.in
|
||||||
|
numpy==1.26.4 ; python_version >= '3.9'
|
||||||
|
# via -r requirements.in
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: The requested Python version 3.8 is not available; 3.12.[X] will be used to build dependencies instead.
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of
|
/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of
|
||||||
/// its transitive dependencies to a specific version.
|
/// its transitive dependencies to a specific version.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue