From d24c42c284f61fa49a6d40a9d88f89910a85f631 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 2 Nov 2024 22:14:07 -0400 Subject: [PATCH] Add failure hint --- crates/uv-distribution/src/error.rs | 3 +- crates/uv-distribution/src/source/mod.rs | 2 +- crates/uv-resolver/src/pubgrub/report.rs | 45 ++++- .../uv-resolver/src/resolver/availability.rs | 14 +- crates/uv-resolver/src/resolver/mod.rs | 156 ++++++++---------- crates/uv-resolver/src/resolver/provider.rs | 7 +- crates/uv/tests/it/pip_compile.rs | 59 +++++++ crates/uv/tests/it/pip_compile_scenarios.rs | 63 ++++--- 8 files changed, 214 insertions(+), 135 deletions(-) diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 7d0091bf66db..d2f9f5aec159 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -96,8 +96,7 @@ pub enum Error { NotFound(Url), #[error("Attempted to re-extract the source distribution for `{0}`, but the hashes didn't match. Run `{}` to clear the cache.", "uv cache clean".green())] CacheHeal(String), - - #[error("The source distribution requires Python {0}, but the current Python version is {1}")] + #[error("The source distribution requires Python {0}, but {1} is installed")] RequiresPython(VersionSpecifiers, Version), /// A generic request middleware error happened while making a request. diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 6de02147c821..399e3eff4d0f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -34,7 +34,7 @@ use uv_distribution_types::{ use uv_extract::hash::Hasher; use uv_fs::{rename_with_retry, write_atomic, LockedFile}; use uv_metadata::read_archive_metadata; -use uv_pep440::{release_specifiers_to_ranges, Version}; +use uv_pep440::release_specifiers_to_ranges; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, Metadata12, RequiresTxt, ResolutionMetadata}; use uv_types::{BuildContext, SourceBuildTrait}; diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 09f6eebc52ac..83b45a87e536 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -11,7 +11,7 @@ use rustc_hash::FxHashMap; use uv_configuration::IndexStrategy; use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl}; use uv_normalize::PackageName; -use uv_pep440::Version; +use uv_pep440::{Version, VersionSpecifiers}; use crate::candidate_selector::CandidateSelector; use crate::error::ErrorTree; @@ -699,7 +699,14 @@ impl PubGrubReportFormatter<'_> { reason: reason.clone(), }); } - IncompletePackage::RequiresPython(_) => {} + IncompletePackage::RequiresPython(requires_python, python_version) => { + hints.insert(PubGrubHint::IncompatibleBuildRequirement { + package: package.clone(), + version: version.clone(), + requires_python: requires_python.clone(), + python_version: python_version.clone(), + }); + } } break; } @@ -863,6 +870,17 @@ pub(crate) enum PubGrubHint { // excluded from `PartialEq` and `Hash` reason: String, }, + /// The source distribution has a `requires-python` requirement that is not met by the installed + /// Python version (and static metadata is not available). + IncompatibleBuildRequirement { + package: PubGrubPackage, + // excluded from `PartialEq` and `Hash` + version: Version, + // excluded from `PartialEq` and `Hash` + requires_python: VersionSpecifiers, + // excluded from `PartialEq` and `Hash` + python_version: Version, + }, /// The `Requires-Python` requirement was not satisfied. RequiresPython { source: PythonRequirementSource, @@ -933,6 +951,9 @@ enum PubGrubHintCore { InvalidVersionStructure { package: PubGrubPackage, }, + IncompatibleBuildRequirement { + package: PubGrubPackage, + }, RequiresPython { source: PythonRequirementSource, requires_python: RequiresPython, @@ -986,6 +1007,9 @@ impl From for PubGrubHintCore { PubGrubHint::InvalidVersionStructure { package, .. } => { Self::InvalidVersionStructure { package } } + PubGrubHint::IncompatibleBuildRequirement { package, .. } => { + Self::IncompatibleBuildRequirement { package } + } PubGrubHint::RequiresPython { source, requires_python, @@ -1188,6 +1212,23 @@ impl std::fmt::Display for PubGrubHint { package_requires_python.bold(), ) } + Self::IncompatibleBuildRequirement { + package, + version, + requires_python, + python_version, + } => { + write!( + f, + "{}{} The source distribution for {}=={} does not include static metadata. Generating metadata for this package requires Python {}, but Python {} is installed.", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + version.bold(), + requires_python.bold(), + python_version.bold(), + ) + } Self::RequiresPython { source: PythonRequirementSource::Interpreter, requires_python: _, diff --git a/crates/uv-resolver/src/resolver/availability.rs b/crates/uv-resolver/src/resolver/availability.rs index 68823c70527a..c3e9067d9455 100644 --- a/crates/uv-resolver/src/resolver/availability.rs +++ b/crates/uv-resolver/src/resolver/availability.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; use uv_distribution_types::IncompatibleDist; -use uv_pep440::Version; +use uv_pep440::{Version, VersionSpecifiers}; /// The reason why a package or a version cannot be used. #[derive(Debug, Clone, Eq, PartialEq)] @@ -42,7 +42,7 @@ pub(crate) enum UnavailableVersion { Offline, /// The source distribution has a `requires-python` requirement that is not met by the installed /// Python version (and static metadata is not available). - RequiresPython, + RequiresPython(VersionSpecifiers), } impl UnavailableVersion { @@ -54,7 +54,9 @@ impl UnavailableVersion { UnavailableVersion::InconsistentMetadata => "inconsistent metadata".into(), UnavailableVersion::InvalidStructure => "an invalid package format".into(), UnavailableVersion::Offline => "to be downloaded from a registry".into(), - UnavailableVersion::RequiresPython => "require a greater Python version".into(), + UnavailableVersion::RequiresPython(requires_python) => { + format!("Python {requires_python}") + } } } @@ -66,7 +68,7 @@ impl UnavailableVersion { UnavailableVersion::InconsistentMetadata => format!("has {self}"), UnavailableVersion::InvalidStructure => format!("has {self}"), UnavailableVersion::Offline => format!("needs {self}"), - UnavailableVersion::RequiresPython => format!("requires {self}"), + UnavailableVersion::RequiresPython(..) => format!("requires {self}"), } } @@ -78,7 +80,7 @@ impl UnavailableVersion { UnavailableVersion::InconsistentMetadata => format!("have {self}"), UnavailableVersion::InvalidStructure => format!("have {self}"), UnavailableVersion::Offline => format!("need {self}"), - UnavailableVersion::RequiresPython => format!("require {self}"), + UnavailableVersion::RequiresPython(..) => format!("require {self}"), } } } @@ -151,7 +153,7 @@ pub(crate) enum IncompletePackage { InvalidStructure(String), /// The source distribution has a `requires-python` requirement that is not met by the installed /// Python version (and static metadata is not available). - RequiresPython(String), + RequiresPython(VersionSpecifiers, Version), } #[derive(Debug, Clone)] diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 7ca145a72398..84b89435edfc 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -958,8 +958,8 @@ impl ResolverState { - unreachable!("`requires-python` is not known upfront for URL requirements") + MetadataResponse::RequiresPython(..) => { + unreachable!("`requires-python` is only known upfront for registry distributions") } }; @@ -1077,72 +1077,54 @@ impl ResolverState None, CompatibleDist::SourceDist { sdist, .. } | CompatibleDist::IncompatibleWheel { sdist, .. } => { - // Source distributions must meet both the _target_ Python version and the - // _installed_ Python version (to build successfully). - sdist - .file - .requires_python - .as_ref() - .and_then(|requires_python| { - // if !python_requirement - // .installed() - // .is_contained_by(requires_python) - // { - // return Some(IncompatibleDist::Source( - // IncompatibleSource::RequiresPython( - // requires_python.clone(), - // PythonRequirementKind::Installed, - // ), - // )); - // } - if !python_requirement.target().is_contained_by(requires_python) { - return Some(IncompatibleDist::Source( - IncompatibleSource::RequiresPython( - requires_python.clone(), - PythonRequirementKind::Target, - ), - )); - } - None - }) - } - CompatibleDist::CompatibleWheel { wheel, .. } => { - // Wheels must meet the _target_ Python version. - wheel - .file - .requires_python - .as_ref() - .and_then(|requires_python| { - if python_requirement.installed() == python_requirement.target() { - if !python_requirement - .installed() - .is_contained_by(requires_python) - { - return Some(IncompatibleDist::Wheel( - IncompatibleWheel::RequiresPython( - requires_python.clone(), - PythonRequirementKind::Installed, - ), - )); - } - } else { - if !python_requirement.target().is_contained_by(requires_python) { - return Some(IncompatibleDist::Wheel( - IncompatibleWheel::RequiresPython( - requires_python.clone(), - PythonRequirementKind::Target, - ), - )); - } - } - None - }) + sdist.file.requires_python.as_ref() } + CompatibleDist::CompatibleWheel { wheel, .. } => wheel.file.requires_python.as_ref(), }; + let incompatibility = requires_python.and_then(|requires_python| { + if python_requirement.installed() == python_requirement.target() { + if !python_requirement + .installed() + .is_contained_by(requires_python) + { + return if matches!(dist, CompatibleDist::CompatibleWheel { .. }) { + Some(IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython( + requires_python.clone(), + PythonRequirementKind::Installed, + ))) + } else { + Some(IncompatibleDist::Source( + IncompatibleSource::RequiresPython( + requires_python.clone(), + PythonRequirementKind::Installed, + ), + )) + }; + } + } else { + if !python_requirement.target().is_contained_by(requires_python) { + return if matches!(dist, CompatibleDist::CompatibleWheel { .. }) { + Some(IncompatibleDist::Wheel(IncompatibleWheel::RequiresPython( + requires_python.clone(), + PythonRequirementKind::Target, + ))) + } else { + Some(IncompatibleDist::Source( + IncompatibleSource::RequiresPython( + requires_python.clone(), + PythonRequirementKind::Target, + ), + )) + }; + } + } + None + }); // The version is incompatible due to its Python requirement. if let Some(incompatibility) = incompatibility { @@ -1345,17 +1327,26 @@ impl ResolverState { - warn!("Unable to extract metadata for {name}: {err}"); + MetadataResponse::RequiresPython(requires_python, python_version) => { + warn!( + "Unable to extract metadata for {name}: {}", + uv_distribution::Error::RequiresPython( + requires_python.clone(), + python_version.clone() + ) + ); self.incomplete_packages .entry(name.clone()) .or_default() .insert( version.clone(), - IncompletePackage::RequiresPython(err.to_string()), + IncompletePackage::RequiresPython( + requires_python.clone(), + python_version.clone(), + ), ); return Ok(Dependencies::Unavailable( - UnavailableVersion::RequiresPython, + UnavailableVersion::RequiresPython(requires_python.clone()), )); } }; @@ -1904,33 +1895,22 @@ impl ResolverState {} + // Validate the Python requirement. + let requires_python = match dist { + CompatibleDist::InstalledDist(_) => None, CompatibleDist::SourceDist { sdist, .. } | CompatibleDist::IncompatibleWheel { sdist, .. } => { - // 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 !python_requirement - // .installed() - // .is_contained_by(requires_python) - // { - // return Ok(None); - // } - if !python_requirement.target().is_contained_by(requires_python) { - return Ok(None); - } - } + sdist.file.requires_python.as_ref() } CompatibleDist::CompatibleWheel { wheel, .. } => { - // Wheels must meet the _target_ Python version. - if let Some(requires_python) = wheel.file.requires_python.as_ref() { - if !python_requirement.target().is_contained_by(requires_python) { - return Ok(None); - } - } + wheel.file.requires_python.as_ref() } }; + if let Some(requires_python) = requires_python.as_ref() { + if !python_requirement.target().is_contained_by(requires_python) { + return Ok(None); + } + } // Emit a request to fetch the metadata for this version. if self.index.distributions().register(candidate.version_id()) { diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 5f37b1fce69e..c9f7c04a0957 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -4,6 +4,7 @@ use uv_configuration::BuildOptions; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_distribution_types::{Dist, IndexCapabilities, IndexUrl}; use uv_normalize::PackageName; +use uv_pep440::{Version, VersionSpecifiers}; use uv_platform_tags::Tags; use uv_types::{BuildContext, HashStrategy}; @@ -44,7 +45,7 @@ pub enum MetadataResponse { Offline, /// The source distribution has a `requires-python` requirement that is not met by the installed /// Python version (and static metadata is not available). - RequiresPython(Box), + RequiresPython(VersionSpecifiers, Version), } pub trait ResolverProvider { @@ -206,8 +207,8 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, uv_distribution::Error::WheelMetadata(_, err) => { Ok(MetadataResponse::InvalidStructure(err)) } - uv_distribution::Error::RequiresPython { .. } => { - Ok(MetadataResponse::RequiresPython(Box::new(err))) + uv_distribution::Error::RequiresPython(requires_python, version) => { + Ok(MetadataResponse::RequiresPython(requires_python, version)) } err => Err(err), }, diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 6612a3b7ec34..2c74b74c12bb 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -12661,3 +12661,62 @@ fn prune_unreachable() -> Result<()> { Ok(()) } + +/// Allow resolving a package that requires a Python version that is not available, as long as it +/// includes static metadata. +/// +/// See: +#[test] +fn unsupported_requires_python_static_metadata() -> Result<()> { + let context = TestContext::new("3.11"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("interpreters-pep-734 <= 0.4.1 ; python_version >= '3.13'")?; + + uv_snapshot!(context.filters(), context + .pip_compile() + .arg("--universal") + .arg("requirements.in") + .env(EnvVars::UV_EXCLUDE_NEWER, "2024-11-04T00:00:00Z"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --universal requirements.in + interpreters-pep-734==0.4.1 ; python_full_version >= '3.13' + # via -r requirements.in + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + Ok(()) +} + +/// Disallow resolving a package that requires a Python version that is not available, if it uses +/// dynamic metadata. +/// +/// See: +#[test] +fn unsupported_requires_python_dynamic_metadata() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("source-distribution==0.0.3 ; python_version >= '3.10'")?; + + uv_snapshot!(context.filters(), context + .pip_compile() + .arg("--universal") + .arg("requirements.in") + .env(EnvVars::UV_EXCLUDE_NEWER, "2024-11-04T00:00:00Z"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies for split (python_full_version >= '3.10'): + ╰─▶ Because source-distribution{python_full_version >= '3.10'}==0.0.3 requires Python >=3.10 and you require source-distribution{python_full_version >= '3.10'}==0.0.3, we can conclude that your requirements are unsatisfiable. + + hint: The source distribution for source-distribution{python_full_version >= '3.10'}==0.0.3 does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed. + "###); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index c9747203d81a..03edb2cc6f2c 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -175,20 +175,22 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> // dependencies. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r#" - success: false - exit_code: 1 + , @r###" + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 + package-a==1.0.0 + # via -r requirements.in ----- stderr ----- warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) 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. - "# + Resolved 1 package in [TIME] + "### ); - output.assert().failure(); + output.assert().success(); Ok(()) } @@ -287,20 +289,22 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> // determine its dependencies. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r#" - success: false - exit_code: 1 + , @r###" + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 + package-a==1.0.0 + # via -r requirements.in ----- stderr ----- warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) 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. - "# + Resolved 1 package in [TIME] + "### ); - output.assert().failure(); + output.assert().success(); Ok(()) } @@ -345,29 +349,22 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { // available, but is not compatible with the target version and cannot be used. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r#" - success: false - exit_code: 1 + , @r###" + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 + package-a==1.0.0 + # via -r requirements.in ----- stderr ----- warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) 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 only the following versions of package-a are available: - package-a==1.0.0 - 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. - 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. - "# + Resolved 1 package in [TIME] + "### ); - output.assert().failure(); + output.assert().success(); Ok(()) }