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-pep508",
"uv-pypi-types", "uv-pypi-types",
"uv-warnings", "uv-warnings",
"version-ranges",
"walkdir", "walkdir",
"zip", "zip",
] ]

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@
#![warn(missing_docs)] #![warn(missing_docs)]
#[cfg(feature = "version-ranges")] #[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 { pub use {
version::{ version::{
LocalSegment, Operator, OperatorParseError, Prerelease, PrereleaseKind, Version, LocalSegment, Operator, OperatorParseError, Prerelease, PrereleaseKind, Version,
@ -42,4 +42,4 @@ mod version_specifier;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[cfg(feature = "version-ranges")] #[cfg(feature = "version-ranges")]
mod version_ranges_specifier; mod version_ranges;

View file

@ -32,6 +32,8 @@ pub enum Operator {
/// `!= 1.2.*` /// `!= 1.2.*`
NotEqualStar, NotEqualStar,
/// `~=` /// `~=`
///
/// Invariant: With `~=`, there are always at least 2 release segments.
TildeEqual, TildeEqual,
/// `<` /// `<`
LessThan, 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 { impl FromStr for VersionSpecifiers {
type Err = VersionSpecifiersParseError; type Err = VersionSpecifiersParseError;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -362,7 +362,7 @@ async fn init_project(
} }
ref ref
python_request @ PythonRequest::Version(VersionRequest::Range(ref specifiers, _)) => { 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 { let python_request = if no_pin_python {
None None
@ -417,10 +417,7 @@ async fn init_project(
(requires_python, python_request) (requires_python, python_request)
} }
} }
} else if let Some(requires_python) = workspace } else if let Some(requires_python) = workspace.as_ref().and_then(find_requires_python) {
.as_ref()
.and_then(|workspace| find_requires_python(workspace).ok().flatten())
{
// (2) `Requires-Python` from the workspace // (2) `Requires-Python` from the workspace
let python_request = PythonRequest::Version(VersionRequest::Range( let python_request = PythonRequest::Version(VersionRequest::Range(
requires_python.specifiers().clone(), 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 // Determine the supported Python range. If no range is defined, and warn and default to the
// current minor version. // 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 { let requires_python = if let Some(requires_python) = requires_python {
if requires_python.is_unbounded() { if requires_python.is_unbounded() {

View file

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

View file

@ -55,7 +55,7 @@ pub(crate) async fn find(
}; };
if let Some(project) = project { if let Some(project) = project {
request = find_requires_python(project.workspace())? request = find_requires_python(project.workspace())
.as_ref() .as_ref()
.map(RequiresPython::specifiers) .map(RequiresPython::specifiers)
.map(|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.project_name(),
project_workspace.workspace().install_path().display() 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") (requires_python, "project")
} }
VirtualProject::NonProject(workspace) => { VirtualProject::NonProject(workspace) => {
@ -261,7 +261,7 @@ fn assert_pin_compatible_with_project(pin: &Pin, virtual_project: &VirtualProjec
"Discovered virtual workspace at: {}", "Discovered virtual workspace at: {}",
workspace.install_path().display() workspace.install_path().display()
); );
let requires_python = find_requires_python(workspace)?; let requires_python = find_requires_python(workspace);
(requires_python, "workspace") (requires_python, "workspace")
} }
}; };

View file

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