mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-17 02:52:45 +00:00
Respect Requires-Python in universal resolution (#3998)
## Summary Closes #3982.
This commit is contained in:
parent
63c84ed4a6
commit
6afb659c9a
13 changed files with 419 additions and 17 deletions
|
|
@ -9,6 +9,13 @@ use crate::ResolveError;
|
|||
#[derive(Debug)]
|
||||
pub(crate) struct PubGrubSpecifier(Range<Version>);
|
||||
|
||||
impl PubGrubSpecifier {
|
||||
/// Returns `true` if the [`PubGrubSpecifier`] is a subset of the other.
|
||||
pub(crate) fn subset_of(&self, other: &Self) -> bool {
|
||||
self.0.subset_of(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PubGrubSpecifier> for Range<Version> {
|
||||
/// Convert a PubGrub specifier to a range of versions.
|
||||
fn from(specifier: PubGrubSpecifier) -> Self {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use pep440_rs::VersionSpecifiers;
|
||||
use pep508_rs::StringVersion;
|
||||
use uv_interpreter::{Interpreter, PythonVersion};
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ pub struct PythonRequirement {
|
|||
/// when specifying an alternate Python version for the resolution.
|
||||
///
|
||||
/// If `None`, the target version is the same as the installed version.
|
||||
target: Option<StringVersion>,
|
||||
target: Option<RequiresPython>,
|
||||
}
|
||||
|
||||
impl PythonRequirement {
|
||||
|
|
@ -19,10 +20,22 @@ impl PythonRequirement {
|
|||
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
|
||||
Self {
|
||||
installed: interpreter.python_full_version().clone(),
|
||||
target: Some(StringVersion {
|
||||
target: Some(RequiresPython::Specifier(StringVersion {
|
||||
string: python_version.to_string(),
|
||||
version: python_version.python_full_version(),
|
||||
}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`PythonRequirement`] to resolve against both an [`Interpreter`] and a
|
||||
/// [`MarkerEnvironment`].
|
||||
pub fn from_requires_python(
|
||||
interpreter: &Interpreter,
|
||||
requires_python: &VersionSpecifiers,
|
||||
) -> Self {
|
||||
Self {
|
||||
installed: interpreter.python_full_version().clone(),
|
||||
target: Some(RequiresPython::Specifiers(requires_python.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +53,53 @@ impl PythonRequirement {
|
|||
}
|
||||
|
||||
/// Return the target version of Python.
|
||||
pub fn target(&self) -> Option<&StringVersion> {
|
||||
pub fn target(&self) -> Option<&RequiresPython> {
|
||||
self.target.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum RequiresPython {
|
||||
/// The `RequiresPython` specifier is a single version specifier, as provided via
|
||||
/// `--python-version` on the command line.
|
||||
///
|
||||
/// The use of a separate enum variant allows us to use a verbatim representation when reporting
|
||||
/// back to the user.
|
||||
Specifier(StringVersion),
|
||||
/// The `RequiresPython` specifier is a set of version specifiers.
|
||||
Specifiers(VersionSpecifiers),
|
||||
}
|
||||
|
||||
impl RequiresPython {
|
||||
/// Returns `true` if the target Python is covered by the [`VersionSpecifiers`].
|
||||
///
|
||||
/// For example, if the target Python is `>=3.8`, then `>=3.7` would cover it. However, `>=3.9`
|
||||
/// would not.
|
||||
pub fn subset_of(&self, requires_python: &VersionSpecifiers) -> bool {
|
||||
match self {
|
||||
RequiresPython::Specifier(specifier) => requires_python.contains(specifier),
|
||||
RequiresPython::Specifiers(specifiers) => {
|
||||
let Ok(target) = crate::pubgrub::PubGrubSpecifier::try_from(specifiers) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(requires_python) =
|
||||
crate::pubgrub::PubGrubSpecifier::try_from(requires_python)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
target.subset_of(&requires_python)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequiresPython {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RequiresPython::Specifier(specifier) => std::fmt::Display::fmt(specifier, f),
|
||||
RequiresPython::Specifiers(specifiers) => std::fmt::Display::fmt(specifiers, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,11 +306,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
let mut resolutions = vec![];
|
||||
|
||||
debug!(
|
||||
"Solving with target Python version {}",
|
||||
self.python_requirement
|
||||
.target()
|
||||
.unwrap_or(self.python_requirement.installed())
|
||||
"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() {
|
||||
loop {
|
||||
|
|
@ -713,7 +714,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
// The version is incompatible due to its Python requirement.
|
||||
if let Some(requires_python) = metadata.requires_python.as_ref() {
|
||||
if let Some(target) = self.python_requirement.target() {
|
||||
if !requires_python.contains(target) {
|
||||
if !target.subset_of(requires_python) {
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
version.clone(),
|
||||
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
||||
|
|
@ -725,9 +726,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let installed = self.python_requirement.installed();
|
||||
if !requires_python.contains(installed) {
|
||||
if !requires_python.contains(self.python_requirement.installed()) {
|
||||
return Ok(Some(ResolverVersion::Unavailable(
|
||||
version.clone(),
|
||||
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ impl VersionMapLazy {
|
|||
// _installed_ Python version (to build successfully)
|
||||
if let Some(requires_python) = requires_python {
|
||||
if let Some(target) = self.python_requirement.target() {
|
||||
if !requires_python.contains(target) {
|
||||
if !target.subset_of(&requires_python) {
|
||||
return SourceDistCompatibility::Incompatible(
|
||||
IncompatibleSource::RequiresPython(
|
||||
requires_python,
|
||||
|
|
@ -531,10 +531,10 @@ impl VersionMapLazy {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for a Python version incompatibility`
|
||||
// Check for a Python version incompatibility
|
||||
if let Some(requires_python) = requires_python {
|
||||
if let Some(target) = self.python_requirement.target() {
|
||||
if !requires_python.contains(target) {
|
||||
if !target.subset_of(&requires_python) {
|
||||
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
|
||||
requires_python,
|
||||
PythonRequirementKind::Target,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue