Simplify pep440 -> version ranges conversion (#8683)

This commit is contained in:
konsti 2024-10-30 13:10:48 +01:00 committed by GitHub
parent d0afd10ca4
commit c1a0fb35e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 286 additions and 415 deletions

1
Cargo.lock generated
View file

@ -4329,6 +4329,7 @@ dependencies = [
"uv-pep508",
"uv-pypi-types",
"uv-warnings",
"version-ranges",
"walkdir",
"zip",
]

View file

@ -31,6 +31,7 @@ spdx = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
version-ranges = { workspace = true }
walkdir = { workspace = true }
zip = { workspace = true }

View file

@ -9,10 +9,11 @@ use std::str::FromStr;
use tracing::debug;
use uv_fs::Simplified;
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{Version, VersionRangesSpecifier, VersionSpecifiers};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::{Requirement, VersionOrUrl};
use uv_pypi_types::{Metadata23, VerbatimParsedUrl};
use uv_warnings::warn_user_once;
use version_ranges::Ranges;
#[derive(Debug, Error)]
pub enum ValidationError {
@ -134,9 +135,9 @@ impl PyProjectToml {
);
passed = false;
}
VersionRangesSpecifier::from_pep440_specifiers(specifier)
.ok()
.and_then(|specifier| Some(specifier.bounding_range()?.1 != Bound::Unbounded))
Ranges::from(specifier.clone())
.bounding_range()
.map(|bounding_range| bounding_range.1 != Bound::Unbounded)
.unwrap_or(false)
}
};

View file

@ -25,6 +25,7 @@ serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true, optional = true }
unicode-width = { workspace = true }
unscanny = { workspace = true }
# Adds conversions from [`VersionSpecifiers`] to [`version_ranges::Ranges`]
version-ranges = { workspace = true, optional = true }
[dev-dependencies]

View file

@ -24,7 +24,7 @@
#![warn(missing_docs)]
#[cfg(feature = "version-ranges")]
pub use version_ranges_specifier::{VersionRangesSpecifier, VersionRangesSpecifierError};
pub use version_ranges::{release_specifier_to_range, release_specifiers_to_ranges};
pub use {
version::{
LocalSegment, Operator, OperatorParseError, Prerelease, PrereleaseKind, Version,
@ -42,4 +42,4 @@ mod version_specifier;
#[cfg(test)]
mod tests;
#[cfg(feature = "version-ranges")]
mod version_ranges_specifier;
mod version_ranges;

View file

@ -32,6 +32,8 @@ pub enum Operator {
/// `!= 1.2.*`
NotEqualStar,
/// `~=`
///
/// Invariant: With `~=`, there are always at least 2 release segments.
TildeEqual,
/// `<`
LessThan,

View file

@ -0,0 +1,192 @@
//! Convert [`VersionSpecifiers`] to [`version_ranges::Ranges`].
use version_ranges::Ranges;
use crate::{Operator, Prerelease, Version, VersionSpecifier, VersionSpecifiers};
impl From<VersionSpecifiers> for Ranges<Version> {
/// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440
/// semantics.
fn from(specifiers: VersionSpecifiers) -> Self {
let mut range = Ranges::full();
for specifier in specifiers {
range = range.intersection(&Self::from(specifier));
}
range
}
}
impl From<VersionSpecifier> for Ranges<Version> {
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440
/// semantics.
fn from(specifier: VersionSpecifier) -> Self {
let VersionSpecifier { operator, version } = specifier;
match operator {
Operator::Equal => Ranges::singleton(version),
Operator::ExactEqual => Ranges::singleton(version),
Operator::NotEqual => Ranges::singleton(version).complement(),
Operator::TildeEqual => {
let [rest @ .., last, _] = version.release() else {
unreachable!("~= must have at least two segments");
};
let upper = Version::new(rest.iter().chain([&(last + 1)]))
.with_epoch(version.epoch())
.with_dev(Some(0));
Ranges::from_range_bounds(version..upper)
}
Operator::LessThan => {
if version.any_prerelease() {
Ranges::strictly_lower_than(version)
} else {
// Per PEP 440: "The exclusive ordered comparison <V MUST NOT allow a
// pre-release of the specified version unless the specified version is itself a
// pre-release."
Ranges::strictly_lower_than(version.with_min(Some(0)))
}
}
Operator::LessThanEqual => Ranges::lower_than(version),
Operator::GreaterThan => {
// Per PEP 440: "The exclusive ordered comparison >V MUST NOT allow a post-release of
// the given version unless V itself is a post release."
if let Some(dev) = version.dev() {
Ranges::higher_than(version.with_dev(Some(dev + 1)))
} else if let Some(post) = version.post() {
Ranges::higher_than(version.with_post(Some(post + 1)))
} else {
Ranges::strictly_higher_than(version.with_max(Some(0)))
}
}
Operator::GreaterThanEqual => Ranges::higher_than(version),
Operator::EqualStar => {
let low = version.with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Ranges::from_range_bounds(low..high)
}
Operator::NotEqualStar => {
let low = version.with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Ranges::from_range_bounds(low..high).complement()
}
}
}
}
/// Convert the [`VersionSpecifiers`] to a PubGrub-compatible version range, using release-only
/// semantics.
///
/// Assumes that the range will only be tested against versions that consist solely of release
/// segments (e.g., `3.12.0`, but not `3.12.0b1`).
///
/// These semantics are used for testing Python compatibility (e.g., `requires-python` against
/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0`
/// is allowed for projects that declare `requires-python = ">3.13"`.
///
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
pub fn release_specifiers_to_ranges(specifiers: VersionSpecifiers) -> Ranges<Version> {
let mut range = Ranges::full();
for specifier in specifiers {
range = range.intersection(&release_specifier_to_range(specifier));
}
range
}
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only
/// semantics.
///
/// Assumes that the range will only be tested against versions that consist solely of release
/// segments (e.g., `3.12.0`, but not `3.12.0b1`).
///
/// These semantics are used for testing Python compatibility (e.g., `requires-python` against
/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0`
/// is allowed for projects that declare `requires-python = ">3.13"`.
///
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
pub fn release_specifier_to_range(specifier: VersionSpecifier) -> Ranges<Version> {
let VersionSpecifier { operator, version } = specifier;
match operator {
Operator::Equal => {
let version = version.only_release();
Ranges::singleton(version)
}
Operator::ExactEqual => {
let version = version.only_release();
Ranges::singleton(version)
}
Operator::NotEqual => {
let version = version.only_release();
Ranges::singleton(version).complement()
}
Operator::TildeEqual => {
let [rest @ .., last, _] = version.release() else {
unreachable!("~= must have at least two segments");
};
let upper = Version::new(rest.iter().chain([&(last + 1)]));
let version = version.only_release();
Ranges::from_range_bounds(version..upper)
}
Operator::LessThan => {
let version = version.only_release();
Ranges::strictly_lower_than(version)
}
Operator::LessThanEqual => {
let version = version.only_release();
Ranges::lower_than(version)
}
Operator::GreaterThan => {
let version = version.only_release();
Ranges::strictly_higher_than(version)
}
Operator::GreaterThanEqual => {
let version = version.only_release();
Ranges::higher_than(version)
}
Operator::EqualStar => {
let low = version.only_release();
let high = {
let mut high = low.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(low..high)
}
Operator::NotEqualStar => {
let low = version.only_release();
let high = {
let mut high = low.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(low..high).complement()
}
}
}

View file

@ -1,273 +0,0 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::ops::Bound;
use version_ranges::Ranges;
use crate::{Operator, Prerelease, Version, VersionSpecifier, VersionSpecifiers};
/// The conversion between PEP 440 [`VersionSpecifier`] and version-ranges
/// [`VersionRangesSpecifier`] failed.
#[derive(Debug)]
pub enum VersionRangesSpecifierError {
/// The `~=` operator requires at least two release segments
InvalidTildeEquals(VersionSpecifier),
}
impl Error for VersionRangesSpecifierError {}
impl Display for VersionRangesSpecifierError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidTildeEquals(specifier) => {
write!(
f,
"The `~=` operator requires at least two release segments: `{specifier}`"
)
}
}
}
}
/// A range of versions that can be used to satisfy a requirement.
#[derive(Debug)]
pub struct VersionRangesSpecifier(Ranges<Version>);
impl VersionRangesSpecifier {
/// Returns an iterator over the bounds of the [`VersionRangesSpecifier`].
pub fn iter(&self) -> impl Iterator<Item = (&Bound<Version>, &Bound<Version>)> {
self.0.iter()
}
/// Return the bounding [`Ranges`] of the [`VersionRangesSpecifier`].
pub fn bounding_range(&self) -> Option<(Bound<&Version>, Bound<&Version>)> {
self.0.bounding_range()
}
}
impl From<Ranges<Version>> for VersionRangesSpecifier {
fn from(range: Ranges<Version>) -> Self {
VersionRangesSpecifier(range)
}
}
impl From<VersionRangesSpecifier> for Ranges<Version> {
/// Convert a PubGrub specifier to a range of versions.
fn from(specifier: VersionRangesSpecifier) -> Self {
specifier.0
}
}
impl VersionRangesSpecifier {
/// Convert [`VersionSpecifiers`] to a PubGrub-compatible version range, using PEP 440
/// semantics.
pub fn from_pep440_specifiers(
specifiers: &VersionSpecifiers,
) -> Result<Self, VersionRangesSpecifierError> {
let mut range = Ranges::full();
for specifier in specifiers.iter() {
range = range.intersection(&Self::from_pep440_specifier(specifier)?.into());
}
Ok(Self(range))
}
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using PEP 440
/// semantics.
pub fn from_pep440_specifier(
specifier: &VersionSpecifier,
) -> Result<Self, VersionRangesSpecifierError> {
let ranges = match specifier.operator() {
Operator::Equal => {
let version = specifier.version().clone();
Ranges::singleton(version)
}
Operator::ExactEqual => {
let version = specifier.version().clone();
Ranges::singleton(version)
}
Operator::NotEqual => {
let version = specifier.version().clone();
Ranges::singleton(version).complement()
}
Operator::TildeEqual => {
let [rest @ .., last, _] = specifier.version().release() else {
return Err(VersionRangesSpecifierError::InvalidTildeEquals(
specifier.clone(),
));
};
let upper = Version::new(rest.iter().chain([&(last + 1)]))
.with_epoch(specifier.version().epoch())
.with_dev(Some(0));
let version = specifier.version().clone();
Ranges::from_range_bounds(version..upper)
}
Operator::LessThan => {
let version = specifier.version().clone();
if version.any_prerelease() {
Ranges::strictly_lower_than(version)
} else {
// Per PEP 440: "The exclusive ordered comparison <V MUST NOT allow a
// pre-release of the specified version unless the specified version is itself a
// pre-release."
Ranges::strictly_lower_than(version.with_min(Some(0)))
}
}
Operator::LessThanEqual => {
let version = specifier.version().clone();
Ranges::lower_than(version)
}
Operator::GreaterThan => {
// Per PEP 440: "The exclusive ordered comparison >V MUST NOT allow a post-release of
// the given version unless V itself is a post release."
let version = specifier.version().clone();
if let Some(dev) = version.dev() {
Ranges::higher_than(version.with_dev(Some(dev + 1)))
} else if let Some(post) = version.post() {
Ranges::higher_than(version.with_post(Some(post + 1)))
} else {
Ranges::strictly_higher_than(version.with_max(Some(0)))
}
}
Operator::GreaterThanEqual => {
let version = specifier.version().clone();
Ranges::higher_than(version)
}
Operator::EqualStar => {
let low = specifier.version().clone().with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Ranges::from_range_bounds(low..high)
}
Operator::NotEqualStar => {
let low = specifier.version().clone().with_dev(Some(0));
let mut high = low.clone();
if let Some(post) = high.post() {
high = high.with_post(Some(post + 1));
} else if let Some(pre) = high.pre() {
high = high.with_pre(Some(Prerelease {
kind: pre.kind,
number: pre.number + 1,
}));
} else {
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
}
Ranges::from_range_bounds(low..high).complement()
}
};
Ok(Self(ranges))
}
/// Convert the [`VersionSpecifiers`] to a PubGrub-compatible version range, using release-only
/// semantics.
///
/// Assumes that the range will only be tested against versions that consist solely of release
/// segments (e.g., `3.12.0`, but not `3.12.0b1`).
///
/// These semantics are used for testing Python compatibility (e.g., `requires-python` against
/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0`
/// is allowed for projects that declare `requires-python = ">3.13"`.
///
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
pub fn from_release_specifiers(
specifiers: &VersionSpecifiers,
) -> Result<Self, VersionRangesSpecifierError> {
let mut range = Ranges::full();
for specifier in specifiers.iter() {
range = range.intersection(&Self::from_release_specifier(specifier)?.into());
}
Ok(Self(range))
}
/// Convert the [`VersionSpecifier`] to a PubGrub-compatible version range, using release-only
/// semantics.
///
/// Assumes that the range will only be tested against versions that consist solely of release
/// segments (e.g., `3.12.0`, but not `3.12.0b1`).
///
/// These semantics are used for testing Python compatibility (e.g., `requires-python` against
/// the user's installed Python version). In that context, it's more intuitive that `3.13.0b0`
/// is allowed for projects that declare `requires-python = ">3.13"`.
///
/// See: <https://github.com/pypa/pip/blob/a432c7f4170b9ef798a15f035f5dfdb4cc939f35/src/pip/_internal/resolution/resolvelib/candidates.py#L540>
pub fn from_release_specifier(
specifier: &VersionSpecifier,
) -> Result<Self, VersionRangesSpecifierError> {
let ranges = match specifier.operator() {
Operator::Equal => {
let version = specifier.version().only_release();
Ranges::singleton(version)
}
Operator::ExactEqual => {
let version = specifier.version().only_release();
Ranges::singleton(version)
}
Operator::NotEqual => {
let version = specifier.version().only_release();
Ranges::singleton(version).complement()
}
Operator::TildeEqual => {
let [rest @ .., last, _] = specifier.version().release() else {
return Err(VersionRangesSpecifierError::InvalidTildeEquals(
specifier.clone(),
));
};
let upper = Version::new(rest.iter().chain([&(last + 1)]));
let version = specifier.version().only_release();
Ranges::from_range_bounds(version..upper)
}
Operator::LessThan => {
let version = specifier.version().only_release();
Ranges::strictly_lower_than(version)
}
Operator::LessThanEqual => {
let version = specifier.version().only_release();
Ranges::lower_than(version)
}
Operator::GreaterThan => {
let version = specifier.version().only_release();
Ranges::strictly_higher_than(version)
}
Operator::GreaterThanEqual => {
let version = specifier.version().only_release();
Ranges::higher_than(version)
}
Operator::EqualStar => {
let low = specifier.version().only_release();
let high = {
let mut high = low.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(low..high)
}
Operator::NotEqualStar => {
let low = specifier.version().only_release();
let high = {
let mut high = low.clone();
let mut release = high.release().to_vec();
*release.last_mut().unwrap() += 1;
high = high.with_release(release);
high
};
Ranges::from_range_bounds(low..high).complement()
}
};
Ok(Self(ranges))
}
}

View file

@ -112,6 +112,15 @@ impl FromIterator<VersionSpecifier> for VersionSpecifiers {
}
}
impl IntoIterator for VersionSpecifiers {
type Item = VersionSpecifier;
type IntoIter = std::vec::IntoIter<VersionSpecifier>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromStr for VersionSpecifiers {
type Err = VersionSpecifiersParseError;

View file

@ -53,8 +53,7 @@ use std::sync::MutexGuard;
use itertools::Either;
use rustc_hash::FxHashMap;
use std::sync::LazyLock;
use uv_pep440::{Operator, VersionRangesSpecifier};
use uv_pep440::{Version, VersionSpecifier};
use uv_pep440::{release_specifier_to_range, Operator, Version, VersionSpecifier};
use version_ranges::Ranges;
use crate::marker::MarkerValueExtra;
@ -744,11 +743,9 @@ impl Edges {
/// Returns the [`Edges`] for a version specifier.
fn from_specifier(specifier: VersionSpecifier) -> Edges {
let specifier =
VersionRangesSpecifier::from_release_specifier(&normalize_specifier(specifier))
.unwrap();
let specifier = release_specifier_to_range(normalize_specifier(specifier));
Edges::Version {
edges: Edges::from_range(&specifier.into()),
edges: Edges::from_range(&specifier),
}
}
@ -763,10 +760,8 @@ impl Edges {
for version in versions {
let specifier = VersionSpecifier::equals_version(version.clone());
let specifier = python_version_to_full_version(specifier)?;
let pubgrub_specifier =
VersionRangesSpecifier::from_release_specifier(&normalize_specifier(specifier))
.unwrap();
range = range.union(&pubgrub_specifier.into());
let pubgrub_specifier = release_specifier_to_range(normalize_specifier(specifier));
range = range.union(&pubgrub_specifier);
}
if negated {

View file

@ -19,7 +19,7 @@ use uv_distribution_types::{
BuiltDist, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist, SourceDist,
};
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionRangesSpecifierError};
use uv_pep440::Version;
use uv_pep508::MarkerTree;
use uv_static::EnvVars;
@ -37,9 +37,6 @@ pub enum ResolveError {
#[error("Attempted to wait on an unregistered task: `{_0}`")]
UnregisteredTask(String),
#[error(transparent)]
VersionRangesSpecifier(#[from] VersionRangesSpecifierError),
#[error("Overrides contain conflicting URLs for package `{0}`:\n- {1}\n- {2}")]
ConflictingOverrideUrls(PackageName, String, String),

View file

@ -12,7 +12,7 @@ pub use options::{Flexibility, Options, OptionsBuilder};
pub use preferences::{Preference, PreferenceError, Preferences};
pub use prerelease::PrereleaseMode;
pub use python_requirement::PythonRequirement;
pub use requires_python::{RequiresPython, RequiresPythonError, RequiresPythonRange};
pub use requires_python::{RequiresPython, RequiresPythonRange};
pub use resolution::{
AnnotationStyle, ConflictingDistributionError, DisplayResolutionGraph, ResolutionGraph,
};

View file

@ -1,23 +1,21 @@
use std::iter;
use itertools::Itertools;
use pubgrub::Range;
use pubgrub::Ranges;
use tracing::warn;
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{Version, VersionRangesSpecifier, VersionSpecifiers};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pypi_types::{
ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, Requirement,
RequirementSource, VerbatimParsedUrl,
};
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner};
use crate::ResolveError;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PubGrubDependency {
pub(crate) package: PubGrubPackage,
pub(crate) version: Range<Version>,
pub(crate) version: Ranges<Version>,
/// The original version specifiers from the requirement.
pub(crate) specifier: Option<VersionSpecifiers>,
@ -32,12 +30,12 @@ impl PubGrubDependency {
pub(crate) fn from_requirement<'a>(
requirement: &'a Requirement,
source_name: Option<&'a PackageName>,
) -> impl Iterator<Item = Result<Self, ResolveError>> + 'a {
) -> impl Iterator<Item = Self> + 'a {
// Add the package, plus any extra variants.
iter::once(None)
.chain(requirement.extras.clone().into_iter().map(Some))
.map(|extra| PubGrubRequirement::from_requirement(requirement, extra))
.filter_map_ok(move |requirement| {
.filter_map(move |requirement| {
let PubGrubRequirement {
package,
version,
@ -87,7 +85,7 @@ impl PubGrubDependency {
#[derive(Debug, Clone)]
pub(crate) struct PubGrubRequirement {
pub(crate) package: PubGrubPackage,
pub(crate) version: Range<Version>,
pub(crate) version: Ranges<Version>,
pub(crate) specifier: Option<VersionSpecifiers>,
pub(crate) url: Option<VerbatimParsedUrl>,
}
@ -95,10 +93,7 @@ pub(crate) struct PubGrubRequirement {
impl PubGrubRequirement {
/// Convert a [`Requirement`] to a PubGrub-compatible package and range, while returning the URL
/// on the [`Requirement`], if any.
pub(crate) fn from_requirement(
requirement: &Requirement,
extra: Option<ExtraName>,
) -> Result<Self, ResolveError> {
pub(crate) fn from_requirement(requirement: &Requirement, extra: Option<ExtraName>) -> Self {
let (verbatim_url, parsed_url) = match &requirement.source {
RequirementSource::Registry { specifier, .. } => {
return Self::from_registry_requirement(specifier, extra, requirement);
@ -159,29 +154,27 @@ impl PubGrubRequirement {
}
};
Ok(Self {
Self {
package: PubGrubPackage::from_package(
requirement.name.clone(),
extra,
requirement.marker.clone(),
),
version: Range::full(),
version: Ranges::full(),
specifier: None,
url: Some(VerbatimParsedUrl {
parsed_url,
verbatim: verbatim_url.clone(),
}),
})
}
}
fn from_registry_requirement(
specifier: &VersionSpecifiers,
extra: Option<ExtraName>,
requirement: &Requirement,
) -> Result<PubGrubRequirement, ResolveError> {
let version = VersionRangesSpecifier::from_pep440_specifiers(specifier)?.into();
let requirement = Self {
) -> PubGrubRequirement {
Self {
package: PubGrubPackage::from_package(
requirement.name.clone(),
extra,
@ -189,9 +182,7 @@ impl PubGrubRequirement {
),
specifier: Some(specifier.clone()),
url: None,
version,
};
Ok(requirement)
version: Ranges::from(specifier.clone()),
}
}
}

View file

@ -1,22 +1,12 @@
use itertools::Itertools;
use pubgrub::Range;
use std::cmp::Ordering;
use std::collections::Bound;
use std::ops::Deref;
use uv_distribution_filename::WheelFilename;
use uv_pep440::{
Version, VersionRangesSpecifier, VersionRangesSpecifierError, VersionSpecifier,
VersionSpecifiers,
};
use uv_pep440::{release_specifiers_to_ranges, Version, VersionSpecifier, VersionSpecifiers};
use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
#[derive(thiserror::Error, Debug)]
pub enum RequiresPythonError {
#[error(transparent)]
VersionRangesSpecifier(#[from] VersionRangesSpecifierError),
}
/// The `Requires-Python` requirement specifier.
///
/// See: <https://packaging.python.org/en/latest/guides/dropping-older-python-versions/>
@ -54,16 +44,15 @@ impl RequiresPython {
}
/// Returns a [`RequiresPython`] from a version specifier.
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Result<Self, RequiresPythonError> {
let (lower_bound, upper_bound) =
VersionRangesSpecifier::from_release_specifiers(specifiers)?
pub fn from_specifiers(specifiers: &VersionSpecifiers) -> Self {
let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
.bounding_range()
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
.unwrap_or((Bound::Unbounded, Bound::Unbounded));
Ok(Self {
Self {
specifiers: specifiers.clone(),
range: RequiresPythonRange(LowerBound(lower_bound), UpperBound(upper_bound)),
})
}
}
/// Returns a [`RequiresPython`] to express the intersection of the given version specifiers.
@ -71,23 +60,19 @@ impl RequiresPython {
/// For example, given `>=3.8` and `>=3.9`, this would return `>=3.9`.
pub fn intersection<'a>(
specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
) -> Result<Option<Self>, RequiresPythonError> {
) -> Option<Self> {
// Convert to PubGrub range and perform an intersection.
let range = specifiers
.into_iter()
.map(VersionRangesSpecifier::from_release_specifiers)
.fold_ok(None, |range: Option<Range<Version>>, requires_python| {
.map(|specifier| release_specifiers_to_ranges(specifier.clone()))
.fold(None, |range: Option<Range<Version>>, requires_python| {
if let Some(range) = range {
Some(range.intersection(&requires_python.into()))
Some(range.intersection(&requires_python))
} else {
Some(requires_python.into())
Some(requires_python)
}
})?;
let Some(range) = range else {
return Ok(None);
};
// Extract the bounds.
let (lower_bound, upper_bound) = range
.bounding_range()
@ -102,10 +87,10 @@ impl RequiresPython {
// Convert back to PEP 440 specifiers.
let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
Ok(Some(Self {
Some(Self {
specifiers,
range: RequiresPythonRange(lower_bound, upper_bound),
}))
})
}
/// Narrow the [`RequiresPython`] by computing the intersection with the given range.
@ -260,14 +245,10 @@ impl RequiresPython {
/// N.B. This operation should primarily be used when evaluating the compatibility of a
/// project's `Requires-Python` specifier against a dependency's `Requires-Python` specifier.
pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
let Ok(target) = VersionRangesSpecifier::from_release_specifiers(target) else {
return false;
};
let target = target
.iter()
.next()
.map(|(lower, _)| lower)
.unwrap_or(&Bound::Unbounded);
let target = release_specifiers_to_ranges(target.clone())
.bounding_range()
.map(|bounding_range| bounding_range.0.cloned())
.unwrap_or(Bound::Unbounded);
// We want, e.g., `self.range.lower()` to be `>=3.8` and `target` to be `>=3.7`.
//
@ -508,9 +489,7 @@ impl serde::Serialize for RequiresPython {
impl<'de> serde::Deserialize<'de> for RequiresPython {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
let (lower_bound, upper_bound) =
VersionRangesSpecifier::from_release_specifiers(&specifiers)
.map_err(serde::de::Error::custom)?
let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
.bounding_range()
.map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
.unwrap_or((Bound::Unbounded, Bound::Unbounded));

View file

@ -11,7 +11,7 @@ use crate::RequiresPython;
#[test]
fn requires_python_included() {
let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &[
"bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl",
"black-24.4.2-cp310-cp310-win_amd64.whl",
@ -30,7 +30,7 @@ fn requires_python_included() {
}
let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"];
for wheel_name in wheel_names {
assert!(
@ -40,7 +40,7 @@ fn requires_python_included() {
}
let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"];
for wheel_name in wheel_names {
assert!(
@ -50,7 +50,7 @@ fn requires_python_included() {
}
let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"];
for wheel_name in wheel_names {
assert!(
@ -63,7 +63,7 @@ fn requires_python_included() {
#[test]
fn requires_python_dropped() {
let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &[
"PySocks-1.7.1-py27-none-any.whl",
"black-24.4.2-cp39-cp39-win_amd64.whl",
@ -83,7 +83,7 @@ fn requires_python_dropped() {
}
let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"];
for wheel_name in wheel_names {
assert!(
@ -152,7 +152,7 @@ fn is_exact_without_patch() {
];
for (version, expected) in test_cases {
let version_specifiers = VersionSpecifiers::from_str(version).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap();
let requires_python = RequiresPython::from_specifiers(&version_specifiers);
assert_eq!(requires_python.is_exact_without_patch(), expected);
}
}

View file

@ -13,7 +13,7 @@ use dashmap::DashMap;
use either::Either;
use futures::{FutureExt, StreamExt};
use itertools::Itertools;
use pubgrub::{Incompatibility, Range, State};
use pubgrub::{Incompatibility, Range, Ranges, State};
use rustc_hash::{FxHashMap, FxHashSet};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio::sync::oneshot;
@ -34,7 +34,7 @@ use uv_distribution_types::{
};
use uv_git::GitResolver;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::{Version, VersionRangesSpecifier, MIN_VERSION};
use uv_pep440::{release_specifiers_to_ranges, Version, MIN_VERSION};
use uv_pep508::MarkerTree;
use uv_platform_tags::Tags;
use uv_pypi_types::{Requirement, ResolutionMetadata, VerbatimParsedUrl};
@ -465,7 +465,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let version = match version {
ResolverVersion::Available(version) => version,
ResolverVersion::Unavailable(version, reason) => {
state.add_unavailable_version(version, reason)?;
state.add_unavailable_version(version, reason);
continue;
}
};
@ -1241,7 +1241,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
requirements
.iter()
.flat_map(|requirement| PubGrubDependency::from_requirement(requirement, None))
.collect::<Result<Vec<_>, _>>()?
.collect()
}
PubGrubPackageInner::Package {
name,
@ -1349,12 +1349,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
python_requirement,
);
let mut dependencies = requirements
let mut dependencies: Vec<_> = requirements
.iter()
.flat_map(|requirement| {
PubGrubDependency::from_requirement(requirement, Some(name))
})
.collect::<Result<Vec<_>, _>>()?;
.collect();
// If a package has metadata for an enabled dependency group,
// add a dependency from it to the same package with the group
@ -1610,10 +1610,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
extras: constraint.extras.clone(),
source: constraint.source.clone(),
origin: constraint.origin.clone(),
marker
marker,
})
}
} else {
let requires_python = python_requirement.target();
let python_marker = python_requirement.to_marker_tree();
@ -1644,7 +1643,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
extras: constraint.extras.clone(),
source: constraint.source.clone(),
origin: constraint.origin.clone(),
marker
marker,
})
}
};
@ -2223,14 +2222,10 @@ impl ForkState {
.map(|specifier| {
Locals::map(local, specifier)
.map_err(ResolveError::InvalidVersion)
.and_then(|specifier| {
Ok(VersionRangesSpecifier::from_pep440_specifier(
&specifier,
)?)
})
.map(Ranges::from)
})
.fold_ok(Range::full(), |range, specifier| {
range.intersection(&specifier.into())
range.intersection(&specifier)
})?;
// Add the local version.
@ -2288,11 +2283,7 @@ impl ForkState {
Ok(())
}
fn add_unavailable_version(
&mut self,
version: Version,
reason: UnavailableVersion,
) -> Result<(), ResolveError> {
fn add_unavailable_version(&mut self, version: Version, reason: UnavailableVersion) {
// Incompatible requires-python versions are special in that we track
// them as incompatible dependencies instead of marking the package version
// as unavailable directly.
@ -2301,9 +2292,6 @@ impl ForkState {
| IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython(requires_python, kind)),
) = reason
{
let python_version: Range<Version> =
VersionRangesSpecifier::from_release_specifiers(&requires_python)?.into();
let package = &self.next;
self.pubgrub
.add_incompatibility(Incompatibility::from_dependency(
@ -2314,13 +2302,13 @@ impl ForkState {
PythonRequirementKind::Installed => PubGrubPython::Installed,
PythonRequirementKind::Target => PubGrubPython::Target,
})),
python_version.clone(),
release_specifiers_to_ranges(requires_python),
),
));
self.pubgrub
.partial_solution
.add_decision(self.next.clone(), version);
return Ok(());
return;
};
self.pubgrub
.add_incompatibility(Incompatibility::custom_version(
@ -2328,7 +2316,6 @@ impl ForkState {
version.clone(),
UnavailableReason::Version(reason),
));
Ok(())
}
/// Subset the current markers with the new markers and update the python requirements fields

View file

@ -396,7 +396,7 @@ async fn build_package(
// (3) `Requires-Python` in `pyproject.toml`
if interpreter_request.is_none() {
if let Ok(workspace) = workspace {
interpreter_request = find_requires_python(workspace)?
interpreter_request = find_requires_python(workspace)
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| {

View file

@ -771,7 +771,4 @@ pub(crate) enum Error {
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error(transparent)]
VersionRangesSpecifier(#[from] uv_pep440::VersionRangesSpecifierError),
}

View file

@ -362,7 +362,7 @@ async fn init_project(
}
ref
python_request @ PythonRequest::Version(VersionRequest::Range(ref specifiers, _)) => {
let requires_python = RequiresPython::from_specifiers(specifiers)?;
let requires_python = RequiresPython::from_specifiers(specifiers);
let python_request = if no_pin_python {
None
@ -417,10 +417,7 @@ async fn init_project(
(requires_python, python_request)
}
}
} else if let Some(requires_python) = workspace
.as_ref()
.and_then(|workspace| find_requires_python(workspace).ok().flatten())
{
} else if let Some(requires_python) = workspace.as_ref().and_then(find_requires_python) {
// (2) `Requires-Python` from the workspace
let python_request = PythonRequest::Version(VersionRequest::Range(
requires_python.specifiers().clone(),

View file

@ -390,7 +390,7 @@ async fn do_lock(
// Determine the supported Python range. If no range is defined, and warn and default to the
// current minor version.
let requires_python = find_requires_python(workspace)?;
let requires_python = find_requires_python(workspace);
let requires_python = if let Some(requires_python) = requires_python {
if requires_python.is_unbounded() {

View file

@ -176,9 +176,6 @@ pub(crate) enum ProjectError {
#[error(transparent)]
Operation(#[from] pip::operations::Error),
#[error(transparent)]
RequiresPython(#[from] uv_resolver::RequiresPythonError),
#[error(transparent)]
Interpreter(#[from] uv_python::InterpreterError),
@ -208,9 +205,7 @@ pub(crate) enum ProjectError {
///
/// For a [`Workspace`] with multiple packages, the `Requires-Python` bound is the union of the
/// `Requires-Python` bounds of all the packages.
pub(crate) fn find_requires_python(
workspace: &Workspace,
) -> Result<Option<RequiresPython>, uv_resolver::RequiresPythonError> {
pub(crate) fn find_requires_python(workspace: &Workspace) -> Option<RequiresPython> {
RequiresPython::intersection(workspace.packages().values().filter_map(|member| {
member
.pyproject_toml()
@ -341,7 +336,7 @@ impl WorkspacePython {
python_request: Option<PythonRequest>,
workspace: &Workspace,
) -> Result<Self, ProjectError> {
let requires_python = find_requires_python(workspace)?;
let requires_python = find_requires_python(workspace);
let (source, python_request) = if let Some(request) = python_request {
// (1) Explicit request from user

View file

@ -55,7 +55,7 @@ pub(crate) async fn find(
};
if let Some(project) = project {
request = find_requires_python(project.workspace())?
request = find_requires_python(project.workspace())
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| {

View file

@ -253,7 +253,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
project_workspace.project_name(),
project_workspace.workspace().install_path().display()
);
let requires_python = find_requires_python(project_workspace.workspace())?;
let requires_python = find_requires_python(project_workspace.workspace());
(requires_python, "project")
}
VirtualProject::NonProject(workspace) => {
@ -261,7 +261,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
"Discovered virtual workspace at: {}",
workspace.install_path().display()
);
let requires_python = find_requires_python(workspace)?;
let requires_python = find_requires_python(workspace);
(requires_python, "workspace")
}
};

View file

@ -198,7 +198,6 @@ async fn venv_impl(
if interpreter_request.is_none() {
if let Some(project) = project {
interpreter_request = find_requires_python(project.workspace())
.into_diagnostic()?
.as_ref()
.map(RequiresPython::specifiers)
.map(|specifiers| {