diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 25676d999ecd6..4de012ba583b7 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -9,7 +9,7 @@ use uv_distribution_filename::DistExtension; use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to}; use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError}; use uv_normalize::{ExtraName, GroupName, PackageName}; -use uv_pep440::VersionSpecifiers; +use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{ MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker, }; @@ -201,9 +201,9 @@ impl From for uv_pep508::Requirement { marker: requirement.marker, origin: requirement.origin, version_or_url: match requirement.source { - RequirementSource::Registry { specifier, .. } => { - Some(VersionOrUrl::VersionSpecifier(specifier)) - } + RequirementSource::Registry { specifier, .. } => Some( + VersionOrUrl::VersionSpecifier(specifier.to_version_specifiers()), + ), RequirementSource::Url { url, .. } | RequirementSource::Git { url, .. } | RequirementSource::Path { url, .. } @@ -222,9 +222,9 @@ impl From for uv_pep508::Requirement { marker: requirement.marker, origin: requirement.origin, version_or_url: match requirement.source { - RequirementSource::Registry { specifier, .. } => { - Some(VersionOrUrl::VersionSpecifier(specifier)) - } + RequirementSource::Registry { specifier, .. } => Some( + VersionOrUrl::VersionSpecifier(specifier.to_version_specifiers()), + ), RequirementSource::Url { location, subdirectory, @@ -285,13 +285,13 @@ impl From> for Requirement { fn from(requirement: uv_pep508::Requirement) -> Self { let source = match requirement.version_or_url { None => RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()), index: None, conflict: None, }, // The most popular case: just a name, a version range and maybe extras. Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry { - specifier, + specifier: VersionSpecifiersOrExact::VersionSpecifiers(specifier), index: None, conflict: None, }, @@ -393,6 +393,9 @@ impl CacheKey for Requirement { conflict: _, } => { 0u8.cache_key(state); + // TODO(konsti): We should use the information whether this is an exact specifier + // or not here, but this changes the cache. + let specifier = specifier.to_version_specifiers(); specifier.len().cache_key(state); for spec in specifier.iter() { spec.operator().as_str().cache_key(state); @@ -466,6 +469,54 @@ impl CacheKey for Requirement { } } +/// A local version aware version specifier. +/// +/// For checking installed requirements against a lockfile, we want to check that the local version +/// matches, too. When checking whether the specifier `=={version}` contains `{version}+{local}`, +/// the check will pass, which we avoid by doing an exact version comparison instead. An example +/// is torch `2.9.0` in the lockfile vs. `2.9.0+cu128` installed. +#[derive(Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum VersionSpecifiersOrExact { + VersionSpecifiers(VersionSpecifiers), + Exact(Version), +} + +impl VersionSpecifiersOrExact { + /// Note that this conversion is lossy wrt to local version matching. + pub fn to_version_specifiers(&self) -> VersionSpecifiers { + match self { + Self::VersionSpecifiers(specifier) => specifier.clone(), + Self::Exact(version) => { + VersionSpecifiers::from(VersionSpecifier::equals_version(version.clone())) + } + } + } + + pub fn is_empty(&self) -> bool { + match self { + Self::VersionSpecifiers(specifier) => specifier.is_empty(), + Self::Exact(_) => false, + } + } + + pub fn contains(&self, other: &Version) -> bool { + match self { + Self::VersionSpecifiers(specifiers) => specifiers.contains(other), + // Compare including the version specifier + Self::Exact(version) => version == other, + } + } +} + +impl Display for VersionSpecifiersOrExact { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::VersionSpecifiers(specifiers) => Display::fmt(specifiers, f), + Self::Exact(version) => write!(f, "=={version}"), + } + } +} + /// The different locations with can install a distribution from: Version specifier (from an index), /// HTTP(S) URL, git repository, and path. /// @@ -479,7 +530,7 @@ impl CacheKey for Requirement { pub enum RequirementSource { /// The requirement has a version specifier, such as `foo >1,<2`. Registry { - specifier: VersionSpecifiers, + specifier: VersionSpecifiersOrExact, /// Choose a version from the index at the given URL. index: Option, /// The conflict item associated with the source, if any. @@ -635,7 +686,9 @@ impl RequirementSource { if specifier.is_empty() { None } else { - Some(VersionOrUrl::VersionSpecifier(specifier.clone())) + Some(VersionOrUrl::VersionSpecifier( + specifier.to_version_specifiers(), + )) } } Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => { @@ -666,9 +719,9 @@ impl RequirementSource { } /// If the source is the registry, return the version specifiers - pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> { + pub fn version_specifiers(&self) -> Option { match self { - Self::Registry { specifier, .. } => Some(specifier), + Self::Registry { specifier, .. } => Some(specifier.to_version_specifiers()), Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => { None } @@ -817,7 +870,7 @@ impl From for RequirementSourceWire { index }); Self::Registry { - specifier, + specifier: specifier.to_version_specifiers(), index, conflict, } @@ -922,7 +975,7 @@ impl TryFrom for RequirementSource { index, conflict, } => Ok(Self::Registry { - specifier, + specifier: VersionSpecifiersOrExact::VersionSpecifiers(specifier), index: index .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))), conflict, @@ -1049,7 +1102,7 @@ mod tests { use uv_pep508::{MarkerTree, VerbatimUrl}; - use crate::{Requirement, RequirementSource}; + use crate::{Requirement, RequirementSource, VersionSpecifiersOrExact}; #[test] fn roundtrip() { @@ -1059,7 +1112,7 @@ mod tests { groups: Box::new([]), marker: MarkerTree::TRUE, source: RequirementSource::Registry { - specifier: ">1,<2".parse().unwrap(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers(">1,<2".parse().unwrap()), index: None, conflict: None, }, diff --git a/crates/uv-distribution-types/src/resolution.rs b/crates/uv-distribution-types/src/resolution.rs index e11d194cfa30e..be14b9ac4e9bf 100644 --- a/crates/uv-distribution-types/src/resolution.rs +++ b/crates/uv-distribution-types/src/resolution.rs @@ -4,6 +4,7 @@ use uv_pypi_types::{HashDigest, HashDigests}; use crate::{ BuiltDist, Diagnostic, Dist, IndexMetadata, Name, RequirementSource, ResolvedDist, SourceDist, + VersionSpecifiersOrExact, }; /// A set of packages pinned at specific versions. @@ -216,11 +217,7 @@ impl From<&ResolvedDist> for RequirementSource { Dist::Built(BuiltDist::Registry(wheels)) => { let wheel = wheels.best_wheel(); Self::Registry { - specifier: uv_pep440::VersionSpecifiers::from( - uv_pep440::VersionSpecifier::equals_version( - wheel.filename.version.clone(), - ), - ), + specifier: VersionSpecifiersOrExact::Exact(wheel.filename.version.clone()), index: Some(IndexMetadata::from(wheel.index.clone())), conflict: None, } @@ -241,9 +238,7 @@ impl From<&ResolvedDist> for RequirementSource { ext: DistExtension::Wheel, }, Dist::Source(SourceDist::Registry(sdist)) => Self::Registry { - specifier: uv_pep440::VersionSpecifiers::from( - uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()), - ), + specifier: VersionSpecifiersOrExact::Exact(sdist.version.clone()), index: Some(IndexMetadata::from(sdist.index.clone())), conflict: None, }, @@ -275,9 +270,7 @@ impl From<&ResolvedDist> for RequirementSource { }, }, ResolvedDist::Installed { dist } => Self::Registry { - specifier: uv_pep440::VersionSpecifiers::from( - uv_pep440::VersionSpecifier::equals_version(dist.version().clone()), - ), + specifier: VersionSpecifiersOrExact::Exact(dist.version().clone()), index: None, conflict: None, }, diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 627e2d4dd14a6..c0d46455d125e 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -8,6 +8,7 @@ use thiserror::Error; use uv_distribution_filename::DistExtension; use uv_distribution_types::{ Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource, + VersionSpecifiersOrExact, }; use uv_git_types::{GitReference, GitUrl, GitUrlParseError}; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -659,17 +660,17 @@ fn registry_source( ) -> RequirementSource { match &requirement.version_or_url { None => RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()), index: Some(index), conflict, }, Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry { - specifier: version.clone(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers(version.clone()), index: Some(index), conflict, }, Some(VersionOrUrl::Url(_)) => RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers(VersionSpecifiers::empty()), index: Some(index), conflict, }, diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 1941a83719a47..aa649bfce8f1f 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -122,6 +122,7 @@ impl<'a> Planner<'a> { [] => {} [installed] => { let source = RequirementSource::from(dist); + match RequirementSatisfaction::check( dist.name(), installed, diff --git a/crates/uv-resolver/src/prerelease.rs b/crates/uv-resolver/src/prerelease.rs index d1d1181ee5e44..8e8c109f180c1 100644 --- a/crates/uv-resolver/src/prerelease.rs +++ b/crates/uv-resolver/src/prerelease.rs @@ -83,6 +83,7 @@ impl PrereleaseStrategy { }; if specifier + .to_version_specifiers() .iter() .filter(|spec| { !matches!(spec.operator(), Operator::NotEqual | Operator::NotEqualStar) diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 7ef848ab26a23..9beb81258bc28 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -191,7 +191,12 @@ impl PubGrubRequirement { ) -> Self { let (verbatim_url, parsed_url) = match &requirement.source { RequirementSource::Registry { specifier, .. } => { - return Self::from_registry_requirement(specifier, extra, group, requirement); + return Self::from_registry_requirement( + specifier.to_version_specifiers(), + extra, + group, + requirement, + ); } RequirementSource::Url { subdirectory, @@ -259,7 +264,7 @@ impl PubGrubRequirement { } fn from_registry_requirement( - specifier: &VersionSpecifiers, + specifier: VersionSpecifiers, extra: Option, group: Option, requirement: &Requirement, @@ -272,7 +277,7 @@ impl PubGrubRequirement { requirement.marker, ), url: None, - version: Ranges::from(specifier.clone()), + version: Ranges::from(specifier), } } } diff --git a/crates/uv-resolver/src/yanks.rs b/crates/uv-resolver/src/yanks.rs index f8cb27d8a2547..3173868104634 100644 --- a/crates/uv-resolver/src/yanks.rs +++ b/crates/uv-resolver/src/yanks.rs @@ -26,6 +26,7 @@ impl AllowedYanks { let RequirementSource::Registry { specifier, .. } = &requirement.source else { continue; }; + let specifier = specifier.to_version_specifiers(); let [specifier] = specifier.as_ref() else { continue; }; diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 9d48c8221bc0a..ca60286b4b1f6 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashMap; use uv_configuration::HashCheckingMode; use uv_distribution_types::{ DistributionMetadata, HashGeneration, HashPolicy, Name, Requirement, RequirementSource, - Resolution, UnresolvedRequirement, VersionId, + Resolution, UnresolvedRequirement, VersionId, VersionSpecifiersOrExact, }; use uv_normalize::PackageName; use uv_pep440::Version; @@ -301,19 +301,27 @@ impl HashStrategy { match &requirement.source { RequirementSource::Registry { specifier, .. } => { // Must be a single specifier. - let [specifier] = specifier.as_ref() else { - return None; - }; + match specifier { + VersionSpecifiersOrExact::VersionSpecifiers(specifier) => { + let [specifier] = specifier.as_ref() else { + return None; + }; - // Must be pinned to a specific version. - if *specifier.operator() != uv_pep440::Operator::Equal { - return None; - } + // Must be pinned to a specific version. + if *specifier.operator() != uv_pep440::Operator::Equal { + return None; + } - Some(VersionId::from_registry( - requirement.name.clone(), - specifier.version().clone(), - )) + Some(VersionId::from_registry( + requirement.name.clone(), + specifier.version().clone(), + )) + } + VersionSpecifiersOrExact::Exact(version) => Some(VersionId::from_registry( + requirement.name.clone(), + version.clone(), + )), + } } RequirementSource::Url { url, .. } | RequirementSource::Git { url, .. } diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 0f35ad70ce0bb..6450f531c0f66 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -12,11 +12,11 @@ use uv_configuration::{Concurrency, Constraints, DryRun, Reinstall, TargetTriple use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ ExtraBuildRequires, NameRequirementSpecification, Requirement, RequirementSource, - UnresolvedRequirementSpecification, + UnresolvedRequirementSpecification, VersionSpecifiersOrExact, }; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; use uv_normalize::PackageName; -use uv_pep440::{VersionSpecifier, VersionSpecifiers}; +use uv_pep440::VersionSpecifiers; use uv_pep508::MarkerTree; use uv_preview::Preview; use uv_python::{ @@ -180,9 +180,7 @@ pub(crate) async fn install( groups: Box::new([]), marker: MarkerTree::default(), source: RequirementSource::Registry { - specifier: VersionSpecifiers::from(VersionSpecifier::equals_version( - version.clone(), - )), + specifier: VersionSpecifiersOrExact::Exact(version.clone()), index: None, conflict: None, }, @@ -204,7 +202,9 @@ pub(crate) async fn install( groups: Box::new([]), marker: MarkerTree::default(), source: RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers( + VersionSpecifiers::empty(), + ), index: None, conflict: None, }, diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 918d014f9bcca..c49bc64414933 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -20,15 +20,15 @@ use uv_configuration::Concurrency; use uv_configuration::Constraints; use uv_configuration::TargetTriple; use uv_distribution::LoweredExtraBuildDependencies; -use uv_distribution_types::InstalledDist; use uv_distribution_types::{ IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification, }; +use uv_distribution_types::{InstalledDist, VersionSpecifiersOrExact}; use uv_fs::Simplified; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; use uv_normalize::PackageName; -use uv_pep440::{VersionSpecifier, VersionSpecifiers}; +use uv_pep440::VersionSpecifiers; use uv_pep508::MarkerTree; use uv_preview::Preview; use uv_python::{ @@ -830,9 +830,7 @@ async fn get_or_create_environment( groups: Box::new([]), marker: MarkerTree::default(), source: RequirementSource::Registry { - specifier: VersionSpecifiers::from(VersionSpecifier::equals_version( - version.clone(), - )), + specifier: VersionSpecifiersOrExact::Exact(version.clone()), index: None, conflict: None, }, @@ -852,7 +850,9 @@ async fn get_or_create_environment( groups: Box::new([]), marker: MarkerTree::default(), source: RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), + specifier: VersionSpecifiersOrExact::VersionSpecifiers( + VersionSpecifiers::empty(), + ), index: None, conflict: None, }, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 330673c244987..4663e64b452ea 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -475,14 +475,13 @@ fn pinned_version_from(requirements: &[Requirement], name: &PackageName) -> Opti .iter() .filter(|requirement| requirement.name == *name) .find_map(|requirement| match &requirement.source { - RequirementSource::Registry { specifier, .. } => { - specifier - .iter() - .find_map(|specifier| match specifier.operator() { - Operator::Equal | Operator::ExactEqual => Some(specifier.version().clone()), - _ => None, - }) - } + RequirementSource::Registry { specifier, .. } => specifier + .to_version_specifiers() + .iter() + .find_map(|specifier| match specifier.operator() { + Operator::Equal | Operator::ExactEqual => Some(specifier.version().clone()), + _ => None, + }), _ => None, }) } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index b9d54bd44d0ba..cb9dd2a60dd75 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -17767,8 +17767,8 @@ fn lock_explicit_default_index() -> Result<()> { DEBUG Found static `pyproject.toml` for: project @ file://[TEMP_DIR]/ DEBUG No workspace root found, using project root DEBUG Resolving despite existing lockfile due to mismatched requirements for: `project==0.1.0` - Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }} - Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }} + Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers(VersionSpecifiers([])), index: None, conflict: None }, origin: None }} + Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers(VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }])), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: DisplaySafeUrl { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }} DEBUG Solving with installed Python version: 3.12.[X] DEBUG Solving with target Python version: >=3.12 DEBUG Adding direct dependency: project* diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 9ee19e5c41847..ca122b94cb8b6 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -8786,7 +8786,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, @@ -9492,7 +9494,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, @@ -9512,7 +9516,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, @@ -9786,7 +9792,9 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, @@ -10260,7 +10268,9 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, @@ -10280,7 +10290,9 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { marker: true, source: Registry { specifier: VersionSpecifiers( - [], + VersionSpecifiers( + [], + ), ), index: None, conflict: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 566011a9eb826..7ceb39259a2ac 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -14362,3 +14362,161 @@ fn sync_no_sources_editable_to_package_switch() -> Result<()> { Ok(()) } + +/// Test that switching between indexes with local and non-local versions of the same package, +/// the package is always reinstalled, not only when switching from a non-local to a local version. +/// +/// +#[test] +fn install_non_local_version_when_local_version_is_installed() -> Result<()> { + let start = std::time::Instant::now(); + dbg!(start.elapsed()); + let context = TestContext::new("3.12"); + dbg!(start.elapsed()); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "main" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + non-local = ["project"] + local = ["project"] + + [tool.uv] + conflicts = [ + [ + { extra = "non-local" }, + { extra = "local" }, + ], + ] + + [tool.uv.sources] + project = [ + { index = "non-local", extra = "non-local" }, + { index = "local", extra = "local" }, + ] + + [[tool.uv.index]] + name = "non-local" + url = "./non-local/dist" + format = "flat" + explicit = true + + [[tool.uv.index]] + name = "local" + url = "./local/dist" + format = "flat" + explicit = true + "#, + )?; + dbg!(start.elapsed()); + + context + .init() + .arg("--lib") + .arg("--no-workspace") + .arg("--name") + .arg("project") + .arg("local") + .assert() + .success(); + dbg!(start.elapsed()); + context + .version() + .arg("1.0.0+local") + .current_dir(context.temp_dir.child("local").as_ref()) + .assert() + .success(); + dbg!(start.elapsed()); + context + .build() + .arg("--wheel") + .current_dir(context.temp_dir.child("local").as_ref()) + .assert() + .success(); + dbg!(start.elapsed()); + context + .init() + .arg("--lib") + .arg("--no-workspace") + .arg("--name") + .arg("project") + .arg("non-local") + .assert() + .success(); + dbg!(start.elapsed()); + context + .version() + .arg("1.0.0") + .current_dir(context.temp_dir.child("non-local").as_ref()) + .assert() + .success(); + dbg!(start.elapsed()); + context + .build() + .arg("--wheel") + .current_dir(context.temp_dir.child("non-local").as_ref()) + .assert() + .success(); + dbg!(start.elapsed()); + + // Install the non-local version. + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("non-local"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==1.0.0 + "); + dbg!(start.elapsed()); + // Check that switching to the local version + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("local"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - project==1.0.0 + + project==1.0.0+local + "); + // Keeping the local version is a noop. + dbg!(start.elapsed()); + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("local"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Audited 1 package in [TIME] + "); + dbg!(start.elapsed()); + // Check that switching to the non-local version invalidates the installed local version. + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("non-local"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - project==1.0.0+local + + project==1.0.0 + "); + dbg!(start.elapsed()); + + Ok(()) +}