From c0650062766f1c324b106f929d5960f86f4ec64b Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 6 Jun 2024 12:03:38 +0200 Subject: [PATCH] Use union of `requires-python` in workspace --- Cargo.lock | 1 + crates/pep440-rs/src/version_specifier.rs | 45 ++++++++++++++++++ crates/uv-resolver/src/error.rs | 10 ++-- crates/uv-resolver/src/lib.rs | 1 + .../uv-resolver/src/pubgrub/dependencies.rs | 2 +- crates/uv-resolver/src/pubgrub/mod.rs | 2 +- crates/uv-resolver/src/pubgrub/specifier.rs | 19 +++++--- crates/uv/Cargo.toml | 1 + crates/uv/src/commands/pip/operations.rs | 3 ++ crates/uv/src/commands/project/lock.rs | 46 ++++++++++++++----- crates/uv/src/commands/project/mod.rs | 3 ++ 11 files changed, 109 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 908845629cd45..62cce7e94bf74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4396,6 +4396,7 @@ dependencies = [ "pep508_rs", "platform-tags", "predicates", + "pubgrub", "pypi-types", "rayon", "regex", diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index fcb783bca8132..4d6f948863809 100644 --- a/crates/pep440-rs/src/version_specifier.rs +++ b/crates/pep440-rs/src/version_specifier.rs @@ -1,5 +1,6 @@ #[cfg(feature = "pyo3")] use std::hash::{Hash, Hasher}; +use std::ops::Bound; use std::{cmp::Ordering, str::FromStr}; #[cfg(feature = "pyo3")] @@ -415,6 +416,50 @@ impl VersionSpecifier { self.version.any_prerelease() } + /// Returns the version specifiers whose union represents the given range. + pub fn from_bounds( + bounds: (&Bound, &Bound), + ) -> impl Iterator { + let (b1, b2) = match bounds { + (Bound::Included(v1), Bound::Included(v2)) if v1 == v2 => { + (Some(VersionSpecifier::equals_version(v1.clone())), None) + } + (lower, upper) => ( + VersionSpecifier::from_lower_bound(lower), + VersionSpecifier::from_upper_bound(upper), + ), + }; + + b1.into_iter().chain(b2) + } + + /// Returns a version specifier representing the given lower bound. + fn from_lower_bound(bound: &Bound) -> Option { + match bound { + Bound::Included(version) => Some( + VersionSpecifier::from_version(Operator::GreaterThanEqual, version.clone()) + .unwrap(), + ), + Bound::Excluded(version) => Some( + VersionSpecifier::from_version(Operator::GreaterThan, version.clone()).unwrap(), + ), + Bound::Unbounded => None, + } + } + + /// Returns a version specifier representing the given upper bound. + fn from_upper_bound(bound: &Bound) -> Option { + match bound { + Bound::Included(version) => Some( + VersionSpecifier::from_version(Operator::LessThanEqual, version.clone()).unwrap(), + ), + Bound::Excluded(version) => { + Some(VersionSpecifier::from_version(Operator::LessThan, version.clone()).unwrap()) + } + Bound::Unbounded => None, + } + } + /// Whether the given version satisfies the version range /// /// e.g. `>=1.19,<2.0` and `1.21` -> true diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 5089a4bbdd63a..5b81d2244d936 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -2,12 +2,12 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; use std::sync::Arc; +use dashmap::DashMap; use indexmap::IndexMap; use pubgrub::range::Range; use pubgrub::report::{DefaultStringReporter, DerivationTree, External, Reporter}; use rustc_hash::{FxHashMap, FxHashSet}; -use dashmap::DashMap; use distribution_types::{BuiltDist, IndexLocations, InstalledDist, SourceDist}; use pep440_rs::Version; use pep508_rs::Requirement; @@ -15,7 +15,9 @@ use uv_normalize::PackageName; use crate::candidate_selector::CandidateSelector; use crate::dependency_provider::UvDependencyProvider; -use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter}; +use crate::pubgrub::{ + PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, PubGrubSpecifierError, +}; use crate::python_requirement::PythonRequirement; use crate::resolver::{ FxOnceMap, IncompletePackage, UnavailablePackage, UnavailableReason, VersionsResponse, @@ -44,8 +46,8 @@ pub enum ResolveError { metadata: PackageName, }, - #[error("~= operator requires at least two release segments: `{0}`")] - InvalidTildeEquals(pep440_rs::VersionSpecifier), + #[error(transparent)] + PubGrubSpecifier(#[from] PubGrubSpecifierError), #[error("Requirements contain conflicting URLs for package `{0}`:\n- {1}\n- {2}")] ConflictingUrlsDirect(PackageName, String, String), diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 7de3437e5a964..023f8aa8272f8 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -8,6 +8,7 @@ pub use manifest::Manifest; pub use options::{Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError}; pub use prerelease_mode::PreReleaseMode; +pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError}; pub use python_requirement::PythonRequirement; pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph}; pub use resolution_mode::ResolutionMode; diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 3f02c3a91a530..6237d93c3c022 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -237,7 +237,7 @@ impl PubGrubRequirement { .map(|specifier| { Locals::map(expected, specifier) .map_err(ResolveError::InvalidVersion) - .and_then(|specifier| PubGrubSpecifier::try_from(&specifier)) + .and_then(|specifier| Ok(PubGrubSpecifier::try_from(&specifier)?)) }) .fold_ok(Range::full(), |range, specifier| { range.intersection(&specifier.into()) diff --git a/crates/uv-resolver/src/pubgrub/mod.rs b/crates/uv-resolver/src/pubgrub/mod.rs index 828d94c0b024c..b6d48cea91af5 100644 --- a/crates/uv-resolver/src/pubgrub/mod.rs +++ b/crates/uv-resolver/src/pubgrub/mod.rs @@ -3,7 +3,7 @@ pub(crate) use crate::pubgrub::distribution::PubGrubDistribution; pub(crate) use crate::pubgrub::package::{PubGrubPackage, PubGrubPackageInner, PubGrubPython}; pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority}; pub(crate) use crate::pubgrub::report::PubGrubReportFormatter; -pub(crate) use crate::pubgrub::specifier::PubGrubSpecifier; +pub use crate::pubgrub::specifier::{PubGrubSpecifier, PubGrubSpecifierError}; mod dependencies; mod distribution; diff --git a/crates/uv-resolver/src/pubgrub/specifier.rs b/crates/uv-resolver/src/pubgrub/specifier.rs index b82cd29e463ef..d2b99eb695d67 100644 --- a/crates/uv-resolver/src/pubgrub/specifier.rs +++ b/crates/uv-resolver/src/pubgrub/specifier.rs @@ -1,13 +1,18 @@ use itertools::Itertools; use pubgrub::range::Range; +use thiserror::Error; use pep440_rs::{Operator, PreRelease, Version, VersionSpecifier, VersionSpecifiers}; -use crate::ResolveError; +#[derive(Debug, Error)] +pub enum PubGrubSpecifierError { + #[error("~= operator requires at least two release segments: `{0}`")] + InvalidTildeEquals(VersionSpecifier), +} /// A range of versions that can be used to satisfy a requirement. #[derive(Debug)] -pub(crate) struct PubGrubSpecifier(Range); +pub struct PubGrubSpecifier(Range); impl PubGrubSpecifier { /// Returns `true` if the [`PubGrubSpecifier`] is a subset of the other. @@ -24,10 +29,10 @@ impl From for Range { } impl TryFrom<&VersionSpecifiers> for PubGrubSpecifier { - type Error = ResolveError; + type Error = PubGrubSpecifierError; /// Convert a PEP 440 specifier to a PubGrub-compatible version range. - fn try_from(specifiers: &VersionSpecifiers) -> Result { + fn try_from(specifiers: &VersionSpecifiers) -> Result { let range = specifiers .iter() .map(crate::pubgrub::PubGrubSpecifier::try_from) @@ -39,10 +44,10 @@ impl TryFrom<&VersionSpecifiers> for PubGrubSpecifier { } impl TryFrom<&VersionSpecifier> for PubGrubSpecifier { - type Error = ResolveError; + type Error = PubGrubSpecifierError; /// Convert a PEP 440 specifier to a PubGrub-compatible version range. - fn try_from(specifier: &VersionSpecifier) -> Result { + fn try_from(specifier: &VersionSpecifier) -> Result { let ranges = match specifier.operator() { Operator::Equal => { let version = specifier.version().clone(); @@ -58,7 +63,7 @@ impl TryFrom<&VersionSpecifier> for PubGrubSpecifier { } Operator::TildeEqual => { let [rest @ .., last, _] = specifier.version().release() else { - return Err(ResolveError::InvalidTildeEquals(specifier.clone())); + return Err(PubGrubSpecifierError::InvalidTildeEquals(specifier.clone())); }; let upper = Version::new(rest.iter().chain([&(last + 1)])) .with_epoch(specifier.version().epoch()) diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 2326c91f0798f..2531b0899ba67 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -50,6 +50,7 @@ indicatif = { workspace = true } itertools = { workspace = true } miette = { workspace = true, features = ["fancy"] } owo-colors = { workspace = true } +pubgrub = { workspace = true } rayon = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index a040c79dd892b..b5b53516028ab 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -738,4 +738,7 @@ pub(crate) enum Error { #[error(transparent)] Anyhow(#[from] anyhow::Error), + + #[error(transparent)] + PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError), } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index a4bde84928349..16b7d21b069b0 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -1,10 +1,11 @@ use anstream::eprint; use anyhow::Result; -use std::borrow::Cow; +use itertools::Itertools; +use pubgrub::range::Range; use distribution_types::{IndexLocations, UnresolvedRequirementSpecification}; use install_wheel_rs::linker::LinkMode; -use pep440_rs::{VersionSpecifier, VersionSpecifiers}; +use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use uv_cache::Cache; use uv_client::RegistryClientBuilder; use uv_configuration::{ @@ -17,7 +18,7 @@ use uv_git::GitResolver; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_requirements::upgrade::{read_lockfile, LockedRequirements}; -use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder}; +use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, PubGrubSpecifier}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -106,13 +107,36 @@ pub(super) async fn do_lock( // Determine the supported Python range. If no range is defined, and warn and default to the // current minor version. - let project = root_project_name - .as_ref() - .and_then(|name| workspace.packages().get(name)); - let requires_python = if let Some(requires_python) = - project.and_then(|root_project| root_project.project().requires_python.as_ref()) - { - Cow::Borrowed(requires_python) + // + // For a workspace, we compute the union of all workspace requires-python values, ensuring we + // keep track of `None` vs. a full range. + let requires_python_workspace = workspace + .packages() + .values() + .filter_map(|member| { + member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.requires_python.as_ref()) + }) + // Convert to pubgrub range, perform the union, convert back to pep440_rs. + .map(PubGrubSpecifier::try_from) + .fold_ok(None, |range: Option>, requires_python| { + if let Some(range) = range { + Some(range.union(&requires_python.into())) + } else { + Some(requires_python.into()) + } + })? + .map(|range| { + range + .iter() + .flat_map(VersionSpecifier::from_bounds) + .collect() + }); + let requires_python = if let Some(requires_python) = requires_python_workspace { + requires_python } else { let requires_python = VersionSpecifiers::from( VersionSpecifier::greater_than_equal_version(venv.interpreter().python_minor_version()), @@ -124,7 +148,7 @@ pub(super) async fn do_lock( .map(ToString::to_string) .unwrap_or("workspace".to_string()), ); - Cow::Owned(requires_python) + requires_python }; // Determine the tags, markers, and interpreter to use for resolution. diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 8bfc05904e6bb..0e342f95e8da4 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -62,6 +62,9 @@ pub(crate) enum ProjectError { #[error(transparent)] Operation(#[from] pip::operations::Error), + + #[error(transparent)] + PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError), } /// Initialize a virtual environment for the current project.