uv/crates/uv-resolver/src/python_requirement.rs
Andrew Gallant 857d2e8f1e uv-resolver: partially revert Requires-Python version narrowing
The PR #4707 introduced the notion of "version narrowing," where a
Requires-Python constraint was _possibly_ narrowed whenever the
universal resolver created a fork. The version narrowing would occur
when the fork was a result of a marker expression on `python_version`
that is *stricter* than the configured `Requires-Python` (via, say,
`pyproject.toml`).

The crucial conceptual change made by #4707 is therefore that
`Requires-Python` is no longer an invariant configuration of resolution,
but rather a mutable constraint that can vary from fork to fork. This in
turn can result in some cases, such as in #4885, where different
versions of dependencies are selected. We aren't sure whether we can fix
those or not, with version narrowing, so for now, we do this revert to
restore the previous behavior and we'll try to address the version
narrowing some other time.

This also adds the case from #4885 as a regression test, ensuring that
we don't break that in the future. I confirmed that with version
narrowing, this test outputs duplicate distributions. Without narrowing,
there are no duplicates.

Ref #4707, Fixes #4885
2024-07-08 09:56:59 -07:00

139 lines
5.2 KiB
Rust

use pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::MarkerTree;
use uv_python::{Interpreter, PythonVersion};
use crate::{RequiresPython, RequiresPythonBound};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PythonRequirement {
/// The installed version of Python.
installed: Version,
/// 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>,
}
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 {
Self {
installed: interpreter.python_full_version().version.only_release(),
target: Some(PythonTarget::Version(
python_version.python_full_version().only_release(),
)),
}
}
/// Create a [`PythonRequirement`] to resolve against both an [`Interpreter`] and a
/// [`MarkerEnvironment`].
pub fn from_requires_python(
interpreter: &Interpreter,
requires_python: &RequiresPython,
) -> Self {
Self {
installed: interpreter.python_full_version().version.only_release(),
target: Some(PythonTarget::RequiresPython(requires_python.clone())),
}
}
/// Create a [`PythonRequirement`] to resolve against an [`Interpreter`].
pub fn from_interpreter(interpreter: &Interpreter) -> Self {
Self {
installed: interpreter.python_full_version().version.only_release(),
target: None,
}
}
/// 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> {
// This represents a "small revert" of the PR that added
// Requires-Python version narrowing[1]. But narrowing has
// led to at least one bug[2] whose fix is not clear. We
// decided to revert narrowing under the idea that it is better
// to be strict (i.e., fail to resolve in some cases, like
// universal_requires_python in uv/tests/pip_compile) than it
// is to output an incorrect lock, as in [2].
//
// [1]: https://github.com/astral-sh/uv/pull/4707
// [2]: https://github.com/astral-sh/uv/issues/4885
None
/*
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) -> &Version {
&self.installed
}
/// Return the target version of Python.
pub fn target(&self) -> Option<&PythonTarget> {
self.target.as_ref()
}
/// Return a [`MarkerTree`] representing the Python requirement.
///
/// See: [`RequiresPython::to_marker_tree`]
pub fn to_marker_tree(&self) -> Option<MarkerTree> {
if let Some(PythonTarget::RequiresPython(requires_python)) = self.target.as_ref() {
Some(requires_python.to_marker_tree())
} else {
None
}
}
}
#[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),
}
}
}