From 87f8e6ac83ad35a7863ed004f76dd48491b4add3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 30 Aug 2024 14:03:39 -0400 Subject: [PATCH] REdo requires python --- crates/bench/benches/uv.rs | 2 +- crates/uv-resolver/src/lock/mod.rs | 36 ++-- ...r__lock__tests__hash_optional_missing.snap | 21 ++- ...r__lock__tests__hash_optional_present.snap | 21 ++- ...r__lock__tests__hash_required_present.snap | 21 ++- ...missing_dependency_source_unambiguous.snap | 21 ++- ...dependency_source_version_unambiguous.snap | 21 ++- ...issing_dependency_version_unambiguous.snap | 21 ++- ...lock__tests__source_direct_has_subdir.snap | 21 ++- ..._lock__tests__source_direct_no_subdir.snap | 21 ++- ...solver__lock__tests__source_directory.snap | 21 ++- ...esolver__lock__tests__source_editable.snap | 21 ++- crates/uv-resolver/src/pubgrub/package.rs | 2 +- crates/uv-resolver/src/pubgrub/report.rs | 74 +++++--- crates/uv-resolver/src/python_requirement.rs | 108 +++++------ crates/uv-resolver/src/resolution/graph.rs | 12 +- .../src/resolver/batch_prefetch.rs | 21 +-- crates/uv-resolver/src/resolver/mod.rs | 172 ++++++++---------- crates/uv-resolver/src/resolver/provider.rs | 8 +- crates/uv-resolver/src/version_map.rs | 14 +- crates/uv/src/commands/pip/compile.rs | 2 +- crates/uv/src/commands/project/lock.rs | 21 +-- crates/uv/src/commands/project/sync.rs | 15 +- crates/uv/tests/pip_compile.rs | 62 ++++--- crates/uv/tests/pip_compile_scenarios.rs | 12 +- crates/uv/tests/pip_install.rs | 8 +- crates/uv/tests/pip_install_scenarios.rs | 14 +- crates/uv/tests/pip_sync.rs | 8 +- 28 files changed, 498 insertions(+), 303 deletions(-) diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index dae7558cf5b2..2076c03d809e 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -164,7 +164,7 @@ mod resolver { let python_requirement = if universal { PythonRequirement::from_requires_python( interpreter, - &RequiresPython::greater_than_equal_version(&Version::new([3, 11])), + RequiresPython::greater_than_equal_version(&Version::new([3, 11])), ) } else { PythonRequirement::from_interpreter(interpreter) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 887e90772ef0..404d95f2df3e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -58,7 +58,7 @@ pub struct Lock { /// The list of supported environments specified by the user. supported_environments: Vec, /// The range of supported Python versions. - requires_python: Option, + requires_python: RequiresPython, /// We discard the lockfile if these options don't match. options: ResolverOptions, /// The actual locked version and their metadata. @@ -186,7 +186,7 @@ impl Lock { fn new( version: u32, mut packages: Vec, - requires_python: Option, + requires_python: RequiresPython, options: ResolverOptions, manifest: ResolverManifest, supported_environments: Vec, @@ -241,11 +241,9 @@ impl Lock { // Remove wheels that don't match `requires-python` and can't be selected for // installation. - if let Some(requires_python) = &requires_python { - package - .wheels - .retain(|wheel| requires_python.matches_wheel_tag(&wheel.filename)); - } + package + .wheels + .retain(|wheel| requires_python.matches_wheel_tag(&wheel.filename)); } packages.sort_by(|dist1, dist2| dist1.id.cmp(&dist2.id)); @@ -390,8 +388,8 @@ impl Lock { } /// Returns the supported Python version range for the lockfile, if present. - pub fn requires_python(&self) -> Option<&RequiresPython> { - self.requires_python.as_ref() + pub fn requires_python(&self) -> &RequiresPython { + &self.requires_python } /// Returns the resolution mode used to generate this lock. @@ -527,9 +525,7 @@ impl Lock { let mut doc = toml_edit::DocumentMut::new(); doc.insert("version", value(i64::from(self.version))); - if let Some(ref requires_python) = self.requires_python { - doc.insert("requires-python", value(requires_python.to_string())); - } + doc.insert("requires-python", value(self.requires_python.to_string())); if !self.fork_markers.is_empty() { let fork_markers = each_element_on_its_line_array( @@ -1158,8 +1154,7 @@ impl ResolverManifest { #[serde(rename_all = "kebab-case")] struct LockWire { version: u32, - #[serde(default)] - requires_python: Option, + requires_python: RequiresPython, /// If this lockfile was built from a forking resolution with non-identical forks, store the /// forks in the lockfile so we can recreate them in subsequent resolutions. #[serde(rename = "resolution-markers", default)] @@ -3685,6 +3680,7 @@ mod tests { fn missing_dependency_source_unambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3710,6 +3706,7 @@ version = "0.1.0" fn missing_dependency_version_unambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3735,6 +3732,7 @@ source = { registry = "https://pypi.org/simple" } fn missing_dependency_source_version_unambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3759,6 +3757,7 @@ name = "a" fn missing_dependency_source_ambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3790,6 +3789,7 @@ version = "0.1.0" fn missing_dependency_version_ambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3821,6 +3821,7 @@ source = { registry = "https://pypi.org/simple" } fn missing_dependency_source_version_ambiguous() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "a" @@ -3851,6 +3852,7 @@ name = "a" fn hash_optional_missing() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3866,6 +3868,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4 fn hash_optional_present() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3881,6 +3884,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4 fn hash_required_present() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3896,6 +3900,7 @@ wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256 fn source_direct_no_subdir() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3910,6 +3915,7 @@ source = { url = "https://burntsushi.net" } fn source_direct_has_subdir() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3924,6 +3930,7 @@ source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" } fn source_directory() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" @@ -3938,6 +3945,7 @@ source = { directory = "path/to/dir" } fn source_editable() { let data = r#" version = 1 +requires-python = ">=3.12" [[package]] name = "anyio" diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 5586b6ed1c28..e63cff079919 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index 6985617f2964..4a9789a0b385 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 67e3789dde01..04f906b20b65 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index e9206fe39dd0..c4b0e734bab4 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index e9206fe39dd0..c4b0e734bab4 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index e9206fe39dd0..c4b0e734bab4 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 7d95a95759fc..640ec123cd5f 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index b0bef40fb3dd..278825029ba1 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index 609c9ad8117e..b40e3bf2164a 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index 9e84159ada23..fa6e8aa2194d 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -7,7 +7,26 @@ Ok( version: 1, fork_markers: [], supported_environments: [], - requires_python: None, + requires_python: RequiresPython { + specifiers: VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.12", + }, + ], + ), + range: RequiresPythonRange( + RequiresPythonBound( + Included( + "3.12", + ), + ), + RequiresPythonBound( + Unbounded, + ), + ), + }, options: ResolverOptions { resolution_mode: Highest, prerelease_mode: IfNecessaryOrExplicit, diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index c631d4403727..98c7944502ed 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -173,7 +173,7 @@ impl PubGrubPackage { } } -#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] pub enum PubGrubPython { /// The Python version installed in the current environment. Installed, diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 6bc066074ab6..c992823e6f6b 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -17,7 +17,7 @@ use crate::candidate_selector::CandidateSelector; use crate::error::ErrorTree; use crate::fork_urls::ForkUrls; use crate::prerelease::AllowPrerelease; -use crate::python_requirement::{PythonRequirement, PythonTarget}; +use crate::python_requirement::{PythonRequirement, PythonRequirementSource}; use crate::resolver::{IncompletePackage, UnavailablePackage, UnavailableReason}; use crate::{RequiresPython, ResolverMarkers}; @@ -52,25 +52,19 @@ impl ReportFormatter, UnavailableReason> &**package, PubGrubPackageInner::Python(PubGrubPython::Target) ) { - return if let Some(target) = self.python_requirement.target() { - format!( - "the requested {package} version ({target}) does not satisfy {}", - self.compatible_range(package, set) - ) - } else { - format!( - "the requested {package} version does not satisfy {}", - self.compatible_range(package, set) - ) - }; + let target = self.python_requirement.target(); + return format!( + "the requested {package} version ({target}) does not satisfy {}", + self.compatible_range(package, set) + ); } if matches!( &**package, PubGrubPackageInner::Python(PubGrubPython::Installed) ) { + let installed = self.python_requirement.exact(); return format!( - "the current {package} version ({}) does not satisfy {}", - self.python_requirement.installed(), + "the current {package} version ({installed}) does not satisfy {}", self.compatible_range(package, set) ); } @@ -554,16 +548,13 @@ impl PubGrubReportFormatter<'_> { &**dependency, PubGrubPackageInner::Python(PubGrubPython::Target) ) { - if let Some(PythonTarget::RequiresPython(requires_python)) = - self.python_requirement.target() - { - hints.insert(PubGrubHint::RequiresPython { - requires_python: requires_python.clone(), - package: package.clone(), - package_set: self.simplify_set(package_set, package).into_owned(), - package_requires_python: dependency_set.clone(), - }); - } + hints.insert(PubGrubHint::RequiresPython { + source: self.python_requirement.source(), + requires_python: self.python_requirement.target().clone(), + package: package.clone(), + package_set: self.simplify_set(package_set, package).into_owned(), + package_requires_python: dependency_set.clone(), + }); } } DerivationTree::External(External::NotRoot(..)) => {} @@ -798,6 +789,7 @@ pub(crate) enum PubGrubHint { }, /// The `Requires-Python` requirement was not satisfied. RequiresPython { + source: PythonRequirementSource, requires_python: RequiresPython, #[derivative(PartialEq = "ignore", Hash = "ignore")] package: PubGrubPackage, @@ -932,6 +924,7 @@ impl std::fmt::Display for PubGrubHint { ) } Self::RequiresPython { + source: PythonRequirementSource::RequiresPython, requires_python, package, package_set, @@ -948,6 +941,39 @@ impl std::fmt::Display for PubGrubHint { package_requires_python.bold(), ) } + Self::RequiresPython { + source: PythonRequirementSource::PythonVersion, + requires_python, + package, + package_set, + package_requires_python, + } => { + write!( + f, + "{}{} The `--python-version` value ({}) includes Python versions that are not supported by your dependencies (e.g., {} only supports {}). Consider using a higher `--python-version` value.", + "hint".bold().cyan(), + ":".bold(), + requires_python.bold(), + PackageRange::compatibility(package, package_set, None).bold(), + package_requires_python.bold(), + ) + } + Self::RequiresPython { + source: PythonRequirementSource::Interpreter, + requires_python: _, + package, + package_set, + package_requires_python, + } => { + write!( + f, + "{}{} The Python interpreter uses a Python version that is not supported by your dependencies (e.g., {} only supports {}). Consider passing a `--python-version` value to raise the minimum supported version.", + "hint".bold().cyan(), + ":".bold(), + PackageRange::compatibility(package, package_set, None).bold(), + package_requires_python.bold(), + ) + } } } } diff --git a/crates/uv-resolver/src/python_requirement.rs b/crates/uv-resolver/src/python_requirement.rs index 29fbb873b079..1c286a212a0e 100644 --- a/crates/uv-resolver/src/python_requirement.rs +++ b/crates/uv-resolver/src/python_requirement.rs @@ -1,29 +1,33 @@ -use pep440_rs::{Version, VersionSpecifiers}; +use pep440_rs::Version; use uv_python::{Interpreter, PythonVersion}; use crate::{RequiresPython, RequiresPythonRange}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct PythonRequirement { + source: PythonRequirementSource, + /// The exact installed version of Python. + exact: Version, /// The installed version of Python. - installed: Version, + installed: RequiresPython, /// The target version of Python; that is, the version of Python for which we are resolving /// dependencies. This is typically the same as the installed version, but may be different /// when specifying an alternate Python version for the resolution. - /// - /// If `None`, the target version is the same as the installed version. - target: Option, + target: RequiresPython, } impl PythonRequirement { /// Create a [`PythonRequirement`] to resolve against both an [`Interpreter`] and a /// [`PythonVersion`]. pub fn from_python_version(interpreter: &Interpreter, python_version: &PythonVersion) -> Self { + let exact = interpreter.python_full_version().version.clone(); + let installed = interpreter.python_full_version().version.only_release(); + let target = python_version.python_full_version().only_release(); Self { - installed: interpreter.python_full_version().version.only_release(), - target: Some(PythonTarget::Version( - python_version.python_full_version().only_release(), - )), + exact, + installed: RequiresPython::greater_than_equal_version(&installed), + target: RequiresPython::greater_than_equal_version(&target), + source: PythonRequirementSource::PythonVersion, } } @@ -31,84 +35,68 @@ impl PythonRequirement { /// [`MarkerEnvironment`]. pub fn from_requires_python( interpreter: &Interpreter, - requires_python: &RequiresPython, + requires_python: RequiresPython, ) -> Self { + let exact = interpreter.python_full_version().version.clone(); + let installed = interpreter.python_full_version().version.only_release(); Self { - installed: interpreter.python_full_version().version.only_release(), - target: Some(PythonTarget::RequiresPython(requires_python.clone())), + exact, + installed: RequiresPython::greater_than_equal_version(&installed), + target: requires_python, + source: PythonRequirementSource::RequiresPython, } } /// Create a [`PythonRequirement`] to resolve against an [`Interpreter`]. pub fn from_interpreter(interpreter: &Interpreter) -> Self { + let exact = interpreter.python_full_version().version.clone(); + let installed = interpreter.python_full_version().version.only_release(); Self { - installed: interpreter.python_full_version().version.only_release(), - target: None, + exact, + installed: RequiresPython::greater_than_equal_version(&installed), + target: RequiresPython::greater_than_equal_version(&installed), + source: PythonRequirementSource::Interpreter, } } /// Narrow the [`PythonRequirement`] to the given version, if it's stricter (i.e., greater) /// than the current `Requires-Python` minimum. pub fn narrow(&self, target: &RequiresPythonRange) -> Option { - let Some(PythonTarget::RequiresPython(requires_python)) = self.target.as_ref() else { - return None; - }; - let requires_python = requires_python.narrow(target)?; Some(Self { + exact: self.exact.clone(), installed: self.installed.clone(), - target: Some(PythonTarget::RequiresPython(requires_python)), + target: self.target.narrow(target)?, + source: self.source, }) } + /// Return the exact version of Python. + pub fn exact(&self) -> &Version { + &self.exact + } + /// Return the installed version of Python. - pub fn installed(&self) -> &Version { + pub fn installed(&self) -> &RequiresPython { &self.installed } /// Return the target version of Python. - pub fn target(&self) -> Option<&PythonTarget> { - self.target.as_ref() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -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. - Version(Version), - /// The [`PythonTarget`] specifier is a set of version specifiers, as extracted from the - /// `Requires-Python` field in a `pyproject.toml` or `METADATA` file. - RequiresPython(RequiresPython), -} - -impl PythonTarget { - /// Returns `true` if the target Python is compatible with the [`VersionSpecifiers`]. - pub fn is_compatible_with(&self, target: &VersionSpecifiers) -> bool { - match self { - PythonTarget::Version(version) => target.contains(version), - PythonTarget::RequiresPython(requires_python) => { - requires_python.is_contained_by(target) - } - } + pub fn target(&self) -> &RequiresPython { + &self.target } - /// Returns the [`RequiresPython`] for the [`PythonTarget`] specifier. - pub fn as_requires_python(&self) -> Option<&RequiresPython> { - match self { - PythonTarget::Version(_) => None, - PythonTarget::RequiresPython(requires_python) => Some(requires_python), - } + /// Return the source of the [`PythonRequirement`]. + pub fn source(&self) -> PythonRequirementSource { + self.source } } -impl std::fmt::Display for PythonTarget { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PythonTarget::Version(specifier) => std::fmt::Display::fmt(specifier, f), - PythonTarget::RequiresPython(specifiers) => std::fmt::Display::fmt(specifiers, f), - } - } +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] +pub enum PythonRequirementSource { + /// `--python-version` + PythonVersion, + /// `Requires-Python` + RequiresPython, + /// The discovered Python interpreter. + Interpreter, } diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index 0d9bc8908c1f..75da6a43bce7 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -19,7 +19,6 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use crate::pins::FilePins; use crate::preferences::Preferences; -use crate::python_requirement::PythonTarget; use crate::redirect::url_to_precise; use crate::resolution::AnnotatedDist; use crate::resolution_mode::ResolutionStrategy; @@ -33,12 +32,12 @@ pub(crate) type MarkersForDistribution = FxHashMap<(Version, Option /// A complete resolution graph in which every node represents a pinned package and every edge /// represents a dependency between two pinned packages. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ResolutionGraph { /// The underlying graph. pub(crate) petgraph: Graph, /// The range of supported Python versions. - pub(crate) requires_python: Option, + pub(crate) requires_python: RequiresPython, /// If the resolution had non-identical forks, store the forks in the lockfile so we can /// recreate them in subsequent resolutions. pub(crate) fork_markers: Vec, @@ -161,12 +160,7 @@ impl ResolutionGraph { } // Extract the `Requires-Python` range, if provided. - // TODO(charlie): Infer the supported Python range from the `Requires-Python` of the - // included packages. - let requires_python = python - .target() - .and_then(PythonTarget::as_requires_python) - .cloned(); + let requires_python = python.target().clone(); let fork_markers = if let [resolution] = resolutions { match resolution.markers { diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs index 5716062b4e66..dec1f68f2467 100644 --- a/crates/uv-resolver/src/resolver/batch_prefetch.rs +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -147,12 +147,13 @@ impl BatchPrefetcher { // Source distributions must meet both the _target_ Python version and the // _installed_ Python version (to build successfully). if let Some(requires_python) = sdist.file.requires_python.as_ref() { - if let Some(target) = python_requirement.target() { - if !target.is_compatible_with(requires_python) { - continue; - } + if !python_requirement + .installed() + .is_contained_by(requires_python) + { + continue; } - if !requires_python.contains(python_requirement.installed()) { + if !python_requirement.target().is_contained_by(requires_python) { continue; } } @@ -160,14 +161,8 @@ impl BatchPrefetcher { CompatibleDist::CompatibleWheel { wheel, .. } => { // Wheels must meet the _target_ Python version. if let Some(requires_python) = wheel.file.requires_python.as_ref() { - if let Some(target) = python_requirement.target() { - if !target.is_compatible_with(requires_python) { - continue; - } - } else { - if !requires_python.contains(python_requirement.installed()) { - continue; - } + if !python_requirement.target().is_contained_by(requires_python) { + continue; } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 48f2048348d3..c5f774ecb63f 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -154,9 +154,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider> database, flat_index, tags, - python_requirement - .target() - .and_then(|target| target.as_requires_python()), + python_requirement.target(), AllowedYanks::from_manifest(&manifest, &markers, options.dependency_mode), hasher, options.exclude_newer, @@ -280,11 +278,12 @@ impl ResolverState Result { debug!( "Solving with installed Python version: {}", - self.python_requirement.installed() + self.python_requirement.exact() + ); + debug!( + "Solving with target Python version: {}", + self.python_requirement.target() ); - if let Some(target) = self.python_requirement.target() { - debug!("Solving with target Python version: {}", target); - } let mut visited = FxHashSet::default(); @@ -315,18 +314,8 @@ impl ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState { // Wheels must meet the _target_ Python version. if let Some(requires_python) = wheel.file.requires_python.as_ref() { - if let Some(target) = python_requirement.target() { - if !target.is_compatible_with(requires_python) { - return Ok(None); - } - } else { - if !requires_python.contains(python_requirement.installed()) { - return Ok(None); - } + if !python_requirement.target().is_contained_by(requires_python) { + return Ok(None); } } } @@ -2247,9 +2241,10 @@ impl ForkState { let python_requirement = marker::requires_python(&combined_markers) .and_then(|marker| self.python_requirement.narrow(&marker)); if let Some(python_requirement) = python_requirement { - if let Some(target) = python_requirement.target() { - debug!("Narrowed `requires-python` bound to: {target}"); - } + debug!( + "Narrowed `requires-python` bound to: {}", + python_requirement.target() + ); self.python_requirement = python_requirement; } @@ -2851,12 +2846,5 @@ impl PartialOrd for Fork { /// Simplify a [`MarkerTree`] based on a [`PythonRequirement`]. fn simplify_python(marker: MarkerTree, python_requirement: &PythonRequirement) -> MarkerTree { - if let Some(requires_python) = python_requirement - .target() - .and_then(|target| target.as_requires_python()) - { - marker.simplify_python_versions(Range::from(requires_python.range().clone())) - } else { - marker - } + marker.simplify_python_versions(Range::from(python_requirement.target().range().clone())) } diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 4a35234c4528..9fd1c883d03b 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -76,7 +76,7 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext> { /// These are the entries from `--find-links` that act as overrides for index responses. flat_index: FlatIndex, tags: Option, - requires_python: Option, + requires_python: RequiresPython, allowed_yanks: AllowedYanks, hasher: HashStrategy, exclude_newer: Option, @@ -89,7 +89,7 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> { fetcher: DistributionDatabase<'a, Context>, flat_index: &'a FlatIndex, tags: Option<&'a Tags>, - requires_python: Option<&'a RequiresPython>, + requires_python: &'a RequiresPython, allowed_yanks: AllowedYanks, hasher: &'a HashStrategy, exclude_newer: Option, @@ -99,7 +99,7 @@ impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> { fetcher, flat_index: flat_index.clone(), tags: tags.cloned(), - requires_python: requires_python.cloned(), + requires_python: requires_python.clone(), allowed_yanks, hasher: hasher.clone(), exclude_newer, @@ -130,7 +130,7 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, package_name, &index, self.tags.as_ref(), - self.requires_python.as_ref(), + &self.requires_python, &self.allowed_yanks, &self.hasher, self.exclude_newer.as_ref(), diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 990e353f444c..751a1ecf8d05 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -43,7 +43,7 @@ impl VersionMap { package_name: &PackageName, index: &IndexUrl, tags: Option<&Tags>, - requires_python: Option<&RequiresPython>, + requires_python: &RequiresPython, allowed_yanks: &AllowedYanks, hasher: &HashStrategy, exclude_newer: Option<&ExcludeNewer>, @@ -102,7 +102,7 @@ impl VersionMap { tags: tags.cloned(), allowed_yanks: allowed_yanks.clone(), hasher: hasher.clone(), - requires_python: requires_python.cloned(), + requires_python: requires_python.clone(), exclude_newer: exclude_newer.copied(), }), } @@ -285,7 +285,7 @@ struct VersionMapLazy { /// The hashes of allowed distributions. hasher: HashStrategy, /// The `requires-python` constraint for the resolution. - requires_python: Option, + requires_python: RequiresPython, } impl VersionMapLazy { @@ -510,12 +510,8 @@ impl VersionMapLazy { // Check if the wheel is compatible with the `requires-python` (i.e., the Python ABI tag // is not less than the `requires-python` minimum version). - if let Some(requires_python) = self.requires_python.as_ref() { - if !requires_python.matches_wheel_tag(filename) { - return WheelCompatibility::Incompatible(IncompatibleWheel::Tag( - IncompatibleTag::Abi, - )); - } + if !self.requires_python.matches_wheel_tag(filename) { + return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(IncompatibleTag::Abi)); } // Break ties with the build tag. diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 99cfb2ff7d6f..8f4b815db1b4 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -238,7 +238,7 @@ pub(crate) async fn pip_compile( interpreter.python_version() }, ); - PythonRequirement::from_requires_python(&interpreter, &requires_python) + PythonRequirement::from_requires_python(&interpreter, requires_python) } else if let Some(python_version) = python_version.as_ref() { PythonRequirement::from_python_version(&interpreter, python_version) } else { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index a7dee655a37e..caf156501514 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -344,7 +344,8 @@ async fn do_lock( } // Determine the Python requirement. - let python_requirement = PythonRequirement::from_requires_python(interpreter, &requires_python); + let python_requirement = + PythonRequirement::from_requires_python(interpreter, requires_python.clone()); // Add all authenticated sources to the cache. for url in index_locations.urls() { @@ -665,16 +666,14 @@ impl ValidatedLock { // If the Requires-Python bound in the lockfile is weaker or equivalent to the // Requires-Python bound in the workspace, we should have the necessary wheels to perform // a locked resolution. - if let Some(locked) = lock.requires_python() { - if locked.range() != requires_python.range() { - // On the other hand, if the bound in the lockfile is stricter, meaning the - // bound has since been weakened, we have to perform a clean resolution to ensure - // we fetch the necessary wheels. - debug!("Ignoring existing lockfile due to change in `requires-python`"); - - // It's fine to prefer the existing versions, though. - return Ok(Self::Preferable(lock)); - } + if lock.requires_python().range() != requires_python.range() { + // On the other hand, if the bound in the lockfile is stricter, meaning the + // bound has since been weakened, we have to perform a clean resolution to ensure + // we fetch the necessary wheels. + debug!("Ignoring existing lockfile due to change in `requires-python`"); + + // It's fine to prefer the existing versions, though. + return Ok(Self::Preferable(lock)); } // If the user provided at least one index URL (from the command line, or from a configuration diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 31fdbe82b748..8c1a95d182b9 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -167,13 +167,14 @@ pub(super) async fn do_sync( } = settings; // 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::LockedPythonIncompatibility( - venv.interpreter().python_version().clone(), - requires_python.clone(), - )); - } + if !lock + .requires_python() + .contains(venv.interpreter().python_version()) + { + return Err(ProjectError::LockedPythonIncompatibility( + venv.interpreter().python_version().clone(), + lock.requires_python().clone(), + )); } // Determine the markers to use for resolution. diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 1d92e28b02e6..3580aa78e9ce 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1131,8 +1131,10 @@ fn compile_python_37() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.7.0) does not satisfy Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude that black==23.10.1 cannot be used. + ╰─▶ Because the requested Python version (>=3.7.0) does not satisfy Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude that black==23.10.1 cannot be used. And because you require black==23.10.1, we can conclude that your requirements are unsatisfiable. + + hint: The `--python-version` value (>=3.7.0) includes Python versions that are not supported by your dependencies (e.g., black==23.10.1 only supports >=3.8). Consider using a higher `--python-version` value. "###); Ok(()) @@ -6431,11 +6433,11 @@ fn no_strip_markers() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --no-strip-markers --python-platform linux - anyio==4.3.0 ; python_full_version >= '3.12' + anyio==4.3.0 # via -r requirements.in - idna==3.6 ; python_full_version >= '3.12' + idna==3.6 # via anyio - sniffio==1.3.1 ; python_full_version >= '3.12' + sniffio==1.3.1 # via anyio ----- stderr ----- @@ -6458,32 +6460,32 @@ fn no_strip_markers_multiple_markers() -> Result<()> { "})?; uv_snapshot!(context.filters(), context.pip_compile() - .arg("requirements.in") - .arg("--no-strip-markers") - .arg("--python-platform") - .arg("windows"), @r###" + .arg("requirements.in") + .arg("--no-strip-markers") + .arg("--python-platform") + .arg("windows"), @r###" success: true exit_code: 0 ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --no-strip-markers --python-platform windows - attrs==23.2.0 ; sys_platform == 'win32' or python_full_version >= '3.12' + attrs==23.2.0 # via # outcome # trio - cffi==1.16.0 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32') or (python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt') + cffi==1.16.0 ; implementation_name != 'pypy' and os_name == 'nt' # via trio - idna==3.6 ; sys_platform == 'win32' or python_full_version >= '3.12' + idna==3.6 # via trio - outcome==1.3.0.post0 ; sys_platform == 'win32' or python_full_version >= '3.12' + outcome==1.3.0.post0 # via trio - pycparser==2.21 ; (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32') or (python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt') + pycparser==2.21 ; implementation_name != 'pypy' and os_name == 'nt' # via cffi - sniffio==1.3.1 ; sys_platform == 'win32' or python_full_version >= '3.12' + sniffio==1.3.1 # via trio - sortedcontainers==2.4.0 ; sys_platform == 'win32' or python_full_version >= '3.12' + sortedcontainers==2.4.0 # via trio - trio==0.25.0 ; sys_platform == 'win32' or python_full_version >= '3.12' + trio==0.25.0 # via -r requirements.in ----- stderr ----- @@ -6512,23 +6514,23 @@ fn no_strip_markers_transitive_marker() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] requirements.in --no-strip-markers --python-platform windows - attrs==23.2.0 ; python_full_version >= '3.12' + attrs==23.2.0 # via # outcome # trio - cffi==1.16.0 ; python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt' + cffi==1.16.0 ; implementation_name != 'pypy' and os_name == 'nt' # via trio - idna==3.6 ; python_full_version >= '3.12' + idna==3.6 # via trio - outcome==1.3.0.post0 ; python_full_version >= '3.12' + outcome==1.3.0.post0 # via trio - pycparser==2.21 ; python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt' + pycparser==2.21 ; implementation_name != 'pypy' and os_name == 'nt' # via cffi - sniffio==1.3.1 ; python_full_version >= '3.12' + sniffio==1.3.1 # via trio - sortedcontainers==2.4.0 ; python_full_version >= '3.12' + sortedcontainers==2.4.0 # via trio - trio==0.25.0 ; python_full_version >= '3.12' + trio==0.25.0 # via -r requirements.in ----- stderr ----- @@ -9076,7 +9078,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.8" +requires-python = ">=3.13" "#, )?; @@ -9092,7 +9094,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); @@ -9116,7 +9118,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.8" +requires-python = ">=3.13" "#, )?; @@ -9144,7 +9146,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.11.0) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); @@ -9356,7 +9358,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.8" +requires-python = ">=3.13" "#, )?; @@ -9372,7 +9374,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index f3e3e8733307..f917c13fd92c 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -127,8 +127,10 @@ fn compatible_python_incompatible_override() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.9 is not available; 3.11.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.9.0) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the requested Python version (>=3.9.0) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + + hint: The `--python-version` value (>=3.9.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.10). Consider using a higher `--python-version` value. "### ); @@ -357,9 +359,11 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { package-a==2.0.0 we can conclude that package-a<2.0.0 cannot be used. (1) - Because the requested Python version (3.11.0) does not satisfy Python>=3.12 and package-a==2.0.0 depends on Python>=3.12, we can conclude that package-a==2.0.0 cannot be used. + Because the requested Python version (>=3.11.0) does not satisfy Python>=3.12 and package-a==2.0.0 depends on Python>=3.12, we can conclude that package-a==2.0.0 cannot be used. And because we know from (1) that package-a<2.0.0 cannot be used, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. + + hint: The `--python-version` value (>=3.11.0) includes Python versions that are not supported by your dependencies (e.g., package-a==2.0.0 only supports >=3.12). Consider using a higher `--python-version` value. "### ); @@ -406,8 +410,10 @@ fn python_patch_override_no_patch() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.8.0) does not satisfy Python>=3.8.4 and package-a==1.0.0 depends on Python>=3.8.4, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the requested Python version (>=3.8.0) does not satisfy Python>=3.8.4 and package-a==1.0.0 depends on Python>=3.8.4, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + + hint: The `--python-version` value (>=3.8.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.8.4). Consider using a higher `--python-version` value. "### ); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 71b1a9ad9bfd..3b0ed90caf38 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -3291,7 +3291,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.8" +requires-python = ">=3.13" "#, )?; @@ -3304,7 +3304,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); @@ -3805,7 +3805,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.8" +requires-python = ">=3.13" "#, )?; @@ -3817,7 +3817,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 7669dd9edf7d..48b2735ad922 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -3742,19 +3742,21 @@ fn python_less_than_current() { uv_snapshot!(filters, command(&context) .arg("python-less-than-current-a==1.0.0") , @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python<=3.8 and package-a==1.0.0 depends on Python<=3.8, we can conclude that package-a==1.0.0 cannot be used. - And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0 "###); - assert_not_installed( + assert_installed( &context.venv, "python_less_than_current_a", + "1.0.0", &context.temp_dir, ); } diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 156c9868f68e..fbc9e8cd4d63 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -3294,7 +3294,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.5" +requires-python = ">=3.13" "#, )?; @@ -3310,7 +3310,7 @@ requires-python = "<=3.5" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### ); @@ -3365,7 +3365,7 @@ version = "0.0.0" dependencies = [ "anyio==4.0.0" ] -requires-python = "<=3.5" +requires-python = ">=3.13" "#, )?; @@ -3381,7 +3381,7 @@ requires-python = "<=3.5" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. "### );