From 6333823236356e9effbd3b6380fb989a9432c5f0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 15 Aug 2024 08:17:28 -0400 Subject: [PATCH] Change the definition of `--locked` to require satisfaction check (#6102) ## Summary This PR changes the definition of `--locked` from: > Produces the same `Lock` To: > Passes `Lock::satisfies` This is a subtle but important difference. Previous, if `Lock::satisfies` failed, we would run a resolution, then do `existing_lock == lock`. If the two weren't equal, and `--locked` was specified, we'd throw an error. The equality check is hard to get right. For example, it means that we can't ship #6076 without changing our marker representation, since the deserialized lockfile "loses" some of the internal marker state that gets accumulated during resolution. The downside of this change is that there could be scenarios in which `uv lock --locked` fails even though the lockfile would actually work and the exact TOML would be unchanged. But... I think it's ok if `--locked` fails after the user modifies something? --- crates/uv-resolver/src/lock.rs | 2 +- crates/uv/src/commands/project/add.rs | 8 +- crates/uv/src/commands/project/lock.rs | 497 ++++++++++++---------- crates/uv/src/commands/project/remove.rs | 5 +- crates/uv/src/commands/project/run.rs | 10 +- crates/uv/src/commands/project/sync.rs | 4 +- crates/uv/src/commands/project/tree.rs | 5 +- crates/uv/tests/lock_scenarios.rs | 338 --------------- scripts/scenarios/templates/lock.mustache | 13 - 9 files changed, 298 insertions(+), 584 deletions(-) diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index f04151699..f58f0bcc9 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -40,7 +40,7 @@ use crate::{ExcludeNewer, PrereleaseMode, RequiresPython, ResolutionGraph, Resol /// The current version of the lockfile format. const VERSION: u32 = 1; -#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, serde::Deserialize)] #[serde(try_from = "LockWire")] pub struct Lock { version: u32, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 46da8e053..3a35c685a 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -441,7 +441,7 @@ pub(crate) async fn add( ) .await { - Ok(lock) => lock, + Ok(result) => result.into_lock(), Err(ProjectError::Operation(pip::operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { @@ -463,8 +463,8 @@ pub(crate) async fn add( if !raw_sources { // Extract the minimum-supported version for each dependency. let mut minimum_version = - FxHashMap::with_capacity_and_hasher(lock.lock.packages().len(), FxBuildHasher); - for dist in lock.lock.packages() { + FxHashMap::with_capacity_and_hasher(lock.packages().len(), FxBuildHasher); + for dist in lock.packages() { let name = dist.name(); let version = dist.version(); match minimum_version.entry(name) { @@ -563,7 +563,7 @@ pub(crate) async fn add( project::sync::do_sync( &project, &venv, - &lock.lock, + &lock, &extras, dev, Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 0c64a2f6c..21eb37a1e 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -9,13 +9,16 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; use distribution_types::{ - FlatIndexLocation, IndexUrl, UnresolvedRequirementSpecification, UrlString, + FlatIndexLocation, IndexLocations, IndexUrl, UnresolvedRequirementSpecification, UrlString, }; use pep440_rs::Version; +use pypi_types::Requirement; use uv_auth::store_credentials_from_url; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; -use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, Reinstall, SetupPyStrategy}; +use uv_configuration::{ + Concurrency, ExtrasSpecification, PreviewMode, Reinstall, SetupPyStrategy, Upgrade, +}; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; use uv_fs::CWD; @@ -25,10 +28,10 @@ use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreferenc use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_requirements::NamedRequirementsResolver; use uv_resolver::{ - FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython, ResolverManifest, + FlatIndex, Lock, Options, OptionsBuilder, PythonRequirement, RequiresPython, ResolverManifest, ResolverMarkers, }; -use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; +use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::{DiscoveryOptions, Workspace}; @@ -41,11 +44,27 @@ use crate::settings::{ResolverSettings, ResolverSettingsRef}; /// The result of running a lock operation. #[derive(Debug, Clone)] -pub(crate) struct LockResult { - /// The previous lock, if any. - pub(crate) previous: Option, - /// The updated lock. - pub(crate) lock: Lock, +pub(crate) enum LockResult { + /// The lock was unchanged. + Unchanged(Lock), + /// The lock was changed. + Changed(Option, Lock), +} + +impl LockResult { + pub(crate) fn lock(&self) -> &Lock { + match self { + LockResult::Unchanged(lock) => lock, + LockResult::Changed(_, lock) => lock, + } + } + + pub(crate) fn into_lock(self) -> Lock { + match self { + LockResult::Unchanged(lock) => lock, + LockResult::Changed(_, lock) => lock, + } + } } /// Resolve the project requirements into a lockfile. @@ -102,8 +121,8 @@ pub(crate) async fn lock( .await { Ok(lock) => { - if let Some(previous) = lock.previous.as_ref() { - report_upgrades(previous, &lock.lock, printer)?; + if let LockResult::Changed(Some(previous), lock) = &lock { + report_upgrades(previous, lock, printer)?; } Ok(ExitStatus::Success) } @@ -149,10 +168,7 @@ pub(super) async fn do_safe_lock( let existing = read(workspace) .await? .ok_or_else(|| ProjectError::MissingLockfile)?; - Ok(LockResult { - previous: None, - lock: existing, - }) + Ok(LockResult::Unchanged(existing)) } else if locked { // Read the existing lockfile. let existing = read(workspace) @@ -160,10 +176,10 @@ pub(super) async fn do_safe_lock( .ok_or_else(|| ProjectError::MissingLockfile)?; // Perform the lock operation, but don't write the lockfile to disk. - let lock = do_lock( + let result = do_lock( workspace, interpreter, - Some(&existing), + Some(existing), settings, &state, logger, @@ -176,24 +192,21 @@ pub(super) async fn do_safe_lock( ) .await?; - // If the locks disagree, return an error. - if lock != existing { + // If the lockfile changed, return an error. + if matches!(result, LockResult::Changed(_, _)) { return Err(ProjectError::LockMismatch); } - Ok(LockResult { - previous: Some(existing), - lock, - }) + Ok(result) } else { // Read the existing lockfile. let existing = read(workspace).await?; // Perform the lock operation. - let lock = do_lock( + let result = do_lock( workspace, interpreter, - existing.as_ref(), + existing, settings, &state, logger, @@ -206,14 +219,12 @@ pub(super) async fn do_safe_lock( ) .await?; - if !existing.as_ref().is_some_and(|existing| *existing == lock) { - commit(&lock, workspace).await?; + // If the lockfile changed, write it to disk. + if let LockResult::Changed(_, lock) = &result { + commit(lock, workspace).await?; } - Ok(LockResult { - previous: existing, - lock, - }) + Ok(result) } } @@ -221,7 +232,7 @@ pub(super) async fn do_safe_lock( async fn do_lock( workspace: &Workspace, interpreter: &Interpreter, - existing_lock: Option<&Lock>, + existing_lock: Option, settings: ResolverSettingsRef<'_>, state: &SharedState, logger: Box, @@ -231,7 +242,9 @@ async fn do_lock( native_tls: bool, cache: &Cache, printer: Printer, -) -> Result { +) -> Result { + let start = std::time::Instant::now(); + // Extract the project settings. let ResolverSettingsRef { index_locations, @@ -392,198 +405,74 @@ async fn do_lock( .await?; // If any of the resolution-determining settings changed, invalidate the lock. - let existing_lock = existing_lock.filter(|lock| { - if lock.resolution_mode() != options.resolution_mode { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to change in resolution mode: `{}` vs. `{}`", - lock.resolution_mode().cyan(), - options.resolution_mode.cyan() - ); - return false; - } - if lock.prerelease_mode() != options.prerelease_mode { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to change in pre-release mode: `{}` vs. `{}`", - lock.prerelease_mode().cyan(), - options.prerelease_mode.cyan() - ); - return false; - } - match (lock.exclude_newer(), options.exclude_newer) { - (None, None) => (), - (Some(existing), Some(provided)) if existing == provided => (), - (Some(existing), Some(provided)) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to change in timestamp cutoff: `{}` vs. `{}`", - existing.cyan(), - provided.cyan() - ); - return false; - } - (Some(existing), None) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to removal of timestamp cutoff: `{}`", - existing.cyan(), - ); - return false; - } - (None, Some(provided)) => { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to addition of timestamp cutoff: `{}`", - provided.cyan() - ); - return false; - } - } - true - }); - - // If an existing lockfile exists, build up a set of preferences. - let LockedRequirements { preferences, git } = existing_lock - .as_ref() - .map(|lock| read_lock_requirements(lock, upgrade)) - .unwrap_or_default(); - - // Populate the Git resolver. - for ResolvedRepositoryReference { reference, sha } in git { - debug!("Inserting Git reference into resolver: `{reference:?}` at `{sha}`"); - state.git.insert(reference, sha); - } - - let start = std::time::Instant::now(); - - let existing_lock = existing_lock.filter(|lock| { - match (lock.requires_python(), requires_python) { - // If the Requires-Python bound in the lockfile is weaker or equivalent to the - // Requires-Python bound in the workspace, we should have the necessary wheels to perform - // a locked resolution. - (None, _) => true, - (Some(locked), specified) => { - if locked.bound() == specified.bound() { - true - } else { - // On the other hand, if the bound in the lockfile is stricter, meaning the - // bound has since been weakened, we have to perform a clean resolution to ensure - // we fetch the necessary wheels. - debug!("Ignoring existing lockfile due to change in `requires-python`"); - false - } - } - } - }); - - // When we run the same resolution from the lockfile again, we could get a different result the - // second time due to the preferences causing us to skip a fork point (see - // "preferences-dependent-forking" packse scenario). To avoid this, we store the forks in the - // lockfile. We read those after all the lockfile filters, to allow the forks to change when - // the environment changed, e.g. the python bound check above can lead to different forking. - let resolver_markers = ResolverMarkers::universal(if upgrade.is_all() { - // We're discarding all preferences, so we're also discarding the existing forks. - vec![] - } else { - existing_lock - .map(|lock| lock.fork_markers().to_vec()) - .unwrap_or_default() - }); - - // If any upgrades are specified, don't use the existing lockfile. - let existing_lock = existing_lock.filter(|_| { - debug!("Ignoring existing lockfile due to `--upgrade`"); - upgrade.is_none() - }); - - // If the user provided at least one index URL (from the command line, or from a configuration - // file), don't use the existing lockfile if it references any registries that are no longer - // included in the current configuration. - let existing_lock = existing_lock.filter(|lock| { - // If _no_ indexes were provided, we assume that the user wants to reuse the existing - // distributions, even though a failure to reuse the lockfile will result in re-resolving - // against PyPI by default. - if settings.index_locations.is_none() { - return true; - } - - // Collect the set of available indexes (both `--index-url` and `--find-links` entries). - let indexes = settings - .index_locations - .indexes() - .map(IndexUrl::redacted) - .chain( - settings - .index_locations - .flat_index() - .map(FlatIndexLocation::redacted), + let existing_lock = if let Some(existing_lock) = existing_lock { + Some( + ValidatedLock::validate( + existing_lock, + workspace, + &members, + &constraints, + &overrides, + interpreter, + &requires_python, + index_locations, + upgrade, + &options, + &database, + printer, ) - .map(UrlString::from) - .collect::>(); - - // Find any packages in the lockfile that reference a registry that is no longer included in - // the current configuration. - for package in lock.packages() { - let Some(index) = package.index() else { - continue; - }; - if !indexes.contains(index) { - let _ = writeln!( - printer.stderr(), - "Ignoring existing lockfile due to removal of referenced registry: {index}" - ); - return false; - } - } - - true - }); - - let existing_lock = match existing_lock { - None => None, - - // Try to resolve using metadata in the lockfile. - // - // When resolving from the lockfile we can still download and install new distributions, - // but we rely on the lockfile for the metadata of any existing distributions. If we have - // any outdated metadata we fall back to a clean resolve. - Some(lock) => { - if lock - .satisfies( - workspace, - &members, - &constraints, - &overrides, - interpreter.tags()?, - &database, - ) - .await? - { - debug!("Existing `uv.lock` satisfies workspace requirements"); - Some(lock) - } else { - debug!("Existing `uv.lock` does not satisfy workspace requirements; ignoring..."); - None - } - } + .await?, + ) + } else { + None }; match existing_lock { // Resolution from the lockfile succeeded. - Some(lock) => { + Some(ValidatedLock::Satisfies(lock)) => { // Print the success message after completing resolution. logger.on_complete(lock.len(), start, printer)?; - // TODO(charlie): Avoid cloning here. - Ok(lock.clone()) + Ok(LockResult::Unchanged(lock)) } // The lockfile did not contain enough information to obtain a resolution, fallback // to a fresh resolve. - None => { + _ => { debug!("Starting clean resolution"); + // If an existing lockfile exists, build up a set of preferences. + let LockedRequirements { preferences, git } = existing_lock + .as_ref() + .and_then(|lock| match &lock { + ValidatedLock::Preferable(lock) => Some(lock), + ValidatedLock::Satisfies(lock) => Some(lock), + ValidatedLock::Unusable(_) => None, + }) + .map(|lock| read_lock_requirements(lock, upgrade)) + .unwrap_or_default(); + + // Populate the Git resolver. + for ResolvedRepositoryReference { reference, sha } in git { + debug!("Inserting Git reference into resolver: `{reference:?}` at `{sha}`"); + state.git.insert(reference, sha); + } + + // When we run the same resolution from the lockfile again, we could get a different result the + // second time due to the preferences causing us to skip a fork point (see + // "preferences-dependent-forking" packse scenario). To avoid this, we store the forks in the + // lockfile. We read those after all the lockfile filters, to allow the forks to change when + // the environment changed, e.g. the python bound check above can lead to different forking. + let resolver_markers = ResolverMarkers::universal(if upgrade.is_all() { + // We're discarding all preferences, so we're also discarding the existing forks. + vec![] + } else { + existing_lock + .as_ref() + .map(|existing_lock| existing_lock.lock().fork_markers().to_vec()) + .unwrap_or_default() + }); + // Resolve the requirements. let resolution = pip::operations::resolve( requirements, @@ -624,13 +513,187 @@ async fn do_lock( // Notify the user of any resolution diagnostics. pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?; - Ok( - Lock::from_resolution_graph(&resolution)?.with_manifest(ResolverManifest::new( - members, - constraints, - overrides, - )), + let previous = existing_lock.map(ValidatedLock::into_lock); + let lock = Lock::from_resolution_graph(&resolution)? + .with_manifest(ResolverManifest::new(members, constraints, overrides)); + + Ok(LockResult::Changed(previous, lock)) + } + } +} + +#[derive(Debug)] +enum ValidatedLock { + /// An existing lockfile was provided, but its contents should be ignored. + Unusable(Lock), + /// An existing lockfile was provided, and it satisfies the workspace requirements. + Satisfies(Lock), + /// An existing lockfile was provided, and the locked versions should be preferred if possible, + /// even though the lockfile does not satisfy the workspace requirements. + Preferable(Lock), +} + +impl ValidatedLock { + /// Validate a [`Lock`] against the workspace requirements. + async fn validate( + lock: Lock, + workspace: &Workspace, + members: &[PackageName], + constraints: &[Requirement], + overrides: &[Requirement], + interpreter: &Interpreter, + requires_python: &RequiresPython, + index_locations: &IndexLocations, + upgrade: &Upgrade, + options: &Options, + database: &DistributionDatabase<'_, Context>, + printer: Printer, + ) -> Result { + // Start with the most severe condition: a fundamental option changed between resolutions. + if lock.resolution_mode() != options.resolution_mode { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to change in resolution mode: `{}` vs. `{}`", + lock.resolution_mode().cyan(), + options.resolution_mode.cyan() + ); + return Ok(Self::Unusable(lock)); + } + if lock.prerelease_mode() != options.prerelease_mode { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to change in pre-release mode: `{}` vs. `{}`", + lock.prerelease_mode().cyan(), + options.prerelease_mode.cyan() + ); + return Ok(Self::Unusable(lock)); + } + match (lock.exclude_newer(), options.exclude_newer) { + (None, None) => (), + (Some(existing), Some(provided)) if existing == provided => (), + (Some(existing), Some(provided)) => { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to change in timestamp cutoff: `{}` vs. `{}`", + existing.cyan(), + provided.cyan() + ); + return Ok(Self::Unusable(lock)); + } + (Some(existing), None) => { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to removal of timestamp cutoff: `{}`", + existing.cyan(), + ); + return Ok(Self::Unusable(lock)); + } + (None, Some(provided)) => { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to addition of timestamp cutoff: `{}`", + provided.cyan() + ); + return Ok(Self::Unusable(lock)); + } + } + + // If the user specified `--upgrade`, then at best we can prefer some of the existing + // versions. + if !upgrade.is_none() { + debug!("Ignoring existing lockfile due to `--upgrade`"); + return Ok(Self::Preferable(lock)); + } + + // If the Requires-Python bound in the lockfile is weaker or equivalent to the + // Requires-Python bound in the workspace, we should have the necessary wheels to perform + // a locked resolution. + if let Some(locked) = lock.requires_python() { + if locked.bound() != requires_python.bound() { + // On the other hand, if the bound in the lockfile is stricter, meaning the + // bound has since been weakened, we have to perform a clean resolution to ensure + // we fetch the necessary wheels. + debug!("Ignoring existing lockfile due to change in `requires-python`"); + + // It's fine to prefer the existing versions, though. + return Ok(Self::Preferable(lock)); + } + } + + // If the user provided at least one index URL (from the command line, or from a configuration + // file), don't use the existing lockfile if it references any registries that are no longer + // included in the current configuration. + // + // However, iIf _no_ indexes were provided, we assume that the user wants to reuse the existing + // distributions, even though a failure to reuse the lockfile will result in re-resolving + // against PyPI by default. + if !index_locations.is_none() { + // Collect the set of available indexes (both `--index-url` and `--find-links` entries). + let indexes = index_locations + .indexes() + .map(IndexUrl::redacted) + .chain( + index_locations + .flat_index() + .map(FlatIndexLocation::redacted), + ) + .map(UrlString::from) + .collect::>(); + + // Find any packages in the lockfile that reference a registry that is no longer included in + // the current configuration. + for package in lock.packages() { + let Some(index) = package.index() else { + continue; + }; + if !indexes.contains(index) { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to removal of referenced registry: {index}" + ); + + // It's fine to prefer the existing versions, though. + return Ok(Self::Preferable(lock)); + } + } + } + + // Determine whether the lockfile satisfies the workspace requirements. + if lock + .satisfies( + workspace, + members, + constraints, + overrides, + interpreter.tags()?, + database, ) + .await? + { + debug!("Existing `uv.lock` satisfies workspace requirements"); + Ok(Self::Satisfies(lock)) + } else { + debug!("Existing `uv.lock` does not satisfy workspace requirements; ignoring..."); + Ok(Self::Preferable(lock)) + } + } + + /// Return the inner [`Lock`]. + fn lock(&self) -> &Lock { + match self { + ValidatedLock::Unusable(lock) => lock, + ValidatedLock::Satisfies(lock) => lock, + ValidatedLock::Preferable(lock) => lock, + } + } + + /// Convert the [`ValidatedLock`] into a [`Lock`]. + #[must_use] + fn into_lock(self) -> Lock { + match self { + ValidatedLock::Unusable(lock) => lock, + ValidatedLock::Satisfies(lock) => lock, + ValidatedLock::Preferable(lock) => lock, } } } diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index a8641edba..f9ac64fff 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -174,7 +174,8 @@ pub(crate) async fn remove( cache, printer, ) - .await?; + .await? + .into_lock(); if no_sync { return Ok(ExitStatus::Success); @@ -191,7 +192,7 @@ pub(crate) async fn remove( project::sync::do_sync( &project, &venv, - &lock.lock, + &lock, &extras, dev, Modifications::Exact, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index e736a9fef..24239e3f0 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -36,7 +36,7 @@ use crate::commands::pip::operations::Modifications; use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::{ProjectError, WorkspacePython}; use crate::commands::reporters::PythonDownloadReporter; -use crate::commands::{pip, project, ExitStatus, SharedState}; +use crate::commands::{project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -387,7 +387,7 @@ pub(crate) async fn run( .await? }; - let lock = match project::lock::do_safe_lock( + let result = match project::lock::do_safe_lock( locked, frozen, project.workspace(), @@ -407,8 +407,8 @@ pub(crate) async fn run( ) .await { - Ok(lock) => lock, - Err(ProjectError::Operation(pip::operations::Error::Resolve( + Ok(result) => result, + Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { let report = miette::Report::msg(format!("{err}")).context(err.header()); @@ -421,7 +421,7 @@ pub(crate) async fn run( project::sync::do_sync( &project, &venv, - &lock.lock, + result.lock(), &extras, dev, Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4f92f92d8..e8c017f61 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -89,7 +89,7 @@ pub(crate) async fn sync( ) .await { - Ok(lock) => lock, + Ok(result) => result.into_lock(), Err(ProjectError::Operation(pip::operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { @@ -107,7 +107,7 @@ pub(crate) async fn sync( do_sync( &project, &venv, - &lock.lock, + &lock, &extras, dev, modifications, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 61b54d8b9..1fb9f7c89 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -79,7 +79,8 @@ pub(crate) async fn tree( cache, printer, ) - .await?; + .await? + .into_lock(); // Apply the platform tags to the markers. let markers = match (python_platform, python_version) { @@ -93,7 +94,7 @@ pub(crate) async fn tree( // Render the tree. let tree = TreeDisplay::new( - &lock.lock, + &lock, (!universal).then(|| markers.as_ref()), depth.into(), prune, diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index 66e6857af..af8da27c7 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -128,19 +128,6 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -254,19 +241,6 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -388,19 +362,6 @@ fn fork_basic() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -741,19 +702,6 @@ fn fork_filter_sibling_dependencies() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -869,19 +817,6 @@ fn fork_upgrade() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1038,19 +973,6 @@ fn fork_incomplete_markers() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1186,19 +1108,6 @@ fn fork_marker_accrue() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1444,19 +1353,6 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1626,19 +1522,6 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1809,19 +1692,6 @@ fn fork_marker_inherit_combined() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -1965,19 +1835,6 @@ fn fork_marker_inherit_isolated() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2139,19 +1996,6 @@ fn fork_marker_inherit_transitive() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2285,19 +2129,6 @@ fn fork_marker_inherit() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2460,19 +2291,6 @@ fn fork_marker_limited_inherit() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2617,19 +2435,6 @@ fn fork_marker_selection() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2798,19 +2603,6 @@ fn fork_marker_track() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -2945,19 +2737,6 @@ fn fork_non_fork_marker_transitive() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -3241,19 +3020,6 @@ fn fork_overlapping_markers_basic() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -3495,19 +3261,6 @@ fn preferences_dependent_forking_bistable() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -3936,19 +3689,6 @@ fn preferences_dependent_forking_tristable() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4151,19 +3891,6 @@ fn preferences_dependent_forking() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4347,19 +4074,6 @@ fn fork_remaining_universe_partitioning() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4445,19 +4159,6 @@ fn fork_requires_python_full_prerelease() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4543,19 +4244,6 @@ fn fork_requires_python_full() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4657,19 +4345,6 @@ fn fork_requires_python_patch_overlap() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } @@ -4752,18 +4427,5 @@ fn fork_requires_python() -> Result<()> { .assert() .success(); - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - Ok(()) } diff --git a/scripts/scenarios/templates/lock.mustache b/scripts/scenarios/templates/lock.mustache index 01c33e0aa..d720901f4 100644 --- a/scripts/scenarios/templates/lock.mustache +++ b/scripts/scenarios/templates/lock.mustache @@ -80,19 +80,6 @@ fn {{module_name}}() -> Result<()> { .arg(packse_index_url()) .assert() .success(); - - // Assert the idempotence of `uv lock` when resolving with the lockfile preferences, - // by upgrading an irrelevant package. - context - .lock() - .arg("--locked") - .arg("--upgrade-package") - .arg("packse") - .env_remove("UV_EXCLUDE_NEWER") - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); {{/expected.satisfiable}} Ok(())