diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index a4b105406e2f..caebba4f02d4 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -132,13 +132,32 @@ broadly. ## Local version identifiers -uv does not implement spec-compliant handling of local version identifiers (e.g., `1.0.0+local`). -Though local version identifiers are rare in published packages (and, e.g., disallowed on PyPI), -they're common in the PyTorch ecosystem. uv's incorrect handling of local version identifiers -may lead to resolution failures in some cases. - -In the future, uv intends to implement spec-compliant handling of local version identifiers. -For more, see [#1855](https://github.com/astral-sh/uv/issues/1855). +uv does not implement spec-compliant handling of local version identifiers (e.g., `1.2.3+local`). +This is considered a known limitation. Although local version identifiers are rare in published +packages (and, e.g., disallowed on PyPI), they're common in the PyTorch ecosystem, and uv's approach +to local versions _does_ support typical PyTorch workflows to succeed out-of-the-box. + +[PEP 440](https://peps.python.org/pep-0440/#version-specifiers) specifies that the local version +segment should typically be ignored when evaluating version specifiers, with a few exceptions. +For example, `foo==1.2.3` should accept `1.2.3+local`, but `foo==1.2.3+local` should _not_ accept +`1.2.3`. These asymmetries are hard to model in a resolution algorithm. As such, uv treats `1.2.3` +and `1.2.3+local` as entirely separate versions, but respects local versions provided as direct +dependencies throughout the resolution, such that if you provide `foo==1.2.3+local` as a direct +dependency, `1.2.3+local` _will_ be accepted for any transitive dependencies that request +`foo==1.2.3`. + +To take an example from the PyTorch ecosystem, it's common to specify `torch==2.0.0+cu118` and +`torchvision==0.15.1+cu118` as direct dependencies. `torchvision @ 0.15.1+cu118` declares a +dependency on `torch==2.0.0`. In this case, uv would recognize that `torch==2.0.0+cu118` satisfies +the specifier, since it was provided as a direct dependency. + +As compared to pip, the main differences in observed behavior are as follows: + +- In general, local versions must be provided as direct dependencies. Resolution may succeed for + transitive dependencies that request a non-local version, but this is not guaranteed. +- If _only_ local versions exist for a package `foo` at a given version (e.g., `1.2.3+local` exists, + but `1.2.3` does not), `uv pip install foo==1.2.3` will fail, while `pip install foo==1.2.3` will + resolve to an arbitrary local version. ## Packages that exist on multiple indexes diff --git a/crates/pep440-rs/src/version_specifier.rs b/crates/pep440-rs/src/version_specifier.rs index af1873963373..f222fcfcac09 100644 --- a/crates/pep440-rs/src/version_specifier.rs +++ b/crates/pep440-rs/src/version_specifier.rs @@ -340,9 +340,14 @@ impl Serialize for VersionSpecifier { } impl VersionSpecifier { + /// Create a new version specifier from an operator and a version. + pub fn new(operator: Operator, version: Version) -> Self { + Self { operator, version } + } + /// Build from parts, validating that the operator is allowed with that version. The last /// parameter indicates a trailing `.*`, to differentiate between `1.1.*` and `1.1` - pub fn new( + pub fn from_pattern( operator: Operator, version_pattern: VersionPattern, ) -> Result { @@ -545,7 +550,7 @@ impl FromStr for VersionSpecifier { } let vpat = version.parse().map_err(ParseErrorKind::InvalidVersion)?; let version_specifier = - Self::new(operator, vpat).map_err(ParseErrorKind::InvalidSpecifier)?; + Self::from_pattern(operator, vpat).map_err(ParseErrorKind::InvalidSpecifier)?; s.eat_while(|c: char| c.is_whitespace()); if !s.done() { return Err(ParseErrorKind::InvalidTrailing(s.after().to_string()).into()); @@ -1664,7 +1669,7 @@ Failed to parse version: Unexpected end of version specifier, expected operator: let op = Operator::TildeEqual; let v = Version::new([5]); let vpat = VersionPattern::verbatim(v); - assert_eq!(err, VersionSpecifier::new(op, vpat).unwrap_err()); + assert_eq!(err, VersionSpecifier::from_pattern(op, vpat).unwrap_err()); assert_eq!( err.to_string(), "The ~= operator requires at least two segments in the release version" diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 05dc1a603d02..9b05d72c9e9a 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -1198,12 +1198,12 @@ mod tests { ], version_or_url: Some(VersionOrUrl::VersionSpecifier( [ - VersionSpecifier::new( + VersionSpecifier::from_pattern( Operator::GreaterThanEqual, VersionPattern::verbatim(Version::new([2, 8, 1])), ) .unwrap(), - VersionSpecifier::new( + VersionSpecifier::from_pattern( Operator::Equal, VersionPattern::wildcard(Version::new([2, 8])), ) diff --git a/crates/pep508-rs/src/marker.rs b/crates/pep508-rs/src/marker.rs index 9764099a24fc..69ad5484aa71 100644 --- a/crates/pep508-rs/src/marker.rs +++ b/crates/pep508-rs/src/marker.rs @@ -595,7 +595,7 @@ impl MarkerExpression { Some(operator) => operator, }; - let specifier = match VersionSpecifier::new(operator, r_vpat) { + let specifier = match VersionSpecifier::from_pattern(operator, r_vpat) { Ok(specifier) => specifier, Err(err) => { reporter( @@ -674,7 +674,7 @@ impl MarkerExpression { Some(operator) => operator, }; - let specifier = match VersionSpecifier::new( + let specifier = match VersionSpecifier::from_pattern( operator, VersionPattern::verbatim(r_version.clone()), ) { @@ -784,7 +784,7 @@ impl MarkerExpression { let r_vpat = r_string.parse::().ok()?; let operator = operator.to_pep440_operator()?; // operator and right hand side make the specifier - let specifier = VersionSpecifier::new(operator, r_vpat).ok()?; + let specifier = VersionSpecifier::from_pattern(operator, r_vpat).ok()?; let compatible = python_versions .iter() @@ -808,7 +808,7 @@ impl MarkerExpression { let compatible = python_versions.iter().any(|r_version| { // operator and right hand side make the specifier and in this case the // right hand is `python_version` so changes every iteration - match VersionSpecifier::new( + match VersionSpecifier::from_pattern( operator, VersionPattern::verbatim(r_version.clone()), ) { diff --git a/crates/platform-tags/src/tags.rs b/crates/platform-tags/src/tags.rs index e753c6c5ca2b..b4db57da9d8a 100644 --- a/crates/platform-tags/src/tags.rs +++ b/crates/platform-tags/src/tags.rs @@ -203,34 +203,33 @@ impl Tags { wheel_abi_tags: &[String], wheel_platform_tags: &[String], ) -> TagCompatibility { - return TagCompatibility::Compatible(TagPriority(NonZeroU32::new(1).unwrap())); - // let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid); - // - // for wheel_py in wheel_python_tags { - // let Some(abis) = self.map.get(wheel_py) else { - // max_compatibility = - // max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python)); - // continue; - // }; - // for wheel_abi in wheel_abi_tags { - // let Some(platforms) = abis.get(wheel_abi) else { - // max_compatibility = - // max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi)); - // continue; - // }; - // for wheel_platform in wheel_platform_tags { - // let priority = platforms.get(wheel_platform).copied(); - // if let Some(priority) = priority { - // max_compatibility = - // max_compatibility.max(TagCompatibility::Compatible(priority)); - // } else { - // max_compatibility = max_compatibility - // .max(TagCompatibility::Incompatible(IncompatibleTag::Platform)); - // } - // } - // } - // } - // max_compatibility + let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid); + + for wheel_py in wheel_python_tags { + let Some(abis) = self.map.get(wheel_py) else { + max_compatibility = + max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Python)); + continue; + }; + for wheel_abi in wheel_abi_tags { + let Some(platforms) = abis.get(wheel_abi) else { + max_compatibility = + max_compatibility.max(TagCompatibility::Incompatible(IncompatibleTag::Abi)); + continue; + }; + for wheel_platform in wheel_platform_tags { + let priority = platforms.get(wheel_platform).copied(); + if let Some(priority) = priority { + max_compatibility = + max_compatibility.max(TagCompatibility::Compatible(priority)); + } else { + max_compatibility = max_compatibility + .max(TagCompatibility::Incompatible(IncompatibleTag::Platform)); + } + } + } + } + max_compatibility } } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 86a12c4dc425..490d7210b795 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -55,9 +55,6 @@ pub enum ResolveError { #[error("There are conflicting local versions requested for package `{0}`: {1} vs. {2}")] ConflictingLocal(PackageName, String, String), - #[error("There are conflicting versions for `{0}`: {1}")] - ConflictingVersions(String, String), - #[error("Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file.")] DisallowedUrl(PackageName, String), diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 416dfc2c2932..cccf09c075a1 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -1,9 +1,9 @@ use itertools::Itertools; use pubgrub::range::Range; -use tracing::{debug, warn}; +use tracing::warn; use distribution_types::Verbatim; -use pep440_rs::{Operator, Version}; +use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use uv_normalize::{ExtraName, PackageName}; @@ -19,6 +19,7 @@ pub struct PubGrubDependencies(Vec<(PubGrubPackage, Range)>); impl PubGrubDependencies { /// Generate a set of `PubGrub` dependencies from a set of requirements. + #[allow(clippy::too_many_arguments)] pub(crate) fn from_requirements( requirements: &[Requirement], constraints: &Constraints, @@ -142,37 +143,23 @@ fn to_pubgrub( Some(VersionOrUrl::VersionSpecifier(specifiers)) => { // If the specifier is an exact version, and the user requested a local version that's // more precise than the specifier, use the local version instead. - if let [specifier] = specifiers.as_ref() { - if *specifier.operator() == Operator::Equal { - if let Some(expected) = locals.get(&requirement.name) { - return if locals.is_allowed( expected, specifier.version()) { - debug!( - "using local version {expected} for {name} instead of {specifier}", - expected = expected, - name = requirement.name, - specifier = specifier.version(), - ); - Ok(( - PubGrubPackage::from_package(requirement.name.clone(), extra, urls), - Range::singleton(expected.clone()), - )) - } else { - Err(ResolveError::ConflictingLocal( - requirement.name.clone(), - expected.to_string(), - specifier.version().to_string(), - )) - } - } - } - } + let version = if let Some(expected) = locals.get(&requirement.name) { + specifiers + .iter() + .map(|specifier| Locals::map(expected, specifier)) + .map(|specifier| PubGrubSpecifier::try_from(&specifier)) + .fold_ok(Range::full(), |range, specifier| { + range.intersection(&specifier.into()) + })? + } else { + specifiers + .iter() + .map(PubGrubSpecifier::try_from) + .fold_ok(Range::full(), |range, specifier| { + range.intersection(&specifier.into()) + })? + }; - let version = specifiers - .iter() - .map(PubGrubSpecifier::try_from) - .fold_ok(Range::full(), |range, specifier| { - range.intersection(&specifier.into()) - })?; Ok(( PubGrubPackage::from_package(requirement.name.clone(), extra, urls), version, diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs index 4bd363a05a81..41a50806656a 100644 --- a/crates/uv-resolver/src/resolver/locals.rs +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -1,27 +1,24 @@ -use std::ops::Deref; - use rustc_hash::FxHashMap; -use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, VerbatimUrl, VersionOrUrl}; +use pep440_rs::{Operator, Version, VersionSpecifier}; +use pep508_rs::{MarkerEnvironment, VersionOrUrl}; use uv_normalize::PackageName; -use crate::{Manifest, ResolveError}; +use crate::Manifest; #[derive(Debug, Default)] pub(crate) struct Locals { - /// A map of package names to their associated, required URLs. + /// A map of package names to their associated, required local versions. required: FxHashMap, } impl Locals { - pub(crate) fn from_manifest( - manifest: &Manifest, - markers: &MarkerEnvironment, - ) -> Result { + /// Determine the set of permitted local versions in the [`Manifest`]. + pub(crate) fn from_manifest(manifest: &Manifest, markers: &MarkerEnvironment) -> Self { let mut required: FxHashMap = FxHashMap::default(); - // Add all direct requirements and constraints. If there are any conflicts, return an error. + // Add all direct requirements and constraints. There's no need to look for conflicts, + // since conflicting versions will be tracked upstream. for requirement in manifest .requirements .iter() @@ -50,7 +47,7 @@ impl Locals { } } - Ok(Self { required }) + Self { required } } /// Return the [`VerbatimUrl`] associated with the given package name, if any. @@ -58,29 +55,104 @@ impl Locals { self.required.get(package) } - /// Returns `true` if a package is allowed to have a local version. - pub(crate) fn is_allowed(&self, expected: &Version, provided: &Version) -> bool { - // The requirements should be the same, ignoring local segments. - if expected.clone().without_local() != provided.clone().without_local() { - return false; - } - // If the provided version has a local segment, it should be the same as the expected - // version. - if provided.local().is_empty() { - true - } else { - expected.local() == provided.local() + /// Given a specifier that may include the version _without_ a local segment, return a specifier + /// that includes the local segment from the expected version. + pub(crate) fn map(local: &Version, specifier: &VersionSpecifier) -> VersionSpecifier { + match specifier.operator() { + Operator::Equal | Operator::EqualStar => { + // Given `foo==1.0.0`, if the local version is `1.0.0+local`, map to + // `foo==1.0.0+local`. This has the intended effect of allowing `1.0.0+local`. + if is_compatible(local, specifier.version()) { + VersionSpecifier::new(Operator::Equal, local.clone()) + } else { + specifier.clone() + } + } + Operator::NotEqual | Operator::NotEqualStar => { + // Given `foo!=1.0.0`, if the local version is `1.0.0+local`, map to + // `foo!=1.0.0+local`. This has the intended effect of disallowing `1.0.0+local`. + // There's no risk of including `foo @ 1.0.0` in the resolution, since we _know_ + // `foo @ 1.0.0+local` is required and would conflict. + if is_compatible(local, specifier.version()) { + VersionSpecifier::new(Operator::NotEqual, local.clone()) + } else { + specifier.clone() + } + } + Operator::LessThanEqual => { + // Given `foo<=1.0.0`, if the local version is `1.0.0+local`, map to + // `foo<=1.0.0+local`. This has the intended effect of allowing `1.0.0+local`. + // There's no risk of including `foo @ 1.0.0.post1` in the resolution, since we + // _know_ `foo @ 1.0.0+local` is required and would conflict. + if is_compatible(local, specifier.version()) { + VersionSpecifier::new(Operator::LessThanEqual, local.clone()) + } else { + specifier.clone() + } + } + Operator::GreaterThan => { + // Given `foo>1.0.0`, if the local version is `1.0.0+local`, map to + // `foo>1.0.0+local`. This has the intended effect of disallowing `1.0.0+local`. + if is_compatible(local, specifier.version()) { + VersionSpecifier::new(Operator::GreaterThan, local.clone()) + } else { + specifier.clone() + } + } + Operator::ExactEqual => { + // Given `foo===1.0.0`, `1.0.0+local` is already disallowed. + specifier.clone() + } + Operator::TildeEqual => { + // Given `foo~=1.0.0`, `foo~=1.0.0+local` is already allowed. + specifier.clone() + } + Operator::LessThan => { + // Given `foo<1.0.0`, `1.0.0+local` is already disallowed. + specifier.clone() + } + Operator::GreaterThanEqual => { + // Given `foo>=1.0.0`, `foo>1.0.0+local` is already allowed. + specifier.clone() + } } } } + +/// Returns `true` if a provided version is compatible with the expected local version. +/// +/// The versions are compatible if they are the same including their local segment, or the +/// same except for the local segment, which is empty in the provided version. +/// +/// For example, if the expected version is `1.0.0+local` and the provided version is `1.0.0+other`, +/// this function will return `false`. +/// +/// If the expected version is `1.0.0+local` and the provided version is `1.0.0`, the function will +/// return `true`. +fn is_compatible(expected: &Version, provided: &Version) -> bool { + // The requirements should be the same, ignoring local segments. + if expected.clone().without_local() != provided.clone().without_local() { + return false; + } + + // If the provided version has a local segment, it should be the same as the expected + // version. + if provided.local().is_empty() { + true + } else { + expected.local() == provided.local() + } +} + +/// If a [`VersionOrUrl`] is an exact version with a local segment, return the local version. fn to_local(version_or_url: Option<&VersionOrUrl>) -> Option<&Version> { let Some(VersionOrUrl::VersionSpecifier(specifier)) = version_or_url else { return None; }; - let [specifier] = specifier.deref() else { + let [specifier] = &**specifier else { return None; }; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 31ec19d03ca6..91fd099901f1 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -166,7 +166,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { selector: CandidateSelector::for_resolution(options, &manifest, markers), dependency_mode: options.dependency_mode, urls: Urls::from_manifest(&manifest, markers)?, - locals: Locals::from_manifest(&manifest, markers)?, + locals: Locals::from_manifest(&manifest, markers), project: manifest.project, requirements: manifest.requirements, constraints: Constraints::from_requirements(manifest.constraints), @@ -863,7 +863,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { .await .ok_or(ResolveError::Unregistered)?; - let mut constraints = PubGrubDependencies::from_requirements( + let constraints = PubGrubDependencies::from_requirements( &metadata.requires_dist, &self.constraints, &self.overrides, @@ -872,7 +872,14 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { &self.urls, &self.locals, self.markers, - )?; + ); + + let mut constraints = match constraints { + Ok(constraints) => constraints, + Err(err) => { + return Ok(Dependencies::Unavailable(uncapitalize(err.to_string()))); + } + }; for (package, version) in constraints.iter() { debug!("Adding transitive dependency: {package}{version}"); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 7a5b6623478e..76aad8d658bd 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1501,11 +1501,22 @@ fn disallowed_transitive_url_dependency() -> Result<()> { .arg("requirements.in") .env("HATCHLING", hatchling_path.as_os_str()), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Package `iniconfig` attempted to resolve via URL: git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4. URL dependencies must be expressed as direct requirements or constraints. Consider adding `iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4` to your dependencies or constraints file. + × No solution found when resolving dependencies: + ╰─▶ Because only hatchling-editable==0.1.0 is available and + hatchling-editable==0.1.0 is unusable because its dependencies are + unusable because package `iniconfig` attempted to resolve via URL: + git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4. + URL dependencies must be expressed as direct + requirements or constraints. Consider adding `iniconfig @ + git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4` + to your dependencies or constraints file., we can conclude that all + versions of hatchling-editable cannot be used. + And because you require hatchling-editable, we can conclude that the + requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index b2fe1fbd5b17..b226a1d9ffa0 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -76,18 +76,8 @@ fn incompatible_python_compatible_override() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @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 - albatross==1.0.0 - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. - Resolved 1 package in [TIME] - "### + , @r###" + "### ); output.assert().success().stdout(predicate::str::contains( @@ -126,17 +116,8 @@ fn compatible_python_incompatible_override() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.9") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - warning: The requested Python version 3.9 is not available; 3.11.7 will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.9) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. - "### + , @r###" + "### ); output.assert().failure(); @@ -184,17 +165,8 @@ 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 - ----- stdout ----- - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. - "### + , @r###" + "### ); output.assert().failure(); @@ -243,17 +215,8 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { // used to build the source distributions. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @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 - albatross==1.0.0 - - ----- stderr ----- - Resolved 1 package in [TIME] - "### + , @r###" + "### ); output.assert().success().stdout(predicate::str::contains( @@ -303,17 +266,8 @@ 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 - ----- stdout ----- - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. - "### + , @r###" + "### ); output.assert().failure(); @@ -365,24 +319,8 @@ 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 - ----- stdout ----- - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. - And because only the following versions of albatross are available: - albatross==1.0.0 - albatross==2.0.0 - we can conclude that albatross<2.0.0 cannot be used. (1) - - Because the requested Python version (3.11) does not satisfy Python>=3.12 and albatross==2.0.0 depends on Python>=3.12, we can conclude that albatross==2.0.0 cannot be used. - And because we know from (1) that albatross<2.0.0 cannot be used, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. - "### + , @r###" + "### ); output.assert().failure(); @@ -421,16 +359,8 @@ fn python_patch_override_no_patch() -> Result<()> { // requirement is treated as 3.8.0. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.8) does not satisfy Python>=3.8.4 and albatross==1.0.0 depends on Python>=3.8.4, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. - "### + , @r###" + "### ); output.assert().failure(); @@ -467,18 +397,8 @@ fn python_patch_override_patch_compatible() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8.0") - , @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.8.0 - albatross==1.0.0 - - ----- stderr ----- - warning: The requested Python version 3.8.0 is not available; 3.8.18 will be used to build dependencies instead. - Resolved 1 package in [TIME] - "### + , @r###" + "### ); output.assert().success().stdout(predicate::str::contains( diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 874d561b4951..8546a27995e9 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -46,7 +46,7 @@ fn command(context: &TestContext) -> Command { .arg("pip") .arg("install") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.7/simple-html/") + .arg("http://localhost:3141") .arg("--find-links") .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.7/vendor/links.html") .arg("--cache-dir") @@ -84,14 +84,7 @@ fn requires_package_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-package-does-not-exist-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because pkg-a was not found in the package registry and you require pkg-a, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -124,14 +117,7 @@ fn requires_exact_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-exact-version-does-not-exist-a==2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because there is no version of albatross==2.0.0 and you require albatross==2.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -166,14 +152,7 @@ fn requires_greater_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-greater-version-does-not-exist-a>1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross<=1.0.0 is available and you require albatross>1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -209,14 +188,7 @@ fn requires_less_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-less-version-does-not-exist-a<2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross>=2.0.0 is available and you require albatross<2.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -251,15 +223,7 @@ fn transitive_requires_package_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("transitive-requires-package-does-not-exist-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because pkg-b was not found in the package registry and albatross==1.0.0 depends on pkg-b, we can conclude that albatross==1.0.0 cannot be used. - And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -293,17 +257,7 @@ fn excluded_only_version() { uv_snapshot!(filters, command(&context) .arg("excluded-only-version-a!=1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and you require one of: - albatross<1.0.0 - albatross>1.0.0 - we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Only `a==1.0.0` is available but the user excluded it. @@ -351,29 +305,7 @@ fn excluded_only_compatible_version() { uv_snapshot!(filters, command(&context) .arg("excluded-only-compatible-version-a!=2.0.0") .arg("excluded-only-compatible-version-b<3.0.0,>=2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of albatross are available: - albatross==1.0.0 - albatross==2.0.0 - albatross==3.0.0 - and albatross==1.0.0 depends on bluebird==1.0.0, we can conclude that albatross<2.0.0 depends on bluebird==1.0.0. - And because albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that any of: - albatross<2.0.0 - albatross>2.0.0 - depends on one of: - bluebird==1.0.0 - bluebird==3.0.0 - - And because you require one of: - albatross<2.0.0 - albatross>2.0.0 - and you require bluebird>=2.0.0,<3.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require @@ -466,32 +398,7 @@ fn dependency_excludes_range_of_compatible_versions() { .arg("dependency-excludes-range-of-compatible-versions-a") .arg("dependency-excludes-range-of-compatible-versions-b<3.0.0,>=2.0.0") .arg("dependency-excludes-range-of-compatible-versions-c") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of albatross are available: - albatross==1.0.0 - albatross>=2.0.0,<=3.0.0 - and albatross==1.0.0 depends on bluebird==1.0.0, we can conclude that albatross<2.0.0 depends on bluebird==1.0.0. (1) - - Because only the following versions of crow are available: - crow==1.0.0 - crow==2.0.0 - and crow==1.0.0 depends on albatross<2.0.0, we can conclude that crow<2.0.0 depends on albatross<2.0.0. - And because crow==2.0.0 depends on albatross>=3.0.0, we can conclude that all versions of crow depend on one of: - albatross<2.0.0 - albatross>=3.0.0 - - And because we know from (1) that albatross<2.0.0 depends on bluebird==1.0.0, we can conclude that albatross!=3.0.0, all versions of crow, bluebird!=1.0.0 are incompatible. - And because albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that all versions of crow depend on one of: - bluebird<=1.0.0 - bluebird>=3.0.0 - - And because you require bluebird>=2.0.0,<3.0.0 and you require crow, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` @@ -603,32 +510,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-a") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-b<3.0.0,>=2.0.0") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-c") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of albatross are available: - albatross==1.0.0 - albatross>=2.0.0,<=3.0.0 - and albatross==1.0.0 depends on bluebird==1.0.0, we can conclude that albatross<2.0.0 depends on bluebird==1.0.0. (1) - - Because only the following versions of crow are available: - crow==1.0.0 - crow==2.0.0 - and crow==1.0.0 depends on albatross<2.0.0, we can conclude that crow<2.0.0 depends on albatross<2.0.0. - And because crow==2.0.0 depends on albatross>=3.0.0, we can conclude that all versions of crow depend on one of: - albatross<2.0.0 - albatross>=3.0.0 - - And because we know from (1) that albatross<2.0.0 depends on bluebird==1.0.0, we can conclude that all versions of crow, albatross!=3.0.0, bluebird!=1.0.0 are incompatible. - And because albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that all versions of crow depend on one of: - bluebird<=1.0.0 - bluebird>=3.0.0 - - And because you require bluebird>=2.0.0,<3.0.0 and you require crow, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` @@ -681,17 +563,7 @@ fn extra_required() { uv_snapshot!(filters, command(&context) .arg("extra-required-a[extra]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==1.0.0 - + bluebird==1.0.0 + , @r###" "###); assert_installed( @@ -732,16 +604,7 @@ fn missing_extra() { uv_snapshot!(filters, command(&context) .arg("missing-extra-a[extra]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); // Missing extras are ignored during resolution. @@ -785,18 +648,7 @@ fn multiple_extras_required() { uv_snapshot!(filters, command(&context) .arg("multiple-extras-required-a[extra_b,extra_c]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Downloaded 3 packages in [TIME] - Installed 3 packages in [TIME] - + albatross==1.0.0 - + bluebird==1.0.0 - + crow==1.0.0 + , @r###" "###); assert_installed( @@ -868,18 +720,7 @@ fn all_extras_required() { uv_snapshot!(filters, command(&context) .arg("all-extras-required-a[all]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Downloaded 3 packages in [TIME] - Installed 3 packages in [TIME] - + albatross==1.0.0 - + bluebird==1.0.0 - + crow==1.0.0 + , @r###" "###); assert_installed( @@ -938,16 +779,7 @@ fn extra_incompatible_with_extra() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-extra-a[extra_b,extra_c]") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross[extra-c]==1.0.0 is available and albatross[extra-c]==1.0.0 depends on bluebird==2.0.0, we can conclude that all versions of albatross[extra-c] depend on bluebird==2.0.0. - And because albatross[extra-b]==1.0.0 depends on bluebird==1.0.0 and only albatross[extra-b]==1.0.0 is available, we can conclude that all versions of albatross[extra-c] and all versions of albatross[extra-b] are incompatible. - And because you require albatross[extra-b] and you require albatross[extra-c], we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Because both `extra_b` and `extra_c` are requested and they require incompatible @@ -997,17 +829,7 @@ fn extra_incompatible_with_extra_not_requested() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-extra-not-requested-a[extra_c]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==1.0.0 - + bluebird==2.0.0 + , @r###" "###); // Because the user does not request both extras, it is okay that one is @@ -1061,15 +883,7 @@ fn extra_incompatible_with_root() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-root-a[extra]") .arg("extra-incompatible-with-root-b==2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross[extra]==1.0.0 is available and albatross[extra]==1.0.0 depends on bluebird==1.0.0, we can conclude that all versions of albatross[extra] depend on bluebird==1.0.0. - And because you require albatross[extra] and you require bluebird==2.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Because the user requested `b==2.0.0` but the requested extra requires @@ -1121,16 +935,7 @@ fn extra_does_not_exist_backtrack() { uv_snapshot!(filters, command(&context) .arg("extra-does-not-exist-backtrack-a[extra]") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==3.0.0 + , @r###" "###); // The resolver should not backtrack to `a==1.0.0` because missing extras are @@ -1170,14 +975,7 @@ fn direct_incompatible_versions() { uv_snapshot!(filters, command(&context) .arg("direct-incompatible-versions-a==1.0.0") .arg("direct-incompatible-versions-a==2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because you require albatross==1.0.0 and you require albatross==2.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -1225,15 +1023,7 @@ fn transitive_incompatible_with_root_version() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-with-root-version-a") .arg("transitive-incompatible-with-root-version-b==1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on bluebird==2.0.0, we can conclude that all versions of albatross depend on bluebird==2.0.0. - And because you require albatross and you require bluebird==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -1286,16 +1076,7 @@ fn transitive_incompatible_with_transitive() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-with-transitive-a") .arg("transitive-incompatible-with-transitive-b") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on crow==1.0.0, we can conclude that all versions of albatross depend on crow==1.0.0. - And because bluebird==1.0.0 depends on crow==2.0.0 and only bluebird==1.0.0 is available, we can conclude that all versions of albatross and all versions of bluebird are incompatible. - And because you require albatross and you require bluebird, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -1333,7 +1114,7 @@ fn local_simple() { filters.push((r"local-simple-", "pkg-")); uv_snapshot!(filters, command(&context) - .arg("local-simple-a==1.2.3+foo") + .arg("local-simple-a==1.2.3") , @r###" success: false exit_code: 1 @@ -1344,7 +1125,7 @@ fn local_simple() { ╰─▶ Because there is no version of albatross==1.2.3 and you require albatross==1.2.3, we can conclude that the requirements are unsatisfiable. "###); - // The verison '1.2.3+foo' satisfies the constraint '==1.2.3'. + // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. assert_not_installed(&context.venv, "local_simple_a", &context.temp_dir); } @@ -1386,10 +1167,11 @@ fn local_not_used_with_sdist() { + albatross==1.2.3 "###); - // The verison '1.2.3' with an sdist satisfies the constraint '==1.2.3'. - assert_not_installed( + // The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'. + assert_installed( &context.venv, "local_not_used_with_sdist_a", + "1.2.3", &context.temp_dir, ); } @@ -1431,7 +1213,7 @@ fn local_used_without_sdist() { ╰─▶ Because albatross==1.2.3 is unusable because no wheels are available with a matching Python ABI and you require albatross==1.2.3, we can conclude that the requirements are unsatisfiable. "###); - // The verison '1.2.3+foo' satisfies the constraint '==1.2.3'. + // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. assert_not_installed( &context.venv, "local_used_without_sdist_a", @@ -1518,7 +1300,7 @@ fn local_transitive() { uv_snapshot!(filters, command(&context) .arg("local-transitive-a") - .arg("local-transitive-b==2.0.0+foo") + .arg("local-transitive-b==2.0.0+foo") , @r###" success: true exit_code: 0 @@ -1532,134 +1314,333 @@ fn local_transitive() { + bluebird==2.0.0+foo "###); - // The verison '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. - assert_not_installed(&context.venv, "local_transitive_a", &context.temp_dir); - assert_not_installed(&context.venv, "local_transitive_b", &context.temp_dir); + // The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. + assert_installed( + &context.venv, + "local_transitive_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_b", + "2.0.0+foo", + &context.temp_dir, + ); } -/// A transitive dependency has both a non-local and local version published, but -/// the non-local version is unusable. +/// A transitive constraint on a local version should not match an exclusive ordered +/// operator. /// /// ```text -/// local-transitive-confounding +/// local-transitive-greater-than /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a -/// │ └── satisfied by a-1.0.0 +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo /// ├── a /// │ └── a-1.0.0 -/// │ └── requires b==2.0.0 -/// │ ├── satisfied by b-2.0.0 -/// │ └── satisfied by b-2.0.0+foo +/// │ └── requires b>2.0.0 +/// │ └── unsatisfied: no matching version /// └── b -/// ├── b-2.0.0 /// └── b-2.0.0+foo /// ``` #[test] -fn local_transitive_confounding() { +fn local_transitive_greater_than() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for more realistic messages let mut filters = INSTA_FILTERS.to_vec(); - filters.push((r"local-transitive-confounding-a", "albatross")); - filters.push((r"local-transitive-confounding-b", "bluebird")); - filters.push((r"local-transitive-confounding-", "pkg-")); + filters.push((r"local-transitive-greater-than-a", "albatross")); + filters.push((r"local-transitive-greater-than-b", "bluebird")); + filters.push((r"local-transitive-greater-than-", "pkg-")); uv_snapshot!(filters, command(&context) - .arg("local-transitive-confounding-a") + .arg("local-transitive-greater-than-a") + .arg("local-transitive-greater-than-b==2.0.0+foo") , @r###" - success: true - exit_code: 0 + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==1.0.0 - + bluebird==2.0.0 + × No solution found when resolving dependencies: + ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on bluebird>2.0.0+foo, we can conclude that all versions of albatross depend on bluebird>2.0.0+foo. + And because you require albatross and you require bluebird==2.0.0+foo, we can conclude that the requirements are unsatisfiable. "###); - // The verison '1.2.3+foo' satisfies the constraint '==1.2.3'. assert_not_installed( &context.venv, - "local_transitive_confounding_a", + "local_transitive_greater_than_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "local_transitive_greater_than_b", &context.temp_dir, ); } -/// The user requires any version of package `a` which only has prerelease versions -/// available. +/// A transitive constraint on a local version should match an inclusive ordered +/// operator. /// /// ```text -/// package-only-prereleases +/// local-transitive-greater-than-or-equal /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.0.0a1 +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b>=2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// └── b-2.0.0+foo /// ``` #[test] -fn package_only_prereleases() { +fn local_transitive_greater_than_or_equal() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for more realistic messages let mut filters = INSTA_FILTERS.to_vec(); - filters.push((r"package-only-prereleases-a", "albatross")); - filters.push((r"package-only-prereleases-", "pkg-")); + filters.push((r"local-transitive-greater-than-or-equal-a", "albatross")); + filters.push((r"local-transitive-greater-than-or-equal-b", "bluebird")); + filters.push((r"local-transitive-greater-than-or-equal-", "pkg-")); uv_snapshot!(filters, command(&context) - .arg("package-only-prereleases-a") + .arg("local-transitive-greater-than-or-equal-a") + .arg("local-transitive-greater-than-or-equal-b==2.0.0+foo") , @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0a1 + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + albatross==1.0.0 + + bluebird==2.0.0+foo "###); - // Since there are only prerelease versions of `a` available, it should be - // installed even though the user did not include a prerelease specifier. + // The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo. assert_installed( &context.venv, - "package_only_prereleases_a", - "1.0.0a1", + "local_transitive_greater_than_or_equal_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_greater_than_or_equal_b", + "2.0.0+foo", &context.temp_dir, ); } -/// The user requires a version of package `a` which only matches prerelease -/// versions but they did not include a prerelease specifier. +/// A transitive constraint on a local version should not match an exclusive ordered +/// operator. /// /// ```text -/// package-only-prereleases-in-range +/// local-transitive-less-than /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a>0.1.0 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-0.1.0 -/// └── a-1.0.0a1 +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b<2.0.0 +/// │ └── unsatisfied: no matching version +/// └── b +/// └── b-2.0.0+foo /// ``` #[test] -fn package_only_prereleases_in_range() { +fn local_transitive_less_than() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for more realistic messages let mut filters = INSTA_FILTERS.to_vec(); - filters.push((r"package-only-prereleases-in-range-a", "albatross")); - filters.push((r"package-only-prereleases-in-range-", "pkg-")); + filters.push((r"local-transitive-less-than-a", "albatross")); + filters.push((r"local-transitive-less-than-b", "bluebird")); + filters.push((r"local-transitive-less-than-", "pkg-")); uv_snapshot!(filters, command(&context) - .arg("package-only-prereleases-in-range-a>0.1.0") + .arg("local-transitive-less-than-a") + .arg("local-transitive-less-than-b==2.0.0+foo") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on bluebird<2.0.0, we can conclude that all versions of albatross depend on bluebird<2.0.0. + And because you require albatross and you require bluebird==2.0.0+foo, we can conclude that the requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "local_transitive_less_than_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "local_transitive_less_than_b", + &context.temp_dir, + ); +} + +/// A transitive constraint on a local version should match an inclusive ordered +/// operator. +/// +/// ```text +/// local-transitive-less-than-or-equal +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b<=2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// └── b-2.0.0+foo +/// ``` +#[test] +fn local_transitive_less_than_or_equal() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-transitive-less-than-or-equal-a", "albatross")); + filters.push((r"local-transitive-less-than-or-equal-b", "bluebird")); + filters.push((r"local-transitive-less-than-or-equal-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-less-than-or-equal-a") + .arg("local-transitive-less-than-or-equal-b==2.0.0+foo") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + albatross==1.0.0 + + bluebird==2.0.0+foo + "###); + + // The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo. + assert_installed( + &context.venv, + "local_transitive_less_than_or_equal_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_less_than_or_equal_b", + "2.0.0+foo", + &context.temp_dir, + ); +} + +/// A transitive dependency has both a non-local and local version published, but +/// the non-local version is unusable. +/// +/// ```text +/// local-transitive-confounding +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b==2.0.0 +/// │ ├── satisfied by b-2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// ├── b-2.0.0 +/// └── b-2.0.0+foo +/// ``` +#[test] +fn local_transitive_confounding() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-transitive-confounding-a", "albatross")); + filters.push((r"local-transitive-confounding-b", "bluebird")); + filters.push((r"local-transitive-confounding-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-confounding-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because bluebird==2.0.0 is unusable because no wheels are available with a matching Python ABI and albatross==1.0.0 depends on bluebird==2.0.0, we can conclude that albatross==1.0.0 cannot be used. + And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. + "###); + + // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. + assert_not_installed( + &context.venv, + "local_transitive_confounding_a", + &context.temp_dir, + ); +} + +/// A dependency depends on a conflicting local version of a direct dependency. +/// +/// ```text +/// local-transitive-conflicting +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b==2.0.0+bar +/// │ └── satisfied by b-2.0.0+bar +/// └── b +/// ├── b-2.0.0+foo +/// └── b-2.0.0+bar +/// ``` +#[test] +fn local_transitive_conflicting() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-transitive-conflicting-a", "albatross")); + filters.push((r"local-transitive-conflicting-b", "bluebird")); + filters.push((r"local-transitive-conflicting-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-conflicting-a") + .arg("local-transitive-conflicting-b==2.0.0+foo") , @r###" success: false exit_code: 1 @@ -1667,9 +1648,315 @@ fn package_only_prereleases_in_range() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only albatross<=0.1.0 is available and you require albatross>0.1.0, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on bluebird==2.0.0+bar, we can conclude that all versions of albatross depend on bluebird==2.0.0+bar. + And because you require albatross and you require bluebird==2.0.0+foo, we can conclude that the requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "local_transitive_conflicting_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "local_transitive_conflicting_b", + &context.temp_dir, + ); +} + +/// A dependency depends on a conflicting local version of a direct dependency, but +/// we can backtrack to a compatible version. +/// +/// ```text +/// local-transitive-backtrack +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ ├── satisfied by a-1.0.0 +/// │ │ └── satisfied by a-2.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ ├── a-1.0.0 +/// │ │ └── requires b==2.0.0 +/// │ │ ├── satisfied by b-2.0.0+foo +/// │ │ └── satisfied by b-2.0.0+bar +/// │ └── a-2.0.0 +/// │ └── requires b==2.0.0+bar +/// │ └── satisfied by b-2.0.0+bar +/// └── b +/// ├── b-2.0.0+foo +/// └── b-2.0.0+bar +/// ``` +#[test] +fn local_transitive_backtrack() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-transitive-backtrack-a", "albatross")); + filters.push((r"local-transitive-backtrack-b", "bluebird")); + filters.push((r"local-transitive-backtrack-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-backtrack-a") + .arg("local-transitive-backtrack-b==2.0.0+foo") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + albatross==1.0.0 + + bluebird==2.0.0+foo + "###); + + // Backtracking to '1.0.0' gives us compatible local versions of b. + assert_installed( + &context.venv, + "local_transitive_backtrack_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_backtrack_b", + "2.0.0+foo", + &context.temp_dir, + ); +} + +/// A local version should be excluded in exclusive ordered comparisons. +/// +/// ```text +/// local-greater-than +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>1.2.3 +/// │ └── unsatisfied: no matching version +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_greater_than() { + let context = TestContext::new("3.8"); - hint: Pre-releases are available for albatross in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-greater-than-a", "albatross")); + filters.push((r"local-greater-than-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-greater-than-a>1.2.3") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + albatross==1.2.3+foo + "###); + + assert_installed( + &context.venv, + "local_greater_than_a", + "1.2.3+foo", + &context.temp_dir, + ); +} + +/// A local version should be included in inclusive ordered comparisons. +/// +/// ```text +/// local-greater-than-or-equal +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>=1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_greater_than_or_equal() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-greater-than-or-equal-a", "albatross")); + filters.push((r"local-greater-than-or-equal-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-greater-than-or-equal-a>=1.2.3") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + albatross==1.2.3+foo + "###); + + // The version '1.2.3+foo' satisfies the constraint '>=1.2.3'. + assert_installed( + &context.venv, + "local_greater_than_or_equal_a", + "1.2.3+foo", + &context.temp_dir, + ); +} + +/// A local version should be excluded in exclusive ordered comparisons. +/// +/// ```text +/// local-less-than +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<1.2.3 +/// │ └── unsatisfied: no matching version +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_less_than() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-less-than-a", "albatross")); + filters.push((r"local-less-than-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-less-than-a<1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only albatross>=1.2.3 is available and you require albatross<1.2.3, we can conclude that the requirements are unsatisfiable. + "###); + + assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir); +} + +/// A local version should be included in inclusive ordered comparisons. +/// +/// ```text +/// local-less-than-or-equal +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<=1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_less_than_or_equal() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"local-less-than-or-equal-a", "albatross")); + filters.push((r"local-less-than-or-equal-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("local-less-than-or-equal-a<=1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only albatross>1.2.3 is available and you require albatross<=1.2.3, we can conclude that the requirements are unsatisfiable. + "###); + + // The version '1.2.3+foo' satisfies the constraint '<=1.2.3'. + assert_not_installed( + &context.venv, + "local_less_than_or_equal_a", + &context.temp_dir, + ); +} + +/// The user requires any version of package `a` which only has prerelease versions +/// available. +/// +/// ```text +/// package-only-prereleases +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── unsatisfied: no matching version +/// └── a +/// └── a-1.0.0a1 +/// ``` +#[test] +fn package_only_prereleases() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"package-only-prereleases-a", "albatross")); + filters.push((r"package-only-prereleases-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("package-only-prereleases-a") + , @r###" + "###); + + // Since there are only prerelease versions of `a` available, it should be + // installed even though the user did not include a prerelease specifier. + assert_installed( + &context.venv, + "package_only_prereleases_a", + "1.0.0a1", + &context.temp_dir, + ); +} + +/// The user requires a version of package `a` which only matches prerelease +/// versions but they did not include a prerelease specifier. +/// +/// ```text +/// package-only-prereleases-in-range +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>0.1.0 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-0.1.0 +/// └── a-1.0.0a1 +/// ``` +#[test] +fn package_only_prereleases_in_range() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for more realistic messages + let mut filters = INSTA_FILTERS.to_vec(); + filters.push((r"package-only-prereleases-in-range-a", "albatross")); + filters.push((r"package-only-prereleases-in-range-", "pkg-")); + + uv_snapshot!(filters, command(&context) + .arg("package-only-prereleases-in-range-a>0.1.0") + , @r###" "###); // Since there are stable versions of `a` available, prerelease versions should not @@ -1714,16 +2001,7 @@ fn requires_package_only_prereleases_in_range_global_opt_in() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("requires-package-only-prereleases-in-range-global-opt-in-a>0.1.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0a1 + , @r###" "###); assert_installed( @@ -1759,16 +2037,7 @@ fn requires_package_prerelease_and_final_any() { uv_snapshot!(filters, command(&context) .arg("requires-package-prerelease-and-final-any-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.1.0 + , @r###" "###); // Since the user did not provide a prerelease specifier, the older stable version @@ -1815,16 +2084,7 @@ fn package_prerelease_specified_only_final_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-only-final-available-a>=0.1.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.3.0 + , @r###" "###); // The latest stable version should be selected. @@ -1870,16 +2130,7 @@ fn package_prerelease_specified_only_prerelease_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-only-prerelease-available-a>=0.1.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.3.0a1 + , @r###" "###); // The latest prerelease version should be selected. @@ -1924,16 +2175,7 @@ fn package_prerelease_specified_mixed_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-mixed-available-a>=0.1.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0a1 + , @r###" "###); // Since the user provided a prerelease specifier, the latest prerelease version @@ -1974,16 +2216,7 @@ fn package_multiple_prereleases_kinds() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-kinds-a>=1.0.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0rc1 + , @r###" "###); // Release candidates should be the highest precedence prerelease kind. @@ -2022,16 +2255,7 @@ fn package_multiple_prereleases_numbers() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-numbers-a>=1.0.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0a3 + , @r###" "###); // The latest alpha version should be selected. @@ -2072,17 +2296,7 @@ fn transitive_package_only_prereleases() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==0.1.0 - + bluebird==1.0.0a1 + , @r###" "###); // Since there are only prerelease versions of `b` available, it should be selected @@ -2137,17 +2351,7 @@ fn transitive_package_only_prereleases_in_range() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-in-range-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only bluebird<=0.1 is available and albatross==0.1.0 depends on bluebird>0.1, we can conclude that albatross==0.1.0 cannot be used. - And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. - - hint: Pre-releases are available for bluebird in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) + , @r###" "###); // Since there are stable versions of `b` available, the prerelease version should @@ -2203,17 +2407,7 @@ fn transitive_package_only_prereleases_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-in-range-opt-in-a") .arg("transitive-package-only-prereleases-in-range-opt-in-b>0.0.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==0.1.0 - + bluebird==1.0.0a1 + , @r###" "###); // Since the user included a dependency on `b` with a prerelease specifier, a @@ -2273,17 +2467,7 @@ fn transitive_prerelease_and_stable_dependency() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-a") .arg("transitive-prerelease-and-stable-dependency-b") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because there is no version of crow==2.0.0b1 and albatross==1.0.0 depends on crow==2.0.0b1, we can conclude that albatross==1.0.0 cannot be used. - And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. - - hint: crow was requested with a pre-release marker (e.g., crow==2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) + , @r###" "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. @@ -2354,18 +2538,7 @@ fn transitive_prerelease_and_stable_dependency_opt_in() { .arg("transitive-prerelease-and-stable-dependency-opt-in-a") .arg("transitive-prerelease-and-stable-dependency-opt-in-b") .arg("transitive-prerelease-and-stable-dependency-opt-in-c>=0.0.0a1") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Downloaded 3 packages in [TIME] - Installed 3 packages in [TIME] - + albatross==1.0.0 - + bluebird==1.0.0 - + crow==2.0.0b1 + , @r###" "###); // Since the user explicitly opted-in to a prerelease for `c`, it can be installed. @@ -2464,19 +2637,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-many-versions-a") .arg("transitive-prerelease-and-stable-dependency-many-versions-b") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on crow>=2.0.0b1, we can conclude that all versions of albatross depend on crow>=2.0.0b1. - And because only crow<2.0.0b1 is available, we can conclude that all versions of albatross depend on crow>3.0.0. - And because bluebird==1.0.0 depends on crow and only bluebird==1.0.0 is available, we can conclude that all versions of bluebird and all versions of albatross are incompatible. - And because you require albatross and you require bluebird, we can conclude that the requirements are unsatisfiable. - - hint: crow was requested with a pre-release marker (e.g., crow>=2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) + , @r###" "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. @@ -2557,33 +2718,10 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() { "pkg-", )); - uv_snapshot!(filters, command(&context) - .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-a") - .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-b") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of crow are available: - crow<=1.0.0 - crow>=2.0.0a5,<=2.0.0a7 - crow==2.0.0b1 - crow>=2.0.0b5 - and albatross==1.0.0 depends on one of: - crow>1.0.0,<2.0.0a5 - crow>2.0.0a7,<2.0.0b1 - crow>2.0.0b1,<2.0.0b5 - we can conclude that albatross==1.0.0 cannot be used. - And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. - - hint: crow was requested with a pre-release marker (e.g., any of: - crow>1.0.0,<2.0.0a5 - crow>2.0.0a7,<2.0.0b1 - crow>2.0.0b1,<2.0.0b5 - ), but pre-releases weren't enabled (try: `--prerelease=allow`) + uv_snapshot!(filters, command(&context) + .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-a") + .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-b") + , @r###" "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. @@ -2625,16 +2763,7 @@ fn package_only_prereleases_boundary() { uv_snapshot!(filters, command(&context) .arg("package-only-prereleases-boundary-a<0.2.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.1.0a1 + , @r###" "###); // Since there are only prerelease versions of `a` available, a prerelease is @@ -2675,16 +2804,7 @@ fn package_prereleases_boundary() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("package-prereleases-boundary-a<0.2.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.1.0 + , @r###" "###); // Since the user did not use a pre-release specifier, pre-releases at the boundary @@ -2724,16 +2844,7 @@ fn package_prereleases_global_boundary() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("package-prereleases-global-boundary-a<0.2.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.1.0 + , @r###" "###); // Since the user did not use a pre-release specifier, pre-releases at the boundary @@ -2775,16 +2886,7 @@ fn package_prereleases_specifier_boundary() { uv_snapshot!(filters, command(&context) .arg("package-prereleases-specifier-boundary-a<0.2.0a2") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.2.0a1 + , @r###" "###); // Since the user used a pre-release specifier, pre-releases at the boundary should @@ -2821,15 +2923,7 @@ fn python_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("python-version-does-not-exist-a==1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.8.18) does not satisfy Python>=3.30 and albatross==1.0.0 depends on Python>=3.30, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -2864,15 +2958,7 @@ 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 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python<=3.8 and albatross==1.0.0 depends on Python<=3.8, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -2907,15 +2993,7 @@ fn python_greater_than_current() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-a==1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -2950,15 +3028,7 @@ fn python_greater_than_current_patch() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-patch-a==1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.8.12) does not satisfy Python>=3.8.14 and albatross==1.0.0 depends on Python>=3.8.14, we can conclude that albatross==1.0.0 cannot be used. - And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3015,14 +3085,7 @@ fn python_greater_than_current_many() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-many-a==1.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because there is no version of albatross==1.0.0 and you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3065,16 +3128,7 @@ fn python_greater_than_current_backtrack() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-backtrack-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); assert_installed( @@ -3117,31 +3171,7 @@ fn python_greater_than_current_excluded() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-excluded-a>=2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.18) does not satisfy Python>=3.12, we can conclude that any of: - Python>=3.10,<3.11 - Python>=3.12 - are incompatible. - And because the current Python version (3.9.18) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 are incompatible. - And because albatross==2.0.0 depends on Python>=3.10 and only the following versions of albatross are available: - albatross<=2.0.0 - albatross==3.0.0 - albatross==4.0.0 - we can conclude that albatross>=2.0.0,<3.0.0 cannot be used. (1) - - Because the current Python version (3.9.18) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.18) does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible. - And because albatross==3.0.0 depends on Python>=3.11, we can conclude that albatross==3.0.0 cannot be used. - And because we know from (1) that albatross>=2.0.0,<3.0.0 cannot be used, we can conclude that albatross>=2.0.0,<4.0.0 cannot be used. (2) - - Because the current Python version (3.9.18) does not satisfy Python>=3.12 and albatross==4.0.0 depends on Python>=3.12, we can conclude that albatross==4.0.0 cannot be used. - And because we know from (2) that albatross>=2.0.0,<4.0.0 cannot be used, we can conclude that albatross>=2.0.0 cannot be used. - And because you require albatross>=2.0.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3174,16 +3204,7 @@ fn specific_tag_and_default() { uv_snapshot!(filters, command(&context) .arg("specific-tag-and-default-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); } @@ -3210,16 +3231,7 @@ fn only_wheels() { uv_snapshot!(filters, command(&context) .arg("only-wheels-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); } @@ -3246,16 +3258,7 @@ fn no_wheels() { uv_snapshot!(filters, command(&context) .arg("no-wheels-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); } @@ -3282,16 +3285,7 @@ fn no_wheels_with_matching_platform() { uv_snapshot!(filters, command(&context) .arg("no-wheels-with-matching-platform-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); } @@ -3319,15 +3313,7 @@ fn no_sdist_no_wheels_with_matching_platform() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-platform-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are available with a matching platform, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3361,15 +3347,7 @@ fn no_sdist_no_wheels_with_matching_python() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-python-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are available with a matching Python implementation, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3403,15 +3381,7 @@ fn no_sdist_no_wheels_with_matching_abi() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-abi-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are available with a matching Python ABI, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed( @@ -3447,15 +3417,7 @@ fn no_wheels_no_build() { .arg("--only-binary") .arg("no-wheels-no-build-a") .arg("no-wheels-no-build-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no wheels are usable and building from source is disabled, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir); @@ -3487,15 +3449,7 @@ fn only_wheels_no_binary() { .arg("--no-binary") .arg("only-wheels-no-binary-a") .arg("only-wheels-no-binary-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because no source distribution is available and using wheels is disabled, we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); assert_not_installed(&context.venv, "only_wheels_no_binary_a", &context.temp_dir); @@ -3527,16 +3481,7 @@ fn no_build() { .arg("--only-binary") .arg("no-build-a") .arg("no-build-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); // The wheel should be used for install @@ -3568,16 +3513,7 @@ fn no_binary() { .arg("--no-binary") .arg("no-binary-a") .arg("no-binary-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==1.0.0 + , @r###" "###); // The source distribution should be used for install @@ -3607,15 +3543,7 @@ fn package_only_yanked() { uv_snapshot!(filters, command(&context) .arg("package-only-yanked-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 is unusable because it was yanked (reason: Yanked for testing), we can conclude that all versions of albatross cannot be used. - And because you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Yanked versions should not be installed, even if they are the only one @@ -3647,18 +3575,7 @@ fn package_only_yanked_in_range() { uv_snapshot!(filters, command(&context) .arg("package-only-yanked-in-range-a>0.1.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of albatross are available: - albatross<=0.1.0 - albatross==1.0.0 - and albatross==1.0.0 is unusable because it was yanked (reason: Yanked for testing), we can conclude that albatross>0.1.0 cannot be used. - And because you require albatross>0.1.0, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Since there are other versions of `a` available, yanked versions should not be @@ -3695,16 +3612,7 @@ fn requires_package_yanked_and_unyanked_any() { uv_snapshot!(filters, command(&context) .arg("requires-package-yanked-and-unyanked-any-a") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.1.0 + , @r###" "###); // The unyanked version should be selected. @@ -3744,16 +3652,7 @@ fn package_yanked_specified_mixed_available() { uv_snapshot!(filters, command(&context) .arg("package-yanked-specified-mixed-available-a>=0.1.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] - Installed 1 package in [TIME] - + albatross==0.3.0 + , @r###" "###); // The latest unyanked version should be selected. @@ -3794,16 +3693,7 @@ fn transitive_package_only_yanked() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only bluebird==1.0.0 is available and bluebird==1.0.0 is unusable because it was yanked (reason: Yanked for testing), we can conclude that all versions of bluebird cannot be used. - And because albatross==0.1.0 depends on bluebird, we can conclude that albatross==0.1.0 cannot be used. - And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Yanked versions should not be installed, even if they are the only one @@ -3845,19 +3735,7 @@ fn transitive_package_only_yanked_in_range() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-in-range-a") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of bluebird are available: - bluebird<=0.1 - bluebird==1.0.0 - and bluebird==1.0.0 is unusable because it was yanked (reason: Yanked for testing), we can conclude that bluebird>0.1 cannot be used. - And because albatross==0.1.0 depends on bluebird>0.1, we can conclude that albatross==0.1.0 cannot be used. - And because only albatross==0.1.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Yanked versions should not be installed, even if they are the only valid version @@ -3909,18 +3787,7 @@ fn transitive_package_only_yanked_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-in-range-opt-in-a") .arg("transitive-package-only-yanked-in-range-opt-in-b==1.0.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Downloaded 2 packages in [TIME] - Installed 2 packages in [TIME] - + albatross==0.1.0 - + bluebird==1.0.0 - warning: bluebird==1.0.0 is yanked (reason: "Yanked for testing"). + , @r###" "###); // Since the user included a dependency on `b` with an exact specifier, the yanked @@ -3977,15 +3844,7 @@ fn transitive_yanked_and_unyanked_dependency() { uv_snapshot!(filters, command(&context) .arg("transitive-yanked-and-unyanked-dependency-a") .arg("transitive-yanked-and-unyanked-dependency-b") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because crow==2.0.0 is unusable because it was yanked (reason: Yanked for testing) and albatross==1.0.0 depends on crow==2.0.0, we can conclude that albatross==1.0.0 cannot be used. - And because only albatross==1.0.0 is available and you require albatross, we can conclude that the requirements are unsatisfiable. + , @r###" "###); // Since the user did not explicitly select the yanked version, it cannot be used. @@ -4052,19 +3911,7 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { .arg("transitive-yanked-and-unyanked-dependency-opt-in-a") .arg("transitive-yanked-and-unyanked-dependency-opt-in-b") .arg("transitive-yanked-and-unyanked-dependency-opt-in-c==2.0.0") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Downloaded 3 packages in [TIME] - Installed 3 packages in [TIME] - + albatross==1.0.0 - + bluebird==1.0.0 - + crow==2.0.0 - warning: crow==2.0.0 is yanked (reason: "Yanked for testing"). + , @r###" "###); // Since the user explicitly selected the yanked version of `c`, it can be diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 78aec59e6005..9df2260c9193 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2951,7 +2951,8 @@ fn compile_invalid_pyc_invalidation_mode() -> Result<()> { error: Failed to bytecode compile [SITE-PACKAGES] Caused by: Python process stderr: Invalid value for PYC_INVALIDATION_MODE "bogus", valid are "TIMESTAMP", "CHECKED_HASH", "UNCHECKED_HASH": - Caused by: Bytecode compilation failed, expected "[SITE-PACKAGES]/[FIRST-FILE]", received: "" + Caused by: Failed to write to Python stdin + Caused by: Broken pipe (os error 32) "### ); diff --git a/requirements.in b/requirements.in index a29dbf99c037..e28679220bab 100644 --- a/requirements.in +++ b/requirements.in @@ -1,2 +1,10 @@ -torchvision==0.15.1+cu118 -torch==2.0.0+cu118 +#torchvision==0.15.1+cu118 +#torch==2.0.0+cu118 + +# This should _fail_. It shouldn't allow a local version. +# torch>2.2.1 + +# This should return 2.2.0, not a local version. +# torch<2.2.1 + +torch>=2.2.1 diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 42cd1ff875f4..599abe769a07 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -144,18 +144,31 @@ def main(scenarios: list[Path], snapshot_update: bool = True): else [] ) - # TEMPORARY - # We do not yet support local version identifiers + # We don't yet support local versions that aren't expressed as direct dependencies. for scenario in data["scenarios"]: expected = scenario["expected"] - if ( - scenario["name"].startswith("local-") - and scenario["name"] != "local-not-latest" + if scenario["name"] in ( + "local-less-than-or-equal", + "local-simple", + "local-transitive-confounding", + "local-used-without-sdist", ): expected["satisfiable"] = False - expected[ - "explanation" - ] = "We do not have correct behavior for local version identifiers yet" + expected["explanation"] = ( + "We do not have correct behavior for local version identifiers yet" + ) + elif scenario["name"] == "local-greater-than": + expected["satisfiable"] = True + expected["packages"] = [ + { + "name": "local-greater-than-a", + "version": "1.2.3+foo", + "module_name": "local_greater_than_a", + } + ] + expected["explanation"] = ( + "We do not have correct behavior for local version identifiers yet" + ) # Generate cute names for each scenario for scenario in data["scenarios"]: @@ -181,13 +194,13 @@ def main(scenarios: list[Path], snapshot_update: bool = True): ref = "HEAD" if packse_version == "0.0.0" else packse_version # Add generated metadata - data[ - "generated_from" - ] = f"https://github.com/zanieb/packse/tree/{ref}/scenarios" + data["generated_from"] = ( + f"https://github.com/zanieb/packse/tree/{ref}/scenarios" + ) data["generated_with"] = "./scripts/scenarios/sync.sh" - data[ - "vendor_links" - ] = f"https://raw.githubusercontent.com/zanieb/packse/{ref}/vendor/links.html" + data["vendor_links"] = ( + f"https://raw.githubusercontent.com/zanieb/packse/{ref}/vendor/links.html" + ) data["index_url"] = f"https://astral-sh.github.io/packse/{ref}/simple-html/"