uv/crates/uv-resolver/src/python_requirement.rs
Charlie Marsh 8111650100
Avoid returning Some for narrowing no-ops (#10705)
The net effect here is (I think) just that we end up logging more
"narrowing" outcomes than we have in practice.
2025-01-17 16:41:17 +00:00

187 lines
6.3 KiB
Rust

use std::collections::Bound;
use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, MarkerTree};
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: 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.
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()
.without_trailing_zeros();
let target = python_version
.python_full_version()
.only_release()
.without_trailing_zeros();
Self {
exact,
installed: RequiresPython::greater_than_equal_version(&installed),
target: RequiresPython::greater_than_equal_version(&target),
source: PythonRequirementSource::PythonVersion,
}
}
/// Create a [`PythonRequirement`] to resolve against both an [`Interpreter`] and a
/// [`MarkerEnvironment`].
pub fn from_requires_python(
interpreter: &Interpreter,
requires_python: RequiresPython,
) -> Self {
Self::from_marker_environment(interpreter.markers(), requires_python)
}
/// Create a [`PythonRequirement`] to resolve against an [`Interpreter`].
pub fn from_interpreter(interpreter: &Interpreter) -> Self {
let exact = interpreter
.python_full_version()
.version
.clone()
.without_trailing_zeros();
let installed = interpreter
.python_full_version()
.version
.only_release()
.without_trailing_zeros();
Self {
exact,
installed: RequiresPython::greater_than_equal_version(&installed),
target: RequiresPython::greater_than_equal_version(&installed),
source: PythonRequirementSource::Interpreter,
}
}
/// Create a [`PythonRequirement`] from a [`MarkerEnvironment`] and a
/// specific `Requires-Python` directive.
///
/// This has the same "source" as
/// [`PythonRequirement::from_requires_python`], but is useful for
/// constructing a `PythonRequirement` without an [`Interpreter`].
pub fn from_marker_environment(
marker_env: &MarkerEnvironment,
requires_python: RequiresPython,
) -> Self {
let exact = marker_env
.python_full_version()
.version
.clone()
.without_trailing_zeros();
let installed = marker_env
.python_full_version()
.version
.only_release()
.without_trailing_zeros();
Self {
exact,
installed: RequiresPython::greater_than_equal_version(&installed),
target: requires_python,
source: PythonRequirementSource::RequiresPython,
}
}
/// Narrow the [`PythonRequirement`] to the given version, if it's stricter (i.e., greater)
/// than the current `Requires-Python` minimum.
///
/// Returns `None` if the given range is not narrower than the current range.
pub fn narrow(&self, target: &RequiresPythonRange) -> Option<Self> {
Some(Self {
exact: self.exact.clone(),
installed: self.installed.clone(),
target: self.target.narrow(target)?,
source: self.source,
})
}
/// Split the [`PythonRequirement`] at the given version.
///
/// For example, if the current requirement is `>=3.10`, and the split point is `3.11`, then
/// the result will be `>=3.10 and <3.11` and `>=3.11`.
pub fn split(&self, at: Bound<Version>) -> Option<(Self, Self)> {
let (lower, upper) = self.target.split(at)?;
Some((
Self {
exact: self.exact.clone(),
installed: self.installed.clone(),
target: lower,
source: self.source,
},
Self {
exact: self.exact.clone(),
installed: self.installed.clone(),
target: upper,
source: self.source,
},
))
}
/// Returns `true` if the minimum version of Python required by the target is greater than the
/// installed version.
pub fn raises(&self, target: &RequiresPythonRange) -> bool {
target.lower() > self.target.range().lower()
}
/// Return the exact version of Python.
pub fn exact(&self) -> &Version {
&self.exact
}
/// Return the installed version of Python.
pub fn installed(&self) -> &RequiresPython {
&self.installed
}
/// Return the target version of Python.
pub fn target(&self) -> &RequiresPython {
&self.target
}
/// Return the source of the [`PythonRequirement`].
pub fn source(&self) -> PythonRequirementSource {
self.source
}
/// A wrapper around `RequiresPython::simplify_markers`. See its docs for
/// more info.
///
/// When this `PythonRequirement` isn't `RequiresPython`, the given markers
/// are returned unchanged.
pub(crate) fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
self.target.simplify_markers(marker)
}
/// Return a [`MarkerTree`] representing the Python requirement.
///
/// See: [`RequiresPython::to_marker_tree`]
pub fn to_marker_tree(&self) -> MarkerTree {
self.target.to_marker_tree()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
pub enum PythonRequirementSource {
/// `--python-version`
PythonVersion,
/// `Requires-Python`
RequiresPython,
/// The discovered Python interpreter.
Interpreter,
}