Skip to content

Commit

Permalink
Add support for --dry-run mode in uv lock (#7783)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
tfsingh and charliermarsh authored Oct 24, 2024
1 parent ede47c0 commit 98523e2
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 18 deletions.
7 changes: 7 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -775,6 +776,7 @@ async fn lock_and_sync(
lock = project::lock::do_safe_lock(
locked,
frozen,
false,
project.workspace(),
venv.interpreter(),
settings.into(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub(crate) async fn export(
let lock = match do_safe_lock(
locked,
frozen,
false,
project.workspace(),
&interpreter,
settings.as_ref(),
Expand Down
72 changes: 54 additions & 18 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -108,6 +110,7 @@ pub(crate) async fn lock(
match do_safe_lock(
locked,
frozen,
dry_run,
&workspace,
&interpreter,
settings.as_ref(),
Expand All @@ -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(
Expand All @@ -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<'_>,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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 {
Expand All @@ -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()
)?;
}
}
Expand All @@ -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)) => {
Expand All @@ -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) => {
Expand All @@ -994,5 +1030,5 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
}
}

Ok(())
Ok(updated)
}
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub(crate) async fn tree(
let lock = project::lock::do_safe_lock(
locked,
frozen,
false,
&workspace,
&interpreter,
settings.as_ref(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,7 @@ async fn run_project(
project_dir,
args.locked,
args.frozen,
args.dry_run,
args.python,
args.settings,
globals.python_preference,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -779,6 +780,7 @@ impl LockSettings {
let LockArgs {
locked,
frozen,
dry_run,
resolver,
build,
refresh,
Expand All @@ -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),
Expand Down
129 changes: 129 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Loading

0 comments on commit 98523e2

Please sign in to comment.