mirror of https://github.com/astral-sh/uv
Add support for `--dry-run` mode in `uv lock` (#7783)
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:
parent
ede47c0793
commit
98523e2014
|
|
@ -2903,6 +2903,13 @@ pub struct LockArgs {
|
||||||
#[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")]
|
#[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "locked")]
|
||||||
pub frozen: bool,
|
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)]
|
#[command(flatten)]
|
||||||
pub resolver: ResolverArgs,
|
pub resolver: ResolverArgs,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -661,6 +661,7 @@ async fn lock_and_sync(
|
||||||
let mut lock = project::lock::do_safe_lock(
|
let mut lock = project::lock::do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
venv.interpreter(),
|
venv.interpreter(),
|
||||||
settings.into(),
|
settings.into(),
|
||||||
|
|
@ -775,6 +776,7 @@ async fn lock_and_sync(
|
||||||
lock = project::lock::do_safe_lock(
|
lock = project::lock::do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
venv.interpreter(),
|
venv.interpreter(),
|
||||||
settings.into(),
|
settings.into(),
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ pub(crate) async fn export(
|
||||||
let lock = match do_safe_lock(
|
let lock = match do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
&interpreter,
|
&interpreter,
|
||||||
settings.as_ref(),
|
settings.as_ref(),
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,12 @@ impl LockResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the project requirements into a lockfile.
|
/// Resolve the project requirements into a lockfile.
|
||||||
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
pub(crate) async fn lock(
|
pub(crate) async fn lock(
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
frozen: bool,
|
frozen: bool,
|
||||||
|
dry_run: bool,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
settings: ResolverSettings,
|
settings: ResolverSettings,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
|
|
@ -108,6 +110,7 @@ pub(crate) async fn lock(
|
||||||
match do_safe_lock(
|
match do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
dry_run,
|
||||||
&workspace,
|
&workspace,
|
||||||
&interpreter,
|
&interpreter,
|
||||||
settings.as_ref(),
|
settings.as_ref(),
|
||||||
|
|
@ -123,9 +126,25 @@ pub(crate) async fn lock(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(lock) => {
|
Ok(lock) => {
|
||||||
if let LockResult::Changed(Some(previous), lock) = &lock {
|
if dry_run {
|
||||||
report_upgrades(previous, lock, printer)?;
|
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)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
Err(ProjectError::Operation(pip::operations::Error::Resolve(
|
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.
|
/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
|
||||||
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
pub(super) async fn do_safe_lock(
|
pub(super) async fn do_safe_lock(
|
||||||
locked: bool,
|
locked: bool,
|
||||||
frozen: bool,
|
frozen: bool,
|
||||||
|
dry_run: bool,
|
||||||
workspace: &Workspace,
|
workspace: &Workspace,
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
settings: ResolverSettingsRef<'_>,
|
settings: ResolverSettingsRef<'_>,
|
||||||
|
|
@ -224,9 +245,11 @@ pub(super) async fn do_safe_lock(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// If the lockfile changed, write it to disk.
|
// If the lockfile changed, write it to disk.
|
||||||
|
if !dry_run {
|
||||||
if let LockResult::Changed(_, lock) = &result {
|
if let LockResult::Changed(_, lock) = &result {
|
||||||
commit(lock, workspace).await?;
|
commit(lock, workspace).await?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
@ -916,8 +939,16 @@ pub(crate) async fn read(workspace: &Workspace) -> Result<Option<Lock>, ProjectE
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports on the versions that were upgraded in the new lockfile.
|
/// 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>> =
|
let existing_packages: FxHashMap<&PackageName, BTreeSet<&Version>> =
|
||||||
|
if let Some(existing_lock) = existing_lock {
|
||||||
existing_lock.packages().iter().fold(
|
existing_lock.packages().iter().fold(
|
||||||
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|
FxHashMap::with_capacity_and_hasher(existing_lock.packages().len(), FxBuildHasher),
|
||||||
|mut acc, package| {
|
|mut acc, package| {
|
||||||
|
|
@ -926,7 +957,10 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
|
||||||
.insert(package.version());
|
.insert(package.version());
|
||||||
acc
|
acc
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
} else {
|
||||||
|
FxHashMap::default()
|
||||||
|
};
|
||||||
|
|
||||||
let new_distributions: FxHashMap<&PackageName, BTreeSet<&Version>> =
|
let new_distributions: FxHashMap<&PackageName, BTreeSet<&Version>> =
|
||||||
new_lock.packages().iter().fold(
|
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
|
for name in existing_packages
|
||||||
.keys()
|
.keys()
|
||||||
.chain(new_distributions.keys())
|
.chain(new_distributions.keys())
|
||||||
.collect::<BTreeSet<_>>()
|
.collect::<BTreeSet<_>>()
|
||||||
{
|
{
|
||||||
|
updated = true;
|
||||||
match (existing_packages.get(name), new_distributions.get(name)) {
|
match (existing_packages.get(name), new_distributions.get(name)) {
|
||||||
(Some(existing_versions), Some(new_versions)) => {
|
(Some(existing_versions), Some(new_versions)) => {
|
||||||
if existing_versions != new_versions {
|
if existing_versions != new_versions {
|
||||||
|
|
@ -960,7 +996,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{} {name} {existing_versions} -> {new_versions}",
|
"{} {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!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{} {name} {existing_versions}",
|
"{} {name} {existing_versions}",
|
||||||
"Removed".red().bold()
|
if dry_run { "Remove" } else { "Removed" }.red().bold()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
(None, Some(new_versions)) => {
|
(None, Some(new_versions)) => {
|
||||||
|
|
@ -985,7 +1021,7 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{} {name} {new_versions}",
|
"{} {name} {new_versions}",
|
||||||
"Added".green().bold()
|
if dry_run { "Add" } else { "Added" }.green().bold()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
|
|
@ -994,5 +1030,5 @@ fn report_upgrades(existing_lock: &Lock, new_lock: &Lock, printer: Printer) -> a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(updated)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@ pub(crate) async fn remove(
|
||||||
let lock = project::lock::do_safe_lock(
|
let lock = project::lock::do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
venv.interpreter(),
|
venv.interpreter(),
|
||||||
settings.as_ref().into(),
|
settings.as_ref().into(),
|
||||||
|
|
|
||||||
|
|
@ -543,6 +543,7 @@ pub(crate) async fn run(
|
||||||
let result = match project::lock::do_safe_lock(
|
let result = match project::lock::do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
venv.interpreter(),
|
venv.interpreter(),
|
||||||
settings.as_ref().into(),
|
settings.as_ref().into(),
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ pub(crate) async fn sync(
|
||||||
let lock = match do_safe_lock(
|
let lock = match do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
target.workspace(),
|
target.workspace(),
|
||||||
venv.interpreter(),
|
venv.interpreter(),
|
||||||
settings.as_ref().into(),
|
settings.as_ref().into(),
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ pub(crate) async fn tree(
|
||||||
let lock = project::lock::do_safe_lock(
|
let lock = project::lock::do_safe_lock(
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
false,
|
||||||
&workspace,
|
&workspace,
|
||||||
&interpreter,
|
&interpreter,
|
||||||
settings.as_ref(),
|
settings.as_ref(),
|
||||||
|
|
|
||||||
|
|
@ -1352,6 +1352,7 @@ async fn run_project(
|
||||||
project_dir,
|
project_dir,
|
||||||
args.locked,
|
args.locked,
|
||||||
args.frozen,
|
args.frozen,
|
||||||
|
args.dry_run,
|
||||||
args.python,
|
args.python,
|
||||||
args.settings,
|
args.settings,
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
|
|
|
||||||
|
|
@ -767,6 +767,7 @@ impl SyncSettings {
|
||||||
pub(crate) struct LockSettings {
|
pub(crate) struct LockSettings {
|
||||||
pub(crate) locked: bool,
|
pub(crate) locked: bool,
|
||||||
pub(crate) frozen: bool,
|
pub(crate) frozen: bool,
|
||||||
|
pub(crate) dry_run: bool,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) settings: ResolverSettings,
|
pub(crate) settings: ResolverSettings,
|
||||||
|
|
@ -779,6 +780,7 @@ impl LockSettings {
|
||||||
let LockArgs {
|
let LockArgs {
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
dry_run,
|
||||||
resolver,
|
resolver,
|
||||||
build,
|
build,
|
||||||
refresh,
|
refresh,
|
||||||
|
|
@ -788,6 +790,7 @@ impl LockSettings {
|
||||||
Self {
|
Self {
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
|
dry_run,
|
||||||
python: python.and_then(Maybe::into_option),
|
python: python.and_then(Maybe::into_option),
|
||||||
refresh: Refresh::from(refresh),
|
refresh: Refresh::from(refresh),
|
||||||
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
||||||
|
|
|
||||||
|
|
@ -16078,3 +16078,132 @@ fn lock_multiple_sources_extra() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1706,6 +1706,10 @@ uv lock [OPTIONS]
|
||||||
|
|
||||||
<p>See <code>--project</code> to only change the project root directory.</p>
|
<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’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>
|
</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’s configured time zone.</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’s configured time zone.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue