Add support for --dry-run mode in uv lock (#7783)
Some checks are pending
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions

This PR adds support for `uv lock --dry-run`, as described in issue
#6408.

One thing to note: this functionality, as implemented, isn't limited to
`-U` (if someone adds a dependency to the project's `pyproject.toml`,
the plan will include these changes).

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
tfsingh 2024-10-23 20:21:55 -07:00 committed by GitHub
parent ede47c0793
commit 98523e2014
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 205 additions and 18 deletions

View file

@ -2903,6 +2903,13 @@ pub struct LockArgs {
#[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")]
pub frozen: bool,
/// Perform a dry run, without writing the lockfile.
///
/// In dry-run mode, uv will resolve the project's dependencies and report on the resulting
/// changes, but will not write the lockfile to disk.
#[arg(long, conflicts_with = "frozen", conflicts_with = "locked")]
pub dry_run: bool,
#[command(flatten)]
pub resolver: ResolverArgs,

View file

@ -661,6 +661,7 @@ async fn lock_and_sync(
let mut lock = project::lock::do_safe_lock(
locked,
frozen,
false,
project.workspace(),
venv.interpreter(),
settings.into(),
@ -775,6 +776,7 @@ async fn lock_and_sync(
lock = project::lock::do_safe_lock(
locked,
frozen,
false,
project.workspace(),
venv.interpreter(),
settings.into(),

View file

@ -95,6 +95,7 @@ pub(crate) async fn export(
let lock = match do_safe_lock(
locked,
frozen,
false,
project.workspace(),
&interpreter,
settings.as_ref(),

View file

@ -70,10 +70,12 @@ impl LockResult {
}
/// Resolve the project requirements into a lockfile.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn lock(
project_dir: &Path,
locked: bool,
frozen: bool,
dry_run: bool,
python: Option<String>,
settings: ResolverSettings,
python_preference: PythonPreference,
@ -108,6 +110,7 @@ pub(crate) async fn lock(
match do_safe_lock(
locked,
frozen,
dry_run,
&workspace,
&interpreter,
settings.as_ref(),
@ -123,9 +126,25 @@ pub(crate) async fn lock(
.await
{
Ok(lock) => {
if let LockResult::Changed(Some(previous), lock) = &lock {
report_upgrades(previous, lock, printer)?;
if dry_run {
let changed = if let LockResult::Changed(previous, lock) = &lock {
report_upgrades(previous.as_ref(), lock, printer, dry_run)?
} else {
false
};
if !changed {
writeln!(
printer.stderr(),
"{}",
"No lockfile changes detected".bold()
)?;
}
} else {
if let LockResult::Changed(Some(previous), lock) = &lock {
report_upgrades(Some(previous), lock, printer, dry_run)?;
}
}
Ok(ExitStatus::Success)
}
Err(ProjectError::Operation(pip::operations::Error::Resolve(
@ -152,9 +171,11 @@ pub(crate) async fn lock(
}
/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
#[allow(clippy::fn_params_excessive_bools)]
pub(super) async fn do_safe_lock(
locked: bool,
frozen: bool,
dry_run: bool,
workspace: &Workspace,
interpreter: &Interpreter,
settings: ResolverSettingsRef<'_>,
@ -224,8 +245,10 @@ pub(super) async fn do_safe_lock(
.await?;
// If the lockfile changed, write it to disk.
if let LockResult::Changed(_, lock) = &result {
commit(lock, workspace).await?;
if !dry_run {
if let LockResult::Changed(_, lock) = &result {
commit(lock, workspace).await?;
}
}
Ok(result)
@ -916,17 +939,28 @@ pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectE
}
/// Reports on the versions that were upgraded in the new lockfile.
fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> anyhow::Result<()> {
///
/// Returns `true` if any upgrades were reported.
fn report_upgrades(
existing_lock: Option<&Lock>,
new_lock: &Lock,
printer: Printer,
dry_run: bool,
) -> anyhow::Result<bool> {
let existing_packages: FxHashMap<&PackageName, BTreeSet<&Version>> =
existing_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|mut acc, package| {
acc.entry(package.name())
.or_default()
.insert(package.version());
acc
},
);
if let Some(existing_lock) = existing_lock {
existing_lock.packages().iter().fold(
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|mut acc, package| {
acc.entry(package.name())
.or_default()
.insert(package.version());
acc
},
)
} else {
FxHashMap::default()
};
let new_distributions: FxHashMap<&PackageName, BTreeSet<&Version>> =
new_lock.packages().iter().fold(
@ -939,11 +973,13 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
},
);
let mut updated = false;
for name in existing_packages
.keys()
.chain(new_distributions.keys())
.collect::<BTreeSet<_>>()
{
updated = true;
match (existing_packages.get(name), new_distributions.get(name)) {
(Some(existing_versions), Some(new_versions)) => {
if existing_versions != new_versions {
@ -960,7 +996,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
writeln!(
printer.stderr(),
"{} {name} {existing_versions} -> {new_versions}",
"Updated".green().bold()
if dry_run { "Update" } else { "Updated" }.green().bold()
)?;
}
}
@ -973,7 +1009,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
writeln!(
printer.stderr(),
"{} {name} {existing_versions}",
"Removed".red().bold()
if dry_run { "Remove" } else { "Removed" }.red().bold()
)?;
}
(None, Some(new_versions)) => {
@ -985,7 +1021,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
writeln!(
printer.stderr(),
"{} {name} {new_versions}",
"Added".green().bold()
if dry_run { "Add" } else { "Added" }.green().bold()
)?;
}
(None, None) => {
@ -994,5 +1030,5 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
}
}
Ok(())
Ok(updated)
}

View file

@ -175,6 +175,7 @@ pub(crate) async fn remove(
let lock = project::lock::do_safe_lock(
locked,
frozen,
false,
project.workspace(),
venv.interpreter(),
settings.as_ref().into(),

View file

@ -543,6 +543,7 @@ pub(crate) async fn run(
let result = match project::lock::do_safe_lock(
locked,
frozen,
false,
project.workspace(),
venv.interpreter(),
settings.as_ref().into(),

View file

@ -112,6 +112,7 @@ pub(crate) async fn sync(
let lock = match do_safe_lock(
locked,
frozen,
false,
target.workspace(),
venv.interpreter(),
settings.as_ref().into(),

View file

@ -67,6 +67,7 @@ pub(crate) async fn tree(
let lock = project::lock::do_safe_lock(
locked,
frozen,
false,
&workspace,
&interpreter,
settings.as_ref(),

View file

@ -1352,6 +1352,7 @@ async fn run_project(
project_dir,
args.locked,
args.frozen,
args.dry_run,
args.python,
args.settings,
globals.python_preference,

View file

@ -767,6 +767,7 @@ impl SyncSettings {
pub(crate) struct LockSettings {
pub(crate) locked: bool,
pub(crate) frozen: bool,
pub(crate) dry_run: bool,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverSettings,
@ -779,6 +780,7 @@ impl LockSettings {
let LockArgs {
locked,
frozen,
dry_run,
resolver,
build,
refresh,
@ -788,6 +790,7 @@ impl LockSettings {
Self {
locked,
frozen,
dry_run,
python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh),
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),

View file

@ -16078,3 +16078,132 @@ fn lock_multiple_sources_extra() -> Result<()> {
Ok(())
}
#[test]
fn lock_dry_run() -> 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 ; python_version == '3.12'",
"anyio >3, <4 ; python_version > '3.12'",
"matplotlib==3.1.0"
]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 12 packages in [TIME]
"###);
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"requests==2.25.1",
"matplotlib==3.5.0"
]
"#,
)?;
uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 19 packages in [TIME]
Remove anyio v2.2.0, v3.7.1
Add certifi v2024.2.2
Add chardet v4.0.0
Add fonttools v4.50.0
Update idna v3.6 -> v2.10
Update matplotlib v3.1.0 -> v3.5.0
Add packaging v24.0
Add pillow v10.2.0
Add requests v2.25.1
Add setuptools v69.2.0
Add setuptools-scm v8.0.4
Remove sniffio v1.3.1
Add typing-extensions v4.10.0
Add urllib3 v1.26.18
"###);
Ok(())
}
#[test]
fn lock_dry_run_noop() -> 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 ; python_version == '3.12'",
"anyio >3, <4 ; python_version > '3.12'",
]
"#,
)?;
uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Add anyio v2.2.0, v3.7.1
Add idna v3.6
Add project v0.1.0
Add sniffio v1.3.1
"###);
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.lock().arg("--dry-run"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
No lockfile changes detected
"###);
uv_snapshot!(context.filters(), context.lock().arg("--dry-run").arg("-U"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
"###);
Ok(())
}

View file

@ -1706,6 +1706,10 @@ uv lock [OPTIONS]
<p>See <code>--project</code> to only change the project root directory.</p>
</dd><dt><code>--dry-run</code></dt><dd><p>Perform a dry run, without writing the lockfile.</p>
<p>In dry-run mode, uv will resolve the project&#8217;s dependencies and report on the resulting changes, but will not write the lockfile to disk.</p>
</dd><dt><code>--exclude-newer</code> <i>exclude-newer</i></dt><dd><p>Limit candidate packages to those that were uploaded prior to the given date.</p>
<p>Accepts both RFC 3339 timestamps (e.g., <code>2006-12-02T02:07:43Z</code>) and local dates in the same format (e.g., <code>2006-12-02</code>) in your system&#8217;s configured time zone.</p>