Remove PubGrub dependency from uv (#4116)

## Summary

Encapsulates more of the details are `Requires-Python` and PubGrub.

Closes https://github.com/astral-sh/uv/issues/4110.
This commit is contained in:
Charlie Marsh 2024-06-06 19:45:58 -04:00 committed by GitHub
parent 52bdee2e85
commit cc7c780523
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 209 additions and 135 deletions

1
Cargo.lock generated
View file

@ -4396,7 +4396,6 @@ dependencies = [
"pep508_rs",
"platform-tags",
"predicates",
"pubgrub",
"pypi-types",
"rayon",
"regex",

View file

@ -10,6 +10,7 @@ pub use preferences::{Preference, PreferenceError};
pub use prerelease_mode::PreReleaseMode;
pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
pub use python_requirement::PythonRequirement;
pub use requires_python::{RequiresPython, RequiresPythonError};
pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
pub use resolution_mode::ResolutionMode;
pub use resolver::{
@ -39,6 +40,7 @@ mod prerelease_mode;
mod pubgrub;
mod python_requirement;
mod redirect;
mod requires_python;
mod resolution;
mod resolution_mode;
mod resolver;

View file

@ -20,7 +20,7 @@ use distribution_types::{
GitSourceDist, IndexUrl, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel,
RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError,
};
use pep440_rs::{Version, VersionSpecifiers};
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl};
use platform_tags::{TagCompatibility, TagPriority, Tags};
use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl};
@ -29,7 +29,7 @@ use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryRefere
use uv_normalize::{ExtraName, GroupName, PackageName};
use crate::resolution::AnnotatedDist;
use crate::ResolutionGraph;
use crate::{RequiresPython, ResolutionGraph};
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(try_from = "LockWire")]
@ -37,7 +37,7 @@ pub struct Lock {
version: u32,
distributions: Vec<Distribution>,
/// The range of supported Python versions.
requires_python: Option<VersionSpecifiers>,
requires_python: Option<RequiresPython>,
/// A map from distribution ID to index in `distributions`.
///
/// This can be used to quickly lookup the full distribution for any ID
@ -107,7 +107,7 @@ impl Lock {
/// Initialize a [`Lock`] from a list of [`Distribution`] entries.
fn new(
distributions: Vec<Distribution>,
requires_python: Option<VersionSpecifiers>,
requires_python: Option<RequiresPython>,
) -> Result<Self, LockError> {
let wire = LockWire {
version: 1,
@ -123,7 +123,7 @@ impl Lock {
}
/// Returns the supported Python version range for the lockfile, if present.
pub fn requires_python(&self) -> Option<&VersionSpecifiers> {
pub fn requires_python(&self) -> Option<&RequiresPython> {
self.requires_python.as_ref()
}
@ -226,7 +226,7 @@ struct LockWire {
#[serde(rename = "distribution")]
distributions: Vec<Distribution>,
#[serde(rename = "requires-python")]
requires_python: Option<VersionSpecifiers>,
requires_python: Option<RequiresPython>,
}
impl From<Lock> for LockWire {

View file

@ -13,12 +13,13 @@ use pubgrub::type_aliases::Map;
use rustc_hash::FxHashMap;
use distribution_types::IndexLocations;
use pep440_rs::{Version, VersionSpecifiers};
use pep440_rs::Version;
use uv_normalize::PackageName;
use crate::candidate_selector::CandidateSelector;
use crate::python_requirement::{PythonRequirement, RequiresPython};
use crate::python_requirement::{PythonRequirement, PythonTarget};
use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
use crate::RequiresPython;
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
@ -534,9 +535,11 @@ impl PubGrubReportFormatter<'_> {
PubGrubPackageInner::Python(PubGrubPython::Target)
) {
if let Some(python) = self.python_requirement {
if let Some(RequiresPython::Specifiers(specifiers)) = python.target() {
if let Some(PythonTarget::RequiresPython(requires_python)) =
python.target()
{
hints.insert(PubGrubHint::RequiresPython {
requires_python: specifiers.clone(),
requires_python: requires_python.clone(),
package: package.clone(),
package_set: self
.simplify_set(package_set, package)
@ -632,7 +635,7 @@ pub(crate) enum PubGrubHint {
},
/// The `Requires-Python` requirement was not satisfied.
RequiresPython {
requires_python: VersionSpecifiers,
requires_python: RequiresPython,
#[derivative(PartialEq = "ignore", Hash = "ignore")]
package: PubGrubPackage,
#[derivative(PartialEq = "ignore", Hash = "ignore")]

View file

@ -1,9 +1,9 @@
use std::collections::Bound;
use pep440_rs::VersionSpecifiers;
use pep508_rs::StringVersion;
use uv_interpreter::{Interpreter, PythonVersion};
use crate::RequiresPython;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PythonRequirement {
/// The installed version of Python.
@ -13,7 +13,7 @@ pub struct PythonRequirement {
/// when specifying an alternate Python version for the resolution.
///
/// If `None`, the target version is the same as the installed version.
target: Option<RequiresPython>,
target: Option<PythonTarget>,
}
impl PythonRequirement {
@ -22,7 +22,7 @@ impl PythonRequirement {
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
Self {
installed: interpreter.python_full_version().clone(),
target: Some(RequiresPython::Specifier(StringVersion {
target: Some(PythonTarget::Version(StringVersion {
string: python_version.to_string(),
version: python_version.python_full_version(),
})),
@ -33,11 +33,11 @@ impl PythonRequirement {
/// [`MarkerEnvironment`].
pub fn from_requires_python(
interpreter: &Interpreter,
requires_python: &VersionSpecifiers,
requires_python: &RequiresPython,
) -> Self {
Self {
installed: interpreter.python_full_version().clone(),
target: Some(RequiresPython::Specifiers(requires_python.clone())),
target: Some(PythonTarget::RequiresPython(requires_python.clone())),
}
}
@ -55,103 +55,49 @@ impl PythonRequirement {
}
/// Return the target version of Python.
pub fn target(&self) -> Option<&RequiresPython> {
pub fn target(&self) -> Option<&PythonTarget> {
self.target.as_ref()
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum RequiresPython {
/// The [`RequiresPython`] specifier is a single version specifier, as provided via
pub enum PythonTarget {
/// The [`PythonTarget`] specifier is a single version specifier, as provided via
/// `--python-version` on the command line.
///
/// The use of a separate enum variant allows us to use a verbatim representation when reporting
/// back to the user.
Specifier(StringVersion),
/// The [`RequiresPython`] specifier is a set of version specifiers, as extracted from the
Version(StringVersion),
/// The [`PythonTarget`] specifier is a set of version specifiers, as extracted from the
/// `Requires-Python` field in a `pyproject.toml` or `METADATA` file.
Specifiers(VersionSpecifiers),
RequiresPython(RequiresPython),
}
impl RequiresPython {
/// Returns `true` if the target Python is covered by the [`VersionSpecifiers`].
///
/// For example, if the target Python is `>=3.8`, then `>=3.7` would cover it. However, `>=3.9`
/// would not.
///
/// We treat `Requires-Python` as a lower bound. For example, if the requirement expresses
/// `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was intended to enable
/// packages to drop support for older versions of Python without breaking installations on
/// those versions, and packages cannot know whether they are compatible with future, unreleased
/// versions of Python.
///
/// See: <https://packaging.python.org/en/latest/guides/dropping-older-python-versions/>
pub fn contains(&self, requires_python: &VersionSpecifiers) -> bool {
impl PythonTarget {
/// Returns `true` if the target Python is compatible with the [`VersionSpecifiers`].
pub fn is_compatible_with(&self, target: &VersionSpecifiers) -> bool {
match self {
RequiresPython::Specifier(specifier) => requires_python.contains(specifier),
RequiresPython::Specifiers(specifiers) => {
let Ok(target) = crate::pubgrub::PubGrubSpecifier::try_from(specifiers) else {
return false;
};
let Ok(requires_python) =
crate::pubgrub::PubGrubSpecifier::try_from(requires_python)
else {
return false;
};
// If the dependency has no lower bound, then it supports all versions.
let Some((requires_python_lower, _)) = requires_python.iter().next() else {
return true;
};
// If we have no lower bound, then there must be versions we support that the
// dependency does not.
let Some((target_lower, _)) = target.iter().next() else {
return false;
};
// We want, e.g., `target_lower` to be `>=3.8` and `requires_python_lower` to be
// `>=3.7`.
//
// That is: `requires_python_lower` should be less than or equal to `target_lower`.
match (requires_python_lower, target_lower) {
(Bound::Included(requires_python_lower), Bound::Included(target_lower)) => {
requires_python_lower <= target_lower
}
(Bound::Excluded(requires_python_lower), Bound::Included(target_lower)) => {
requires_python_lower < target_lower
}
(Bound::Included(requires_python_lower), Bound::Excluded(target_lower)) => {
requires_python_lower <= target_lower
}
(Bound::Excluded(requires_python_lower), Bound::Excluded(target_lower)) => {
requires_python_lower < target_lower
}
// If the dependency has no lower bound, then it supports all versions.
(Bound::Unbounded, _) => true,
// If we have no lower bound, then there must be versions we support that the
// dependency does not.
(_, Bound::Unbounded) => false,
}
PythonTarget::Version(version) => target.contains(version),
PythonTarget::RequiresPython(requires_python) => {
requires_python.is_contained_by(target)
}
}
}
/// Returns the [`VersionSpecifiers`] for the [`RequiresPython`] specifier.
pub fn as_specifiers(&self) -> Option<&VersionSpecifiers> {
/// Returns the [`RequiresPython`] for the [`PythonTarget`] specifier.
pub fn as_requires_python(&self) -> Option<&RequiresPython> {
match self {
RequiresPython::Specifier(_) => None,
RequiresPython::Specifiers(specifiers) => Some(specifiers),
PythonTarget::Version(_) => None,
PythonTarget::RequiresPython(requires_python) => Some(requires_python),
}
}
}
impl std::fmt::Display for RequiresPython {
impl std::fmt::Display for PythonTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequiresPython::Specifier(specifier) => std::fmt::Display::fmt(specifier, f),
RequiresPython::Specifiers(specifiers) => std::fmt::Display::fmt(specifiers, f),
PythonTarget::Version(specifier) => std::fmt::Display::fmt(specifier, f),
PythonTarget::RequiresPython(specifiers) => std::fmt::Display::fmt(specifiers, f),
}
}
}

View file

@ -0,0 +1,146 @@
use std::collections::Bound;
use itertools::Itertools;
use pubgrub::range::Range;
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
#[derive(thiserror::Error, Debug)]
pub enum RequiresPythonError {
#[error(transparent)]
PubGrub(#[from] crate::pubgrub::PubGrubSpecifierError),
}
/// The `Requires-Python` requirement specifier.
///
/// We treat `Requires-Python` as a lower bound. For example, if the requirement expresses
/// `>=3.8, <4`, we treat it as `>=3.8`. `Requires-Python` itself was intended to enable
/// packages to drop support for older versions of Python without breaking installations on
/// those versions, and packages cannot know whether they are compatible with future, unreleased
/// versions of Python.
///
/// See: <https://packaging.python.org/en/latest/guides/dropping-older-python-versions/>
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct RequiresPython(VersionSpecifiers);
impl RequiresPython {
/// Returns a [`RequiresPython`] to express `>=` equality with the given version.
pub fn greater_than_equal_version(version: Version) -> Self {
Self(VersionSpecifiers::from(
VersionSpecifier::greater_than_equal_version(version),
))
}
/// Returns a [`RequiresPython`] to express the union of the given version specifiers.
///
/// For example, given `>=3.8` and `>=3.9`, this would return `>=3.8`.
pub fn union<'a>(
specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
) -> Result<Option<Self>, RequiresPythonError> {
// Convert to PubGrub range and perform a union.
let range = specifiers
.into_iter()
.map(crate::pubgrub::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())
}
})?;
let Some(range) = range else {
return Ok(None);
};
// Convert back to PEP 440 specifiers.
let requires_python = Self(
range
.iter()
.flat_map(VersionSpecifier::from_bounds)
.collect(),
);
Ok(Some(requires_python))
}
/// Returns `true` if the `Requires-Python` is compatible with the given version.
pub fn contains(&self, version: &Version) -> bool {
self.0.contains(version)
}
/// Returns `true` if the `Requires-Python` is compatible with the given version specifiers.
///
/// For example, if the `Requires-Python` is `>=3.8`, then `>=3.7` would be considered
/// compatible, since all versions in the `Requires-Python` range are also covered by the
/// provided range. However, `>=3.9` would not be considered compatible, as the
/// `Requires-Python` includes Python 3.8, but `>=3.9` does not.
pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
let Ok(requires_python) = crate::pubgrub::PubGrubSpecifier::try_from(&self.0) else {
return false;
};
let Ok(target) = crate::pubgrub::PubGrubSpecifier::try_from(target) else {
return false;
};
// If the dependency has no lower bound, then it supports all versions.
let Some((target_lower, _)) = target.iter().next() else {
return true;
};
// If we have no lower bound, then there must be versions we support that the
// dependency does not.
let Some((requires_python_lower, _)) = requires_python.iter().next() else {
return false;
};
// We want, e.g., `requires_python_lower` to be `>=3.8` and `version_lower` to be
// `>=3.7`.
//
// That is: `version_lower` should be less than or equal to `requires_python_lower`.
match (target_lower, requires_python_lower) {
(Bound::Included(target_lower), Bound::Included(requires_python_lower)) => {
target_lower <= requires_python_lower
}
(Bound::Excluded(target_lower), Bound::Included(requires_python_lower)) => {
target_lower < requires_python_lower
}
(Bound::Included(target_lower), Bound::Excluded(requires_python_lower)) => {
target_lower <= requires_python_lower
}
(Bound::Excluded(target_lower), Bound::Excluded(requires_python_lower)) => {
target_lower < requires_python_lower
}
// If the dependency has no lower bound, then it supports all versions.
(Bound::Unbounded, _) => true,
// If we have no lower bound, then there must be versions we support that the
// dependency does not.
(_, Bound::Unbounded) => false,
}
}
/// Returns the [`VersionSpecifiers`] for the `Requires-Python` specifier.
pub fn specifiers(&self) -> &VersionSpecifiers {
&self.0
}
}
impl std::fmt::Display for RequiresPython {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl serde::Serialize for RequiresPython {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for RequiresPython {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let specifiers = VersionSpecifiers::deserialize(deserializer)?;
Ok(Self(specifiers))
}
}

View file

@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use distribution_types::{
Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
};
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
use pep440_rs::{Version, VersionSpecifier};
use pep508_rs::{MarkerEnvironment, MarkerTree};
use pypi_types::{ParsedUrlError, Yanked};
use uv_git::GitResolver;
@ -17,12 +17,13 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use crate::preferences::Preferences;
use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner};
use crate::python_requirement::RequiresPython;
use crate::python_requirement::PythonTarget;
use crate::redirect::url_to_precise;
use crate::resolution::AnnotatedDist;
use crate::resolver::Resolution;
use crate::{
InMemoryIndex, Manifest, MetadataResponse, PythonRequirement, ResolveError, VersionsResponse,
InMemoryIndex, Manifest, MetadataResponse, PythonRequirement, RequiresPython, ResolveError,
VersionsResponse,
};
/// A complete resolution graph in which every node represents a pinned package and every edge
@ -32,7 +33,7 @@ pub struct ResolutionGraph {
/// The underlying graph.
pub(crate) petgraph: Graph<AnnotatedDist, Version, Directed>,
/// The range of supported Python versions.
pub(crate) requires_python: Option<VersionSpecifiers>,
pub(crate) requires_python: Option<RequiresPython>,
/// Any diagnostics that were encountered while building the graph.
pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
}
@ -319,7 +320,7 @@ impl ResolutionGraph {
// included packages.
let requires_python = python
.target()
.and_then(RequiresPython::as_specifiers)
.and_then(PythonTarget::as_requires_python)
.cloned();
Ok(Self {

View file

@ -730,7 +730,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
// The version is incompatible due to its Python requirement.
if let Some(requires_python) = metadata.requires_python.as_ref() {
if let Some(target) = self.python_requirement.target() {
if !target.contains(requires_python) {
if !target.is_compatible_with(requires_python) {
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(

View file

@ -467,7 +467,7 @@ impl VersionMapLazy {
// _installed_ Python version (to build successfully)
if let Some(requires_python) = requires_python {
if let Some(target) = self.python_requirement.target() {
if !target.contains(&requires_python) {
if !target.is_compatible_with(&requires_python) {
return SourceDistCompatibility::Incompatible(
IncompatibleSource::RequiresPython(
requires_python,
@ -534,7 +534,7 @@ impl VersionMapLazy {
// Check for a Python version incompatibility
if let Some(requires_python) = requires_python {
if let Some(target) = self.python_requirement.target() {
if !target.contains(&requires_python) {
if !target.is_compatible_with(&requires_python) {
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
requires_python,
PythonRequirementKind::Target,

View file

@ -50,7 +50,6 @@ 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 }

View file

@ -15,7 +15,6 @@ use distribution_types::{
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
};
use install_wheel_rs::linker::LinkMode;
use pep440_rs::VersionSpecifiers;
use pep508_rs::MarkerEnvironment;
use platform_tags::Tags;
use pypi_types::Requirement;
@ -37,7 +36,7 @@ use uv_requirements::{
};
use uv_resolver::{
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
PythonRequirement, ResolutionGraph, Resolver,
PythonRequirement, RequiresPython, ResolutionGraph, Resolver,
};
use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
use uv_warnings::warn_user;
@ -91,7 +90,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
interpreter: &Interpreter,
tags: &Tags,
markers: Option<&MarkerEnvironment>,
requires_python: Option<&VersionSpecifiers>,
requires_python: Option<&RequiresPython>,
client: &RegistryClient,
flat_index: &FlatIndex,
index: &InMemoryIndex,

View file

@ -1,10 +1,7 @@
use anstream::eprint;
use itertools::Itertools;
use pubgrub::range::Range;
use distribution_types::{IndexLocations, UnresolvedRequirementSpecification};
use install_wheel_rs::linker::LinkMode;
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
use uv_cache::Cache;
use uv_client::RegistryClientBuilder;
use uv_configuration::{
@ -17,7 +14,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, PubGrubSpecifier};
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, RequiresPython};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
@ -109,38 +106,20 @@ pub(super) async fn do_lock(
//
// 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| {
let requires_python_workspace =
RequiresPython::union(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()),
);
let requires_python =
RequiresPython::greater_than_equal_version(venv.interpreter().python_minor_version());
if let Some(root_project_name) = root_project_name.as_ref() {
warn_user!(
"No `requires-python` field found in `{root_project_name}`. Defaulting to `{requires_python}`.",

View file

@ -7,7 +7,7 @@ use tracing::debug;
use distribution_types::{IndexLocations, Resolution};
use install_wheel_rs::linker::LinkMode;
use pep440_rs::{Version, VersionSpecifiers};
use pep440_rs::Version;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
use uv_configuration::{
@ -21,7 +21,7 @@ use uv_git::GitResolver;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_interpreter::{find_default_interpreter, PythonEnvironment};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, Options};
use uv_resolver::{FlatIndex, InMemoryIndex, Options, RequiresPython};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip;
@ -34,7 +34,7 @@ pub(crate) mod sync;
#[derive(thiserror::Error, Debug)]
pub(crate) enum ProjectError {
#[error("The current Python version ({0}) is not compatible with the locked Python requirement ({1})")]
RequiresPython(Version, VersionSpecifiers),
PythonIncompatibility(Version, RequiresPython),
#[error(transparent)]
Interpreter(#[from] uv_interpreter::Error),
@ -64,7 +64,7 @@ pub(crate) enum ProjectError {
Operation(#[from] pip::operations::Error),
#[error(transparent)]
PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError),
RequiresPython(#[from] uv_resolver::RequiresPythonError),
}
/// Initialize a virtual environment for the current project.

View file

@ -82,7 +82,7 @@ pub(super) async fn do_sync(
// Validate that the Python version is supported by the lockfile.
if let Some(requires_python) = lock.requires_python() {
if !requires_python.contains(venv.interpreter().python_version()) {
return Err(ProjectError::RequiresPython(
return Err(ProjectError::PythonIncompatibility(
venv.interpreter().python_version().clone(),
requires_python.clone(),
));