Skip to content

Commit

Permalink
Ignore upper-bounds on Requires-Python
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jun 6, 2024
1 parent fa2b6a2 commit 7ddb7dd
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 60 deletions.
6 changes: 3 additions & 3 deletions crates/uv-resolver/src/pubgrub/specifier.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use itertools::Itertools;
use pubgrub::range::Range;
use std::ops::Bound;

use pep440_rs::{Operator, PreRelease, Version, VersionSpecifier, VersionSpecifiers};

Expand All @@ -10,9 +11,8 @@ use crate::ResolveError;
pub(crate) struct PubGrubSpecifier(Range<Version>);

impl PubGrubSpecifier {
/// Returns `true` if the [`PubGrubSpecifier`] is a subset of the other.
pub(crate) fn subset_of(&self, other: &Self) -> bool {
self.0.subset_of(&other.0)
pub(crate) fn iter(&self) -> impl Iterator<Item = (&Bound<Version>, &Bound<Version>)> {
self.0.iter()
}
}

Expand Down
47 changes: 45 additions & 2 deletions crates/uv-resolver/src/python_requirement.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::Bound;

use pep440_rs::VersionSpecifiers;
use pep508_rs::StringVersion;
use uv_interpreter::{Interpreter, PythonVersion};
Expand Down Expand Up @@ -76,7 +78,15 @@ impl RequiresPython {
///
/// For example, if the target Python is `>=3.8`, then `>=3.7` would cover it. However, `>=3.9`
/// would not.
pub fn subset_of(&self, requires_python: &VersionSpecifiers) -> bool {
///
/// 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 {
RequiresPython::Specifier(specifier) => requires_python.contains(specifier),
RequiresPython::Specifiers(specifiers) => {
Expand All @@ -90,7 +100,40 @@ impl RequiresPython {
return false;
};

target.subset_of(&requires_python)
// 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,
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.subset_of(requires_python) {
if !target.contains(requires_python) {
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-resolver/src/version_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.subset_of(&requires_python) {
if !target.contains(&requires_python) {
return SourceDistCompatibility::Incompatible(
IncompatibleSource::RequiresPython(
requires_python,
Expand Down Expand Up @@ -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.subset_of(&requires_python) {
if !target.contains(&requires_python) {
return WheelCompatibility::Incompatible(IncompatibleWheel::RequiresPython(
requires_python,
PythonRequirementKind::Target,
Expand Down
Loading

0 comments on commit 7ddb7dd

Please sign in to comment.