Skip to content

Commit

Permalink
Use union of requires-python in workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
konstin committed Jun 6, 2024
1 parent c10d224 commit c065006
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 24 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions crates/pep440-rs/src/version_specifier.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -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<Version>, &Bound<Version>),
) -> impl Iterator<Item = VersionSpecifier> {
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<Version>) -> Option<VersionSpecifier> {
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<Version>) -> Option<VersionSpecifier> {
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
Expand Down
10 changes: 6 additions & 4 deletions crates/uv-resolver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ 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;
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,
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/pubgrub/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/pubgrub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 12 additions & 7 deletions crates/uv-resolver/src/pubgrub/specifier.rs
Original file line number Diff line number Diff line change
@@ -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<Version>);
pub struct PubGrubSpecifier(Range<Version>);

impl PubGrubSpecifier {
/// Returns `true` if the [`PubGrubSpecifier`] is a subset of the other.
Expand All @@ -24,10 +29,10 @@ impl From<PubGrubSpecifier> for Range<Version> {
}

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<Self, ResolveError> {
fn try_from(specifiers: &VersionSpecifiers) -> Result<Self, PubGrubSpecifierError> {
let range = specifiers
.iter()
.map(crate::pubgrub::PubGrubSpecifier::try_from)
Expand All @@ -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<Self, ResolveError> {
fn try_from(specifier: &VersionSpecifier) -> Result<Self, PubGrubSpecifierError> {
let ranges = match specifier.operator() {
Operator::Equal => {
let version = specifier.version().clone();
Expand All @@ -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())
Expand Down
1 change: 1 addition & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/commands/pip/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,4 +738,7 @@ pub(crate) enum Error {

#[error(transparent)]
Anyhow(#[from] anyhow::Error),

#[error(transparent)]
PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError),
}
46 changes: 35 additions & 11 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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;

Expand Down Expand Up @@ -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<Range<Version>>, 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()),
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit c065006

Please sign in to comment.