From 5a23f0579962da26dbf117ca008822c26f5d15e5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 22 Jul 2024 08:28:22 -0400 Subject: [PATCH] Store resolution options in lockfile (#5264) ## Summary This PR modifies the lockfile to include the impactful resolution settings, like the resolution and pre-release mode. If any of those values change, we want to ignore the existing lockfile. Otherwise, `--resolution lowest-direct` will typically have no effect, which is really unintuitive. Closes https://github.com/astral-sh/uv/issues/5226. --- crates/uv-configuration/src/build_options.rs | 2 +- crates/uv-resolver/src/dependency_mode.rs | 2 +- crates/uv-resolver/src/exclude_newer.rs | 2 +- crates/uv-resolver/src/lock.rs | 75 ++++++- crates/uv-resolver/src/options.rs | 2 +- crates/uv-resolver/src/prerelease_mode.rs | 12 ++ crates/uv-resolver/src/resolution/graph.rs | 8 +- crates/uv-resolver/src/resolution_mode.rs | 12 +- crates/uv-resolver/src/resolver/mod.rs | 13 +- ...r__lock__tests__hash_optional_missing.snap | 3 + ...r__lock__tests__hash_optional_present.snap | 3 + ...r__lock__tests__hash_required_present.snap | 3 + ...missing_dependency_source_unambiguous.snap | 3 + ...dependency_source_version_unambiguous.snap | 3 + ...issing_dependency_version_unambiguous.snap | 3 + ...lock__tests__source_direct_has_subdir.snap | 3 + ..._lock__tests__source_direct_no_subdir.snap | 3 + ...solver__lock__tests__source_directory.snap | 3 + ...esolver__lock__tests__source_editable.snap | 3 + crates/uv/src/commands/project/lock.rs | 52 +++++ crates/uv/tests/branching_urls.rs | 5 + crates/uv/tests/edit.rs | 14 ++ crates/uv/tests/lock.rs | 193 ++++++++++++++++++ 23 files changed, 406 insertions(+), 16 deletions(-) diff --git a/crates/uv-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index 67049981934d..9ae5274ffc71 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -305,7 +305,7 @@ impl NoBuild { } } -#[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv-resolver/src/dependency_mode.rs b/crates/uv-resolver/src/dependency_mode.rs index b107e25daea8..8feb0d2d6cc8 100644 --- a/crates/uv-resolver/src/dependency_mode.rs +++ b/crates/uv-resolver/src/dependency_mode.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] pub enum DependencyMode { /// Include all dependencies, whether direct or transitive. #[default] diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index d2fd6b3582b3..be9095eebe8f 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use chrono::{DateTime, Days, NaiveDate, NaiveTime, Utc}; /// A timestamp that excludes files newer than it. -#[derive(Debug, Copy, Clone, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub struct ExcludeNewer(DateTime); impl ExcludeNewer { diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 722eb9071615..890937567aa5 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -42,7 +42,8 @@ use uv_workspace::VirtualProject; use crate::resolution::{AnnotatedDist, ResolutionGraphNode}; use crate::resolver::FxOnceMap; use crate::{ - InMemoryIndex, MetadataResponse, RequiresPython, ResolutionGraph, VersionMap, VersionsResponse, + ExcludeNewer, InMemoryIndex, MetadataResponse, PreReleaseMode, RequiresPython, ResolutionGraph, + ResolutionMode, VersionMap, VersionsResponse, }; /// The current version of the lock file format. @@ -55,6 +56,12 @@ pub struct Lock { distributions: Vec, /// The range of supported Python versions. requires_python: Option, + /// The [`ResolutionMode`] used to generate this lock. + resolution_mode: ResolutionMode, + /// The [`PreReleaseMode`] used to generate this lock. + prerelease_mode: PreReleaseMode, + /// The [`ExcludeNewer`] used to generate this lock. + exclude_newer: Option, /// A map from distribution ID to index in `distributions`. /// /// This can be used to quickly lookup the full distribution for any ID @@ -144,7 +151,15 @@ impl Lock { let distributions = locked_dists.into_values().collect(); let requires_python = graph.requires_python.clone(); - let lock = Self::new(VERSION, distributions, requires_python)?; + let options = graph.options; + let lock = Self::new( + VERSION, + distributions, + requires_python, + options.resolution_mode, + options.prerelease_mode, + options.exclude_newer, + )?; Ok(lock) } @@ -153,6 +168,9 @@ impl Lock { version: u32, mut distributions: Vec, requires_python: Option, + resolution_mode: ResolutionMode, + prerelease_mode: PreReleaseMode, + exclude_newer: Option, ) -> Result { // Put all dependencies for each distribution in a canonical order and // check for duplicates. @@ -307,10 +325,13 @@ impl Lock { } } } - Ok(Lock { + Ok(Self { version, distributions, requires_python, + resolution_mode, + prerelease_mode, + exclude_newer, by_id, }) } @@ -330,6 +351,21 @@ impl Lock { self.requires_python.as_ref() } + /// Returns the resolution mode used to generate this lock. + pub fn resolution_mode(&self) -> ResolutionMode { + self.resolution_mode + } + + /// Returns the pre-release mode used to generate this lock. + pub fn prerelease_mode(&self) -> PreReleaseMode { + self.prerelease_mode + } + + /// Returns the exclude newer setting used to generate this lock. + pub fn exclude_newer(&self) -> Option { + self.exclude_newer + } + /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root. pub fn to_resolution( &self, @@ -419,6 +455,19 @@ impl Lock { doc.insert("requires-python", value(requires_python.to_string())); } + // Write the settings that were used to generate the resolution. + // This enables us to invalidate the lockfile if the user changes + // their settings. + if self.resolution_mode != ResolutionMode::default() { + doc.insert("resolution-mode", value(self.resolution_mode.to_string())); + } + if self.prerelease_mode != PreReleaseMode::default() { + doc.insert("prerelease-mode", value(self.prerelease_mode.to_string())); + } + if let Some(exclude_newer) = self.exclude_newer { + doc.insert("exclude-newer", value(exclude_newer.to_string())); + } + // Count the number of distributions for each package name. When // there's only one distribution for a particular package name (the // overwhelmingly common case), we can omit some data (like source and @@ -522,12 +571,18 @@ impl Lock { } #[derive(Clone, Debug, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] struct LockWire { version: u32, #[serde(rename = "distribution")] distributions: Vec, - #[serde(rename = "requires-python")] requires_python: Option, + #[serde(default)] + resolution_mode: ResolutionMode, + #[serde(default)] + prerelease_mode: PreReleaseMode, + #[serde(default)] + exclude_newer: Option, } impl From for LockWire { @@ -540,6 +595,9 @@ impl From for LockWire { .map(DistributionWire::from) .collect(), requires_python: lock.requires_python, + resolution_mode: lock.resolution_mode, + prerelease_mode: lock.prerelease_mode, + exclude_newer: lock.exclude_newer, } } } @@ -570,7 +628,14 @@ impl TryFrom for Lock { .into_iter() .map(|dist| dist.unwire(&unambiguous_dist_ids)) .collect::, _>>()?; - Lock::new(wire.version, distributions, wire.requires_python) + Lock::new( + wire.version, + distributions, + wire.requires_python, + wire.resolution_mode, + wire.prerelease_mode, + wire.exclude_newer, + ) } } diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index 4b7e9244b360..88dd8131dc03 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -3,7 +3,7 @@ use uv_configuration::IndexStrategy; use crate::{DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; /// Options for resolving a manifest. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] pub struct Options { pub resolution_mode: ResolutionMode, pub prerelease_mode: PreReleaseMode, diff --git a/crates/uv-resolver/src/prerelease_mode.rs b/crates/uv-resolver/src/prerelease_mode.rs index db6cec1255ae..a04b127f2c41 100644 --- a/crates/uv-resolver/src/prerelease_mode.rs +++ b/crates/uv-resolver/src/prerelease_mode.rs @@ -30,6 +30,18 @@ pub enum PreReleaseMode { IfNecessaryOrExplicit, } +impl std::fmt::Display for PreReleaseMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Disallow => write!(f, "disallow"), + Self::Allow => write!(f, "allow"), + Self::IfNecessary => write!(f, "if-necessary"), + Self::Explicit => write!(f, "explicit"), + Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"), + } + } +} + /// Like [`PreReleaseMode`], but with any additional information required to select a candidate, /// like the set of direct dependencies. #[derive(Debug, Clone)] diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index 5aad5d16368f..60b65f40da84 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -22,7 +22,7 @@ use crate::redirect::url_to_precise; use crate::resolution::AnnotatedDist; use crate::resolver::{Resolution, ResolutionPackage}; use crate::{ - InMemoryIndex, MetadataResponse, PythonRequirement, RequiresPython, ResolveError, + InMemoryIndex, MetadataResponse, Options, PythonRequirement, RequiresPython, ResolveError, VersionsResponse, }; @@ -42,6 +42,8 @@ pub struct ResolutionGraph { pub(crate) constraints: Constraints, /// The overrides that were used to build the graph. pub(crate) overrides: Overrides, + /// The options that were used to build the graph. + pub(crate) options: Options, } #[derive(Debug)] @@ -53,6 +55,7 @@ pub(crate) enum ResolutionGraphNode { impl ResolutionGraph { /// Create a new graph from the resolved PubGrub state. pub(crate) fn from_state( + resolution: Resolution, requirements: &[Requirement], constraints: &Constraints, overrides: &Overrides, @@ -60,7 +63,7 @@ impl ResolutionGraph { index: &InMemoryIndex, git: &GitResolver, python: &PythonRequirement, - resolution: Resolution, + options: Options, ) -> Result { type NodeKey<'a> = ( &'a PackageName, @@ -308,6 +311,7 @@ impl ResolutionGraph { requirements: requirements.to_vec(), constraints: constraints.clone(), overrides: overrides.clone(), + options, }) } diff --git a/crates/uv-resolver/src/resolution_mode.rs b/crates/uv-resolver/src/resolution_mode.rs index c1d9dd6779f6..300b83381b62 100644 --- a/crates/uv-resolver/src/resolution_mode.rs +++ b/crates/uv-resolver/src/resolution_mode.rs @@ -5,7 +5,7 @@ use uv_normalize::PackageName; use crate::{DependencyMode, Manifest}; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -20,6 +20,16 @@ pub enum ResolutionMode { LowestDirect, } +impl std::fmt::Display for ResolutionMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Highest => write!(f, "highest"), + Self::Lowest => write!(f, "lowest"), + Self::LowestDirect => write!(f, "lowest-direct"), + } + } +} + /// Like [`ResolutionMode`], but with any additional information required to select a candidate, /// like the set of direct dependencies. #[derive(Debug, Clone)] diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 97f6c5bfe48a..d72948e0b15c 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -116,6 +116,9 @@ struct ResolverState { unavailable_packages: DashMap, /// Incompatibilities for packages that are unavailable at specific versions. incomplete_packages: DashMap>, + /// The options that were used to configure this resolver. + options: Options, + /// The reporter to use for this resolver. reporter: Option>, } @@ -202,8 +205,6 @@ impl let state = ResolverState { index: index.clone(), git: git.clone(), - unavailable_packages: DashMap::default(), - incomplete_packages: DashMap::default(), selector: CandidateSelector::for_resolution( options, &manifest, @@ -236,8 +237,11 @@ impl }, markers, python_requirement: python_requirement.clone(), - reporter: None, installed_packages, + unavailable_packages: DashMap::default(), + incomplete_packages: DashMap::default(), + options, + reporter: None, }; Ok(Self { state, provider }) } @@ -677,6 +681,7 @@ impl ResolverState ResolverState (), + (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() diff --git a/crates/uv/tests/branching_urls.rs b/crates/uv/tests/branching_urls.rs index 969935890fa3..e56219ea2a20 100644 --- a/crates/uv/tests/branching_urls.rs +++ b/crates/uv/tests/branching_urls.rs @@ -208,6 +208,7 @@ fn root_package_splits_transitive_too() -> Result<()> { assert_snapshot!(fs_err::read_to_string(context.temp_dir.join("uv.lock"))?, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" @@ -365,6 +366,7 @@ fn root_package_splits_other_dependencies_too() -> Result<()> { assert_snapshot!(fs_err::read_to_string(context.temp_dir.join("uv.lock"))?, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" @@ -493,6 +495,7 @@ fn branching_between_registry_and_direct_url() -> Result<()> { assert_snapshot!(fs_err::read_to_string(context.temp_dir.join("uv.lock"))?, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" @@ -559,6 +562,7 @@ fn branching_urls_of_different_sources_disjoint() -> Result<()> { assert_snapshot!(fs_err::read_to_string(context.temp_dir.join("uv.lock"))?, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" @@ -667,6 +671,7 @@ fn dont_pre_visit_url_packages() -> Result<()> { assert_snapshot!(fs_err::read_to_string(context.temp_dir.join("uv.lock"))?, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 63bd71ff38c0..5c25bf95ab73 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -68,6 +68,7 @@ fn add_registry() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -220,6 +221,7 @@ fn add_git() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -366,6 +368,7 @@ fn add_git_raw() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -488,6 +491,7 @@ fn add_unnamed() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -580,6 +584,7 @@ fn add_remove_dev() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -695,6 +700,7 @@ fn add_remove_dev() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -776,6 +782,7 @@ fn add_remove_optional() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -852,6 +859,7 @@ fn add_remove_optional() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -975,6 +983,7 @@ fn add_remove_workspace() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "child1" @@ -1047,6 +1056,7 @@ fn add_remove_workspace() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "child1" @@ -1155,6 +1165,7 @@ fn add_workspace_editable() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "child1" @@ -1348,6 +1359,7 @@ fn update() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "certifi" @@ -1555,6 +1567,7 @@ fn add_no_clean() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -1686,6 +1699,7 @@ fn remove_registry() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index ed38362b0ed2..19b783221532 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -65,6 +65,7 @@ fn lock_wheel_registry() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -236,6 +237,7 @@ fn lock_sdist_git() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -307,6 +309,7 @@ fn lock_wheel_url() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -405,6 +408,7 @@ fn lock_sdist_url() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -504,6 +508,7 @@ fn lock_project_extra() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "anyio" @@ -729,6 +734,7 @@ fn lock_dependency_extra() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -915,6 +921,7 @@ fn lock_conditional_dependency_extra() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.7" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "certifi" @@ -1165,6 +1172,7 @@ fn lock_dependency_non_existent_extra() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -1334,6 +1342,7 @@ fn lock_upgrade_log() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -1487,6 +1496,7 @@ fn lock_upgrade_log() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -1636,6 +1646,7 @@ fn lock_upgrade_log_multi_version() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -1806,6 +1817,7 @@ fn lock_upgrade_log_multi_version() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "blinker" @@ -1954,6 +1966,7 @@ fn lock_preference() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -2009,6 +2022,7 @@ fn lock_preference() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -2055,6 +2069,7 @@ fn lock_preference() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -2117,6 +2132,7 @@ fn lock_git_sha() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -2173,6 +2189,7 @@ fn lock_git_sha() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -2213,6 +2230,7 @@ fn lock_git_sha() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "project" @@ -2311,6 +2329,7 @@ fn lock_requires_python() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.7" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -2459,6 +2478,7 @@ fn lock_requires_python() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.7.9" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -2597,6 +2617,7 @@ fn lock_requires_python() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -2726,6 +2747,7 @@ fn lock_requires_python_wheels() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "frozenlist" @@ -2795,6 +2817,7 @@ fn lock_requires_python_wheels() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.11, <3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "frozenlist" @@ -2874,6 +2897,7 @@ fn lock_requires_python_star() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.11, <3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -2983,6 +3007,7 @@ fn lock_requires_python_pre() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.11" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -3091,6 +3116,7 @@ fn lock_requires_python_unbounded() -> Result<()> { lock, @r###" version = 1 requires-python = "<=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -3167,6 +3193,7 @@ fn lock_python_version_marker_complement() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.8" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "attrs" @@ -3251,6 +3278,7 @@ fn lock_dev() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -3351,6 +3379,7 @@ fn lock_conditional_unconditional() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -3412,6 +3441,7 @@ fn lock_multiple_markers() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "iniconfig" @@ -3513,6 +3543,7 @@ fn relative_and_absolute_paths() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.11, <3.13" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "a" @@ -3576,6 +3607,7 @@ fn lock_cycles() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "argparse" @@ -3763,6 +3795,7 @@ fn lock_new_extras() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "certifi" @@ -3872,6 +3905,7 @@ fn lock_new_extras() -> Result<()> { lock, @r###" version = 1 requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" [[distribution]] name = "certifi" @@ -4053,3 +4087,162 @@ fn lock_invalid_hash() -> Result<()> { Ok(()) } + +/// Vary the `--resolution-mode`, and ensure that the lockfile is updated. +#[test] +fn lock_resolution_mode() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio>=3"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[distribution]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[distribution]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[distribution]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [[distribution]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Locking again should be a no-op. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 4 packages in [TIME] + "###); + + // Locking with `lowest-direct` should ignore the existing lockfile. + uv_snapshot!(context.filters(), context.lock().arg("--resolution").arg("lowest-direct"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Ignoring existing lockfile due to change in resolution mode: `highest` vs. `lowest-direct` + Resolved 4 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + resolution-mode = "lowest-direct" + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[distribution]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[distribution]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[distribution]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [[distribution]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +}