Respect release-only semantics of python_full_version when constructing markers (#6171)

## Summary

In the resolver, we use release-only semantics to normalize
`python_full_version`. So, if we see `python_full_version < '3.13'`, we
treat that as `(Unbounded, Exclude(3.13))`. `3.13b0` evaluates as `true`
to that range, so we were accepting pre-releases for these markers.

Instead, we need to exclude pre-release segments when performing these
evaluations.

Closes https://github.com/astral-sh/uv/issues/6169.

## Test Plan

Hard to write a test for this because you need a pre-release Python
locally... so:

`echo "sqlalchemy==2.0.32" | cargo run pip compile - --python 3.13 -n`
This commit is contained in:
Charlie Marsh 2024-08-17 15:29:57 -04:00 committed by GitHub
parent ad8e3a2c32
commit 5ac0b98e00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 61 additions and 8 deletions

View file

@ -380,6 +380,15 @@ pub struct StringVersion {
pub version: Version,
}
impl From<Version> for StringVersion {
fn from(version: Version) -> Self {
Self {
string: version.to_string(),
version,
}
}
}
impl FromStr for StringVersion {
type Err = VersionParseError;

View file

@ -155,7 +155,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
.index_strategy(self.index_strategy)
.build(),
&python_requirement,
ResolverMarkers::SpecificEnvironment(markers.clone()),
ResolverMarkers::specific_environment(markers.clone()),
Some(tags),
self.flat_index,
self.index,

View file

@ -1,4 +1,5 @@
use std::fmt::{Display, Formatter};
use tracing::debug;
use pep508_rs::{MarkerEnvironment, MarkerTree};
@ -6,7 +7,7 @@ use pep508_rs::{MarkerEnvironment, MarkerTree};
/// Whether we're solving for a specific environment, universally or for a specific fork.
pub enum ResolverMarkers {
/// We're solving for this specific environment only.
SpecificEnvironment(MarkerEnvironment),
SpecificEnvironment(ResolverMarkerEnvironment),
/// We're doing a universal resolution for all environments (a python version
/// constraint is expressed separately).
Universal {
@ -20,7 +21,7 @@ pub enum ResolverMarkers {
impl ResolverMarkers {
/// Set the resolver to perform a resolution for a specific environment.
pub fn specific_environment(markers: MarkerEnvironment) -> Self {
Self::SpecificEnvironment(markers)
Self::SpecificEnvironment(ResolverMarkerEnvironment::from(markers))
}
/// Set the resolver to perform a universal resolution.
@ -70,3 +71,46 @@ impl Display for ResolverMarkers {
}
}
}
/// A wrapper type around [`MarkerEnvironment`] that ensures the Python version markers are
/// release-only, to match the resolver's semantics.
#[derive(Debug, Clone)]
pub struct ResolverMarkerEnvironment(MarkerEnvironment);
impl From<MarkerEnvironment> for ResolverMarkerEnvironment {
fn from(value: MarkerEnvironment) -> Self {
// Strip `python_version`.
let python_version = value.python_version().only_release();
let value = if python_version == **value.python_version() {
value
} else {
debug!(
"Stripping pre-release from `python_version`: {}",
value.python_version()
);
value.with_python_version(python_version)
};
// Strip `python_full_version`.
let python_full_version = value.python_full_version().only_release();
let value = if python_full_version == **value.python_full_version() {
value
} else {
debug!(
"Stripping pre-release from `python_full_version`: {}",
value.python_full_version()
);
value.with_python_full_version(python_full_version)
};
Self(value)
}
}
impl std::ops::Deref for ResolverMarkerEnvironment {
type Target = MarkerEnvironment;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -251,7 +251,7 @@ pub(crate) async fn pip_compile(
resolution_environment(python_version, python_platform, &interpreter)?;
(
Some(tags),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
ResolverMarkers::specific_environment((*markers).clone()),
)
};

View file

@ -348,7 +348,7 @@ pub(crate) async fn pip_install(
&reinstall,
&upgrade,
Some(&tags),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
ResolverMarkers::specific_environment((*markers).clone()),
python_requirement,
&client,
&flat_index,

View file

@ -296,7 +296,7 @@ pub(crate) async fn pip_sync(
&reinstall,
&upgrade,
Some(&tags),
ResolverMarkers::SpecificEnvironment((*markers).clone()),
ResolverMarkers::specific_environment((*markers).clone()),
python_requirement,
&client,
&flat_index,

View file

@ -626,7 +626,7 @@ pub(crate) async fn resolve_environment<'a>(
&reinstall,
&upgrade,
Some(tags),
ResolverMarkers::SpecificEnvironment(markers.clone()),
ResolverMarkers::specific_environment(markers.clone()),
python_requirement,
&client,
&flat_index,
@ -949,7 +949,7 @@ pub(crate) async fn update_environment(
reinstall,
upgrade,
Some(tags),
ResolverMarkers::SpecificEnvironment(markers.clone()),
ResolverMarkers::specific_environment(markers.clone()),
python_requirement,
&client,
&flat_index,