Respect Requires-Python in universal resolution (#3998)

## Summary

Closes #3982.
This commit is contained in:
Charlie Marsh 2024-06-04 09:56:08 -04:00 committed by GitHub
parent 63c84ed4a6
commit 6afb659c9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 419 additions and 17 deletions

View file

@ -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),
}
}
}