mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 05:15: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.
|
||||
fn from_lower_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
||||
pub fn from_lower_bound(bound: &Bound<Version>) -> Option<VersionSpecifier> {
|
||||
match bound {
|
||||
Bound::Included(version) => Some(
|
||||
VersionSpecifier::from_version(Operator::GreaterThanEqual, version.clone())
|
||||
|
@ -450,7 +450,7 @@ impl VersionSpecifier {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
Bound::Included(version) => Some(
|
||||
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 pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
||||
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_mode::ResolutionMode;
|
||||
pub use resolver::{
|
||||
|
|
|
@ -4,6 +4,8 @@ use std::collections::HashMap;
|
|||
use std::ops::Bound::{self, *};
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
use pubgrub::range::Range as PubGrubRange;
|
||||
|
||||
use pep440_rs::{Operator, Version, VersionSpecifier};
|
||||
use pep508_rs::{
|
||||
ExtraName, ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
|
||||
|
@ -11,7 +13,7 @@ use pep508_rs::{
|
|||
};
|
||||
|
||||
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.
|
||||
/// the expression `first and second` is always false.
|
||||
|
@ -80,6 +82,42 @@ fn string_is_disjoint(this: &MarkerExpression, other: &MarkerExpression) -> bool
|
|||
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.
|
||||
///
|
||||
/// 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::sync::Arc;
|
||||
|
||||
|
@ -17,9 +16,9 @@ impl Deref for PubGrubPackage {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for PubGrubPackage {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
impl std::fmt::Display for PubGrubPackage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use pep440_rs::VersionSpecifiers;
|
|||
use pep508_rs::{MarkerTree, StringVersion};
|
||||
use uv_toolchain::{Interpreter, PythonVersion};
|
||||
|
||||
use crate::RequiresPython;
|
||||
use crate::{RequiresPython, RequiresPythonBound};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
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.
|
||||
pub fn installed(&self) -> &StringVersion {
|
||||
&self.installed
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::collections::Bound;
|
||||
use std::ops::Deref;
|
||||
|
||||
use itertools::Itertools;
|
||||
use pubgrub::range::Range;
|
||||
|
@ -24,7 +26,7 @@ pub enum RequiresPythonError {
|
|||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RequiresPython {
|
||||
specifiers: VersionSpecifiers,
|
||||
bound: Bound<Version>,
|
||||
bound: RequiresPythonBound,
|
||||
}
|
||||
|
||||
impl RequiresPython {
|
||||
|
@ -34,7 +36,7 @@ impl RequiresPython {
|
|||
specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(
|
||||
version.clone(),
|
||||
)),
|
||||
bound: Bound::Included(version),
|
||||
bound: RequiresPythonBound(Bound::Included(version)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,11 +63,13 @@ impl RequiresPython {
|
|||
};
|
||||
|
||||
// Extract the lower bound.
|
||||
let bound = range
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(lower, _)| lower.clone())
|
||||
.unwrap_or(Bound::Unbounded);
|
||||
let bound = RequiresPythonBound(
|
||||
range
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(lower, _)| lower.clone())
|
||||
.unwrap_or(Bound::Unbounded),
|
||||
);
|
||||
|
||||
// Convert back to PEP 440 specifiers.
|
||||
let specifiers = range
|
||||
|
@ -76,6 +80,16 @@ impl RequiresPython {
|
|||
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.
|
||||
pub fn contains(&self, version: &Version) -> bool {
|
||||
self.specifiers.contains(version)
|
||||
|
@ -140,7 +154,7 @@ impl RequiresPython {
|
|||
// 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
|
||||
// explicitly requested it.
|
||||
match (target, &self.bound) {
|
||||
match (target, self.bound.as_ref()) {
|
||||
(Bound::Included(target_lower), Bound::Included(requires_python_lower)) => {
|
||||
target_lower.release() <= requires_python_lower.release()
|
||||
}
|
||||
|
@ -166,9 +180,9 @@ impl RequiresPython {
|
|||
&self.specifiers
|
||||
}
|
||||
|
||||
/// Returns the lower [`Bound`] for the `Requires-Python` specifier.
|
||||
pub fn bound(&self) -> &Bound<Version> {
|
||||
&self.bound
|
||||
/// Returns `true` if the `Requires-Python` specifier is unbounded.
|
||||
pub fn is_unbounded(&self) -> bool {
|
||||
self.bound.as_ref() == Bound::Unbounded
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// environments.
|
||||
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
|
||||
// tree we would generate would always evaluate to
|
||||
// `true` because every possible Python version would
|
||||
// satisfy it.
|
||||
Bound::Unbounded => return MarkerTree::And(vec![]),
|
||||
Bound::Excluded(ref version) => {
|
||||
(Operator::GreaterThan, version.clone().without_local())
|
||||
}
|
||||
Bound::Included(ref version) => {
|
||||
Bound::Excluded(version) => (Operator::GreaterThan, version.clone().without_local()),
|
||||
Bound::Included(version) => {
|
||||
(Operator::GreaterThanEqual, version.clone().without_local())
|
||||
}
|
||||
};
|
||||
|
@ -247,12 +259,50 @@ impl serde::Serialize for RequiresPython {
|
|||
impl<'de> serde::Deserialize<'de> for RequiresPython {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
|
||||
let bound = crate::pubgrub::PubGrubSpecifier::try_from(&specifiers)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(lower, _)| lower.clone())
|
||||
.unwrap_or(Bound::Unbounded);
|
||||
let bound = RequiresPythonBound(
|
||||
crate::pubgrub::PubGrubSpecifier::try_from(&specifiers)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(lower, _)| lower.clone())
|
||||
.unwrap_or(Bound::Unbounded),
|
||||
);
|
||||
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::fork_urls::ForkUrls;
|
||||
use crate::manifest::Manifest;
|
||||
use crate::marker::normalize;
|
||||
use crate::marker::{normalize, requires_python_marker};
|
||||
use crate::pins::FilePins;
|
||||
use crate::preferences::Preferences;
|
||||
use crate::pubgrub::{
|
||||
|
@ -312,6 +312,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
visited: &mut FxHashSet<PackageName>,
|
||||
request_sink: Sender<Request>,
|
||||
) -> 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 mut prefetcher = BatchPrefetcher::default();
|
||||
let state = ForkState {
|
||||
|
@ -322,22 +330,20 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
priorities: PubGrubPriorities::default(),
|
||||
added_dependencies: FxHashMap::default(),
|
||||
markers: MarkerTree::And(vec![]),
|
||||
python_requirement: self.python_requirement.clone(),
|
||||
requires_python: self.requires_python.clone(),
|
||||
};
|
||||
let mut preferences = self.preferences.clone();
|
||||
let mut forked_states = vec![state];
|
||||
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() {
|
||||
if !state.markers.is_universal() {
|
||||
debug!("Solving split {}", state.markers);
|
||||
if let Some(requires_python) = state.requires_python.as_ref() {
|
||||
debug!("Solving split {} ({})", state.markers, requires_python);
|
||||
} else {
|
||||
debug!("Solving split {}", state.markers);
|
||||
}
|
||||
}
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
|
@ -424,6 +430,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&mut state.pins,
|
||||
&preferences,
|
||||
&state.fork_urls,
|
||||
&state.python_requirement,
|
||||
visited,
|
||||
&request_sink,
|
||||
)?;
|
||||
|
@ -502,6 +509,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&version,
|
||||
&state.fork_urls,
|
||||
&state.markers,
|
||||
state.requires_python.as_ref(),
|
||||
)?;
|
||||
match forked_deps {
|
||||
ForkedDependencies::Unavailable(reason) => {
|
||||
|
@ -562,10 +570,29 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
if !is_last {
|
||||
cur_state = Some(forked_state.clone());
|
||||
}
|
||||
|
||||
forked_state.markers.and(fork.markers);
|
||||
forked_state.markers = normalize(forked_state.markers)
|
||||
.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(
|
||||
for_package.as_deref(),
|
||||
&version,
|
||||
|
@ -726,6 +753,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
pins: &mut FilePins,
|
||||
preferences: &Preferences,
|
||||
fork_urls: &ForkUrls,
|
||||
python_requirement: &PythonRequirement,
|
||||
visited: &mut FxHashSet<PackageName>,
|
||||
request_sink: &Sender<Request>,
|
||||
) -> Result<Option<ResolverVersion>, ResolveError> {
|
||||
|
@ -746,13 +774,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
| PubGrubPackageInner::Dev { name, .. }
|
||||
| PubGrubPackageInner::Package { 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 {
|
||||
self.choose_version_registry(
|
||||
name,
|
||||
range,
|
||||
package,
|
||||
preferences,
|
||||
python_requirement,
|
||||
pins,
|
||||
visited,
|
||||
request_sink,
|
||||
|
@ -769,6 +798,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
name: &PackageName,
|
||||
range: &Range<Version>,
|
||||
url: &VerbatimParsedUrl,
|
||||
python_requirement: &PythonRequirement,
|
||||
) -> Result<Option<ResolverVersion>, ResolveError> {
|
||||
debug!(
|
||||
"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.
|
||||
// STOPSHIP(charlie): Merge markers into `python_requirement`.
|
||||
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) {
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
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(
|
||||
version.clone(),
|
||||
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
||||
|
@ -864,6 +895,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
range: &Range<Version>,
|
||||
package: &PubGrubPackage,
|
||||
preferences: &Preferences,
|
||||
python_requirement: &PythonRequirement,
|
||||
pins: &mut FilePins,
|
||||
visited: &mut FxHashSet<PackageName>,
|
||||
request_sink: &Sender<Request>,
|
||||
|
@ -932,7 +964,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.requires_python
|
||||
.as_ref()
|
||||
.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) {
|
||||
return Some(IncompatibleDist::Source(
|
||||
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(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python.clone(),
|
||||
|
@ -960,7 +992,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.requires_python
|
||||
.as_ref()
|
||||
.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) {
|
||||
return Some(IncompatibleDist::Wheel(
|
||||
IncompatibleWheel::RequiresPython(
|
||||
|
@ -970,7 +1002,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
));
|
||||
}
|
||||
} else {
|
||||
if !requires_python.contains(self.python_requirement.installed()) {
|
||||
if !requires_python.contains(python_requirement.installed()) {
|
||||
return Some(IncompatibleDist::Wheel(
|
||||
IncompatibleWheel::RequiresPython(
|
||||
requires_python.clone(),
|
||||
|
@ -1034,8 +1066,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
version: &Version,
|
||||
fork_urls: &ForkUrls,
|
||||
markers: &MarkerTree,
|
||||
requires_python: Option<&MarkerTree>,
|
||||
) -> 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() {
|
||||
return result.map(|deps| match deps {
|
||||
Dependencies::Available(deps) => ForkedDependencies::Unforked(deps),
|
||||
|
@ -1053,6 +1086,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
version: &Version,
|
||||
fork_urls: &ForkUrls,
|
||||
markers: &MarkerTree,
|
||||
requires_python: Option<&MarkerTree>,
|
||||
) -> Result<Dependencies, ResolveError> {
|
||||
let url = package.name().and_then(|name| fork_urls.get(name));
|
||||
let dependencies = match &**package {
|
||||
|
@ -1065,6 +1099,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
None,
|
||||
None,
|
||||
markers,
|
||||
requires_python,
|
||||
);
|
||||
|
||||
requirements
|
||||
|
@ -1193,6 +1228,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dev.as_ref(),
|
||||
Some(name),
|
||||
markers,
|
||||
requires_python,
|
||||
);
|
||||
|
||||
let mut dependencies = requirements
|
||||
|
@ -1313,6 +1349,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dev: Option<&'a GroupName>,
|
||||
name: Option<&PackageName>,
|
||||
markers: &'a MarkerTree,
|
||||
requires_python: Option<&'a MarkerTree>,
|
||||
) -> Vec<Cow<'a, Requirement>> {
|
||||
// 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
|
||||
|
@ -1323,7 +1360,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
Either::Right(dependencies.iter())
|
||||
};
|
||||
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<_>>();
|
||||
|
||||
// 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()) {
|
||||
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) {
|
||||
// Add each transitively included extra.
|
||||
queue.extend(requirement.extras.iter().cloned());
|
||||
|
@ -1370,6 +1414,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
|
||||
extra: Option<&'parameters ExtraName>,
|
||||
markers: &'parameters MarkerTree,
|
||||
requires_python: Option<&'parameters MarkerTree>,
|
||||
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
|
||||
where
|
||||
'data: 'parameters,
|
||||
|
@ -1379,12 +1424,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.filter(move |requirement| {
|
||||
// If the requirement would not be selected with any Python version
|
||||
// supported by the root, skip it.
|
||||
if !satisfies_requires_python(self.requires_python.as_ref(), requirement) {
|
||||
if !satisfies_requires_python(requires_python, requirement) {
|
||||
trace!(
|
||||
"skipping {requirement} because of Requires-Python {requires_python}",
|
||||
// OK because this filter only applies when there is a present
|
||||
// Requires-Python specifier.
|
||||
requires_python = self.requires_python.as_ref().unwrap()
|
||||
requires_python = requires_python.unwrap()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -1428,10 +1473,10 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
.into_iter()
|
||||
.flatten()
|
||||
.filter(move |constraint| {
|
||||
if !satisfies_requires_python(self.requires_python.as_ref(), constraint) {
|
||||
if !satisfies_requires_python(requires_python, constraint) {
|
||||
trace!(
|
||||
"skipping {constraint} because of Requires-Python {requires_python}",
|
||||
requires_python = self.requires_python.as_ref().unwrap()
|
||||
requires_python = requires_python.unwrap()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -1793,6 +1838,21 @@ struct ForkState {
|
|||
/// that the marker expression that provoked the fork is true), then that
|
||||
/// dependency is completely ignored.
|
||||
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 {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::collections::Bound;
|
||||
|
||||
use anstream::eprint;
|
||||
|
||||
use distribution_types::UnresolvedRequirementSpecification;
|
||||
|
@ -129,7 +127,7 @@ pub(super) async fn do_lock(
|
|||
let requires_python = find_requires_python(workspace)?;
|
||||
|
||||
let requires_python = if let Some(requires_python) = requires_python {
|
||||
if matches!(requires_python.bound(), Bound::Unbounded) {
|
||||
if requires_python.is_unbounded() {
|
||||
let default =
|
||||
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}`).");
|
||||
|
|
|
@ -6564,6 +6564,41 @@ fn universal_multi_version() -> Result<()> {
|
|||
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
|
||||
/// its transitive dependencies to a specific version.
|
||||
#[test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue