diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 6fc529a2864b..233d9a378d0a 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -132,6 +132,10 @@ impl<'a> BuildDispatch<'a> { impl<'a> BuildContext for BuildDispatch<'a> { type SourceDistBuilder = SourceBuild; + fn interpreter(&self) -> &Interpreter { + self.interpreter + } + fn cache(&self) -> &Cache { self.cache } diff --git a/crates/uv-distribution-types/src/buildable.rs b/crates/uv-distribution-types/src/buildable.rs index 9f096f3f5e14..ae6897b4cfb7 100644 --- a/crates/uv-distribution-types/src/buildable.rs +++ b/crates/uv-distribution-types/src/buildable.rs @@ -4,7 +4,7 @@ use std::path::Path; use url::Url; use uv_distribution_filename::SourceDistExtension; use uv_git::GitUrl; -use uv_pep440::Version; +use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::VerbatimUrl; use uv_normalize::PackageName; @@ -64,6 +64,14 @@ impl BuildableSource<'_> { Self::Url(url) => matches!(url, SourceUrl::Directory(_)), } } + + /// Return the Python version specifier required by the source, if available. + pub fn requires_python(&self) -> Option<&VersionSpecifiers> { + let Self::Dist(SourceDist::Registry(dist)) = self else { + return None; + }; + dist.file.requires_python.as_ref() + } } impl std::fmt::Display for BuildableSource<'_> { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 01f4cd6b034b..d2f9f5aec159 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -10,7 +10,7 @@ use uv_client::WrappedReqwestError; use uv_distribution_filename::WheelFilenameError; use uv_fs::Simplified; use uv_normalize::PackageName; -use uv_pep440::Version; +use uv_pep440::{Version, VersionSpecifiers}; use uv_pypi_types::{HashDigest, ParsedUrlError}; #[derive(Debug, thiserror::Error)] @@ -96,6 +96,8 @@ 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 {1} is installed")] + RequiresPython(VersionSpecifiers, Version), /// A generic request middleware error happened while making a request. /// Refer to the error message for more details. diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index b3bbbe94a843..399e3eff4d0f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1,10 +1,18 @@ //! Fetch and build source distributions from remote sources. use std::borrow::Cow; +use std::ops::Bound; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; +use crate::distribution_database::ManagedClient; +use crate::error::Error; +use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; +use crate::reporter::Facade; +use crate::source::built_wheel_metadata::BuiltWheelMetadata; +use crate::source::revision::Revision; +use crate::{Reporter, RequiresDist}; use fs_err::tokio as fs; use futures::{FutureExt, TryStreamExt}; use reqwest::Response; @@ -26,19 +34,12 @@ 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; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, Metadata12, RequiresTxt, ResolutionMetadata}; use uv_types::{BuildContext, SourceBuildTrait}; use zip::ZipArchive; -use crate::distribution_database::ManagedClient; -use crate::error::Error; -use crate::metadata::{ArchiveMetadata, GitWorkspaceMember, Metadata}; -use crate::reporter::Facade; -use crate::source::built_wheel_metadata::BuiltWheelMetadata; -use crate::source::revision::Revision; -use crate::{Reporter, RequiresDist}; - mod built_wheel_metadata; mod revision; @@ -1798,6 +1799,27 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ) -> Result, Error> { debug!("Preparing metadata for: {source}"); + // Ensure that the _installed_ Python version is compatible with the `requires-python` + // specifier. + if let Some(requires_python) = source.requires_python() { + let installed = self.build_context.interpreter().python_version(); + let target = release_specifiers_to_ranges(requires_python.clone()) + .bounding_range() + .map(|bounding_range| bounding_range.0.cloned()) + .unwrap_or(Bound::Unbounded); + let is_compatible = match target { + Bound::Included(target) => *installed >= target, + Bound::Excluded(target) => *installed > target, + Bound::Unbounded => true, + }; + if !is_compatible { + return Err(Error::RequiresPython( + requires_python.clone(), + installed.clone(), + )); + } + } + // Set up the builder. let mut builder = self .build_context diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index abd616e55aad..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,6 +699,14 @@ impl PubGrubReportFormatter<'_> { reason: reason.clone(), }); } + 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; } @@ -862,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, @@ -932,6 +951,9 @@ enum PubGrubHintCore { InvalidVersionStructure { package: PubGrubPackage, }, + IncompatibleBuildRequirement { + package: PubGrubPackage, + }, RequiresPython { source: PythonRequirementSource, requires_python: RequiresPython, @@ -985,6 +1007,9 @@ impl From for PubGrubHintCore { PubGrubHint::InvalidVersionStructure { package, .. } => { Self::InvalidVersionStructure { package } } + PubGrubHint::IncompatibleBuildRequirement { package, .. } => { + Self::IncompatibleBuildRequirement { package } + } PubGrubHint::RequiresPython { source, requires_python, @@ -1187,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 26c6a3e52b07..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)] @@ -40,6 +40,9 @@ pub(crate) enum UnavailableVersion { InvalidStructure, /// The wheel metadata was not found in the cache and the network is not available. 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(VersionSpecifiers), } impl UnavailableVersion { @@ -51,6 +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(requires_python) => { + format!("Python {requires_python}") + } } } @@ -62,6 +68,7 @@ impl UnavailableVersion { UnavailableVersion::InconsistentMetadata => format!("has {self}"), UnavailableVersion::InvalidStructure => format!("has {self}"), UnavailableVersion::Offline => format!("needs {self}"), + UnavailableVersion::RequiresPython(..) => format!("requires {self}"), } } @@ -73,6 +80,7 @@ impl UnavailableVersion { UnavailableVersion::InconsistentMetadata => format!("have {self}"), UnavailableVersion::InvalidStructure => format!("have {self}"), UnavailableVersion::Offline => format!("need {self}"), + UnavailableVersion::RequiresPython(..) => format!("require {self}"), } } } @@ -143,6 +151,9 @@ pub(crate) enum IncompletePackage { InconsistentMetadata(String), /// The wheel has an invalid structure. 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(VersionSpecifiers, Version), } #[derive(Debug, Clone)] diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 226937145673..84b89435edfc 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -958,6 +958,9 @@ impl ResolverState { + unreachable!("`requires-python` is only known upfront for registry distributions") + } }; let version = &metadata.version; @@ -1074,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 { @@ -1342,6 +1327,28 @@ impl ResolverState { + 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( + requires_python.clone(), + python_version.clone(), + ), + ); + return Ok(Dependencies::Unavailable( + UnavailableVersion::RequiresPython(requires_python.clone()), + )); + } }; let requirements = self.flatten_requirements( @@ -1888,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 66729e4a8d2d..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}; @@ -42,6 +43,9 @@ pub enum MetadataResponse { InvalidStructure(Box), /// The wheel metadata was not found in the cache and the network is not available. 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(VersionSpecifiers, Version), } pub trait ResolverProvider { @@ -203,6 +207,9 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, uv_distribution::Error::WheelMetadata(_, err) => { Ok(MetadataResponse::InvalidStructure(err)) } + uv_distribution::Error::RequiresPython(requires_python, version) => { + Ok(MetadataResponse::RequiresPython(requires_python, version)) + } err => Err(err), }, } diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 72b84d95fb56..b39e261383c2 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -14,7 +14,7 @@ use uv_distribution_types::{ use uv_git::GitResolver; use uv_pep508::PackageName; use uv_pypi_types::Requirement; -use uv_python::PythonEnvironment; +use uv_python::{Interpreter, PythonEnvironment}; /// Avoids cyclic crate dependencies between resolver, installer and builder. /// @@ -56,6 +56,9 @@ use uv_python::PythonEnvironment; pub trait BuildContext { type SourceDistBuilder: SourceBuildTrait; + /// Return a reference to the interpreter. + fn interpreter(&self) -> &Interpreter; + /// Return a reference to the cache. fn cache(&self) -> &Cache; diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 149ed32b29c7..5f81f522c4dd 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(()) }