mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 20:19:08 +00:00
Add gap-preserving range-to-PEP 440 routine (#8060)
## Summary These are changes I apparently forgot to push as per https://github.com/astral-sh/uv/pull/7897/files#r1794312988.
This commit is contained in:
parent
77ea9d9626
commit
1c5309080b
5 changed files with 89 additions and 50 deletions
|
@ -45,7 +45,7 @@ uv-metadata = { path = "crates/uv-metadata" }
|
||||||
uv-normalize = { path = "crates/uv-normalize" }
|
uv-normalize = { path = "crates/uv-normalize" }
|
||||||
uv-once-map = { path = "crates/uv-once-map" }
|
uv-once-map = { path = "crates/uv-once-map" }
|
||||||
uv-options-metadata = { path = "crates/uv-options-metadata" }
|
uv-options-metadata = { path = "crates/uv-options-metadata" }
|
||||||
uv-pep440 = { path = "crates/uv-pep440" }
|
uv-pep440 = { path = "crates/uv-pep440", features = ["tracing"] }
|
||||||
uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
|
uv-pep508 = { path = "crates/uv-pep508", features = ["non-pep508-extensions"] }
|
||||||
uv-platform-tags = { path = "crates/uv-platform-tags" }
|
uv-platform-tags = { path = "crates/uv-platform-tags" }
|
||||||
uv-pubgrub = { path = "crates/uv-pubgrub" }
|
uv-pubgrub = { path = "crates/uv-pubgrub" }
|
||||||
|
|
|
@ -2,11 +2,11 @@ use std::cmp::Ordering;
|
||||||
use std::ops::Bound;
|
use std::ops::Bound;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
version, Operator, OperatorParseError, Version, VersionPattern, VersionPatternParseError,
|
version, Operator, OperatorParseError, Version, VersionPattern, VersionPatternParseError,
|
||||||
};
|
};
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
/// Sorted version specifiers, such as `>=2.1,<3`.
|
/// Sorted version specifiers, such as `>=2.1,<3`.
|
||||||
///
|
///
|
||||||
|
@ -69,6 +69,46 @@ impl VersionSpecifiers {
|
||||||
specifiers.sort_by(|a, b| a.version().cmp(b.version()));
|
specifiers.sort_by(|a, b| a.version().cmp(b.version()));
|
||||||
Self(specifiers)
|
Self(specifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [`VersionSpecifiers`] whose union represents the given range.
|
||||||
|
///
|
||||||
|
/// This function is not applicable to ranges involving pre-release versions.
|
||||||
|
pub fn from_release_only_bounds<'a>(
|
||||||
|
mut bounds: impl Iterator<Item = (&'a Bound<Version>, &'a Bound<Version>)>,
|
||||||
|
) -> Self {
|
||||||
|
let mut specifiers = Vec::new();
|
||||||
|
|
||||||
|
let Some((start, mut next)) = bounds.next() else {
|
||||||
|
return Self::empty();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add specifiers for the holes between the bounds.
|
||||||
|
for (lower, upper) in bounds {
|
||||||
|
match (next, lower) {
|
||||||
|
// Ex) [3.7, 3.8.5), (3.8.5, 3.9] -> >=3.7,!=3.8.5,<=3.9
|
||||||
|
(Bound::Excluded(prev), Bound::Excluded(lower)) if prev == lower => {
|
||||||
|
specifiers.push(VersionSpecifier::not_equals_version(prev.clone()));
|
||||||
|
}
|
||||||
|
// Ex) [3.7, 3.8), (3.8, 3.9] -> >=3.7,!=3.8.*,<=3.9
|
||||||
|
(Bound::Excluded(prev), Bound::Included(lower))
|
||||||
|
if prev.release().len() == 2
|
||||||
|
&& lower.release() == [prev.release()[0], prev.release()[1] + 1] =>
|
||||||
|
{
|
||||||
|
specifiers.push(VersionSpecifier::not_equals_star_version(prev.clone()));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Ignoring unsupported gap in `requires-python` version: {next:?} -> {lower:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next = upper;
|
||||||
|
}
|
||||||
|
let end = next;
|
||||||
|
|
||||||
|
// Add the specifiers for the bounding range.
|
||||||
|
specifiers.extend(VersionSpecifier::from_release_only_bounds((start, end)));
|
||||||
|
|
||||||
|
Self::from_unsorted(specifiers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromIterator<VersionSpecifier> for VersionSpecifiers {
|
impl FromIterator<VersionSpecifier> for VersionSpecifiers {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::cmp::Ordering;
|
use itertools::Itertools;
|
||||||
use std::collections::{BTreeSet, Bound};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use pubgrub::Range;
|
use pubgrub::Range;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::Bound;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
use uv_distribution_filename::WheelFilename;
|
use uv_distribution_filename::WheelFilename;
|
||||||
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||||
|
use uv_pubgrub::PubGrubSpecifier;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum RequiresPythonError {
|
pub enum RequiresPythonError {
|
||||||
|
@ -52,11 +53,10 @@ impl RequiresPython {
|
||||||
|
|
||||||
/// Returns a [`RequiresPython`] from a version specifier.
|
/// Returns a [`RequiresPython`] from a version specifier.
|
||||||
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result<Self, RequiresPythonError> {
|
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result<Self, RequiresPythonError> {
|
||||||
let (lower_bound, upper_bound) =
|
let (lower_bound, upper_bound) = PubGrubSpecifier::from_release_specifiers(specifiers)?
|
||||||
crate::pubgrub::PubGrubSpecifier::from_release_specifiers(specifiers)?
|
.bounding_range()
|
||||||
.bounding_range()
|
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
|
||||||
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
|
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
|
||||||
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
specifiers: specifiers.clone(),
|
specifiers: specifiers.clone(),
|
||||||
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
|
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
|
||||||
|
@ -69,35 +69,35 @@ impl RequiresPython {
|
||||||
pub fn intersection<'a>(
|
pub fn intersection<'a>(
|
||||||
specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
|
specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
|
||||||
) -> Result<Option<Self>, RequiresPythonError> {
|
) -> Result<Option<Self>, RequiresPythonError> {
|
||||||
let mut combined: BTreeSet<VersionSpecifier> = BTreeSet::new();
|
// Convert to PubGrub range and perform an intersection.
|
||||||
let mut lower_bound: LowerBound = LowerBound(Bound::Unbounded);
|
let range = specifiers
|
||||||
let mut upper_bound: UpperBound = UpperBound(Bound::Unbounded);
|
.into_iter()
|
||||||
|
.map(PubGrubSpecifier::from_release_specifiers)
|
||||||
for specifier in specifiers {
|
.fold_ok(None, |range: Option<Range<Version>>, requires_python| {
|
||||||
// Convert to PubGrub range and perform an intersection.
|
if let Some(range) = range {
|
||||||
let requires_python =
|
Some(range.intersection(&requires_python.into()))
|
||||||
crate::pubgrub::PubGrubSpecifier::from_release_specifiers(specifier)?;
|
} else {
|
||||||
if let Some((lower, upper)) = requires_python.bounding_range() {
|
Some(requires_python.into())
|
||||||
let lower = LowerBound(lower.cloned());
|
|
||||||
let upper = UpperBound(upper.cloned());
|
|
||||||
if lower > lower_bound {
|
|
||||||
lower_bound = lower;
|
|
||||||
}
|
}
|
||||||
if upper < upper_bound {
|
})?;
|
||||||
upper_bound = upper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track all specifiers for the final result.
|
let Some(range) = range else {
|
||||||
combined.extend(specifier.iter().cloned());
|
|
||||||
}
|
|
||||||
|
|
||||||
if combined.is_empty() {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Compute the intersection by combining the specifiers.
|
// Extract the bounds.
|
||||||
let specifiers = combined.into_iter().collect();
|
let (lower_bound, upper_bound) = range
|
||||||
|
.bounding_range()
|
||||||
|
.map(|(lower_bound, upper_bound)| {
|
||||||
|
(
|
||||||
|
LowerBound(lower_bound.cloned()),
|
||||||
|
UpperBound(upper_bound.cloned()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or((LowerBound::default(), UpperBound::default()));
|
||||||
|
|
||||||
|
// Convert back to PEP 440 specifiers.
|
||||||
|
let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
specifiers,
|
specifiers,
|
||||||
|
@ -223,7 +223,7 @@ impl RequiresPython {
|
||||||
/// provided range. However, `>=3.9` would not be considered compatible, as the
|
/// provided range. However, `>=3.9` would not be considered compatible, as the
|
||||||
/// `Requires-Python` includes Python 3.8, but `>=3.9` does not.
|
/// `Requires-Python` includes Python 3.8, but `>=3.9` does not.
|
||||||
pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
|
pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
|
||||||
let Ok(target) = crate::pubgrub::PubGrubSpecifier::from_release_specifiers(target) else {
|
let Ok(target) = PubGrubSpecifier::from_release_specifiers(target) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let target = target
|
let target = target
|
||||||
|
@ -458,12 +458,11 @@ impl serde::Serialize for RequiresPython {
|
||||||
impl<'de> serde::Deserialize<'de> for RequiresPython {
|
impl<'de> serde::Deserialize<'de> for RequiresPython {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
|
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
|
||||||
let (lower_bound, upper_bound) =
|
let (lower_bound, upper_bound) = PubGrubSpecifier::from_release_specifiers(&specifiers)
|
||||||
crate::pubgrub::PubGrubSpecifier::from_release_specifiers(&specifiers)
|
.map_err(serde::de::Error::custom)?
|
||||||
.map_err(serde::de::Error::custom)?
|
.bounding_range()
|
||||||
.bounding_range()
|
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
|
||||||
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
|
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
|
||||||
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
specifiers,
|
specifiers,
|
||||||
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
|
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
|
||||||
|
|
|
@ -3818,7 +3818,7 @@ fn lock_requires_python_star() -> Result<()> {
|
||||||
/// `Requires-Python` uses the != operator.
|
/// `Requires-Python` uses the != operator.
|
||||||
#[test]
|
#[test]
|
||||||
fn lock_requires_python_not_equal() -> Result<()> {
|
fn lock_requires_python_not_equal() -> Result<()> {
|
||||||
let context = TestContext::new("3.11");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let lockfile = context.temp_dir.join("uv.lock");
|
let lockfile = context.temp_dir.join("uv.lock");
|
||||||
|
|
||||||
|
@ -3828,7 +3828,7 @@ fn lock_requires_python_not_equal() -> Result<()> {
|
||||||
[project]
|
[project]
|
||||||
name = "project"
|
name = "project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">3.10, !=3.10.9, <3.13"
|
requires-python = ">3.10, !=3.10.9, !=3.10.10, !=3.11.*, <3.13"
|
||||||
dependencies = ["iniconfig"]
|
dependencies = ["iniconfig"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -3854,7 +3854,7 @@ fn lock_requires_python_not_equal() -> Result<()> {
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
lock, @r###"
|
lock, @r###"
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">3.10, !=3.10.9, <3.13"
|
requires-python = ">3.10, !=3.10.9, !=3.10.10, !=3.11.*, <3.13"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
exclude-newer = "2024-03-25T00:00:00Z"
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
@ -3936,7 +3936,7 @@ fn lock_requires_python_pre() -> Result<()> {
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
lock, @r###"
|
lock, @r###"
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.11b1"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
exclude-newer = "2024-03-25T00:00:00Z"
|
exclude-newer = "2024-03-25T00:00:00Z"
|
||||||
|
@ -12954,7 +12954,7 @@ fn lock_simplified_environments() -> Result<()> {
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
lock, @r###"
|
lock, @r###"
|
||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.11, <3.12"
|
requires-python = "==3.11.*"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"sys_platform == 'darwin'",
|
"sys_platform == 'darwin'",
|
||||||
"sys_platform != 'darwin'",
|
"sys_platform != 'darwin'",
|
||||||
|
|
|
@ -378,7 +378,7 @@ fn mixed_requires_python() -> Result<()> {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Using CPython 3.8.[X] interpreter at: [PYTHON-3.8]
|
Using CPython 3.8.[X] interpreter at: [PYTHON-3.8]
|
||||||
error: The requested interpreter resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.8, >=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.8. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.8.[X]` followed by `uv pip install -e .`.
|
error: The requested interpreter resolved to Python 3.8.[X], which is incompatible with the project's Python requirement: `>=3.12`. However, a workspace member (`bird-feeder`) supports Python >=3.8. To install the workspace member on its own, navigate to `packages/bird-feeder`, then run `uv venv --python 3.8.[X]` followed by `uv pip install -e .`.
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue