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.
This commit is contained in:
Charlie Marsh 2024-07-22 08:28:22 -04:00 committed by GitHub
parent 178300d16b
commit 5a23f05799
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 406 additions and 16 deletions

View file

@ -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))]

View file

@ -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]

View file

@ -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<Utc>);
impl ExcludeNewer {

View file

@ -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<Distribution>,
/// The range of supported Python versions.
requires_python: Option<RequiresPython>,
/// 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<ExcludeNewer>,
/// 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<Distribution>,
requires_python: Option<RequiresPython>,
resolution_mode: ResolutionMode,
prerelease_mode: PreReleaseMode,
exclude_newer: Option<ExcludeNewer>,
) -> Result<Self, LockError> {
// 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<ExcludeNewer> {
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<DistributionWire>,
#[serde(rename = "requires-python")]
requires_python: Option<RequiresPython>,
#[serde(default)]
resolution_mode: ResolutionMode,
#[serde(default)]
prerelease_mode: PreReleaseMode,
#[serde(default)]
exclude_newer: Option<ExcludeNewer>,
}
impl From<Lock> for LockWire {
@ -540,6 +595,9 @@ impl From<Lock> 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<LockWire> for Lock {
.into_iter()
.map(|dist| dist.unwire(&unambiguous_dist_ids))
.collect::<Result<Vec<_>, _>>()?;
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,
)
}
}

View file

@ -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,

View file

@ -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)]

View file

@ -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<Self, ResolveError> {
type NodeKey<'a> = (
&'a PackageName,
@ -308,6 +311,7 @@ impl ResolutionGraph {
requirements: requirements.to_vec(),
constraints: constraints.clone(),
overrides: overrides.clone(),
options,
})
}

View file

@ -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)]

View file

@ -116,6 +116,9 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
unavailable_packages: DashMap<PackageName, UnavailablePackage>,
/// Incompatibilities for packages that are unavailable at specific versions.
incomplete_packages: DashMap<PackageName, DashMap<Version, IncompletePackage>>,
/// The options that were used to configure this resolver.
options: Options,
/// The reporter to use for this resolver.
reporter: Option<Arc<dyn Reporter>>,
}
@ -202,8 +205,6 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
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<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
},
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<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
}
ResolutionGraph::from_state(
combined,
&self.requirements,
&self.constraints,
&self.overrides,
@ -684,7 +689,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&self.index,
&self.git,
&self.python_requirement,
combined,
self.options,
)
}

View file

@ -62,6 +62,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -69,6 +69,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -55,6 +55,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -43,6 +43,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -41,6 +41,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -24,6 +24,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -24,6 +24,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(

View file

@ -267,6 +267,58 @@ pub(super) async fn do_lock(
FlatIndex::from_entries(entries, None, &hasher, build_options)
};
// 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 prerelease 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()

View file

@ -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"

View file

@ -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"

View file

@ -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(())
}