mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-14 01:32:24 +00:00
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:
parent
52bdee2e85
commit
cc7c780523
14 changed files with 209 additions and 135 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4396,7 +4396,6 @@ dependencies = [
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"platform-tags",
|
"platform-tags",
|
||||||
"predicates",
|
"predicates",
|
||||||
"pubgrub",
|
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub use preferences::{Preference, PreferenceError};
|
||||||
pub use prerelease_mode::PreReleaseMode;
|
pub use prerelease_mode::PreReleaseMode;
|
||||||
pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
pub use pubgrub::{PubGrubSpecifier, PubGrubSpecifierError};
|
||||||
pub use python_requirement::PythonRequirement;
|
pub use python_requirement::PythonRequirement;
|
||||||
|
pub use requires_python::{RequiresPython, RequiresPythonError};
|
||||||
pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
|
pub use resolution::{AnnotationStyle, DisplayResolutionGraph, ResolutionGraph};
|
||||||
pub use resolution_mode::ResolutionMode;
|
pub use resolution_mode::ResolutionMode;
|
||||||
pub use resolver::{
|
pub use resolver::{
|
||||||
|
|
@ -39,6 +40,7 @@ mod prerelease_mode;
|
||||||
mod pubgrub;
|
mod pubgrub;
|
||||||
mod python_requirement;
|
mod python_requirement;
|
||||||
mod redirect;
|
mod redirect;
|
||||||
|
mod requires_python;
|
||||||
mod resolution;
|
mod resolution;
|
||||||
mod resolution_mode;
|
mod resolution_mode;
|
||||||
mod resolver;
|
mod resolver;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use distribution_types::{
|
||||||
GitSourceDist, IndexUrl, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel,
|
GitSourceDist, IndexUrl, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel,
|
||||||
RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError,
|
RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError,
|
||||||
};
|
};
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl};
|
use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl};
|
||||||
use platform_tags::{TagCompatibility, TagPriority, Tags};
|
use platform_tags::{TagCompatibility, TagPriority, Tags};
|
||||||
use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl};
|
use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl};
|
||||||
|
|
@ -29,7 +29,7 @@ use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryRefere
|
||||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||||
|
|
||||||
use crate::resolution::AnnotatedDist;
|
use crate::resolution::AnnotatedDist;
|
||||||
use crate::ResolutionGraph;
|
use crate::{RequiresPython, ResolutionGraph};
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize)]
|
#[derive(Clone, Debug, serde::Deserialize)]
|
||||||
#[serde(try_from = "LockWire")]
|
#[serde(try_from = "LockWire")]
|
||||||
|
|
@ -37,7 +37,7 @@ pub struct Lock {
|
||||||
version: u32,
|
version: u32,
|
||||||
distributions: Vec<Distribution>,
|
distributions: Vec<Distribution>,
|
||||||
/// The range of supported Python versions.
|
/// The range of supported Python versions.
|
||||||
requires_python: Option<VersionSpecifiers>,
|
requires_python: Option<RequiresPython>,
|
||||||
/// A map from distribution ID to index in `distributions`.
|
/// A map from distribution ID to index in `distributions`.
|
||||||
///
|
///
|
||||||
/// This can be used to quickly lookup the full distribution for any ID
|
/// 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.
|
/// Initialize a [`Lock`] from a list of [`Distribution`] entries.
|
||||||
fn new(
|
fn new(
|
||||||
distributions: Vec<Distribution>,
|
distributions: Vec<Distribution>,
|
||||||
requires_python: Option<VersionSpecifiers>,
|
requires_python: Option<RequiresPython>,
|
||||||
) -> Result<Self, LockError> {
|
) -> Result<Self, LockError> {
|
||||||
let wire = LockWire {
|
let wire = LockWire {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|
@ -123,7 +123,7 @@ impl Lock {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the supported Python version range for the lockfile, if present.
|
/// 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()
|
self.requires_python.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,7 +226,7 @@ struct LockWire {
|
||||||
#[serde(rename = "distribution")]
|
#[serde(rename = "distribution")]
|
||||||
distributions: Vec<Distribution>,
|
distributions: Vec<Distribution>,
|
||||||
#[serde(rename = "requires-python")]
|
#[serde(rename = "requires-python")]
|
||||||
requires_python: Option<VersionSpecifiers>,
|
requires_python: Option<RequiresPython>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Lock> for LockWire {
|
impl From<Lock> for LockWire {
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ use pubgrub::type_aliases::Map;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use distribution_types::IndexLocations;
|
use distribution_types::IndexLocations;
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::Version;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::candidate_selector::CandidateSelector;
|
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::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason};
|
||||||
|
use crate::RequiresPython;
|
||||||
|
|
||||||
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
use super::{PubGrubPackage, PubGrubPackageInner, PubGrubPython};
|
||||||
|
|
||||||
|
|
@ -534,9 +535,11 @@ impl PubGrubReportFormatter<'_> {
|
||||||
PubGrubPackageInner::Python(PubGrubPython::Target)
|
PubGrubPackageInner::Python(PubGrubPython::Target)
|
||||||
) {
|
) {
|
||||||
if let Some(python) = self.python_requirement {
|
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 {
|
hints.insert(PubGrubHint::RequiresPython {
|
||||||
requires_python: specifiers.clone(),
|
requires_python: requires_python.clone(),
|
||||||
package: package.clone(),
|
package: package.clone(),
|
||||||
package_set: self
|
package_set: self
|
||||||
.simplify_set(package_set, package)
|
.simplify_set(package_set, package)
|
||||||
|
|
@ -632,7 +635,7 @@ pub(crate) enum PubGrubHint {
|
||||||
},
|
},
|
||||||
/// The `Requires-Python` requirement was not satisfied.
|
/// The `Requires-Python` requirement was not satisfied.
|
||||||
RequiresPython {
|
RequiresPython {
|
||||||
requires_python: VersionSpecifiers,
|
requires_python: RequiresPython,
|
||||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||||
package: PubGrubPackage,
|
package: PubGrubPackage,
|
||||||
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
#[derivative(PartialEq = "ignore", Hash = "ignore")]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::collections::Bound;
|
|
||||||
|
|
||||||
use pep440_rs::VersionSpecifiers;
|
use pep440_rs::VersionSpecifiers;
|
||||||
use pep508_rs::StringVersion;
|
use pep508_rs::StringVersion;
|
||||||
use uv_interpreter::{Interpreter, PythonVersion};
|
use uv_interpreter::{Interpreter, PythonVersion};
|
||||||
|
|
||||||
|
use crate::RequiresPython;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct PythonRequirement {
|
pub struct PythonRequirement {
|
||||||
/// The installed version of Python.
|
/// The installed version of Python.
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct PythonRequirement {
|
||||||
/// when specifying an alternate Python version for the resolution.
|
/// when specifying an alternate Python version for the resolution.
|
||||||
///
|
///
|
||||||
/// If `None`, the target version is the same as the installed version.
|
/// If `None`, the target version is the same as the installed version.
|
||||||
target: Option<RequiresPython>,
|
target: Option<PythonTarget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PythonRequirement {
|
impl PythonRequirement {
|
||||||
|
|
@ -22,7 +22,7 @@ impl PythonRequirement {
|
||||||
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
|
pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self {
|
||||||
Self {
|
Self {
|
||||||
installed: interpreter.python_full_version().clone(),
|
installed: interpreter.python_full_version().clone(),
|
||||||
target: Some(RequiresPython::Specifier(StringVersion {
|
target: Some(PythonTarget::Version(StringVersion {
|
||||||
string: python_version.to_string(),
|
string: python_version.to_string(),
|
||||||
version: python_version.python_full_version(),
|
version: python_version.python_full_version(),
|
||||||
})),
|
})),
|
||||||
|
|
@ -33,11 +33,11 @@ impl PythonRequirement {
|
||||||
/// [`MarkerEnvironment`].
|
/// [`MarkerEnvironment`].
|
||||||
pub fn from_requires_python(
|
pub fn from_requires_python(
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
requires_python: &VersionSpecifiers,
|
requires_python: &RequiresPython,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
installed: interpreter.python_full_version().clone(),
|
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.
|
/// Return the target version of Python.
|
||||||
pub fn target(&self) -> Option<&RequiresPython> {
|
pub fn target(&self) -> Option<&PythonTarget> {
|
||||||
self.target.as_ref()
|
self.target.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum RequiresPython {
|
pub enum PythonTarget {
|
||||||
/// The [`RequiresPython`] specifier is a single version specifier, as provided via
|
/// The [`PythonTarget`] specifier is a single version specifier, as provided via
|
||||||
/// `--python-version` on the command line.
|
/// `--python-version` on the command line.
|
||||||
///
|
///
|
||||||
/// The use of a separate enum variant allows us to use a verbatim representation when reporting
|
/// The use of a separate enum variant allows us to use a verbatim representation when reporting
|
||||||
/// back to the user.
|
/// back to the user.
|
||||||
Specifier(StringVersion),
|
Version(StringVersion),
|
||||||
/// The [`RequiresPython`] specifier is a set of version specifiers, as extracted from the
|
/// The [`PythonTarget`] specifier is a set of version specifiers, as extracted from the
|
||||||
/// `Requires-Python` field in a `pyproject.toml` or `METADATA` file.
|
/// `Requires-Python` field in a `pyproject.toml` or `METADATA` file.
|
||||||
Specifiers(VersionSpecifiers),
|
RequiresPython(RequiresPython),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiresPython {
|
impl PythonTarget {
|
||||||
/// Returns `true` if the target Python is covered by the [`VersionSpecifiers`].
|
/// Returns `true` if the target Python is compatible with the [`VersionSpecifiers`].
|
||||||
///
|
pub fn is_compatible_with(&self, target: &VersionSpecifiers) -> bool {
|
||||||
/// 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 {
|
|
||||||
match self {
|
match self {
|
||||||
RequiresPython::Specifier(specifier) => requires_python.contains(specifier),
|
PythonTarget::Version(version) => target.contains(version),
|
||||||
RequiresPython::Specifiers(specifiers) => {
|
PythonTarget::RequiresPython(requires_python) => {
|
||||||
let Ok(target) = crate::pubgrub::PubGrubSpecifier::try_from(specifiers) else {
|
requires_python.is_contained_by(target)
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the [`VersionSpecifiers`] for the [`RequiresPython`] specifier.
|
/// Returns the [`RequiresPython`] for the [`PythonTarget`] specifier.
|
||||||
pub fn as_specifiers(&self) -> Option<&VersionSpecifiers> {
|
pub fn as_requires_python(&self) -> Option<&RequiresPython> {
|
||||||
match self {
|
match self {
|
||||||
RequiresPython::Specifier(_) => None,
|
PythonTarget::Version(_) => None,
|
||||||
RequiresPython::Specifiers(specifiers) => Some(specifiers),
|
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
RequiresPython::Specifier(specifier) => std::fmt::Display::fmt(specifier, f),
|
PythonTarget::Version(specifier) => std::fmt::Display::fmt(specifier, f),
|
||||||
RequiresPython::Specifiers(specifiers) => std::fmt::Display::fmt(specifiers, f),
|
PythonTarget::RequiresPython(specifiers) => std::fmt::Display::fmt(specifiers, f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
146
crates/uv-resolver/src/requires_python.rs
Normal file
146
crates/uv-resolver/src/requires_python.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use distribution_types::{
|
use distribution_types::{
|
||||||
Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
|
Dist, DistributionMetadata, Name, ResolutionDiagnostic, VersionId, VersionOrUrlRef,
|
||||||
};
|
};
|
||||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
use pep440_rs::{Version, VersionSpecifier};
|
||||||
use pep508_rs::{MarkerEnvironment, MarkerTree};
|
use pep508_rs::{MarkerEnvironment, MarkerTree};
|
||||||
use pypi_types::{ParsedUrlError, Yanked};
|
use pypi_types::{ParsedUrlError, Yanked};
|
||||||
use uv_git::GitResolver;
|
use uv_git::GitResolver;
|
||||||
|
|
@ -17,12 +17,13 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||||
|
|
||||||
use crate::preferences::Preferences;
|
use crate::preferences::Preferences;
|
||||||
use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner};
|
use crate::pubgrub::{PubGrubDistribution, PubGrubPackageInner};
|
||||||
use crate::python_requirement::RequiresPython;
|
use crate::python_requirement::PythonTarget;
|
||||||
use crate::redirect::url_to_precise;
|
use crate::redirect::url_to_precise;
|
||||||
use crate::resolution::AnnotatedDist;
|
use crate::resolution::AnnotatedDist;
|
||||||
use crate::resolver::Resolution;
|
use crate::resolver::Resolution;
|
||||||
use crate::{
|
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
|
/// 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.
|
/// The underlying graph.
|
||||||
pub(crate) petgraph: Graph<AnnotatedDist, Version, Directed>,
|
pub(crate) petgraph: Graph<AnnotatedDist, Version, Directed>,
|
||||||
/// The range of supported Python versions.
|
/// 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.
|
/// Any diagnostics that were encountered while building the graph.
|
||||||
pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
|
pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
|
||||||
}
|
}
|
||||||
|
|
@ -319,7 +320,7 @@ impl ResolutionGraph {
|
||||||
// included packages.
|
// included packages.
|
||||||
let requires_python = python
|
let requires_python = python
|
||||||
.target()
|
.target()
|
||||||
.and_then(RequiresPython::as_specifiers)
|
.and_then(PythonTarget::as_requires_python)
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
|
||||||
|
|
@ -730,7 +730,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
||||||
// The version is incompatible due to its Python requirement.
|
// The version is incompatible due to its Python requirement.
|
||||||
if let Some(requires_python) = metadata.requires_python.as_ref() {
|
if let Some(requires_python) = metadata.requires_python.as_ref() {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
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(
|
return Ok(Some(ResolverVersion::Unavailable(
|
||||||
version.clone(),
|
version.clone(),
|
||||||
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@ impl VersionMapLazy {
|
||||||
// _installed_ Python version (to build successfully)
|
// _installed_ Python version (to build successfully)
|
||||||
if let Some(requires_python) = requires_python {
|
if let Some(requires_python) = requires_python {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
if let Some(target) = self.python_requirement.target() {
|
||||||
if !target.contains(&requires_python) {
|
if !target.is_compatible_with(&requires_python) {
|
||||||
return SourceDistCompatibility::Incompatible(
|
return SourceDistCompatibility::Incompatible(
|
||||||
IncompatibleSource::RequiresPython(
|
IncompatibleSource::RequiresPython(
|
||||||
requires_python,
|
requires_python,
|
||||||
|
|
@ -534,7 +534,7 @@ impl VersionMapLazy {
|
||||||
// Check for a Python version incompatibility
|
// Check for a Python version incompatibility
|
||||||
if let Some(requires_python) = requires_python {
|
if let Some(requires_python) = requires_python {
|
||||||
if let Some(target) = self.python_requirement.target() {
|
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(
|
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
|
||||||
requires_python,
|
requires_python,
|
||||||
PythonRequirementKind::Target,
|
PythonRequirementKind::Target,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ indicatif = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
miette = { workspace = true, features = ["fancy"] }
|
miette = { workspace = true, features = ["fancy"] }
|
||||||
owo-colors = { workspace = true }
|
owo-colors = { workspace = true }
|
||||||
pubgrub = { workspace = true }
|
|
||||||
rayon = { workspace = true }
|
rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use distribution_types::{
|
||||||
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
|
DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, Name, Resolution,
|
||||||
};
|
};
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep440_rs::VersionSpecifiers;
|
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Requirement;
|
use pypi_types::Requirement;
|
||||||
|
|
@ -37,7 +36,7 @@ use uv_requirements::{
|
||||||
};
|
};
|
||||||
use uv_resolver::{
|
use uv_resolver::{
|
||||||
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
|
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
|
||||||
PythonRequirement, ResolutionGraph, Resolver,
|
PythonRequirement, RequiresPython, ResolutionGraph, Resolver,
|
||||||
};
|
};
|
||||||
use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
|
use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
@ -91,7 +90,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
markers: Option<&MarkerEnvironment>,
|
markers: Option<&MarkerEnvironment>,
|
||||||
requires_python: Option<&VersionSpecifiers>,
|
requires_python: Option<&RequiresPython>,
|
||||||
client: &RegistryClient,
|
client: &RegistryClient,
|
||||||
flat_index: &FlatIndex,
|
flat_index: &FlatIndex,
|
||||||
index: &InMemoryIndex,
|
index: &InMemoryIndex,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
use anstream::eprint;
|
use anstream::eprint;
|
||||||
use itertools::Itertools;
|
|
||||||
use pubgrub::range::Range;
|
|
||||||
|
|
||||||
use distribution_types::{IndexLocations, UnresolvedRequirementSpecification};
|
use distribution_types::{IndexLocations, UnresolvedRequirementSpecification};
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::RegistryClientBuilder;
|
use uv_client::RegistryClientBuilder;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
|
@ -17,7 +14,7 @@ use uv_git::GitResolver;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::upgrade::{read_lockfile, LockedRequirements};
|
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_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
||||||
use uv_warnings::warn_user;
|
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
|
// For a workspace, we compute the union of all workspace requires-python values, ensuring we
|
||||||
// keep track of `None` vs. a full range.
|
// keep track of `None` vs. a full range.
|
||||||
let requires_python_workspace = workspace
|
let requires_python_workspace =
|
||||||
.packages()
|
RequiresPython::union(workspace.packages().values().filter_map(|member| {
|
||||||
.values()
|
|
||||||
.filter_map(|member| {
|
|
||||||
member
|
member
|
||||||
.pyproject_toml()
|
.pyproject_toml()
|
||||||
.project
|
.project
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|project| project.requires_python.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 {
|
let requires_python = if let Some(requires_python) = requires_python_workspace {
|
||||||
requires_python
|
requires_python
|
||||||
} else {
|
} else {
|
||||||
let requires_python = VersionSpecifiers::from(
|
let requires_python =
|
||||||
VersionSpecifier::greater_than_equal_version(venv.interpreter().python_minor_version()),
|
RequiresPython::greater_than_equal_version(venv.interpreter().python_minor_version());
|
||||||
);
|
|
||||||
if let Some(root_project_name) = root_project_name.as_ref() {
|
if let Some(root_project_name) = root_project_name.as_ref() {
|
||||||
warn_user!(
|
warn_user!(
|
||||||
"No `requires-python` field found in `{root_project_name}`. Defaulting to `{requires_python}`.",
|
"No `requires-python` field found in `{root_project_name}`. Defaulting to `{requires_python}`.",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::{IndexLocations, Resolution};
|
use distribution_types::{IndexLocations, Resolution};
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
use pep440_rs::{Version, VersionSpecifiers};
|
use pep440_rs::Version;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
|
@ -21,7 +21,7 @@ use uv_git::GitResolver;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_interpreter::{find_default_interpreter, PythonEnvironment};
|
use uv_interpreter::{find_default_interpreter, PythonEnvironment};
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
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 uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
|
||||||
use crate::commands::pip;
|
use crate::commands::pip;
|
||||||
|
|
@ -34,7 +34,7 @@ pub(crate) mod sync;
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub(crate) enum ProjectError {
|
pub(crate) enum ProjectError {
|
||||||
#[error("The current Python version ({0}) is not compatible with the locked Python requirement ({1})")]
|
#[error("The current Python version ({0}) is not compatible with the locked Python requirement ({1})")]
|
||||||
RequiresPython(Version, VersionSpecifiers),
|
PythonIncompatibility(Version, RequiresPython),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Interpreter(#[from] uv_interpreter::Error),
|
Interpreter(#[from] uv_interpreter::Error),
|
||||||
|
|
@ -64,7 +64,7 @@ pub(crate) enum ProjectError {
|
||||||
Operation(#[from] pip::operations::Error),
|
Operation(#[from] pip::operations::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PubGrubSpecifier(#[from] uv_resolver::PubGrubSpecifierError),
|
RequiresPython(#[from] uv_resolver::RequiresPythonError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a virtual environment for the current project.
|
/// Initialize a virtual environment for the current project.
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ pub(super) async fn do_sync(
|
||||||
// Validate that the Python version is supported by the lockfile.
|
// Validate that the Python version is supported by the lockfile.
|
||||||
if let Some(requires_python) = lock.requires_python() {
|
if let Some(requires_python) = lock.requires_python() {
|
||||||
if !requires_python.contains(venv.interpreter().python_version()) {
|
if !requires_python.contains(venv.interpreter().python_version()) {
|
||||||
return Err(ProjectError::RequiresPython(
|
return Err(ProjectError::PythonIncompatibility(
|
||||||
venv.interpreter().python_version().clone(),
|
venv.interpreter().python_version().clone(),
|
||||||
requires_python.clone(),
|
requires_python.clone(),
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue