Add `uv sync --check` flag (#12342)

## Summary

Closes #12338 

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Ahmed Ilyas 2025-03-21 16:48:27 +01:00 committed by GitHub
parent 26d40cb8a5
commit a80353de2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 3 deletions

View File

@ -3285,6 +3285,15 @@ pub struct SyncArgs {
value_parser = parse_maybe_string,
)]
pub python: Option<Maybe<String>>,
/// Check if the Python environment is synchronized with the project.
///
/// If the environment is not up to date, uv will exit with an error.
#[arg(long, overrides_with("no_check"))]
pub check: bool,
#[arg(long, overrides_with("check"), hide = true)]
pub no_check: bool,
}
#[derive(Args)]

View File

@ -2,6 +2,9 @@
pub enum DryRun {
/// The operation should execute in dry run mode.
Enabled,
/// The operation should execute in dry run mode and check if the current environment is
/// synced.
Check,
/// The operation should execute in normal mode.
#[default]
Disabled,
@ -19,6 +22,6 @@ impl DryRun {
/// Returns `true` if dry run mode is enabled.
pub const fn enabled(&self) -> bool {
matches!(self, DryRun::Enabled)
matches!(self, DryRun::Enabled) || matches!(self, DryRun::Check)
}
}

View File

@ -438,7 +438,7 @@ pub(crate) async fn install(
.context("Failed to determine installation plan")?;
if dry_run.enabled() {
report_dry_run(resolution, plan, modifications, start, printer)?;
report_dry_run(dry_run, resolution, plan, modifications, start, printer)?;
return Ok(Changelog::default());
}
@ -665,6 +665,7 @@ pub(crate) fn report_target_environment(
/// Report on the results of a dry-run installation.
fn report_dry_run(
dry_run: DryRun,
resolution: &Resolution,
plan: Plan,
modifications: Modifications,
@ -788,6 +789,10 @@ fn report_dry_run(
}
}
if matches!(dry_run, DryRun::Check) {
return Err(Error::OutdatedEnvironment);
}
Ok(())
}
@ -859,4 +864,7 @@ pub(crate) enum Error {
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("The environment is outdated; run `{}` to update the environment", "uv sync".cyan())]
OutdatedEnvironment,
}

View File

@ -1075,6 +1075,8 @@ impl SyncSettings {
package,
script,
python,
check,
no_check,
} = args;
let install_mirrors = filesystem
.clone()
@ -1086,10 +1088,17 @@ impl SyncSettings {
filesystem,
);
let check = flag(check, no_check).unwrap_or_default();
let dry_run = if check {
DryRun::Check
} else {
DryRun::from_args(dry_run)
};
Self {
locked,
frozen,
dry_run: DryRun::from_args(dry_run),
dry_run,
script,
active: flag(active, no_active),
extras: ExtrasSpecification::from_args(

View File

@ -352,6 +352,68 @@ fn mixed_requires_python() -> Result<()> {
Ok(())
}
#[test]
fn check() -> 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 = ["iniconfig"]
"#,
)?;
// Running `uv sync --check` should fail.
uv_snapshot!(context.filters(), context.sync().arg("--check"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Discovered existing environment at: .venv
Resolved 2 packages in [TIME]
Would create lockfile at: uv.lock
Would download 1 package
Would install 1 package
+ iniconfig==2.0.0
error: The environment is outdated; run `uv sync` to update the environment
"###);
// Sync the environment.
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
assert!(context.temp_dir.child("uv.lock").exists());
// Running `uv sync --check` should pass now that the environment is up to date.
uv_snapshot!(context.filters(), context.sync().arg("--check"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Discovered existing environment at: .venv
Resolved 2 packages in [TIME]
Found up-to-date lockfile at: uv.lock
Audited 1 package in [TIME]
Would make no changes
"###);
Ok(())
}
/// Sync development dependencies in a (legacy) non-project workspace root.
#[test]
fn sync_legacy_non_project_dev_dependencies() -> Result<()> {

View File

@ -1513,6 +1513,10 @@ uv sync [OPTIONS]
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p>
</dd><dt id="uv-sync--check"><a href="#uv-sync--check"><code>--check</code></a></dt><dd><p>Check if the Python environment is synchronized with the project.</p>
<p>If the environment is not up to date, uv will exit with an error.</p>
</dd><dt id="uv-sync--color"><a href="#uv-sync--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
<p>By default, uv will automatically detect support for colors when writing to a terminal.</p>