mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-08 11:40:36 +00:00
Use lower-bound semantics for all Python compatibility comparisons (#6882)
## Summary Right now, we have slightly different `requires-python` semantics for `-p 3.11` vs. `-p 3.11 --universal`, and slightly different (wrong) semantics for how we compare against the _installed_ Python version (which doesn't ignore upper bounds, but should). This PR rips it all out and replaces it with consistent semantics across `uv lock`, `uv pip compile -p 3.11`, and `uv pip compile -p 3.11 --universal`. We now always ignore upper bounds. Closes https://github.com/astral-sh/uv/issues/6859. Closes https://github.com/astral-sh/uv/issues/5045.
This commit is contained in:
parent
cbe2827e97
commit
8eef1a2314
28 changed files with 508 additions and 313 deletions
|
@ -1,29 +1,33 @@
|
|||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use pep440_rs::Version;
|
||||
use uv_python::{Interpreter, PythonVersion};
|
||||
|
||||
use crate::{RequiresPython, RequiresPythonRange};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct PythonRequirement {
|
||||
source: PythonRequirementSource,
|
||||
/// The exact installed version of Python.
|
||||
exact: Version,
|
||||
/// The installed version of Python.
|
||||
installed: Version,
|
||||
installed: RequiresPython,
|
||||
/// The target version of Python; that is, the version of Python for which we are resolving
|
||||
/// dependencies. This is typically the same as the installed version, but may be different
|
||||
/// when specifying an alternate Python version for the resolution.
|
||||
///
|
||||
/// If `None`, the target version is the same as the installed version.
|
||||
target: Option<PythonTarget>,
|
||||
target: RequiresPython,
|
||||
}
|
||||
|
||||
impl PythonRequirement {
|
||||
/// Create a [`PythonRequirement`] to resolve against both an [`Interpreter`] and a
|
||||
/// [`PythonVersion`].
|
||||
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
|
||||
let exact = interpreter.python_full_version().version.clone();
|
||||
let installed = interpreter.python_full_version().version.only_release();
|
||||
let target = python_version.python_full_version().only_release();
|
||||
Self {
|
||||
installed: interpreter.python_full_version().version.only_release(),
|
||||
target: Some(PythonTarget::Version(
|
||||
python_version.python_full_version().only_release(),
|
||||
)),
|
||||
exact,
|
||||
installed: RequiresPython::greater_than_equal_version(&installed),
|
||||
target: RequiresPython::greater_than_equal_version(&target),
|
||||
source: PythonRequirementSource::PythonVersion,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,84 +35,68 @@ impl PythonRequirement {
|
|||
/// [`MarkerEnvironment`].
|
||||
pub fn from_requires_python(
|
||||
interpreter: &Interpreter,
|
||||
requires_python: &RequiresPython,
|
||||
requires_python: RequiresPython,
|
||||
) -> Self {
|
||||
let exact = interpreter.python_full_version().version.clone();
|
||||
let installed = interpreter.python_full_version().version.only_release();
|
||||
Self {
|
||||
installed: interpreter.python_full_version().version.only_release(),
|
||||
target: Some(PythonTarget::RequiresPython(requires_python.clone())),
|
||||
exact,
|
||||
installed: RequiresPython::greater_than_equal_version(&installed),
|
||||
target: requires_python,
|
||||
source: PythonRequirementSource::RequiresPython,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`PythonRequirement`] to resolve against an [`Interpreter`].
|
||||
pub fn from_interpreter(interpreter: &Interpreter) -> Self {
|
||||
let exact = interpreter.python_full_version().version.clone();
|
||||
let installed = interpreter.python_full_version().version.only_release();
|
||||
Self {
|
||||
installed: interpreter.python_full_version().version.only_release(),
|
||||
target: None,
|
||||
exact,
|
||||
installed: RequiresPython::greater_than_equal_version(&installed),
|
||||
target: RequiresPython::greater_than_equal_version(&installed),
|
||||
source: PythonRequirementSource::Interpreter,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: &RequiresPythonRange) -> Option<Self> {
|
||||
let Some(PythonTarget::RequiresPython(requires_python)) = self.target.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let requires_python = requires_python.narrow(target)?;
|
||||
Some(Self {
|
||||
exact: self.exact.clone(),
|
||||
installed: self.installed.clone(),
|
||||
target: Some(PythonTarget::RequiresPython(requires_python)),
|
||||
target: self.target.narrow(target)?,
|
||||
source: self.source,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the exact version of Python.
|
||||
pub fn exact(&self) -> &Version {
|
||||
&self.exact
|
||||
}
|
||||
|
||||
/// Return the installed version of Python.
|
||||
pub fn installed(&self) -> &Version {
|
||||
pub fn installed(&self) -> &RequiresPython {
|
||||
&self.installed
|
||||
}
|
||||
|
||||
/// Return the target version of Python.
|
||||
pub fn target(&self) -> Option<&PythonTarget> {
|
||||
self.target.as_ref()
|
||||
pub fn target(&self) -> &RequiresPython {
|
||||
&self.target
|
||||
}
|
||||
|
||||
/// Return the source of the [`PythonRequirement`].
|
||||
pub fn source(&self) -> PythonRequirementSource {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum PythonTarget {
|
||||
/// The [`PythonTarget`] 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.
|
||||
Version(Version),
|
||||
/// The [`PythonTarget`] specifier is a set of version specifiers, as extracted from the
|
||||
/// `Requires-Python` field in a `pyproject.toml` or `METADATA` file.
|
||||
RequiresPython(RequiresPython),
|
||||
}
|
||||
|
||||
impl PythonTarget {
|
||||
/// Returns `true` if the target Python is compatible with the [`VersionSpecifiers`].
|
||||
pub fn is_compatible_with(&self, target: &VersionSpecifiers) -> bool {
|
||||
match self {
|
||||
PythonTarget::Version(version) => target.contains(version),
|
||||
PythonTarget::RequiresPython(requires_python) => {
|
||||
requires_python.is_contained_by(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`RequiresPython`] for the [`PythonTarget`] specifier.
|
||||
pub fn as_requires_python(&self) -> Option<&RequiresPython> {
|
||||
match self {
|
||||
PythonTarget::Version(_) => None,
|
||||
PythonTarget::RequiresPython(requires_python) => Some(requires_python),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PythonTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PythonTarget::Version(specifier) => std::fmt::Display::fmt(specifier, f),
|
||||
PythonTarget::RequiresPython(specifiers) => std::fmt::Display::fmt(specifiers, f),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
|
||||
pub enum PythonRequirementSource {
|
||||
/// `--python-version`
|
||||
PythonVersion,
|
||||
/// `Requires-Python`
|
||||
RequiresPython,
|
||||
/// The discovered Python interpreter.
|
||||
Interpreter,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue