From 179450c93345823c17cc26dbc92e7e0b1c124e5f Mon Sep 17 00:00:00 2001 From: x0rw Date: Sun, 23 Mar 2025 04:55:45 +0000 Subject: [PATCH 01/22] Add --format=Json | Text --- crates/uv-cli/src/lib.rs | 12 + crates/uv/src/commands/project/sync.rs | 300 ++++++++++++++++++------- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + 4 files changed, 232 insertions(+), 84 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0dac96000f368..58295ee95d29d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -45,6 +45,15 @@ pub enum PythonListFormat { Json, } +#[derive(Debug, Default, Clone, clap::ValueEnum)] +pub enum SyncFormat { + /// Display as a text + #[default] + Text, + /// Display as json + Json, +} + #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum ListFormat { /// Display the list of packages in a human-readable table. @@ -3187,6 +3196,9 @@ pub struct SyncArgs { #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, + /// Select the output format. + #[arg(long, value_enum, default_value_t = SyncFormat::default())] + pub format: SyncFormat, /// Include all optional dependencies. /// /// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 8e78836b464b0..7f6da3fb889c1 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,6 +1,6 @@ use std::fmt::Write; use std::ops::Deref; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context, Result}; @@ -46,6 +46,18 @@ use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; +/// JSON. +#[derive(Debug, Serialize)] +struct SyncEntry { + project_dir: PathBuf, + environment: Environment, +} +#[derive(Serialize, Debug)] +struct Environment { + path: PathBuf, + action: String, +} + /// Sync the project environment. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn sync( @@ -74,6 +86,7 @@ pub(crate) async fn sync( cache: &Cache, printer: Printer, preview: PreviewMode, + format: SyncFormat, ) -> Result { // Identify the target. let workspace_cache = WorkspaceCache::default(); @@ -166,49 +179,11 @@ pub(crate) async fn sync( ), }; - // Notify the user of any environment changes. - match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) - if dry_run.enabled() => - { - writeln!( - printer.stderr(), - "{}", - format!( - "Discovered existing environment at: {}", - environment.root().user_display().bold() - ) - .dimmed() - )?; - } - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing virtual environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create virtual environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } - SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { - if dry_run.enabled() { + match format { + SyncFormat::Text => match &environment { + SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) + if dry_run.enabled() => + { writeln!( printer.stderr(), "{}", @@ -218,52 +193,209 @@ pub(crate) async fn sync( ) .dimmed() )?; - } else { + } + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) + if dry_run.enabled() => + { + writeln!( + printer.stderr(), + "{}", + format!( + "Would replace existing virtual environment at: {}", + root.user_display().bold() + ) + .dimmed() + )?; + } + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) + if dry_run.enabled() => + { writeln!( printer.stderr(), - "Using script environment at: {}", + "{}", + format!( + "Would create virtual environment at: {}", + root.user_display().bold() + ) + .dimmed() + )?; + } + SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { + if dry_run.enabled() { + writeln!( + printer.stderr(), + "{}", + format!( + "Discovered existing environment at: {}", + environment.root().user_display().bold() + ) + .dimmed() + )?; + } else { + writeln!( + printer.stderr(), + "Using script environment at: {}", + environment.root().user_display().cyan() + )?; + } + } + SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) + if !dry_run.enabled() => + { + writeln!( + printer.stderr(), + "Recreating script environment at: {}", environment.root().user_display().cyan() )?; } - } - SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) if !dry_run.enabled() => { - writeln!( - printer.stderr(), - "Recreating script environment at: {}", - environment.root().user_display().cyan() - )?; - } - SyncEnvironment::Script(ScriptEnvironment::Created(environment)) if !dry_run.enabled() => { - writeln!( - printer.stderr(), - "Creating script environment at: {}", - environment.root().user_display().cyan() - )?; - } - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing script environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create script environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } - _ => {} + SyncEnvironment::Script(ScriptEnvironment::Created(environment)) + if !dry_run.enabled() => + { + writeln!( + printer.stderr(), + "Creating script environment at: {}", + environment.root().user_display().cyan() + )?; + } + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) + if dry_run.enabled() => + { + writeln!( + printer.stderr(), + "{}", + format!( + "Would replace existing script environment at: {}", + root.user_display().bold() + ) + .dimmed() + )?; + } + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) + if dry_run.enabled() => + { + writeln!( + printer.stderr(), + "{}", + format!( + "Would create script environment at: {}", + root.user_display().bold() + ) + .dimmed() + )?; + } + _ => {} + }, + + SyncFormat::Json => match &environment { + SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) + if dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Discovered".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) + if dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Replace".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) + if dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: root.to_owned(), + action: "Replace".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { + if dry_run.enabled() { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Discovered".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } else { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Using".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + } + SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) + if !dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Recreating".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Script(ScriptEnvironment::Created(environment)) + if !dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: environment.root().to_owned(), + action: "Creating".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) + if dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: root.to_owned(), + action: "Replace_existing".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) + if dry_run.enabled() => + { + let sync_json = SyncEntry { + project_dir: project_dir.to_owned(), + environment: Environment { + path: root.to_owned(), + action: "Create".to_string(), + }, + }; + writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; + } + _ => {} + }, } + // Notify the user of any environment changes. // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, // we don't create a lockfile, so the resolve-and-install semantics are different. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 7cc125c813004..07b84237721fe 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1764,6 +1764,7 @@ async fn run_project( &cache, printer, globals.preview, + args.format, )) .await } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 007fdc8725a54..0e855bd441001 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1095,6 +1095,7 @@ pub(crate) struct SyncSettings { pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, + pub(crate) format: SyncFormat, } impl SyncSettings { @@ -1134,6 +1135,7 @@ impl SyncSettings { python, check, no_check, + format, } = args; let install_mirrors = filesystem .clone() @@ -1153,6 +1155,7 @@ impl SyncSettings { }; Self { + format, locked, frozen, dry_run, From 55eea25383b639df75ae18db5c5bfdbdde178c49 Mon Sep 17 00:00:00 2001 From: x0rw Date: Sun, 23 Mar 2025 05:29:11 +0000 Subject: [PATCH 02/22] Replace env actions strings with enums --- crates/uv/src/commands/project/sync.rs | 34 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 7f6da3fb889c1..2a533d0b8ce34 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -52,10 +52,22 @@ struct SyncEntry { project_dir: PathBuf, environment: Environment, } + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +enum DryRunAction { + DiscoveredExisting, + ReplacingExistingVenv, + CreatingVenv, + UsingScriptEnv, + RecreatingScriptEnv, + ReplacingExistingScriptVenv, + CreatingScriptEnv, +} #[derive(Serialize, Debug)] struct Environment { path: PathBuf, - action: String, + action: DryRunAction, } /// Sync the project environment. @@ -294,7 +306,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: environment.root().to_owned(), - action: "Discovered".to_string(), + action: DryRunAction::DiscoveredExisting, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -305,8 +317,8 @@ pub(crate) async fn sync( let sync_json = SyncEntry { project_dir: project_dir.to_owned(), environment: Environment { - path: environment.root().to_owned(), - action: "Replace".to_string(), + path: root.to_owned(), + action: DryRunAction::ReplacingExistingVenv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -318,7 +330,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: root.to_owned(), - action: "Replace".to_string(), + action: DryRunAction::CreatingVenv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -329,7 +341,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: environment.root().to_owned(), - action: "Discovered".to_string(), + action: DryRunAction::DiscoveredExisting, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -338,7 +350,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: environment.root().to_owned(), - action: "Using".to_string(), + action: DryRunAction::UsingScriptEnv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -351,7 +363,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: environment.root().to_owned(), - action: "Recreating".to_string(), + action: DryRunAction::RecreatingScriptEnv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -363,7 +375,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: environment.root().to_owned(), - action: "Creating".to_string(), + action: DryRunAction::CreatingScriptEnv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -375,7 +387,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: root.to_owned(), - action: "Replace_existing".to_string(), + action: DryRunAction::ReplacingExistingScriptVenv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; @@ -387,7 +399,7 @@ pub(crate) async fn sync( project_dir: project_dir.to_owned(), environment: Environment { path: root.to_owned(), - action: "Create".to_string(), + action: DryRunAction::CreatingScriptEnv, }, }; writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; From a6738ec665efee6bfc9a2d50940eec6e21d46d02 Mon Sep 17 00:00:00 2001 From: x0rw Date: Sun, 23 Mar 2025 22:40:00 +0000 Subject: [PATCH 03/22] Refactor, proper design, improve design to support future JSON logging --- crates/uv-cli/src/lib.rs | 7 +- crates/uv/src/commands/project/sync.rs | 259 +++++++++++-------------- 2 files changed, 115 insertions(+), 151 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 58295ee95d29d..035714bb3d5be 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -53,6 +53,11 @@ pub enum SyncFormat { /// Display as json Json, } +impl SyncFormat { + pub fn is_json(&self) -> bool { + matches!(self, SyncFormat::Json) + } +} #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum ListFormat { @@ -3197,7 +3202,7 @@ pub struct SyncArgs { pub extra: Option>, /// Select the output format. - #[arg(long, value_enum, default_value_t = SyncFormat::default())] + #[arg(long, value_enum, requires = "dry_run", default_value_t = SyncFormat::default())] pub format: SyncFormat, /// Include all optional dependencies. /// diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 2a533d0b8ce34..cb7868b49ca5a 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -46,13 +46,16 @@ use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; -/// JSON. +/// Represents an entry for synchronization, formatted as JSON. #[derive(Debug, Serialize)] struct SyncEntry { + /// The project directory path. project_dir: PathBuf, + /// The environment details, including path and action. environment: Environment, } +/// Represents the action taken during a dry run. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum DryRunAction { @@ -63,13 +66,44 @@ enum DryRunAction { RecreatingScriptEnv, ReplacingExistingScriptVenv, CreatingScriptEnv, + /// No action is being taken(should this ever happen?). + None, } + #[derive(Serialize, Debug)] struct Environment { + /// The path to the environment. path: PathBuf, + /// The action taken for the environment. action: DryRunAction, } +impl SyncEntry { + /// Creates a new `SyncEntry` with the given project directory. + fn new(project_dir: &Path) -> Self { + Self { + project_dir: project_dir.to_owned(), + environment: Environment { + path: PathBuf::new(), + action: DryRunAction::None, + }, + } + } + /// Sets the environment path and action. + fn set_env_path(&mut self, path: PathBuf, action: DryRunAction) { + self.environment.path = path; + self.environment.action = action; + } + + /// Serializes the `SyncEntry` into a JSON string. + fn as_json(&self) -> Result { + serde_json::to_string(self) + } + fn as_pretty_json(&self) -> Result { + serde_json::to_string_pretty(self) + } +} + /// Sync the project environment. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn sync( @@ -191,11 +225,18 @@ pub(crate) async fn sync( ), }; - match format { - SyncFormat::Text => match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) - if dry_run.enabled() => - { + let mut sync_json = SyncEntry::new(project_dir); + match &environment { + SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) + if dry_run.enabled() => + { + // if + if format.is_json() { + sync_json.set_env_path( + environment.root().to_owned(), + DryRunAction::DiscoveredExisting, + ); + } else { writeln!( printer.stderr(), "{}", @@ -206,9 +247,13 @@ pub(crate) async fn sync( .dimmed() )?; } - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { + } + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) + if dry_run.enabled() => + { + if format.is_json() { + sync_json.set_env_path(root.to_owned(), DryRunAction::ReplacingExistingVenv); + } else { writeln!( printer.stderr(), "{}", @@ -219,9 +264,13 @@ pub(crate) async fn sync( .dimmed() )?; } - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { + } + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) + if dry_run.enabled() => + { + if format.is_json() { + sync_json.set_env_path(root.to_owned(), DryRunAction::CreatingVenv); + } else { writeln!( printer.stderr(), "{}", @@ -232,8 +281,15 @@ pub(crate) async fn sync( .dimmed() )?; } - SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { - if dry_run.enabled() { + } + SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { + if dry_run.enabled() { + if format.is_json() { + sync_json.set_env_path( + environment.root().to_owned(), + DryRunAction::DiscoveredExisting, + ); + } else { writeln!( printer.stderr(), "{}", @@ -243,35 +299,36 @@ pub(crate) async fn sync( ) .dimmed() )?; - } else { - writeln!( - printer.stderr(), - "Using script environment at: {}", - environment.root().user_display().cyan() - )?; } - } - SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) - if !dry_run.enabled() => - { - writeln!( - printer.stderr(), - "Recreating script environment at: {}", - environment.root().user_display().cyan() - )?; - } - SyncEnvironment::Script(ScriptEnvironment::Created(environment)) - if !dry_run.enabled() => - { + } else { writeln!( printer.stderr(), - "Creating script environment at: {}", + "Using script environment at: {}", environment.root().user_display().cyan() )?; } - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { + } + SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) if !dry_run.enabled() => { + writeln!( + printer.stderr(), + "Recreating script environment at: {}", + environment.root().user_display().cyan() + )?; + } + SyncEnvironment::Script(ScriptEnvironment::Created(environment)) if !dry_run.enabled() => { + writeln!( + printer.stderr(), + "Creating script environment at: {}", + environment.root().user_display().cyan() + )?; + } + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => { + if format.is_json() { + sync_json.set_env_path( + environment.root().to_owned(), + DryRunAction::ReplacingExistingScriptVenv, + ); + } else { writeln!( printer.stderr(), "{}", @@ -282,9 +339,14 @@ pub(crate) async fn sync( .dimmed() )?; } - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { + } + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => { + if format.is_json() { + sync_json.set_env_path( + environment.root().to_owned(), + DryRunAction::CreatingScriptEnv, + ); + } else { writeln!( printer.stderr(), "{}", @@ -295,118 +357,15 @@ pub(crate) async fn sync( .dimmed() )?; } - _ => {} - }, + } + _ => {} + } - SyncFormat::Json => match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) - if dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: environment.root().to_owned(), - action: DryRunAction::DiscoveredExisting, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: root.to_owned(), - action: DryRunAction::ReplacingExistingVenv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: root.to_owned(), - action: DryRunAction::CreatingVenv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { - if dry_run.enabled() { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: environment.root().to_owned(), - action: DryRunAction::DiscoveredExisting, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } else { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: environment.root().to_owned(), - action: DryRunAction::UsingScriptEnv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - } - SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) - if !dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: environment.root().to_owned(), - action: DryRunAction::RecreatingScriptEnv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Script(ScriptEnvironment::Created(environment)) - if !dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: environment.root().to_owned(), - action: DryRunAction::CreatingScriptEnv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: root.to_owned(), - action: DryRunAction::ReplacingExistingScriptVenv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { - let sync_json = SyncEntry { - project_dir: project_dir.to_owned(), - environment: Environment { - path: root.to_owned(), - action: DryRunAction::CreatingScriptEnv, - }, - }; - writeln!(printer.stderr(), "{}", serde_json::to_string(&sync_json)?)?; - } - _ => {} - }, + // Pretty-print JSON for better readability and write it to stdout + if format.is_json() { + writeln!(printer.stdout(), "{}", sync_json.as_json()?); } + // Notify the user of any environment changes. // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, From 84ef6dc0ff9af37134168b8ba3680d453ca06663 Mon Sep 17 00:00:00 2001 From: x0rw Date: Mon, 24 Mar 2025 02:24:46 +0000 Subject: [PATCH 04/22] Add --format=pretty-json, Update cli.md --- crates/uv-cli/src/lib.rs | 14 ++++++++++++-- crates/uv/src/commands/project/sync.rs | 10 +++++----- docs/reference/cli.md | 6 ++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 035714bb3d5be..ce4428ed99e35 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -47,16 +47,25 @@ pub enum PythonListFormat { #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum SyncFormat { - /// Display as a text + /// Display the result in a human-readable format. #[default] Text, - /// Display as json + /// Display the result in a machine-readable JSON format. Json, + + /// Output the result in a pretty-printed, human-readable JSON format. + PrettyJson, } impl SyncFormat { pub fn is_json(&self) -> bool { + matches!(self, SyncFormat::Json | SyncFormat::PrettyJson) + } + pub fn is_raw_json(&self) -> bool { matches!(self, SyncFormat::Json) } + pub fn is_pretty(&self) -> bool { + matches!(self, SyncFormat::PrettyJson) + } } #[derive(Debug, Default, Clone, clap::ValueEnum)] @@ -3202,6 +3211,7 @@ pub struct SyncArgs { pub extra: Option>, /// Select the output format. + /// **Note:** This option is only available when `--dry-run` is enabled. #[arg(long, value_enum, requires = "dry_run", default_value_t = SyncFormat::default())] pub format: SyncFormat, /// Include all optional dependencies. diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index cb7868b49ca5a..b8a75bb9bb84b 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -62,8 +62,6 @@ enum DryRunAction { DiscoveredExisting, ReplacingExistingVenv, CreatingVenv, - UsingScriptEnv, - RecreatingScriptEnv, ReplacingExistingScriptVenv, CreatingScriptEnv, /// No action is being taken(should this ever happen?). @@ -230,7 +228,7 @@ pub(crate) async fn sync( SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) if dry_run.enabled() => { - // if + // formatting if format.is_json() { sync_json.set_env_path( environment.root().to_owned(), @@ -362,8 +360,10 @@ pub(crate) async fn sync( } // Pretty-print JSON for better readability and write it to stdout - if format.is_json() { - writeln!(printer.stdout(), "{}", sync_json.as_json()?); + if format.is_raw_json() { + writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; + } else if format.is_pretty() { + writeln!(printer.stdout(), "{}", sync_json.as_pretty_json()?)?; } // Notify the user of any environment changes. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 65fcf9fd05704..4070fb05ce542 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1005,6 +1005,12 @@ uv sync [OPTIONS]
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • +
--format format

Select the output format. Note: This option is only available when --dry-run is enabled

+

[default: text]

Possible values:

+
    +
  • text: Display the result in a human-readable format
  • +
  • json: Display the result in a machine-readable JSON format
  • +
  • pretty-json: Output the result in a pretty-printed, human-readable JSON format
--frozen

Sync without updating the uv.lock file.

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

May also be set with the UV_FROZEN environment variable.

--group group

Include dependencies from the specified dependency group.

From 03238e850041fefed8ebe47b0a5736396bee8055 Mon Sep 17 00:00:00 2001 From: x0rw Date: Mon, 24 Mar 2025 13:22:56 +0000 Subject: [PATCH 05/22] chore: Remove PrettyJson from SyncFormat enum (per review feedback) --- crates/uv-cli/src/lib.rs | 11 +---------- crates/uv/src/commands/project/sync.rs | 8 +------- docs/reference/cli.md | 3 +-- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ce4428ed99e35..f6481826c79e6 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -50,22 +50,13 @@ pub enum SyncFormat { /// Display the result in a human-readable format. #[default] Text, - /// Display the result in a machine-readable JSON format. + /// Display the result in JSON format. Json, - - /// Output the result in a pretty-printed, human-readable JSON format. - PrettyJson, } impl SyncFormat { pub fn is_json(&self) -> bool { - matches!(self, SyncFormat::Json | SyncFormat::PrettyJson) - } - pub fn is_raw_json(&self) -> bool { matches!(self, SyncFormat::Json) } - pub fn is_pretty(&self) -> bool { - matches!(self, SyncFormat::PrettyJson) - } } #[derive(Debug, Default, Clone, clap::ValueEnum)] diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index b8a75bb9bb84b..47da1dde74daa 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -97,9 +97,6 @@ impl SyncEntry { fn as_json(&self) -> Result { serde_json::to_string(self) } - fn as_pretty_json(&self) -> Result { - serde_json::to_string_pretty(self) - } } /// Sync the project environment. @@ -360,12 +357,9 @@ pub(crate) async fn sync( } // Pretty-print JSON for better readability and write it to stdout - if format.is_raw_json() { + if format.is_json() { writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; - } else if format.is_pretty() { - writeln!(printer.stdout(), "{}", sync_json.as_pretty_json()?)?; } - // Notify the user of any environment changes. // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4070fb05ce542..8ea0fafac8c09 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1009,8 +1009,7 @@ uv sync [OPTIONS]

[default: text]

Possible values:

  • text: Display the result in a human-readable format
  • -
  • json: Display the result in a machine-readable JSON format
  • -
  • pretty-json: Output the result in a pretty-printed, human-readable JSON format
  • +
  • json: Display the result in JSON format
--frozen

Sync without updating the uv.lock file.

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

May also be set with the UV_FROZEN environment variable.

--group group

Include dependencies from the specified dependency group.

From 349564cbdbab2a415a8e2c35935fd83212c21dd2 Mon Sep 17 00:00:00 2001 From: x0rw Date: Tue, 25 Mar 2025 00:18:34 +0000 Subject: [PATCH 06/22] Enhance with Python executable and version information --- crates/uv/src/commands/project/sync.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 47da1dde74daa..e9cc6dafcbfc5 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -74,6 +74,10 @@ struct Environment { path: PathBuf, /// The action taken for the environment. action: DryRunAction, + // Python executable path + python_executable: PathBuf, + // Python full version + python_version: String, } impl SyncEntry { @@ -84,6 +88,8 @@ impl SyncEntry { environment: Environment { path: PathBuf::new(), action: DryRunAction::None, + python_executable: PathBuf::new(), + python_version: String::new(), }, } } @@ -93,6 +99,12 @@ impl SyncEntry { self.environment.action = action; } + /// Sets python executable path and full version. + fn set_python(&mut self, executable: PathBuf, version: String) { + self.environment.python_executable = executable; + self.environment.python_version = version; + } + /// Serializes the `SyncEntry` into a JSON string. fn as_json(&self) -> Result { serde_json::to_string(self) @@ -221,6 +233,13 @@ pub(crate) async fn sync( }; let mut sync_json = SyncEntry::new(project_dir); + // set python version and executable path + sync_json.set_python( + environment.python_executable().to_owned(), + environment.interpreter().python_full_version().to_string(), + ); + + // Notify the user of any environment changes. match &environment { SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) if dry_run.enabled() => From 3b10fff210830836a8b9c033129ffd7ab601e8b6 Mon Sep 17 00:00:00 2001 From: x0rw Date: Tue, 25 Mar 2025 01:08:12 +0000 Subject: [PATCH 07/22] Remove failing --- crates/uv/src/commands/project/sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index e9cc6dafcbfc5..4adc5cfc990c0 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -375,7 +375,7 @@ pub(crate) async fn sync( _ => {} } - // Pretty-print JSON for better readability and write it to stdout + // Print JSON for better readability and write it to stdout if format.is_json() { writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; } From 9c2cdffd4d2e58e7416b2374ea4d05fab1cc66fd Mon Sep 17 00:00:00 2001 From: x0rw Date: Wed, 9 Apr 2025 21:57:43 +0100 Subject: [PATCH 08/22] Serialize lockfile actions, generalise DryRunAction enum to work on multiple resources, update comments --- crates/uv/src/commands/project/sync.rs | 97 ++++++++++++++++++-------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 4adc5cfc990c0..a46e98a288c0a 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -46,28 +46,50 @@ use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; -/// Represents an entry for synchronization, formatted as JSON. +/// Represents an single synchronization entry. +/// +/// This structure captures the state of a project directory +/// or a script during a dry run. #[derive(Debug, Serialize)] struct SyncEntry { /// The project directory path. project_dir: PathBuf, /// The environment details, including path and action. environment: Environment, + // Lockfile details, including path and actions taken + lockfile: Option, +} + +/// Represents the lockfile details during a dru run. +/// +/// this struct captures the path to the lockfile and action +/// that would be taken on it. +#[derive(Debug, Serialize)] +struct LockfileEntry { + lockfile_path: PathBuf, + action: DryRunAction, } /// Represents the action taken during a dry run. +/// +/// this struct describe the simulated action applied to a +/// resource (environment, lockfile, package) #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum DryRunAction { - DiscoveredExisting, - ReplacingExistingVenv, - CreatingVenv, - ReplacingExistingScriptVenv, - CreatingScriptEnv, - /// No action is being taken(should this ever happen?). + /// No changes are needed. + AlreadyExist, + /// The resource would be replaced, Equivalent to `Update` but more expressive. + Replace, + /// The resource would be updated. + Update, + /// Create a new resource + Create, + // No action is being taken(should this ever happen?). None, } +/// The dry run environment details, including path, action, and Python configuration. #[derive(Serialize, Debug)] struct Environment { /// The path to the environment. @@ -91,6 +113,7 @@ impl SyncEntry { python_executable: PathBuf::new(), python_version: String::new(), }, + lockfile: None, } } /// Sets the environment path and action. @@ -105,9 +128,17 @@ impl SyncEntry { self.environment.python_version = version; } + /// Sets lockfile path and action. + fn set_lockfile(&mut self, lockfile_path: PathBuf, action: DryRunAction) { + self.lockfile = Some(LockfileEntry { + lockfile_path, + action, + }) + } + /// Serializes the `SyncEntry` into a JSON string. fn as_json(&self) -> Result { - serde_json::to_string(self) + serde_json::to_string_pretty(self) } } @@ -246,10 +277,7 @@ pub(crate) async fn sync( { // formatting if format.is_json() { - sync_json.set_env_path( - environment.root().to_owned(), - DryRunAction::DiscoveredExisting, - ); + sync_json.set_env_path(environment.root().to_owned(), DryRunAction::AlreadyExist); } else { writeln!( printer.stderr(), @@ -266,7 +294,7 @@ pub(crate) async fn sync( if dry_run.enabled() => { if format.is_json() { - sync_json.set_env_path(root.to_owned(), DryRunAction::ReplacingExistingVenv); + sync_json.set_env_path(root.to_owned(), DryRunAction::Replace); } else { writeln!( printer.stderr(), @@ -283,7 +311,7 @@ pub(crate) async fn sync( if dry_run.enabled() => { if format.is_json() { - sync_json.set_env_path(root.to_owned(), DryRunAction::CreatingVenv); + sync_json.set_env_path(root.to_owned(), DryRunAction::Create); } else { writeln!( printer.stderr(), @@ -299,10 +327,8 @@ pub(crate) async fn sync( SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { if dry_run.enabled() { if format.is_json() { - sync_json.set_env_path( - environment.root().to_owned(), - DryRunAction::DiscoveredExisting, - ); + sync_json + .set_env_path(environment.root().to_owned(), DryRunAction::AlreadyExist); } else { writeln!( printer.stderr(), @@ -338,10 +364,7 @@ pub(crate) async fn sync( } SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => { if format.is_json() { - sync_json.set_env_path( - environment.root().to_owned(), - DryRunAction::ReplacingExistingScriptVenv, - ); + sync_json.set_env_path(environment.root().to_owned(), DryRunAction::Replace); } else { writeln!( printer.stderr(), @@ -356,10 +379,7 @@ pub(crate) async fn sync( } SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => { if format.is_json() { - sync_json.set_env_path( - environment.root().to_owned(), - DryRunAction::CreatingScriptEnv, - ); + sync_json.set_env_path(environment.root().to_owned(), DryRunAction::Create); } else { writeln!( printer.stderr(), @@ -375,10 +395,6 @@ pub(crate) async fn sync( _ => {} } - // Print JSON for better readability and write it to stdout - if format.is_json() { - writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; - } // Notify the user of any environment changes. // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, @@ -442,7 +458,12 @@ pub(crate) async fn sync( ) .await { - Ok(..) => return Ok(ExitStatus::Success), + Ok(..) => { + if format.is_json() { + writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; + } + return Ok(ExitStatus::Success); + } Err(ProjectError::Operation(err)) => { return diagnostics::OperationDiagnostic::native_tls( network_settings.native_tls, @@ -492,6 +513,10 @@ pub(crate) async fn sync( if dry_run.enabled() { match result { LockResult::Unchanged(..) => { + if format.is_json() { + sync_json + .set_lockfile(lock_target.lock_path(), DryRunAction::AlreadyExist); + } writeln!( printer.stderr(), "{}", @@ -503,6 +528,9 @@ pub(crate) async fn sync( )?; } LockResult::Changed(None, ..) => { + if format.is_json() { + sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Create); + } writeln!( printer.stderr(), "{}", @@ -514,6 +542,9 @@ pub(crate) async fn sync( )?; } LockResult::Changed(Some(..), ..) => { + if format.is_json() { + sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Update); + } writeln!( printer.stderr(), "{}", @@ -541,6 +572,10 @@ pub(crate) async fn sync( Err(err) => return Err(err.into()), }; + if format.is_json() { + writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; + } + // Identify the installation target. let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, package.as_ref()); From 3194aee4139bf0b05118981c7258697acae78fe6 Mon Sep 17 00:00:00 2001 From: x0rw Date: Wed, 9 Apr 2025 23:19:30 +0100 Subject: [PATCH 09/22] Fix lint issue ';', Add missing if stmnt --- crates/uv/src/commands/project/sync.rs | 70 ++++++++++++++------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a46e98a288c0a..d32dc70f822ed 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -101,7 +101,6 @@ struct Environment { // Python full version python_version: String, } - impl SyncEntry { /// Creates a new `SyncEntry` with the given project directory. fn new(project_dir: &Path) -> Self { @@ -133,7 +132,7 @@ impl SyncEntry { self.lockfile = Some(LockfileEntry { lockfile_path, action, - }) + }); } /// Serializes the `SyncEntry` into a JSON string. @@ -265,10 +264,12 @@ pub(crate) async fn sync( let mut sync_json = SyncEntry::new(project_dir); // set python version and executable path - sync_json.set_python( - environment.python_executable().to_owned(), - environment.interpreter().python_full_version().to_string(), - ); + if format.is_json() { + sync_json.set_python( + environment.python_executable().to_owned(), + environment.interpreter().python_full_version().to_string(), + ); + } // Notify the user of any environment changes. match &environment { @@ -516,44 +517,47 @@ pub(crate) async fn sync( if format.is_json() { sync_json .set_lockfile(lock_target.lock_path(), DryRunAction::AlreadyExist); + } else { + writeln!( + printer.stderr(), + "{}", + format!( + "Found up-to-date lockfile at: {}", + lock_target.lock_path().user_display().bold() + ) + .dimmed() + )?; } - writeln!( - printer.stderr(), - "{}", - format!( - "Found up-to-date lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; } LockResult::Changed(None, ..) => { if format.is_json() { sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Create); + } else { + writeln!( + printer.stderr(), + "{}", + format!( + "Would create lockfile at: {}", + lock_target.lock_path().user_display().bold() + ) + .dimmed() + )?; } - writeln!( - printer.stderr(), - "{}", - format!( - "Would create lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; } LockResult::Changed(Some(..), ..) => { if format.is_json() { sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Update); + } else { + writeln!( + printer.stderr(), + "{}", + format!( + "Would update lockfile at: {}", + lock_target.lock_path().user_display().bold() + ) + .dimmed() + )?; } - writeln!( - printer.stderr(), - "{}", - format!( - "Would update lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; } } } From 451c51199e7935440499631e81bc9d7b3789d44e Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 27 May 2025 15:14:06 -0400 Subject: [PATCH 10/22] cleanup and centralize printing logic --- crates/uv/src/commands/project/sync.rs | 641 +++++++++++++++---------- 1 file changed, 381 insertions(+), 260 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index d32dc70f822ed..90bee1f92ae54 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -46,101 +46,6 @@ use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; -/// Represents an single synchronization entry. -/// -/// This structure captures the state of a project directory -/// or a script during a dry run. -#[derive(Debug, Serialize)] -struct SyncEntry { - /// The project directory path. - project_dir: PathBuf, - /// The environment details, including path and action. - environment: Environment, - // Lockfile details, including path and actions taken - lockfile: Option, -} - -/// Represents the lockfile details during a dru run. -/// -/// this struct captures the path to the lockfile and action -/// that would be taken on it. -#[derive(Debug, Serialize)] -struct LockfileEntry { - lockfile_path: PathBuf, - action: DryRunAction, -} - -/// Represents the action taken during a dry run. -/// -/// this struct describe the simulated action applied to a -/// resource (environment, lockfile, package) -#[derive(Serialize, Debug)] -#[serde(rename_all = "snake_case")] -enum DryRunAction { - /// No changes are needed. - AlreadyExist, - /// The resource would be replaced, Equivalent to `Update` but more expressive. - Replace, - /// The resource would be updated. - Update, - /// Create a new resource - Create, - // No action is being taken(should this ever happen?). - None, -} - -/// The dry run environment details, including path, action, and Python configuration. -#[derive(Serialize, Debug)] -struct Environment { - /// The path to the environment. - path: PathBuf, - /// The action taken for the environment. - action: DryRunAction, - // Python executable path - python_executable: PathBuf, - // Python full version - python_version: String, -} -impl SyncEntry { - /// Creates a new `SyncEntry` with the given project directory. - fn new(project_dir: &Path) -> Self { - Self { - project_dir: project_dir.to_owned(), - environment: Environment { - path: PathBuf::new(), - action: DryRunAction::None, - python_executable: PathBuf::new(), - python_version: String::new(), - }, - lockfile: None, - } - } - /// Sets the environment path and action. - fn set_env_path(&mut self, path: PathBuf, action: DryRunAction) { - self.environment.path = path; - self.environment.action = action; - } - - /// Sets python executable path and full version. - fn set_python(&mut self, executable: PathBuf, version: String) { - self.environment.python_executable = executable; - self.environment.python_version = version; - } - - /// Sets lockfile path and action. - fn set_lockfile(&mut self, lockfile_path: PathBuf, action: DryRunAction) { - self.lockfile = Some(LockfileEntry { - lockfile_path, - action, - }); - } - - /// Serializes the `SyncEntry` into a JSON string. - fn as_json(&self) -> Result { - serde_json::to_string_pretty(self) - } -} - /// Sync the project environment. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn sync( @@ -262,141 +167,115 @@ pub(crate) async fn sync( ), }; - let mut sync_json = SyncEntry::new(project_dir); - // set python version and executable path - if format.is_json() { - sync_json.set_python( - environment.python_executable().to_owned(), - environment.interpreter().python_full_version().to_string(), - ); - } - + let mut report = ProjectReport::new(project_dir); // Notify the user of any environment changes. match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) - if dry_run.enabled() => - { - // formatting - if format.is_json() { - sync_json.set_env_path(environment.root().to_owned(), DryRunAction::AlreadyExist); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Discovered existing environment at: {}", - environment.root().user_display().bold() - ) - .dimmed() - )?; - } + SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) => { + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::AlreadyExist, + kind: EnvKind::Project, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) - if dry_run.enabled() => - { - if format.is_json() { - sync_json.set_env_path(root.to_owned(), DryRunAction::Replace); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing virtual environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) => { + report.sync = Some(SyncReport { + env_path: root.to_owned(), + action: SyncAction::Replace, + kind: EnvKind::Project, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) - if dry_run.enabled() => - { - if format.is_json() { - sync_json.set_env_path(root.to_owned(), DryRunAction::Create); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create virtual environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) => { + report.sync = Some(SyncReport { + env_path: root.to_owned(), + action: SyncAction::Create, + kind: EnvKind::Project, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); + } + SyncEnvironment::Project(ProjectEnvironment::Replaced(environment)) => { + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::Replace, + kind: EnvKind::Project, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); + } + SyncEnvironment::Project(ProjectEnvironment::Created(environment)) => { + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::Create, + kind: EnvKind::Project, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { - if dry_run.enabled() { - if format.is_json() { - sync_json - .set_env_path(environment.root().to_owned(), DryRunAction::AlreadyExist); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Discovered existing environment at: {}", - environment.root().user_display().bold() - ) - .dimmed() - )?; - } - } else { - writeln!( - printer.stderr(), - "Using script environment at: {}", - environment.root().user_display().cyan() - )?; - } + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::AlreadyExist, + kind: EnvKind::Script, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) if !dry_run.enabled() => { - writeln!( - printer.stderr(), - "Recreating script environment at: {}", - environment.root().user_display().cyan() - )?; + SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) => { + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::Replace, + kind: EnvKind::Script, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Script(ScriptEnvironment::Created(environment)) if !dry_run.enabled() => { - writeln!( - printer.stderr(), - "Creating script environment at: {}", - environment.root().user_display().cyan() - )?; + SyncEnvironment::Script(ScriptEnvironment::Created(environment)) => { + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + action: SyncAction::Create, + kind: EnvKind::Script, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => { - if format.is_json() { - sync_json.set_env_path(environment.root().to_owned(), DryRunAction::Replace); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing script environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) => { + report.sync = Some(SyncReport { + env_path: root.to_owned(), + action: SyncAction::Replace, + kind: EnvKind::Script, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => { - if format.is_json() { - sync_json.set_env_path(environment.root().to_owned(), DryRunAction::Create); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create script environment at: {}", - root.user_display().bold() - ) - .dimmed() - )?; - } + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) => { + report.sync = Some(SyncReport { + env_path: root.to_owned(), + action: SyncAction::Create, + kind: EnvKind::Script, + dry: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + }); } - _ => {} } - // Notify the user of any environment changes. + // If we're printing human, eagerly report these partial results + if !format.is_json() { + report.print_sync_text(printer); + } // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, // we don't create a lockfile, so the resolve-and-install semantics are different. @@ -460,8 +339,9 @@ pub(crate) async fn sync( .await { Ok(..) => { + // If we're succesfully exiting early, be sure to print buffered json output if format.is_json() { - writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; + report.print_json(printer)?; } return Ok(ExitStatus::Success); } @@ -514,50 +394,25 @@ pub(crate) async fn sync( if dry_run.enabled() { match result { LockResult::Unchanged(..) => { - if format.is_json() { - sync_json - .set_lockfile(lock_target.lock_path(), DryRunAction::AlreadyExist); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Found up-to-date lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; - } + report.lock = Some(LockReport { + action: LockAction::AlreadyExist, + dry: dry_run.enabled(), + lock_path: lock_target.lock_path(), + }); } LockResult::Changed(None, ..) => { - if format.is_json() { - sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Create); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; - } + report.lock = Some(LockReport { + action: LockAction::Create, + dry: dry_run.enabled(), + lock_path: lock_target.lock_path(), + }); } LockResult::Changed(Some(..), ..) => { - if format.is_json() { - sync_json.set_lockfile(lock_target.lock_path(), DryRunAction::Update); - } else { - writeln!( - printer.stderr(), - "{}", - format!( - "Would update lockfile at: {}", - lock_target.lock_path().user_display().bold() - ) - .dimmed() - )?; - } + report.lock = Some(LockReport { + action: LockAction::Update, + dry: dry_run.enabled(), + lock_path: lock_target.lock_path(), + }); } } } @@ -576,8 +431,15 @@ pub(crate) async fn sync( Err(err) => return Err(err.into()), }; - if format.is_json() { - writeln!(printer.stdout(), "{}", sync_json.as_json()?)?; + match format { + // If printing human, report the new results + SyncFormat::Text => { + report.print_lock_text(printer)?; + } + // If printing json, report the full buffered results + SyncFormat::Json => { + report.print_json(printer)?; + } } // Identify the installation target. @@ -1036,3 +898,262 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { } } } + +/// A report of a project's state and updates to make. +#[derive(Debug, Serialize)] +struct ProjectReport { + /// The project directory path. + project_dir: PathBuf, + /// The environment details, including path and action. + sync: Option, + // Lockfile details, including path and actions taken + lock: Option, +} + +/// The kind of environment this is +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +enum EnvKind { + /// An environment for a project + Project, + /// An environment for a script + Script, +} + +/// Represents the lockfile details during a dry run. +/// +/// this struct captures the path to the lockfile and action +/// that would be taken on it. +#[derive(Debug, Serialize)] +struct LockReport { + /// The path to the lockfile + lock_path: PathBuf, + /// The action to perform on the lockfile + action: LockAction, + /// Whether this is a dry run + dry: bool, +} + +/// Represents the action taken during a dry run. +/// +/// this struct describe the simulated action applied to a +/// resource (environment, lockfile, package) +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +enum SyncAction { + /// No changes are needed. + AlreadyExist, + /// The environment would be replaced, Equivalent to `Update` but more expressive. + Replace, + /// The environment would be updated. + Update, + /// Create a new environment. + Create, +} + +/// Represents the action taken during a dry run. +/// +/// this struct describe the simulated action applied to a +/// resource (environment, lockfile, package) +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +enum LockAction { + /// No changes are needed. + AlreadyExist, + /// The lock would be updated. + Update, + /// Create a new lock. + Create, +} + +/// The dry run environment details, including path, action, and Python configuration. +#[derive(Serialize, Debug)] +struct SyncReport { + /// The path to the environment. + env_path: PathBuf, + /// The kind of environment this is. + kind: EnvKind, + /// Whether this is a dry run. + dry: bool, + /// The action taken for the environment. + action: SyncAction, + // Python executable path. + python_executable: PathBuf, + // Python full version. + python_version: String, +} + +impl ProjectReport { + /// Creates a new `SyncEntry` with the given project directory. + fn new(project_dir: &Path) -> Self { + Self { + project_dir: project_dir.to_owned(), + sync: None, + lock: None, + } + } + + /// Prints the entire report as JSON + fn print_json(&self, printer: Printer) -> Result<()> { + let output = serde_json::to_string_pretty(self)?; + writeln!(printer.stdout(), "{output}")?; + Ok(()) + } + + /// Prints the sync component in human readable form + fn print_sync_text(&self, printer: Printer) -> Result<()> { + let Some(SyncReport { + env_path: path, + kind, + dry, + action, + python_executable, + python_version, + }) = &self.sync + else { + return Ok(()); + }; + + match (kind, action, dry) { + (EnvKind::Project, SyncAction::AlreadyExist, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Discovered existing environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + + (EnvKind::Project, SyncAction::Replace, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Would replace existing virtual environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + + (EnvKind::Project, SyncAction::Create, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Would create virtual environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + + (EnvKind::Script, SyncAction::AlreadyExist, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Discovered existing environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + + (EnvKind::Script, SyncAction::AlreadyExist, false) => { + writeln!( + printer.stderr(), + "Using script environment at: {}", + path.user_display().cyan() + )?; + } + + (EnvKind::Script, SyncAction::Replace, false) => { + writeln!( + printer.stderr(), + "Recreating script environment at: {}", + path.user_display().cyan() + )?; + } + + (EnvKind::Script, SyncAction::Create, false) => { + writeln!( + printer.stderr(), + "Creating script environment at: {}", + path.user_display().cyan() + )?; + } + + (EnvKind::Script, SyncAction::Replace, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Would replace existing script environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + + (EnvKind::Script, SyncAction::Create, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Would create script environment at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + } + + Ok(()) + } + + /// Prints the lock component in human readable form + fn print_lock_text(&self, printer: Printer) -> Result<()> { + let Some(LockReport { + lock_path: path, + action, + dry, + }) = &self.lock + else { + return Ok(()); + }; + + match (action, dry) { + (LockAction::AlreadyExist, true) => { + writeln!( + printer.stderr(), + "{}", + format!( + "Found up-to-date lockfile at: {}", + path.user_display().bold() + ) + .dimmed() + )?; + } + (LockAction::Update, true) => { + writeln!( + printer.stderr(), + "{}", + format!("Would update lockfile at: {}", path.user_display().bold()).dimmed() + )?; + } + (LockAction::Create, true) => { + writeln!( + printer.stderr(), + "{}", + format!("Would create lockfile at: {}", path.user_display().bold()).dimmed() + )?; + } + } + + Ok(()) + } +} From 9552b140583b9e69f2a27d061ccde6d99d3598fe Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 27 May 2025 15:32:08 -0400 Subject: [PATCH 11/22] fill in noop prints --- crates/uv/src/commands/project/sync.rs | 36 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 90bee1f92ae54..ffd65ccf3fa21 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -274,7 +274,7 @@ pub(crate) async fn sync( // If we're printing human, eagerly report these partial results if !format.is_json() { - report.print_sync_text(printer); + report.print_sync_text(printer)?; } // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, @@ -943,10 +943,8 @@ struct LockReport { enum SyncAction { /// No changes are needed. AlreadyExist, - /// The environment would be replaced, Equivalent to `Update` but more expressive. + /// The environment would be replaced. Replace, - /// The environment would be updated. - Update, /// Create a new environment. Create, } @@ -1007,8 +1005,8 @@ impl ProjectReport { kind, dry, action, - python_executable, - python_version, + python_executable: _, + python_version: _, }) = &self.sync else { return Ok(()); @@ -1026,7 +1024,9 @@ impl ProjectReport { .dimmed() )?; } - + (EnvKind::Project, SyncAction::AlreadyExist, false) => { + // Currently intentionally silent + } (EnvKind::Project, SyncAction::Replace, true) => { writeln!( printer.stderr(), @@ -1038,7 +1038,9 @@ impl ProjectReport { .dimmed() )?; } - + (EnvKind::Project, SyncAction::Replace, false) => { + // Currently intentionally silent + } (EnvKind::Project, SyncAction::Create, true) => { writeln!( printer.stderr(), @@ -1050,7 +1052,9 @@ impl ProjectReport { .dimmed() )?; } - + (EnvKind::Project, SyncAction::Create, false) => { + // Currently intentionally silent + } (EnvKind::Script, SyncAction::AlreadyExist, true) => { writeln!( printer.stderr(), @@ -1062,7 +1066,6 @@ impl ProjectReport { .dimmed() )?; } - (EnvKind::Script, SyncAction::AlreadyExist, false) => { writeln!( printer.stderr(), @@ -1070,7 +1073,6 @@ impl ProjectReport { path.user_display().cyan() )?; } - (EnvKind::Script, SyncAction::Replace, false) => { writeln!( printer.stderr(), @@ -1078,7 +1080,6 @@ impl ProjectReport { path.user_display().cyan() )?; } - (EnvKind::Script, SyncAction::Create, false) => { writeln!( printer.stderr(), @@ -1086,7 +1087,6 @@ impl ProjectReport { path.user_display().cyan() )?; } - (EnvKind::Script, SyncAction::Replace, true) => { writeln!( printer.stderr(), @@ -1098,7 +1098,6 @@ impl ProjectReport { .dimmed() )?; } - (EnvKind::Script, SyncAction::Create, true) => { writeln!( printer.stderr(), @@ -1138,6 +1137,9 @@ impl ProjectReport { .dimmed() )?; } + (LockAction::AlreadyExist, false) => { + // Currently intentionally silent + } (LockAction::Update, true) => { writeln!( printer.stderr(), @@ -1145,6 +1147,9 @@ impl ProjectReport { format!("Would update lockfile at: {}", path.user_display().bold()).dimmed() )?; } + (LockAction::Update, false) => { + // Currently intentionally silent + } (LockAction::Create, true) => { writeln!( printer.stderr(), @@ -1152,6 +1157,9 @@ impl ProjectReport { format!("Would create lockfile at: {}", path.user_display().bold()).dimmed() )?; } + (LockAction::Create, false) => { + // Currently intentionally silent + } } Ok(()) From 3e49b24f22166516a8f8b9fcf0f59bf0bc359482 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 27 May 2025 15:38:25 -0400 Subject: [PATCH 12/22] fixup rebase --- crates/uv/src/commands/project/sync.rs | 2 ++ crates/uv/src/settings.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index ffd65ccf3fa21..88df6372621e7 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -6,8 +6,10 @@ use std::sync::Arc; use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; +use serde::Serialize; use uv_cache::Cache; +use uv_cli::SyncFormat; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 0e855bd441001..aea83f0eb5851 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -11,8 +11,8 @@ use uv_cli::{ PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, PythonPinArgs, PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, - ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, - VersionArgs, VersionBump, VersionFormat, + SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, + TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat, }; use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, From 4b0e8c7ec08cf9bc54f91ec8ce9c4df292a35591 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 27 May 2025 15:54:01 -0400 Subject: [PATCH 13/22] remove --dry-run requirement and add tests --- crates/uv-cli/src/lib.rs | 2 +- crates/uv/tests/it/sync.rs | 244 +++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index f6481826c79e6..0015d45a2c896 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3203,7 +3203,7 @@ pub struct SyncArgs { /// Select the output format. /// **Note:** This option is only available when `--dry-run` is enabled. - #[arg(long, value_enum, requires = "dry_run", default_value_t = SyncFormat::default())] + #[arg(long, value_enum, default_value_t = SyncFormat::default())] pub format: SyncFormat, /// Include all optional dependencies. /// diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index c7cfd1dbd9945..5ebbc01615ff3 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -267,6 +267,105 @@ fn package() -> Result<()> { Ok(()) } +/// Test json output +#[test] +fn sync_json() -> 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` should generate a lockfile. + uv_snapshot!(context.filters(), context.sync() + .arg("--format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "project_dir": "[TEMP_DIR]/", + "sync": { + "env_path": "[VENV]/", + "kind": "project", + "dry": false, + "action": "already_exist", + "python_executable": "[VENV]/bin/python3", + "python_version": "3.12.[X]" + }, + "lock": null + } + + ----- 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()); + + Ok(()) +} + +/// Test --dry json output +#[test] +fn sync_dry_json() -> 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` should generate a lockfile. + uv_snapshot!(context.filters(), context.sync() + .arg("--format").arg("json") + .arg("--dry-run"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "project_dir": "[TEMP_DIR]/", + "sync": { + "env_path": "[VENV]/", + "kind": "project", + "dry": true, + "action": "already_exist", + "python_executable": "[VENV]/bin/python3", + "python_version": "3.12.[X]" + }, + "lock": { + "lock_path": "[TEMP_DIR]/uv.lock", + "action": "create", + "dry": true + } + } + + ----- stderr ----- + Resolved 2 packages in [TIME] + Would download 1 package + Would install 1 package + + iniconfig==2.0.0 + "#); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} + /// Ensure that we use the maximum Python version when a workspace contains mixed requirements. #[test] fn mixed_requires_python() -> Result<()> { @@ -4344,6 +4443,151 @@ fn sync_active_script_environment() -> Result<()> { Ok(()) } +#[test] +fn sync_active_script_environment_json() -> Result<()> { + let context = TestContext::new_with_versions(&["3.11", "3.12"]) + .with_filtered_virtualenv_bin() + .with_filtered_python_names(); + + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "anyio", + # ] + # /// + + import anyio + "# + })?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Running `uv sync --script` with `VIRTUAL_ENV` should warn + uv_snapshot!(&filters, context.sync() + .arg("--script").arg("script.py") + .arg("--format").arg("json") + .env(EnvVars::VIRTUAL_ENV, "foo"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "project_dir": "[TEMP_DIR]/", + "sync": { + "env_path": "[CACHE_DIR]/environments-v2/script-[HASH]", + "kind": "script", + "dry": false, + "action": "create", + "python_executable": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python", + "python_version": "3.11.[X]" + }, + "lock": null + } + + ----- stderr ----- + warning: `VIRTUAL_ENV=foo` does not match the script environment path `[CACHE_DIR]/environments-v2/script-[HASH]` and will be ignored; use `--active` to target the active environment instead + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "#); + + context + .temp_dir + .child("foo") + .assert(predicate::path::missing()); + + // Using `--active` should create the environment + uv_snapshot!(&filters, context.sync() + .arg("--script").arg("script.py") + .arg("--format").arg("json") + .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "project_dir": "[TEMP_DIR]/", + "sync": { + "env_path": "[TEMP_DIR]/foo", + "kind": "script", + "dry": false, + "action": "create", + "python_executable": "[TEMP_DIR]/foo/[BIN]/python", + "python_version": "3.11.[X]" + }, + "lock": null + } + + ----- stderr ----- + Resolved 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "#); + + context + .temp_dir + .child("foo") + .assert(predicate::path::is_dir()); + + // A subsequent sync will re-use the environment + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using script environment at: foo + Resolved 3 packages in [TIME] + Audited 3 packages in [TIME] + "###); + + // Requesting another Python version will invalidate the environment + uv_snapshot!(&filters, context.sync() + .arg("--script").arg("script.py") + .arg("--format").arg("json") + .env(EnvVars::VIRTUAL_ENV, "foo") + .arg("--active") + .arg("-p") + .arg("3.12"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "project_dir": "[TEMP_DIR]/", + "sync": { + "env_path": "[TEMP_DIR]/foo", + "kind": "script", + "dry": false, + "action": "replace", + "python_executable": "[TEMP_DIR]/foo/[BIN]/python", + "python_version": "3.12.[X]" + }, + "lock": null + } + + ----- stderr ----- + Resolved 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "#); + + Ok(()) +} + #[test] #[cfg(feature = "git")] fn sync_workspace_custom_environment_path() -> Result<()> { From c3ee1449dfeec4df57c956c35a6c5e53405a8d45 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Tue, 27 May 2025 16:09:48 -0400 Subject: [PATCH 14/22] fixup --- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/tests/it/sync.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 88df6372621e7..6e7e53e9892bb 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -341,7 +341,7 @@ pub(crate) async fn sync( .await { Ok(..) => { - // If we're succesfully exiting early, be sure to print buffered json output + // If we're successfully exiting early, be sure to print buffered json output if format.is_json() { report.print_json(printer)?; } diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5ebbc01615ff3..15c22f3c9b4a7 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -361,8 +361,6 @@ fn sync_dry_json() -> Result<()> { + iniconfig==2.0.0 "#); - assert!(context.temp_dir.child("uv.lock").exists()); - Ok(()) } From bd5312a64ae4aea9a9ff054f5445a43a5aa73f61 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 28 May 2025 15:02:04 -0400 Subject: [PATCH 15/22] wip cleanups --- crates/uv/src/commands/project/sync.rs | 222 +++++++------------------ crates/uv/tests/it/sync.rs | 35 ++-- 2 files changed, 82 insertions(+), 175 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 6e7e53e9892bb..22a9600ae2cf4 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -169,111 +169,38 @@ pub(crate) async fn sync( ), }; - let mut report = ProjectReport::new(project_dir); // Notify the user of any environment changes. - match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::AlreadyExist, - kind: EnvKind::Project, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) => { - report.sync = Some(SyncReport { - env_path: root.to_owned(), - action: SyncAction::Replace, - kind: EnvKind::Project, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) => { - report.sync = Some(SyncReport { - env_path: root.to_owned(), - action: SyncAction::Create, - kind: EnvKind::Project, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Project(ProjectEnvironment::Replaced(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::Replace, - kind: EnvKind::Project, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Project(ProjectEnvironment::Created(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::Create, - kind: EnvKind::Project, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::AlreadyExist, - kind: EnvKind::Script, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::Replace, - kind: EnvKind::Script, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Script(ScriptEnvironment::Created(environment)) => { - report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), - action: SyncAction::Create, - kind: EnvKind::Script, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) => { - report.sync = Some(SyncReport { - env_path: root.to_owned(), - action: SyncAction::Replace, - kind: EnvKind::Script, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) => { - report.sync = Some(SyncReport { - env_path: root.to_owned(), - action: SyncAction::Create, - kind: EnvKind::Script, - dry: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), - python_version: environment.interpreter().python_full_version().to_string(), - }); - } - } - + let mut report = ProjectReport { + project_dir: project_dir.to_owned(), + workspace_dir: match &target { + SyncTarget::Project(project) => Some(project.root().to_owned()), + SyncTarget::Script(_) => None, + }, + sync: None, + lock: None, + }; + report.sync = Some(SyncReport { + env_path: environment.root().to_owned(), + dry_run: dry_run.enabled(), + python_executable: environment.python_executable().to_owned(), + python_version: environment.interpreter().python_full_version().to_string(), + env_kind: match &environment { + SyncEnvironment::Project(..) => EnvKind::Project, + SyncEnvironment::Script(..) => EnvKind::Script, + }, + action: match &environment { + SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::AlreadyExist, + SyncEnvironment::Project(ProjectEnvironment::Created(..)) => SyncAction::Create, + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(..)) => SyncAction::Create, + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(..)) => SyncAction::Update, + SyncEnvironment::Project(ProjectEnvironment::Replaced(..)) => SyncAction::Update, + SyncEnvironment::Script(ScriptEnvironment::Existing(..)) => SyncAction::AlreadyExist, + SyncEnvironment::Script(ScriptEnvironment::Created(..)) => SyncAction::Create, + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(..)) => SyncAction::Create, + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(..)) => SyncAction::Update, + SyncEnvironment::Script(ScriptEnvironment::Replaced(..)) => SyncAction::Update, + }, + }); // If we're printing human, eagerly report these partial results if !format.is_json() { report.print_sync_text(printer)?; @@ -393,31 +320,15 @@ pub(crate) async fn sync( .await { Ok(result) => { - if dry_run.enabled() { - match result { - LockResult::Unchanged(..) => { - report.lock = Some(LockReport { - action: LockAction::AlreadyExist, - dry: dry_run.enabled(), - lock_path: lock_target.lock_path(), - }); - } - LockResult::Changed(None, ..) => { - report.lock = Some(LockReport { - action: LockAction::Create, - dry: dry_run.enabled(), - lock_path: lock_target.lock_path(), - }); - } - LockResult::Changed(Some(..), ..) => { - report.lock = Some(LockReport { - action: LockAction::Update, - dry: dry_run.enabled(), - lock_path: lock_target.lock_path(), - }); - } - } - } + report.lock = Some(LockReport { + lock_path: lock_target.lock_path(), + dry_run: dry_run.enabled(), + action: match &result { + LockResult::Unchanged(..) => LockAction::AlreadyExist, + LockResult::Changed(None, ..) => LockAction::Create, + LockResult::Changed(Some(_), ..) => LockAction::Update, + }, + }); Outcome::Success(result.into_lock()) } Err(ProjectError::Operation(err)) => { @@ -906,6 +817,8 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { struct ProjectReport { /// The project directory path. project_dir: PathBuf, + /// The workspace root + workspace_dir: Option, /// The environment details, including path and action. sync: Option, // Lockfile details, including path and actions taken @@ -930,31 +843,25 @@ enum EnvKind { struct LockReport { /// The path to the lockfile lock_path: PathBuf, - /// The action to perform on the lockfile + /// Whether the lockfile was preserved, created, or updated. action: LockAction, /// Whether this is a dry run - dry: bool, + dry_run: bool, } -/// Represents the action taken during a dry run. -/// -/// this struct describe the simulated action applied to a -/// resource (environment, lockfile, package) +/// Represents the action taken during a sync. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum SyncAction { /// No changes are needed. AlreadyExist, - /// The environment would be replaced. - Replace, + /// The environment would be updated. + Update, /// Create a new environment. Create, } -/// Represents the action taken during a dry run. -/// -/// this struct describe the simulated action applied to a -/// resource (environment, lockfile, package) +/// Represents the action taken during a lock. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum LockAction { @@ -966,16 +873,16 @@ enum LockAction { Create, } -/// The dry run environment details, including path, action, and Python configuration. +/// The synced environment details, including path, action, and Python configuration. #[derive(Serialize, Debug)] struct SyncReport { /// The path to the environment. env_path: PathBuf, - /// The kind of environment this is. - kind: EnvKind, + /// Whether a project or a script environment is used. + env_kind: EnvKind, /// Whether this is a dry run. - dry: bool, - /// The action taken for the environment. + dry_run: bool, + /// Whether the environment was preserved, created, or updated. action: SyncAction, // Python executable path. python_executable: PathBuf, @@ -984,15 +891,6 @@ struct SyncReport { } impl ProjectReport { - /// Creates a new `SyncEntry` with the given project directory. - fn new(project_dir: &Path) -> Self { - Self { - project_dir: project_dir.to_owned(), - sync: None, - lock: None, - } - } - /// Prints the entire report as JSON fn print_json(&self, printer: Printer) -> Result<()> { let output = serde_json::to_string_pretty(self)?; @@ -1004,8 +902,8 @@ impl ProjectReport { fn print_sync_text(&self, printer: Printer) -> Result<()> { let Some(SyncReport { env_path: path, - kind, - dry, + env_kind: kind, + dry_run: dry, action, python_executable: _, python_version: _, @@ -1029,7 +927,7 @@ impl ProjectReport { (EnvKind::Project, SyncAction::AlreadyExist, false) => { // Currently intentionally silent } - (EnvKind::Project, SyncAction::Replace, true) => { + (EnvKind::Project, SyncAction::Update, true) => { writeln!( printer.stderr(), "{}", @@ -1040,7 +938,7 @@ impl ProjectReport { .dimmed() )?; } - (EnvKind::Project, SyncAction::Replace, false) => { + (EnvKind::Project, SyncAction::Update, false) => { // Currently intentionally silent } (EnvKind::Project, SyncAction::Create, true) => { @@ -1075,7 +973,7 @@ impl ProjectReport { path.user_display().cyan() )?; } - (EnvKind::Script, SyncAction::Replace, false) => { + (EnvKind::Script, SyncAction::Update, false) => { writeln!( printer.stderr(), "Recreating script environment at: {}", @@ -1089,7 +987,7 @@ impl ProjectReport { path.user_display().cyan() )?; } - (EnvKind::Script, SyncAction::Replace, true) => { + (EnvKind::Script, SyncAction::Update, true) => { writeln!( printer.stderr(), "{}", @@ -1121,7 +1019,7 @@ impl ProjectReport { let Some(LockReport { lock_path: path, action, - dry, + dry_run: dry, }) = &self.lock else { return Ok(()); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 15c22f3c9b4a7..da9d630f2d614 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -291,15 +291,20 @@ fn sync_json() -> Result<()> { ----- stdout ----- { "project_dir": "[TEMP_DIR]/", + "workspace_dir": "[TEMP_DIR]/", "sync": { "env_path": "[VENV]/", - "kind": "project", - "dry": false, + "env_kind": "project", + "dry_run": false, "action": "already_exist", "python_executable": "[VENV]/bin/python3", "python_version": "3.12.[X]" }, - "lock": null + "lock": { + "lock_path": "[TEMP_DIR]/uv.lock", + "action": "create", + "dry_run": false + } } ----- stderr ----- @@ -339,10 +344,11 @@ fn sync_dry_json() -> Result<()> { ----- stdout ----- { "project_dir": "[TEMP_DIR]/", + "workspace_dir": "[TEMP_DIR]/", "sync": { "env_path": "[VENV]/", - "kind": "project", - "dry": true, + "env_kind": "project", + "dry_run": true, "action": "already_exist", "python_executable": "[VENV]/bin/python3", "python_version": "3.12.[X]" @@ -350,7 +356,7 @@ fn sync_dry_json() -> Result<()> { "lock": { "lock_path": "[TEMP_DIR]/uv.lock", "action": "create", - "dry": true + "dry_run": true } } @@ -4479,10 +4485,11 @@ fn sync_active_script_environment_json() -> Result<()> { ----- stdout ----- { "project_dir": "[TEMP_DIR]/", + "workspace_dir": null, "sync": { "env_path": "[CACHE_DIR]/environments-v2/script-[HASH]", - "kind": "script", - "dry": false, + "env_kind": "script", + "dry_run": false, "action": "create", "python_executable": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python", "python_version": "3.11.[X]" @@ -4515,10 +4522,11 @@ fn sync_active_script_environment_json() -> Result<()> { ----- stdout ----- { "project_dir": "[TEMP_DIR]/", + "workspace_dir": null, "sync": { "env_path": "[TEMP_DIR]/foo", - "kind": "script", - "dry": false, + "env_kind": "script", + "dry_run": false, "action": "create", "python_executable": "[TEMP_DIR]/foo/[BIN]/python", "python_version": "3.11.[X]" @@ -4564,11 +4572,12 @@ fn sync_active_script_environment_json() -> Result<()> { ----- stdout ----- { "project_dir": "[TEMP_DIR]/", + "workspace_dir": null, "sync": { "env_path": "[TEMP_DIR]/foo", - "kind": "script", - "dry": false, - "action": "replace", + "env_kind": "script", + "dry_run": false, + "action": "update", "python_executable": "[TEMP_DIR]/foo/[BIN]/python", "python_version": "3.12.[X]" }, From 2662d0aa0362905ad92b7284308af509bf9c9f82 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 28 May 2025 15:17:30 -0400 Subject: [PATCH 16/22] wip --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-fs/src/path.rs | 6 ++++++ crates/uv/src/commands/project/mod.rs | 8 ++++---- crates/uv/src/commands/project/sync.rs | 24 ++++++++++++------------ 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0015d45a2c896..24a20977e4ad2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3202,9 +3202,9 @@ pub struct SyncArgs { pub extra: Option>, /// Select the output format. - /// **Note:** This option is only available when `--dry-run` is enabled. #[arg(long, value_enum, default_value_t = SyncFormat::default())] pub format: SyncFormat, + /// Include all optional dependencies. /// /// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 7a75c76c3c004..29dac18a717a4 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -413,6 +413,12 @@ impl From> for PortablePathBuf { } } +impl<'a> From<&'a Path> for PortablePathBuf { + fn from(path: &'a Path) -> Self { + Box::::from(path).into() + } +} + #[cfg(feature = "serde")] impl serde::Serialize for PortablePathBuf { fn serialize(&self, serializer: S) -> Result diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 2569b962f1cd7..0680466bef385 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1214,14 +1214,14 @@ enum ProjectEnvironment { /// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as /// such, a temporary environment was created instead. WouldReplace( - PathBuf, + #[allow(dead_code)] PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), /// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such, /// a temporary environment was created instead. WouldCreate( - PathBuf, + #[allow(dead_code)] PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), @@ -1420,14 +1420,14 @@ enum ScriptEnvironment { /// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as /// such, a temporary environment was created instead. WouldReplace( - PathBuf, + #[allow(dead_code)] PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), /// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such, /// a temporary environment was created instead. WouldCreate( - PathBuf, + #[allow(dead_code)] PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 22a9600ae2cf4..ff7f7f3d45ff8 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,6 +1,6 @@ use std::fmt::Write; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; @@ -20,7 +20,7 @@ use uv_dispatch::BuildDispatch; use uv_distribution_types::{ DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; -use uv_fs::Simplified; +use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; @@ -171,18 +171,18 @@ pub(crate) async fn sync( // Notify the user of any environment changes. let mut report = ProjectReport { - project_dir: project_dir.to_owned(), + project_dir: project_dir.into(), workspace_dir: match &target { - SyncTarget::Project(project) => Some(project.root().to_owned()), + SyncTarget::Project(project) => Some(project.root().into()), SyncTarget::Script(_) => None, }, sync: None, lock: None, }; report.sync = Some(SyncReport { - env_path: environment.root().to_owned(), + env_path: environment.root().into(), dry_run: dry_run.enabled(), - python_executable: environment.python_executable().to_owned(), + python_executable: environment.python_executable().into(), python_version: environment.interpreter().python_full_version().to_string(), env_kind: match &environment { SyncEnvironment::Project(..) => EnvKind::Project, @@ -321,7 +321,7 @@ pub(crate) async fn sync( { Ok(result) => { report.lock = Some(LockReport { - lock_path: lock_target.lock_path(), + lock_path: lock_target.lock_path().deref().into(), dry_run: dry_run.enabled(), action: match &result { LockResult::Unchanged(..) => LockAction::AlreadyExist, @@ -816,9 +816,9 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { #[derive(Debug, Serialize)] struct ProjectReport { /// The project directory path. - project_dir: PathBuf, + project_dir: PortablePathBuf, /// The workspace root - workspace_dir: Option, + workspace_dir: Option, /// The environment details, including path and action. sync: Option, // Lockfile details, including path and actions taken @@ -842,7 +842,7 @@ enum EnvKind { #[derive(Debug, Serialize)] struct LockReport { /// The path to the lockfile - lock_path: PathBuf, + lock_path: PortablePathBuf, /// Whether the lockfile was preserved, created, or updated. action: LockAction, /// Whether this is a dry run @@ -877,7 +877,7 @@ enum LockAction { #[derive(Serialize, Debug)] struct SyncReport { /// The path to the environment. - env_path: PathBuf, + env_path: PortablePathBuf, /// Whether a project or a script environment is used. env_kind: EnvKind, /// Whether this is a dry run. @@ -885,7 +885,7 @@ struct SyncReport { /// Whether the environment was preserved, created, or updated. action: SyncAction, // Python executable path. - python_executable: PathBuf, + python_executable: PortablePathBuf, // Python full version. python_version: String, } From b48250231f9281b37a4672c584292e9cf6ec1d07 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 28 May 2025 15:19:46 -0400 Subject: [PATCH 17/22] regenerate --- docs/reference/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 8ea0fafac8c09..07e9096c3cfb5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1005,7 +1005,7 @@ uv sync [OPTIONS]
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • -
--format format

Select the output format. Note: This option is only available when --dry-run is enabled

+
--format format

Select the output format

[default: text]

Possible values:

  • text: Display the result in a human-readable format
  • From 0c9d9307298aa8dfc412b17fd1ac2685d7ba7bd0 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 28 May 2025 15:56:35 -0400 Subject: [PATCH 18/22] fixup root path lookup --- crates/uv/src/commands/project/sync.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index ff7f7f3d45ff8..3011d4d68d55f 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -180,7 +180,6 @@ pub(crate) async fn sync( lock: None, }; report.sync = Some(SyncReport { - env_path: environment.root().into(), dry_run: dry_run.enabled(), python_executable: environment.python_executable().into(), python_version: environment.interpreter().python_full_version().to_string(), @@ -188,6 +187,20 @@ pub(crate) async fn sync( SyncEnvironment::Project(..) => EnvKind::Project, SyncEnvironment::Script(..) => EnvKind::Script, }, + env_path: match &environment { + SyncEnvironment::Project(ProjectEnvironment::Existing(env)) + | SyncEnvironment::Project(ProjectEnvironment::Created(env)) + | SyncEnvironment::Project(ProjectEnvironment::Replaced(env)) + | SyncEnvironment::Script(ScriptEnvironment::Existing(env)) + | SyncEnvironment::Script(ScriptEnvironment::Created(env)) + | SyncEnvironment::Script(ScriptEnvironment::Replaced(env)) => env.root().into(), + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) + | SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) + | SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) + | SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) => { + root.as_path().into() + } + }, action: match &environment { SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::AlreadyExist, SyncEnvironment::Project(ProjectEnvironment::Created(..)) => SyncAction::Create, From a28d0c816b8c7153ccd8497759f846898e715c4b Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 28 May 2025 16:15:43 -0400 Subject: [PATCH 19/22] better windows masking --- crates/uv/src/commands/project/sync.rs | 30 +++++++++++++++----------- crates/uv/tests/it/sync.rs | 26 +++++++++++++++------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 3011d4d68d55f..02b4c1637fccc 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -188,18 +188,24 @@ pub(crate) async fn sync( SyncEnvironment::Script(..) => EnvKind::Script, }, env_path: match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(env)) - | SyncEnvironment::Project(ProjectEnvironment::Created(env)) - | SyncEnvironment::Project(ProjectEnvironment::Replaced(env)) - | SyncEnvironment::Script(ScriptEnvironment::Existing(env)) - | SyncEnvironment::Script(ScriptEnvironment::Created(env)) - | SyncEnvironment::Script(ScriptEnvironment::Replaced(env)) => env.root().into(), - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..)) - | SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..)) - | SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) - | SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) => { - root.as_path().into() - } + SyncEnvironment::Project( + ProjectEnvironment::Existing(env) + | ProjectEnvironment::Created(env) + | ProjectEnvironment::Replaced(env), + ) + | SyncEnvironment::Script( + ScriptEnvironment::Existing(env) + | ScriptEnvironment::Created(env) + | ScriptEnvironment::Replaced(env), + ) => env.root().into(), + SyncEnvironment::Project( + ProjectEnvironment::WouldCreate(root, ..) + | ProjectEnvironment::WouldReplace(root, ..), + ) + | SyncEnvironment::Script( + ScriptEnvironment::WouldCreate(root, ..) + | ScriptEnvironment::WouldReplace(root, ..), + ) => root.as_path().into(), }, action: match &environment { SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::AlreadyExist, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index da9d630f2d614..a4d902679c3f9 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -284,7 +284,10 @@ fn sync_json() -> Result<()> { )?; // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters(), context.sync() + uv_snapshot!(context.filters().into_iter().chain([ + ("bin/python3", "[PYTHON]"), + ("Scripts/python.exe", "[PYTHON]"), + ]).collect::>(), context.sync() .arg("--format").arg("json"), @r#" success: true exit_code: 0 @@ -297,7 +300,7 @@ fn sync_json() -> Result<()> { "env_kind": "project", "dry_run": false, "action": "already_exist", - "python_executable": "[VENV]/bin/python3", + "python_executable": "[VENV]/[PYTHON]", "python_version": "3.12.[X]" }, "lock": { @@ -336,7 +339,10 @@ fn sync_dry_json() -> Result<()> { )?; // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters(), context.sync() + uv_snapshot!(context.filters().into_iter().chain([ + ("bin/python3", "[PYTHON]"), + ("Scripts/python.exe", "[PYTHON]"), + ]).collect::>(), context.sync() .arg("--format").arg("json") .arg("--dry-run"), @r#" success: true @@ -350,7 +356,7 @@ fn sync_dry_json() -> Result<()> { "env_kind": "project", "dry_run": true, "action": "already_exist", - "python_executable": "[VENV]/bin/python3", + "python_executable": "[VENV]/[PYTHON]", "python_version": "3.12.[X]" }, "lock": { @@ -4469,10 +4475,14 @@ fn sync_active_script_environment_json() -> Result<()> { let filters = context .filters() .into_iter() - .chain(vec![( - r"environments-v2/script-[a-z0-9]+", - "environments-v2/script-[HASH]", - )]) + .chain(vec![ + ( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + ), + ("bin/python3", "[PYTHON]"), + ("Scripts/python.exe", "[PYTHON]"), + ]) .collect::>(); // Running `uv sync --script` with `VIRTUAL_ENV` should warn From d3dc2fd9b23b12fa5d683ef4de1bf98563bba16d Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 29 May 2025 12:53:44 -0400 Subject: [PATCH 20/22] fix windows snap regex --- crates/uv/tests/it/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 48e36468a1a2d..bc26741847745 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -204,7 +204,7 @@ impl TestContext { pub fn with_filtered_python_names(mut self) -> Self { if cfg!(windows) { self.filters - .push(("python.exe".to_string(), "python".to_string())); + .push((r"python\.exe".to_string(), "python".to_string())); } else { self.filters .push((r"python\d.\d\d".to_string(), "python".to_string())); From 7faf7cc472d4d8e5fea860e5907f0b6b5f1356c5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 10 Jul 2025 10:16:06 -0500 Subject: [PATCH 21/22] Review --- crates/uv-cli/src/lib.rs | 9 +- crates/uv/src/commands/project/mod.rs | 24 +- crates/uv/src/commands/project/sync.rs | 703 ++++++++------ crates/uv/src/lib.rs | 2 +- crates/uv/src/settings.rs | 6 +- crates/uv/tests/it/sync.rs | 1205 +++++++++++++----------- docs/reference/cli.md | 12 +- 7 files changed, 1126 insertions(+), 835 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index e26943bd0e4df..dd571f6de455d 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -46,7 +46,7 @@ pub enum PythonListFormat { Json, } -#[derive(Debug, Default, Clone, clap::ValueEnum)] +#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)] pub enum SyncFormat { /// Display the result in a human-readable format. #[default] @@ -54,11 +54,6 @@ pub enum SyncFormat { /// Display the result in JSON format. Json, } -impl SyncFormat { - pub fn is_json(&self) -> bool { - matches!(self, SyncFormat::Json) - } -} #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum ListFormat { @@ -3179,7 +3174,7 @@ pub struct SyncArgs { /// Select the output format. #[arg(long, value_enum, default_value_t = SyncFormat::default())] - pub format: SyncFormat, + pub output_format: SyncFormat, /// Include all optional dependencies. /// diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 18a04ab61f385..a89116f7b1c1f 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1212,14 +1212,14 @@ enum ProjectEnvironment { /// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as /// such, a temporary environment was created instead. WouldReplace( - #[allow(dead_code)] PathBuf, + PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), /// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such, /// a temporary environment was created instead. WouldCreate( - #[allow(dead_code)] PathBuf, + PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), @@ -1406,6 +1406,14 @@ impl ProjectEnvironment { Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment), } } + + /// Return the path to the actual target, if this was a dry run environment. + pub(crate) fn dry_run_target(&self) -> Option<&Path> { + match self { + Self::WouldReplace(path, _, _) | Self::WouldCreate(path, _, _) => Some(path), + Self::Created(_) | Self::Existing(_) | Self::Replaced(_) => None, + } + } } impl std::ops::Deref for ProjectEnvironment { @@ -1436,14 +1444,14 @@ enum ScriptEnvironment { /// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as /// such, a temporary environment was created instead. WouldReplace( - #[allow(dead_code)] PathBuf, + PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), /// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such, /// a temporary environment was created instead. WouldCreate( - #[allow(dead_code)] PathBuf, + PathBuf, PythonEnvironment, #[allow(unused)] tempfile::TempDir, ), @@ -1586,6 +1594,14 @@ impl ScriptEnvironment { Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment), } } + + /// Return the path to the actual target, if this was a dry run environment. + pub(crate) fn dry_run_target(&self) -> Option<&Path> { + match self { + Self::WouldReplace(path, _, _) | Self::WouldCreate(path, _, _) => Some(path), + Self::Created(_) | Self::Existing(_) | Self::Replaced(_) => None, + } + } } impl std::ops::Deref for ScriptEnvironment { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index d65beacc02009..c3cfa75874937 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -6,8 +6,10 @@ use std::sync::Arc; use anyhow::{Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; +use serde::Serialize; use tracing::warn; use uv_cache::Cache; +use uv_cli::SyncFormat; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, @@ -74,8 +76,14 @@ pub(crate) async fn sync( cache: &Cache, printer: Printer, preview: PreviewMode, - format: SyncFormat, + output_format: SyncFormat, ) -> Result { + if preview.is_enabled() && matches!(output_format, SyncFormat::Json) { + warn_user!( + "The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview` to disable this warning." + ); + } + // Identify the target. let workspace_cache = WorkspaceCache::default(); let target = if let Some(script) = script { @@ -178,60 +186,16 @@ pub(crate) async fn sync( }) .ok(); - // Notify the user of any environment changes. - let mut report = ProjectReport { - project_dir: project_dir.into(), - workspace_dir: match &target { - SyncTarget::Project(project) => Some(project.root().into()), - SyncTarget::Script(_) => None, - }, - sync: None, - lock: None, - }; - report.sync = Some(SyncReport { + let sync_report = SyncReport { dry_run: dry_run.enabled(), - python_executable: environment.python_executable().into(), - python_version: environment.interpreter().python_full_version().to_string(), - env_kind: match &environment { - SyncEnvironment::Project(..) => EnvKind::Project, - SyncEnvironment::Script(..) => EnvKind::Script, - }, - env_path: match &environment { - SyncEnvironment::Project( - ProjectEnvironment::Existing(env) - | ProjectEnvironment::Created(env) - | ProjectEnvironment::Replaced(env), - ) - | SyncEnvironment::Script( - ScriptEnvironment::Existing(env) - | ScriptEnvironment::Created(env) - | ScriptEnvironment::Replaced(env), - ) => env.root().into(), - SyncEnvironment::Project( - ProjectEnvironment::WouldCreate(root, ..) - | ProjectEnvironment::WouldReplace(root, ..), - ) - | SyncEnvironment::Script( - ScriptEnvironment::WouldCreate(root, ..) - | ScriptEnvironment::WouldReplace(root, ..), - ) => root.as_path().into(), - }, - action: match &environment { - SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::AlreadyExist, - SyncEnvironment::Project(ProjectEnvironment::Created(..)) => SyncAction::Create, - SyncEnvironment::Project(ProjectEnvironment::WouldCreate(..)) => SyncAction::Create, - SyncEnvironment::Project(ProjectEnvironment::WouldReplace(..)) => SyncAction::Update, - SyncEnvironment::Project(ProjectEnvironment::Replaced(..)) => SyncAction::Update, - SyncEnvironment::Script(ScriptEnvironment::Existing(..)) => SyncAction::AlreadyExist, - SyncEnvironment::Script(ScriptEnvironment::Created(..)) => SyncAction::Create, - SyncEnvironment::Script(ScriptEnvironment::WouldCreate(..)) => SyncAction::Create, - SyncEnvironment::Script(ScriptEnvironment::WouldReplace(..)) => SyncAction::Update, - SyncEnvironment::Script(ScriptEnvironment::Replaced(..)) => SyncAction::Update, - }, - }); - // If we're printing human, eagerly report these partial results - if !format.is_json() { - report.print_sync_text(printer)?; + environment: EnvironmentReport::from(&environment), + action: SyncAction::from(&environment), + target: TargetName::from(&target), + }; + + // Show the intermediate results if relevant + if let Some(message) = sync_report.format(output_format) { + writeln!(printer.stderr(), "{message}")?; } // Special-case: we're syncing a script that doesn't have an associated lockfile. In that case, @@ -296,12 +260,22 @@ pub(crate) async fn sync( .await { Ok(..) => { - // If we're successfully exiting early, be sure to print buffered json output - if format.is_json() { - report.print_json(printer)?; + // Generate a report for the script without a lockfile + let report = Report { + schema: SchemaReport::default(), + target: TargetName::from(&target), + project: None, + script: Some(ScriptReport::from(script)), + sync: sync_report, + lock: None, + dry_run: dry_run.enabled(), + }; + if let Some(output) = report.format(output_format) { + writeln!(printer.stdout(), "{output}")?; } return Ok(ExitStatus::Success); } + // TODO(zanieb): We should respect `--output-format json` for the error case Err(ProjectError::Operation(err)) => { return diagnostics::OperationDiagnostic::native_tls( network_settings.native_tls, @@ -348,18 +322,7 @@ pub(crate) async fn sync( .execute(lock_target) .await { - Ok(result) => { - report.lock = Some(LockReport { - lock_path: lock_target.lock_path().deref().into(), - dry_run: dry_run.enabled(), - action: match &result { - LockResult::Unchanged(..) => LockAction::AlreadyExist, - LockResult::Changed(None, ..) => LockAction::Create, - LockResult::Changed(Some(_), ..) => LockAction::Update, - }, - }); - Outcome::Success(result.into_lock()) - } + Ok(result) => Outcome::Success(result), Err(ProjectError::Operation(err)) => { return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls) .report(err) @@ -373,15 +336,23 @@ pub(crate) async fn sync( Err(err) => return Err(err.into()), }; - match format { - // If printing human, report the new results - SyncFormat::Text => { - report.print_lock_text(printer)?; - } - // If printing json, report the full buffered results - SyncFormat::Json => { - report.print_json(printer)?; - } + let lock_report = LockReport::from((&lock_target, &mode, &outcome)); + if let Some(message) = lock_report.format(output_format) { + writeln!(printer.stderr(), "{message}")?; + } + + let report = Report { + schema: SchemaReport::default(), + target: TargetName::from(&target), + project: target.project().map(ProjectReport::from), + script: target.script().map(ScriptReport::from), + sync: sync_report, + lock: Some(lock_report), + dry_run: dry_run.enabled(), + }; + + if let Some(output) = report.format(output_format) { + writeln!(printer.stdout(), "{output}")?; } // Identify the installation target. @@ -433,7 +404,7 @@ pub(crate) async fn sync( #[allow(clippy::large_enum_variant)] enum Outcome { /// The `lock` operation was successful. - Success(Lock), + Success(LockResult), /// The `lock` operation successfully resolved, but failed due to a mismatch (e.g., with `--locked`). LockMismatch(Box), } @@ -442,7 +413,7 @@ impl Outcome { /// Return the [`Lock`] associated with this outcome. fn lock(&self) -> &Lock { match self { - Self::Success(lock) => lock, + Self::Success(lock) => lock.lock(), Self::LockMismatch(lock) => lock, } } @@ -506,6 +477,22 @@ enum SyncTarget { Script(Pep723Script), } +impl SyncTarget { + fn project(&self) -> Option<&VirtualProject> { + match self { + Self::Project(project) => Some(project), + Self::Script(_) => None, + } + } + + fn script(&self) -> Option<&Pep723Script> { + match self { + Self::Project(_) => None, + Self::Script(script) => Some(script), + } + } +} + #[derive(Debug)] enum SyncEnvironment { /// A Python environment for a project. @@ -514,6 +501,15 @@ enum SyncEnvironment { Script(ScriptEnvironment), } +impl SyncEnvironment { + fn dry_run_target(&self) -> Option<&Path> { + match self { + Self::Project(env) => env.dry_run_target(), + Self::Script(env) => env.dry_run_target(), + } + } +} + impl Deref for SyncEnvironment { type Target = PythonEnvironment; @@ -834,256 +830,391 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { } } -/// A report of a project's state and updates to make. #[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +struct WorkspaceReport { + /// The workspace directory path. + path: PortablePathBuf, +} + +impl From<&Workspace> for WorkspaceReport { + fn from(workspace: &Workspace) -> Self { + WorkspaceReport { + path: workspace.install_path().as_path().into(), + } + } +} +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] struct ProjectReport { - /// The project directory path. - project_dir: PortablePathBuf, - /// The workspace root - workspace_dir: Option, - /// The environment details, including path and action. - sync: Option, - // Lockfile details, including path and actions taken - lock: Option, + // + path: PortablePathBuf, + workspace: WorkspaceReport, } -/// The kind of environment this is +impl From<&VirtualProject> for ProjectReport { + fn from(project: &VirtualProject) -> Self { + ProjectReport { + path: project.root().into(), + workspace: WorkspaceReport::from(project.workspace()), + } + } +} + +impl From<&SyncTarget> for TargetName { + fn from(target: &SyncTarget) -> Self { + match target { + SyncTarget::Project(_) => TargetName::Project, + SyncTarget::Script(_) => TargetName::Script, + } + } +} + +#[derive(Serialize, Debug)] +struct ScriptReport { + /// The path to the script. + path: PortablePathBuf, +} + +impl From<&Pep723Script> for ScriptReport { + fn from(script: &Pep723Script) -> Self { + ScriptReport { + path: script.path.as_path().into(), + } + } +} + +#[derive(Serialize, Debug, Default)] +#[serde(rename_all = "snake_case")] +enum SchemaVersion { + /// An unstable, experimental schema. + #[default] + Preview, +} + +#[derive(Serialize, Debug, Default)] +struct SchemaReport { + /// The version of the schema. + version: SchemaVersion, +} + +/// A report of the uv sync operation #[derive(Debug, Serialize)] #[serde(rename_all = "snake_case")] -enum EnvKind { - /// An environment for a project +struct Report { + /// The schema of this report. + schema: SchemaReport, + /// The target of the sync operation, either a project or a script. + target: TargetName, + /// The report for a [`TargetName::Project`], if applicable. + #[serde(skip_serializing_if = "Option::is_none")] + project: Option, + /// The report for a [`TargetName::Script`], if applicable. + #[serde(skip_serializing_if = "Option::is_none")] + script: Option, + /// The report for the sync operation. + sync: SyncReport, + /// The report for the lock operation. + lock: Option, + /// Whether this is a dry run. + dry_run: bool, +} + +/// The kind of target +#[derive(Debug, Serialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum TargetName { Project, - /// An environment for a script Script, } -/// Represents the lockfile details during a dry run. -/// -/// this struct captures the path to the lockfile and action -/// that would be taken on it. -#[derive(Debug, Serialize)] -struct LockReport { - /// The path to the lockfile - lock_path: PortablePathBuf, - /// Whether the lockfile was preserved, created, or updated. - action: LockAction, - /// Whether this is a dry run - dry_run: bool, +impl std::fmt::Display for TargetName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetName::Project => write!(f, "project"), + TargetName::Script => write!(f, "script"), + } + } } /// Represents the action taken during a sync. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum SyncAction { - /// No changes are needed. - AlreadyExist, - /// The environment would be updated. + /// The environment was checked and required no updates. + Check, + /// The environment was updated. Update, - /// Create a new environment. + /// The environment was replaced. + Replace, + /// A new environment was created. Create, } +impl From<&SyncEnvironment> for SyncAction { + fn from(env: &SyncEnvironment) -> Self { + match &env { + SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::Check, + SyncEnvironment::Project(ProjectEnvironment::Created(..)) => SyncAction::Create, + SyncEnvironment::Project(ProjectEnvironment::WouldCreate(..)) => SyncAction::Create, + SyncEnvironment::Project(ProjectEnvironment::WouldReplace(..)) => SyncAction::Replace, + SyncEnvironment::Project(ProjectEnvironment::Replaced(..)) => SyncAction::Update, + SyncEnvironment::Script(ScriptEnvironment::Existing(..)) => SyncAction::Check, + SyncEnvironment::Script(ScriptEnvironment::Created(..)) => SyncAction::Create, + SyncEnvironment::Script(ScriptEnvironment::WouldCreate(..)) => SyncAction::Create, + SyncEnvironment::Script(ScriptEnvironment::WouldReplace(..)) => SyncAction::Replace, + SyncEnvironment::Script(ScriptEnvironment::Replaced(..)) => SyncAction::Update, + } + } +} + +impl SyncAction { + fn message(&self, target: TargetName, dry_run: bool) -> Option<&'static str> { + let message = if dry_run { + match self { + SyncAction::Check => "Would use", + SyncAction::Update => "Would update", + SyncAction::Replace => "Would replace", + SyncAction::Create => "Would create", + } + } else { + // For projects, we omit some of these messages when we're not in dry-run mode + let is_project = matches!(target, TargetName::Project); + match self { + SyncAction::Check | SyncAction::Update | SyncAction::Create if is_project => { + return None; + } + SyncAction::Check => "Using", + SyncAction::Update => "Updating", + SyncAction::Replace => "Replacing", + SyncAction::Create => "Creating", + } + }; + Some(message) + } +} + /// Represents the action taken during a lock. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] enum LockAction { - /// No changes are needed. - AlreadyExist, - /// The lock would be updated. + /// The lockfile was used without checking. + Use, + /// The lockfile was checked and required no updates. + Check, + /// The lockfile was updated. Update, - /// Create a new lock. + /// A new lockfile was created. Create, } -/// The synced environment details, including path, action, and Python configuration. +impl LockAction { + fn message(&self, dry_run: bool) -> Option<&'static str> { + let message = if dry_run { + match self { + LockAction::Use => return None, + LockAction::Check => "Found up-to-date", + LockAction::Update => "Would update", + LockAction::Create => "Would create", + } + } else { + return None; + }; + Some(message) + } +} + #[derive(Serialize, Debug)] -struct SyncReport { +struct PythonReport { + path: PortablePathBuf, + version: uv_pep508::StringVersion, + implementation: String, +} + +impl From<&uv_python::Interpreter> for PythonReport { + fn from(interpreter: &uv_python::Interpreter) -> Self { + PythonReport { + path: interpreter.sys_executable().into(), + version: interpreter.python_full_version().clone(), + implementation: interpreter.implementation_name().to_string(), + } + } +} + +impl PythonReport { + /// Set the path for this Python report. + #[must_use] + fn with_path(mut self, path: PortablePathBuf) -> Self { + self.path = path; + self + } +} + +#[derive(Serialize, Debug)] +struct EnvironmentReport { /// The path to the environment. - env_path: PortablePathBuf, - /// Whether a project or a script environment is used. - env_kind: EnvKind, - /// Whether this is a dry run. - dry_run: bool, - /// Whether the environment was preserved, created, or updated. + path: PortablePathBuf, + /// The Python interpreter for the environment. + python: PythonReport, +} + +impl From<&PythonEnvironment> for EnvironmentReport { + fn from(env: &PythonEnvironment) -> Self { + EnvironmentReport { + python: PythonReport::from(env.interpreter()), + path: env.root().into(), + } + } +} + +impl From<&SyncEnvironment> for EnvironmentReport { + fn from(env: &SyncEnvironment) -> Self { + let report = EnvironmentReport::from(&**env); + // Replace the path if necessary; we construct a temporary virtual environment during dry + // run invocations and want to report the path we _would_ use. + if let Some(path) = env.dry_run_target() { + report.with_path(path.into()) + } else { + report + } + } +} + +impl EnvironmentReport { + /// Set the path for this environment report. + #[must_use] + fn with_path(mut self, path: PortablePathBuf) -> Self { + let python_path = &self.python.path; + if let Ok(python_path) = python_path.as_ref().strip_prefix(self.path) { + let new_path = path.as_ref().to_path_buf().join(python_path); + self.python = self.python.with_path(new_path.as_path().into()); + } + self.path = path; + self + } +} + +/// The report for a sync operation. +#[derive(Serialize, Debug)] +struct SyncReport { + /// The environment. + environment: EnvironmentReport, + /// The action performed during the sync, e.g., what was done to the environment. action: SyncAction, - // Python executable path. - python_executable: PortablePathBuf, - // Python full version. - python_version: String, + + // We store these fields so the report can format itself self-contained, but the outer + // [`Report`] is intended to include these in user-facing output + #[serde(skip)] + dry_run: bool, + #[serde(skip)] + target: TargetName, } -impl ProjectReport { - /// Prints the entire report as JSON - fn print_json(&self, printer: Printer) -> Result<()> { - let output = serde_json::to_string_pretty(self)?; - writeln!(printer.stdout(), "{output}")?; - Ok(()) +impl SyncReport { + fn format(&self, output_format: SyncFormat) -> Option { + match output_format { + // This is an intermediate report, when using JSON, it's only rendered at the end + SyncFormat::Json => None, + SyncFormat::Text => self.to_human_readable_string(), + } } - /// Prints the sync component in human readable form - fn print_sync_text(&self, printer: Printer) -> Result<()> { - let Some(SyncReport { - env_path: path, - env_kind: kind, - dry_run: dry, + fn to_human_readable_string(&self) -> Option { + let Self { + environment, action, - python_executable: _, - python_version: _, - }) = &self.sync - else { - return Ok(()); - }; + dry_run, + target, + } = self; + + let action = action.message(*target, *dry_run)?; + + let message = format!( + "{action} {target} environment at: {path}", + path = environment.path.user_display().cyan(), + ); + if *dry_run { + return Some(message.dimmed().to_string()); + } - match (kind, action, dry) { - (EnvKind::Project, SyncAction::AlreadyExist, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Discovered existing environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (EnvKind::Project, SyncAction::AlreadyExist, false) => { - // Currently intentionally silent - } - (EnvKind::Project, SyncAction::Update, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing virtual environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (EnvKind::Project, SyncAction::Update, false) => { - // Currently intentionally silent - } - (EnvKind::Project, SyncAction::Create, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create virtual environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (EnvKind::Project, SyncAction::Create, false) => { - // Currently intentionally silent - } - (EnvKind::Script, SyncAction::AlreadyExist, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Discovered existing environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (EnvKind::Script, SyncAction::AlreadyExist, false) => { - writeln!( - printer.stderr(), - "Using script environment at: {}", - path.user_display().cyan() - )?; - } - (EnvKind::Script, SyncAction::Update, false) => { - writeln!( - printer.stderr(), - "Recreating script environment at: {}", - path.user_display().cyan() - )?; - } - (EnvKind::Script, SyncAction::Create, false) => { - writeln!( - printer.stderr(), - "Creating script environment at: {}", - path.user_display().cyan() - )?; - } - (EnvKind::Script, SyncAction::Update, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would replace existing script environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (EnvKind::Script, SyncAction::Create, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Would create script environment at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } + Some(message) + } +} + +/// The report for a lock operation. +#[derive(Debug, Serialize)] +struct LockReport { + /// The path to the lockfile + path: PortablePathBuf, + /// Whether the lockfile was preserved, created, or updated. + action: LockAction, + + // We store this field so the report can format itself self-contained, but the outer + // [`Report`] is intended to include this in user-facing output + #[serde(skip)] + dry_run: bool, +} + +impl From<(&LockTarget<'_>, &LockMode<'_>, &Outcome)> for LockReport { + fn from((target, mode, outcome): (&LockTarget, &LockMode, &Outcome)) -> Self { + LockReport { + path: target.lock_path().deref().into(), + action: match outcome { + Outcome::Success(result) => { + match result { + LockResult::Unchanged(..) => match mode { + // When `--frozen` is used, we don't check the lockfile + LockMode::Frozen => LockAction::Use, + LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(_) => { + LockAction::Check + } + }, + LockResult::Changed(None, ..) => LockAction::Create, + LockResult::Changed(Some(_), ..) => LockAction::Update, + } + } + // TODO(zanieb): We don't have a way to report the outcome of the lock yet + Outcome::LockMismatch(_) => LockAction::Check, + }, + dry_run: matches!(mode, LockMode::DryRun(_)), } + } +} - Ok(()) +impl LockReport { + fn format(&self, output_format: SyncFormat) -> Option { + match output_format { + SyncFormat::Json => None, + SyncFormat::Text => self.to_human_readable_string(), + } } - /// Prints the lock component in human readable form - fn print_lock_text(&self, printer: Printer) -> Result<()> { - let Some(LockReport { - lock_path: path, + fn to_human_readable_string(&self) -> Option { + let Self { + path, action, - dry_run: dry, - }) = &self.lock - else { - return Ok(()); - }; + dry_run, + } = self; - match (action, dry) { - (LockAction::AlreadyExist, true) => { - writeln!( - printer.stderr(), - "{}", - format!( - "Found up-to-date lockfile at: {}", - path.user_display().bold() - ) - .dimmed() - )?; - } - (LockAction::AlreadyExist, false) => { - // Currently intentionally silent - } - (LockAction::Update, true) => { - writeln!( - printer.stderr(), - "{}", - format!("Would update lockfile at: {}", path.user_display().bold()).dimmed() - )?; - } - (LockAction::Update, false) => { - // Currently intentionally silent - } - (LockAction::Create, true) => { - writeln!( - printer.stderr(), - "{}", - format!("Would create lockfile at: {}", path.user_display().bold()).dimmed() - )?; - } - (LockAction::Create, false) => { - // Currently intentionally silent - } + let action = action.message(*dry_run)?; + + let message = format!( + "{action} lockfile at: {path}", + path = path.user_display().cyan(), + ); + if *dry_run { + return Some(message.dimmed().to_string()); } - Ok(()) + Some(message) + } +} + +impl Report { + fn format(&self, output_format: SyncFormat) -> Option { + match output_format { + SyncFormat::Json => serde_json::to_string_pretty(self).ok(), + SyncFormat::Text => None, + } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f32b974a9aa26..5f5ae8e7a6cab 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1814,7 +1814,7 @@ async fn run_project( &cache, printer, globals.preview, - args.format, + args.output_format, )) .await } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 0438e54ed1236..3088ca00993cb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1153,7 +1153,7 @@ pub(crate) struct SyncSettings { pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) refresh: Refresh, pub(crate) settings: ResolverInstallerSettings, - pub(crate) format: SyncFormat, + pub(crate) output_format: SyncFormat, } impl SyncSettings { @@ -1193,7 +1193,7 @@ impl SyncSettings { python, check, no_check, - format, + output_format, } = args; let install_mirrors = filesystem .clone() @@ -1213,7 +1213,7 @@ impl SyncSettings { }; Self { - format, + output_format, locked, frozen, dry_run, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 3308ea01c7132..6a02e01791347 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -27,7 +27,7 @@ fn sync() -> Result<()> { )?; // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -37,7 +37,7 @@ fn sync() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -60,14 +60,14 @@ fn locked() -> Result<()> { )?; // Running with `--locked` should error, if no lockfile is present. - uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`. - "###); + "); // Lock the initial requirements. context.lock().assert().success(); @@ -86,7 +86,7 @@ fn locked() -> Result<()> { )?; // Running with `--locked` should error. - uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r" success: false exit_code: 2 ----- stdout ----- @@ -94,7 +94,7 @@ fn locked() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. - "###); + "); let updated = context.read("uv.lock"); @@ -120,14 +120,14 @@ fn frozen() -> Result<()> { )?; // Running with `--frozen` should error, if no lockfile is present. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Unable to find lockfile at `uv.lock`. To create a lockfile, run `uv lock` or `uv sync`. - "###); + "); context.lock().assert().success(); @@ -143,7 +143,7 @@ fn frozen() -> Result<()> { )?; // Running with `--frozen` should install the stale lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -154,7 +154,7 @@ fn frozen() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -172,7 +172,7 @@ fn empty() -> Result<()> { )?; // Running `uv sync` should generate an empty lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -181,12 +181,12 @@ fn empty() -> Result<()> { warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. Resolved in [TIME] Audited in [TIME] - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); // Running `uv sync` again should succeed. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -195,7 +195,7 @@ fn empty() -> Result<()> { warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. Resolved in [TIME] Audited in [TIME] - "###); + "); Ok(()) } @@ -252,7 +252,7 @@ fn package() -> Result<()> { let init = src.child("__init__.py"); init.touch()?; - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child"), @r" success: true exit_code: 0 ----- stdout ----- @@ -263,7 +263,7 @@ fn package() -> Result<()> { Installed 2 packages in [TIME] + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -271,7 +271,9 @@ fn package() -> Result<()> { /// Test json output #[test] fn sync_json() -> Result<()> { - let context = TestContext::new("3.12"); + let context = TestContext::new("3.12") + .with_filtered_python_names() + .with_filtered_virtualenv_bin(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -284,31 +286,38 @@ fn sync_json() -> Result<()> { "#, )?; - // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters().into_iter().chain([ - ("bin/python3", "[PYTHON]"), - ("Scripts/python.exe", "[PYTHON]"), - ]).collect::>(), context.sync() - .arg("--format").arg("json"), @r#" + uv_snapshot!(context.filters(), context.sync() + .arg("--output-format").arg("json"), @r#" success: true exit_code: 0 ----- stdout ----- { - "project_dir": "[TEMP_DIR]/", - "workspace_dir": "[TEMP_DIR]/", + "schema": { + "version": "preview" + }, + "target": "project", + "project": { + "path": "[TEMP_DIR]/", + "workspace": { + "path": "[TEMP_DIR]/" + } + }, "sync": { - "env_path": "[VENV]/", - "env_kind": "project", - "dry_run": false, - "action": "already_exist", - "python_executable": "[VENV]/[PYTHON]", - "python_version": "3.12.[X]" + "environment": { + "path": "[VENV]/", + "python": { + "path": "[VENV]/[BIN]/python", + "version": "3.12.[X]", + "implementation": "cpython" + } + }, + "action": "check" }, "lock": { - "lock_path": "[TEMP_DIR]/uv.lock", - "action": "create", - "dry_run": false - } + "path": "[TEMP_DIR]/uv.lock", + "action": "create" + }, + "dry_run": false } ----- stderr ----- @@ -320,13 +329,118 @@ fn sync_json() -> Result<()> { assert!(context.temp_dir.child("uv.lock").exists()); + uv_snapshot!(context.filters(), context.sync() + .arg("--frozen") + .arg("--output-format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "schema": { + "version": "preview" + }, + "target": "project", + "project": { + "path": "[TEMP_DIR]/", + "workspace": { + "path": "[TEMP_DIR]/" + } + }, + "sync": { + "environment": { + "path": "[VENV]/", + "python": { + "path": "[VENV]/[BIN]/python", + "version": "3.12.[X]", + "implementation": "cpython" + } + }, + "action": "check" + }, + "lock": { + "path": "[TEMP_DIR]/uv.lock", + "action": "use" + }, + "dry_run": false + } + + ----- stderr ----- + Audited 1 package in [TIME] + "#); + + uv_snapshot!(context.filters(), context.sync() + .arg("--locked") + .arg("--output-format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "schema": { + "version": "preview" + }, + "target": "project", + "project": { + "path": "[TEMP_DIR]/", + "workspace": { + "path": "[TEMP_DIR]/" + } + }, + "sync": { + "environment": { + "path": "[VENV]/", + "python": { + "path": "[VENV]/[BIN]/python", + "version": "3.12.[X]", + "implementation": "cpython" + } + }, + "action": "check" + }, + "lock": { + "path": "[TEMP_DIR]/uv.lock", + "action": "check" + }, + "dry_run": false + } + + ----- stderr ----- + Resolved 2 packages in [TIME] + Audited 1 package in [TIME] + "#); + + // Invalidate the lockfile by changing the requirements. + 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<2"] + "#, + )?; + + uv_snapshot!(context.filters(), context.sync() + .arg("--locked") + .arg("--output-format").arg("json"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. + "); + Ok(()) } /// Test --dry json output #[test] fn sync_dry_json() -> Result<()> { - let context = TestContext::new("3.12"); + let context = TestContext::new_with_versions(&["3.12"]) + .with_filtered_python_names() + .with_filtered_virtualenv_bin(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -339,35 +453,44 @@ fn sync_dry_json() -> Result<()> { "#, )?; - // Running `uv sync` should generate a lockfile. - uv_snapshot!(context.filters().into_iter().chain([ - ("bin/python3", "[PYTHON]"), - ("Scripts/python.exe", "[PYTHON]"), - ]).collect::>(), context.sync() - .arg("--format").arg("json") + // Running `uv sync` should report intent to create the environment and lockfile + uv_snapshot!(context.filters(), context.sync() + .arg("--output-format").arg("json") .arg("--dry-run"), @r#" success: true exit_code: 0 ----- stdout ----- { - "project_dir": "[TEMP_DIR]/", - "workspace_dir": "[TEMP_DIR]/", + "schema": { + "version": "preview" + }, + "target": "project", + "project": { + "path": "[TEMP_DIR]/", + "workspace": { + "path": "[TEMP_DIR]/" + } + }, "sync": { - "env_path": "[VENV]/", - "env_kind": "project", - "dry_run": true, - "action": "already_exist", - "python_executable": "[VENV]/[PYTHON]", - "python_version": "3.12.[X]" + "environment": { + "path": "[VENV]/", + "python": { + "path": "[VENV]/[BIN]/python", + "version": "3.12.[X]", + "implementation": "cpython" + } + }, + "action": "create" }, "lock": { - "lock_path": "[TEMP_DIR]/uv.lock", - "action": "create", - "dry_run": true - } + "path": "[TEMP_DIR]/uv.lock", + "action": "create" + }, + "dry_run": true } ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 2 packages in [TIME] Would download 1 package Would install 1 package @@ -431,7 +554,7 @@ fn mixed_requires_python() -> Result<()> { )?; // Running `uv sync` should succeed, locking for Python 3.12. - uv_snapshot!(context.filters(), context.sync().arg("-p").arg("3.12"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("-p").arg("3.12"), @r" success: true exit_code: 0 ----- stdout ----- @@ -446,7 +569,7 @@ fn mixed_requires_python() -> Result<()> { + bird-feeder==0.1.0 (from file://[TEMP_DIR]/packages/bird-feeder) + idna==3.6 + sniffio==1.3.1 - "###); + "); // Running `uv sync` again should fail. uv_snapshot!(context.filters(), context.sync().arg("-p").arg("3.9"), @r" @@ -769,23 +892,23 @@ fn check() -> Result<()> { )?; // Running `uv sync --check` should fail. - uv_snapshot!(context.filters(), context.sync().arg("--check"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--check"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv + Would use project 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###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -795,23 +918,23 @@ fn check() -> Result<()> { 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###" + uv_snapshot!(context.filters(), context.sync().arg("--check"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv + Would use project 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(()) } @@ -859,7 +982,7 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { .touch()?; // Syncing with `--no-dev` should omit all dependencies except `iniconfig`. - uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -870,11 +993,11 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { Installed 2 packages in [TIME] + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 - "###); + "); // Syncing without `--no-dev` should include `anyio`, `requests`, `pysocks`, and their // dependencies, but not `typing-extensions`. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -891,7 +1014,7 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { + requests==2.31.0 + sniffio==1.3.1 + urllib3==2.2.1 - "###); + "); Ok(()) } @@ -939,7 +1062,7 @@ fn sync_legacy_non_project_frozen() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -948,9 +1071,9 @@ fn sync_legacy_non_project_frozen() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -959,7 +1082,7 @@ fn sync_legacy_non_project_frozen() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -1012,7 +1135,7 @@ fn sync_legacy_non_project_group() -> Result<()> { .child("__init__.py") .touch()?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1023,9 +1146,9 @@ fn sync_legacy_non_project_group() -> Result<()> { Installed 2 packages in [TIME] + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1037,9 +1160,9 @@ fn sync_legacy_non_project_group() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1055,9 +1178,9 @@ fn sync_legacy_non_project_group() -> Result<()> { - iniconfig==2.0.0 - sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1067,9 +1190,9 @@ fn sync_legacy_non_project_group() -> Result<()> { Installed 2 packages in [TIME] + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bop"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bop"), @r" success: false exit_code: 2 ----- stdout ----- @@ -1077,7 +1200,7 @@ fn sync_legacy_non_project_group() -> Result<()> { ----- stderr ----- Resolved 6 packages in [TIME] error: Group `bop` is not defined in any project's `dependency-groups` table - "###); + "); Ok(()) } @@ -1102,7 +1225,7 @@ fn sync_legacy_non_project_frozen_modification() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1113,7 +1236,7 @@ fn sync_legacy_non_project_frozen_modification() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Modify the "live" dependency groups. pyproject_toml.write_str( @@ -1127,14 +1250,14 @@ fn sync_legacy_non_project_frozen_modification() -> Result<()> { )?; // This should succeed. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("async"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Audited 3 packages in [TIME] - "###); + "); Ok(()) } @@ -1183,7 +1306,7 @@ fn sync_build_isolation() -> Result<()> { "###); // Running `uv sync` should succeed. - uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1201,7 +1324,7 @@ fn sync_build_isolation() -> Result<()> { + source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz) - trove-classifiers==2024.3.3 - wheel==0.43.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -1231,7 +1354,7 @@ fn sync_build_isolation_package() -> Result<()> { )?; // Running `uv sync` should fail for iniconfig. - uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -1249,7 +1372,7 @@ fn sync_build_isolation_package() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution` - "###); + "#); // Install `hatchling` for `source-distribution`. uv_snapshot!(context.filters(), context.pip_install().arg("hatchling"), @r###" @@ -1269,7 +1392,7 @@ fn sync_build_isolation_package() -> Result<()> { "###); // Running `uv sync` should succeed. - uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation-package").arg("source-distribution"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1286,7 +1409,7 @@ fn sync_build_isolation_package() -> Result<()> { + project==0.1.0 (from file://[TEMP_DIR]/) + source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz) - trove-classifiers==2024.3.3 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -1321,7 +1444,7 @@ fn sync_build_isolation_extra() -> Result<()> { )?; // Running `uv sync` should fail for the `compile` extra. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -1339,10 +1462,10 @@ fn sync_build_isolation_extra() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution` - "###); + "#); // Running `uv sync` with `--all-extras` should also fail. - uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-extras"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -1360,10 +1483,10 @@ fn sync_build_isolation_extra() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution` - "###); + "#); // Install the build dependencies. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("build"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("build"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1378,10 +1501,10 @@ fn sync_build_isolation_extra() -> Result<()> { + pluggy==1.4.0 + project==0.1.0 (from file://[TEMP_DIR]/) + trove-classifiers==2024.3.3 - "###); + "); // Running `uv sync` for the `compile` extra should succeed, and remove the build dependencies. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("compile"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1397,7 +1520,7 @@ fn sync_build_isolation_extra() -> Result<()> { - pluggy==1.4.0 + source-distribution==0.0.1 (from https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz) - trove-classifiers==2024.3.3 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -1451,7 +1574,7 @@ fn sync_reset_state() -> Result<()> { init.touch()?; // Running `uv sync` should succeed. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1463,7 +1586,7 @@ fn sync_reset_state() -> Result<()> { + project==0.1.0 (from file://[TEMP_DIR]/) + pydantic-core==2.17.0 + typing-extensions==4.10.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -1505,7 +1628,7 @@ fn sync_relative_wheel() -> Result<()> { context.temp_dir.join("wheels/ok-1.0.0-py3-none-any.whl"), )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1516,7 +1639,7 @@ fn sync_relative_wheel() -> Result<()> { Installed 2 packages in [TIME] + ok==1.0.0 (from file://[TEMP_DIR]/wheels/ok-1.0.0-py3-none-any.whl) + relative-wheel==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); let lock = context.read("uv.lock"); @@ -1558,7 +1681,7 @@ fn sync_relative_wheel() -> Result<()> { ); // Check that we can re-read the lockfile. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1566,7 +1689,7 @@ fn sync_relative_wheel() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 2 packages in [TIME] - "###); + "); Ok(()) } @@ -1590,7 +1713,7 @@ fn sync_environment() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 2 ----- stdout ----- @@ -1598,7 +1721,7 @@ fn sync_environment() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] error: The current Python platform is not compatible with the lockfile's supported environments: `python_full_version < '3.11'` - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -1625,7 +1748,7 @@ fn sync_dev() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--only-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1637,9 +1760,9 @@ fn sync_dev() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1653,9 +1776,9 @@ fn sync_dev() -> Result<()> { - idna==3.6 - sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1666,10 +1789,10 @@ fn sync_dev() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Using `--no-default-groups` should remove dev dependencies - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1680,7 +1803,7 @@ fn sync_dev() -> Result<()> { - anyio==4.3.0 - idna==3.6 - sniffio==1.3.1 - "###); + "); Ok(()) } @@ -1709,7 +1832,7 @@ fn sync_group() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1720,9 +1843,9 @@ fn sync_group() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1734,9 +1857,9 @@ fn sync_group() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1754,9 +1877,9 @@ fn sync_group() -> Result<()> { - sniffio==1.3.1 - typing-extensions==4.10.0 + urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1768,9 +1891,9 @@ fn sync_group() -> Result<()> { + iniconfig==2.0.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1778,9 +1901,9 @@ fn sync_group() -> Result<()> { ----- stderr ----- Resolved 10 packages in [TIME] Audited 9 packages in [TIME] - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1792,9 +1915,9 @@ fn sync_group() -> Result<()> { - charset-normalizer==3.3.2 - requests==2.31.0 - urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups").arg("--no-dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1808,9 +1931,9 @@ fn sync_group() -> Result<()> { - iniconfig==2.0.0 + requests==2.31.0 + urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1827,9 +1950,9 @@ fn sync_group() -> Result<()> { - requests==2.31.0 - sniffio==1.3.1 - urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--dev").arg("--no-group").arg("dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dev").arg("--no-group").arg("dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1838,9 +1961,9 @@ fn sync_group() -> Result<()> { Resolved 10 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--no-dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev").arg("--no-dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1848,9 +1971,9 @@ fn sync_group() -> Result<()> { ----- stderr ----- Resolved 10 packages in [TIME] Audited 1 package in [TIME] - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1866,10 +1989,10 @@ fn sync_group() -> Result<()> { + requests==2.31.0 + sniffio==1.3.1 + urllib3==2.2.1 - "###); + "); // Using `--no-default-groups` should exclude all groups - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1885,9 +2008,9 @@ fn sync_group() -> Result<()> { - requests==2.31.0 - sniffio==1.3.1 - urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1903,11 +2026,11 @@ fn sync_group() -> Result<()> { + requests==2.31.0 + sniffio==1.3.1 + urllib3==2.2.1 - "###); + "); // Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups, // excluding the remaining `dev` group. - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1916,7 +2039,7 @@ fn sync_group() -> Result<()> { Resolved 10 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -1942,7 +2065,7 @@ fn sync_include_group() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -1952,9 +2075,9 @@ fn sync_include_group() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1967,9 +2090,9 @@ fn sync_include_group() -> Result<()> { + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1981,9 +2104,9 @@ fn sync_include_group() -> Result<()> { - idna==3.6 - sniffio==1.3.1 - typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1995,9 +2118,9 @@ fn sync_include_group() -> Result<()> { + idna==3.6 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2006,9 +2129,9 @@ fn sync_include_group() -> Result<()> { Resolved 6 packages in [TIME] Uninstalled 1 package in [TIME] - typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2017,9 +2140,9 @@ fn sync_include_group() -> Result<()> { Resolved 6 packages in [TIME] Installed 1 package in [TIME] + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2031,9 +2154,9 @@ fn sync_include_group() -> Result<()> { - idna==3.6 - iniconfig==2.0.0 - sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2045,9 +2168,9 @@ fn sync_include_group() -> Result<()> { + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2055,7 +2178,7 @@ fn sync_include_group() -> Result<()> { ----- stderr ----- Resolved 6 packages in [TIME] Audited 5 packages in [TIME] - "###); + "); Ok(()) } @@ -2081,7 +2204,7 @@ fn sync_exclude_group() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2095,9 +2218,9 @@ fn sync_exclude_group() -> Result<()> { + iniconfig==2.0.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--no-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--no-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2109,9 +2232,9 @@ fn sync_exclude_group() -> Result<()> { - idna==3.6 - iniconfig==2.0.0 - sniffio==1.3.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2122,9 +2245,9 @@ fn sync_exclude_group() -> Result<()> { Installed 1 package in [TIME] + iniconfig==2.0.0 - typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar").arg("--no-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar").arg("--no-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2133,7 +2256,7 @@ fn sync_exclude_group() -> Result<()> { Resolved 6 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -2161,7 +2284,7 @@ fn sync_dev_group() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -2175,7 +2298,7 @@ fn sync_dev_group() -> Result<()> { + iniconfig==2.0.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -2202,7 +2325,7 @@ fn sync_non_existent_group() -> Result<()> { context.lock().assert().success(); // Requesting a non-existent group should fail. - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -2210,9 +2333,9 @@ fn sync_non_existent_group() -> Result<()> { ----- stderr ----- Resolved 7 packages in [TIME] error: Group `baz` is not defined in the project's `dependency-groups` table - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -2220,10 +2343,10 @@ fn sync_non_existent_group() -> Result<()> { ----- stderr ----- Resolved 7 packages in [TIME] error: Group `baz` is not defined in the project's `dependency-groups` table - "###); + "); // Requesting an empty group should succeed. - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2233,11 +2356,11 @@ fn sync_non_existent_group() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + typing-extensions==4.10.0 - "###); + "); // Requesting with `--frozen` should respect the groups in the lockfile, rather than the // `pyproject.toml`. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2250,7 +2373,7 @@ fn sync_non_existent_group() -> Result<()> { + idna==3.6 + requests==2.31.0 + urllib3==2.2.1 - "###); + "); // Replace `bar` with `baz`. pyproject_toml.write_str( @@ -2266,23 +2389,23 @@ fn sync_non_existent_group() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Audited 6 packages in [TIME] - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Group `baz` is not defined in the project's `dependency-groups` table - "###); + "); Ok(()) } @@ -2562,7 +2685,7 @@ fn sync_default_groups() -> Result<()> { context.lock().assert().success(); // The `dev` group should be synced by default. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -2573,7 +2696,7 @@ fn sync_default_groups() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + typing-extensions==4.10.0 - "###); + "); // If we remove it from the `default-groups` list, it should be removed. pyproject_toml.write_str( @@ -2594,7 +2717,7 @@ fn sync_default_groups() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -2603,7 +2726,7 @@ fn sync_default_groups() -> Result<()> { Resolved 10 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); // If we set a different default group, it should be synced instead. pyproject_toml.write_str( @@ -2624,7 +2747,7 @@ fn sync_default_groups() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -2636,7 +2759,7 @@ fn sync_default_groups() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // `--no-group` should remove from the defaults. pyproject_toml.write_str( @@ -2657,7 +2780,7 @@ fn sync_default_groups() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2668,10 +2791,10 @@ fn sync_default_groups() -> Result<()> { - anyio==4.3.0 - idna==3.6 - sniffio==1.3.1 - "###); + "); // Using `--group` should include the defaults - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2683,10 +2806,10 @@ fn sync_default_groups() -> Result<()> { + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); // Using `--all-groups` should include the defaults - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2699,10 +2822,10 @@ fn sync_default_groups() -> Result<()> { + charset-normalizer==3.3.2 + requests==2.31.0 + urllib3==2.2.1 - "###); + "); // Using `--only-group` should exclude the defaults - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2718,9 +2841,9 @@ fn sync_default_groups() -> Result<()> { - sniffio==1.3.1 - typing-extensions==4.10.0 - urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2736,10 +2859,10 @@ fn sync_default_groups() -> Result<()> { + sniffio==1.3.1 + typing-extensions==4.10.0 + urllib3==2.2.1 - "###); + "); // Using `--no-default-groups` should exclude all groups - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2755,9 +2878,9 @@ fn sync_default_groups() -> Result<()> { - requests==2.31.0 - sniffio==1.3.1 - urllib3==2.2.1 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2773,11 +2896,11 @@ fn sync_default_groups() -> Result<()> { + requests==2.31.0 + sniffio==1.3.1 + urllib3==2.2.1 - "###); + "); // Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups, // excluding the remaining `dev` group. - uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2786,7 +2909,7 @@ fn sync_default_groups() -> Result<()> { Resolved 10 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -2858,7 +2981,7 @@ fn sync_default_groups_all() -> Result<()> { "); // Using `--all-groups` should be redundant and work fine - uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2874,7 +2997,7 @@ fn sync_default_groups_all() -> Result<()> { + requests==2.31.0 + sniffio==1.3.1 + urllib3==2.2.1 - "###); + "); // Using `--no-dev` should exclude just the dev group uv_snapshot!(context.filters(), context.sync().arg("--no-dev"), @r" @@ -3009,7 +3132,7 @@ fn sync_group_member() -> Result<()> { // Generate a lockfile. context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3021,7 +3144,7 @@ fn sync_group_member() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -3132,7 +3255,7 @@ fn sync_group_legacy_non_project_member() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3144,7 +3267,7 @@ fn sync_group_legacy_non_project_member() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) + iniconfig==2.0.0 + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -3266,7 +3389,7 @@ fn sync_group_self() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3278,9 +3401,9 @@ fn sync_group_self() -> Result<()> { + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/) + typing-extensions==4.10.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3292,7 +3415,7 @@ fn sync_group_self() -> Result<()> { Installed 1 package in [TIME] + idna==3.6 - typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -3317,7 +3440,7 @@ fn sync_non_existent_extra() -> Result<()> { context.lock().assert().success(); // Requesting a non-existent extra should fail. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3325,10 +3448,10 @@ fn sync_non_existent_extra() -> Result<()> { ----- stderr ----- Resolved 4 packages in [TIME] error: Extra `baz` is not defined in the project's `optional-dependencies` table - "###); + "); // Excluding a non-existing extra when requesting all extras should fail. - uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3336,7 +3459,7 @@ fn sync_non_existent_extra() -> Result<()> { ----- stderr ----- Resolved 4 packages in [TIME] error: Extra `baz` is not defined in the project's `optional-dependencies` table - "###); + "); Ok(()) } @@ -3358,7 +3481,7 @@ fn sync_non_existent_extra_no_optional_dependencies() -> Result<()> { context.lock().assert().success(); // Requesting a non-existent extra should fail. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3366,10 +3489,10 @@ fn sync_non_existent_extra_no_optional_dependencies() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] error: Extra `baz` is not defined in the project's `optional-dependencies` table - "###); + "); // Excluding a non-existing extra when requesting all extras should fail. - uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-extras").arg("--no-extra").arg("baz"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3377,7 +3500,7 @@ fn sync_non_existent_extra_no_optional_dependencies() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] error: Extra `baz` is not defined in the project's `optional-dependencies` table - "###); + "); Ok(()) } @@ -3430,14 +3553,14 @@ fn sync_ignore_extras_check_when_no_provides_extras() -> Result<()> { "#})?; // Requesting a non-existent extra should not fail, as no validation should be performed. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("baz"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Audited in [TIME] - "###); + "); Ok(()) } @@ -3485,7 +3608,7 @@ fn sync_non_existent_extra_workspace_member() -> Result<()> { context.lock().assert().success(); // Requesting an extra that only exists in the child should fail. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3493,10 +3616,10 @@ fn sync_non_existent_extra_workspace_member() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] error: Extra `async` is not defined in the project's `optional-dependencies` table - "###); + "); // Unless we sync from the child directory. - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3508,7 +3631,7 @@ fn sync_non_existent_extra_workspace_member() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -3558,7 +3681,7 @@ fn sync_non_existent_extra_non_project_workspace() -> Result<()> { // Requesting an extra that only exists in the child should succeed, since we sync all members // by default. - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3570,10 +3693,10 @@ fn sync_non_existent_extra_non_project_workspace() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Syncing from the child should also succeed. - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3581,10 +3704,10 @@ fn sync_non_existent_extra_non_project_workspace() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] Audited 3 packages in [TIME] - "###); + "); // Syncing from an unrelated child should fail. - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("other").arg("--extra").arg("async"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("other").arg("--extra").arg("async"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3592,7 +3715,7 @@ fn sync_non_existent_extra_non_project_workspace() -> Result<()> { ----- stderr ----- Resolved 5 packages in [TIME] error: Extra `async` is not defined in the project's `optional-dependencies` table - "###); + "); Ok(()) } @@ -3660,7 +3783,7 @@ fn no_install_project() -> Result<()> { context.lock().assert().success(); // Running with `--no-install-project` should install `anyio`, but not `project`. - uv_snapshot!(context.filters(), context.sync().arg("--no-install-project"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-project"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3672,7 +3795,7 @@ fn no_install_project() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // However, we do require the `pyproject.toml`. fs_err::remove_file(pyproject_toml)?; @@ -3742,7 +3865,7 @@ fn no_install_workspace() -> Result<()> { // Running with `--no-install-workspace` should install `anyio` and `iniconfig`, but not // `project` or `child`. - uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3755,7 +3878,7 @@ fn no_install_workspace() -> Result<()> { + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); // Remove the virtual environment. fs_err::remove_dir_all(&context.venv)?; @@ -3763,7 +3886,7 @@ fn no_install_workspace() -> Result<()> { // We don't require the `pyproject.toml` for non-root members, if `--frozen` is provided. fs_err::remove_file(child.join("pyproject.toml"))?; - uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-workspace").arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3776,10 +3899,10 @@ fn no_install_workspace() -> Result<()> { + idna==3.6 + iniconfig==2.0.0 + sniffio==1.3.1 - "###); + "); // Even if `--package` is used. - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-install-workspace").arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3789,20 +3912,20 @@ fn no_install_workspace() -> Result<()> { - anyio==3.7.0 - idna==3.6 - sniffio==1.3.1 - "###); + "); // Unless the package doesn't exist. - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("fake").arg("--no-install-workspace").arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("fake").arg("--no-install-workspace").arg("--frozen"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Could not find root package `fake` - "###); + "); // Even if `--all-packages` is used. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--no-install-workspace").arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--no-install-workspace").arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3812,7 +3935,7 @@ fn no_install_workspace() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // But we do require the root `pyproject.toml`. fs_err::remove_file(context.temp_dir.join("pyproject.toml"))?; @@ -3853,7 +3976,7 @@ fn no_install_package() -> Result<()> { context.lock().assert().success(); // Running with `--no-install-package anyio` should skip anyio but include everything else - uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("anyio"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("anyio"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3865,11 +3988,11 @@ fn no_install_package() -> Result<()> { + idna==3.6 + project==0.1.0 (from file://[TEMP_DIR]/) + sniffio==1.3.1 - "###); + "); // Running with `--no-install-package project` should skip the project itself (not as a special // case, that's just the name of the project) - uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("project"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-package").arg("project"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3881,7 +4004,7 @@ fn no_install_package() -> Result<()> { Installed 1 package in [TIME] + anyio==3.7.0 - project==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); Ok(()) } @@ -3910,7 +4033,7 @@ fn no_install_project_no_build() -> Result<()> { context.lock().assert().success(); // `--no-build` should raise an error, since we try to install the project. - uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" success: false exit_code: 2 ----- stdout ----- @@ -3918,11 +4041,11 @@ fn no_install_project_no_build() -> Result<()> { ----- stderr ----- Resolved 4 packages in [TIME] error: Distribution `project==0.1.0 @ editable+.` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + "); // But it's fine to combine `--no-install-project` with `--no-build`. We shouldn't error, since // we aren't building the project. - uv_snapshot!(context.filters(), context.sync().arg("--no-install-project").arg("--no-build").arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-install-project").arg("--no-build").arg("--locked"), @r" success: true exit_code: 0 ----- stdout ----- @@ -3934,7 +4057,7 @@ fn no_install_project_no_build() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -4087,7 +4210,7 @@ fn convert_to_virtual() -> Result<()> { )?; // Running `uv sync` should install the project itself. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4098,7 +4221,7 @@ fn convert_to_virtual() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); let lock = context.read("uv.lock"); @@ -4149,7 +4272,7 @@ fn convert_to_virtual() -> Result<()> { )?; // Running `uv sync` should remove the project itself. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4158,7 +4281,7 @@ fn convert_to_virtual() -> Result<()> { Resolved 2 packages in [TIME] Uninstalled 1 package in [TIME] - project==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); let lock = context.read("uv.lock"); @@ -4217,7 +4340,7 @@ fn convert_to_package() -> Result<()> { )?; // Running `uv sync` should not install the project itself. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4227,7 +4350,7 @@ fn convert_to_package() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); let lock = context.read("uv.lock"); @@ -4282,7 +4405,7 @@ fn convert_to_package() -> Result<()> { )?; // Running `uv sync` should install the project itself. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4292,7 +4415,7 @@ fn convert_to_package() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + project==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); let lock = context.read("uv.lock"); @@ -4352,7 +4475,7 @@ fn sync_custom_environment_path() -> Result<()> { )?; // Running `uv sync` should create `.venv` by default - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4364,7 +4487,7 @@ fn sync_custom_environment_path() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4372,7 +4495,7 @@ fn sync_custom_environment_path() -> Result<()> { .assert(predicate::path::is_dir()); // Running `uv sync` should create `foo` in the project directory when customized - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4383,7 +4506,7 @@ fn sync_custom_environment_path() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4397,7 +4520,7 @@ fn sync_custom_environment_path() -> Result<()> { .assert(predicate::path::is_dir()); // An absolute path can be provided - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foobar/.venv"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foobar/.venv"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4408,7 +4531,7 @@ fn sync_custom_environment_path() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4422,7 +4545,7 @@ fn sync_custom_environment_path() -> Result<()> { .assert(predicate::path::is_dir()); // An absolute path can be provided - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, context.temp_dir.join("bar")), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, context.temp_dir.join("bar")), @r" success: true exit_code: 0 ----- stdout ----- @@ -4433,7 +4556,7 @@ fn sync_custom_environment_path() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4443,7 +4566,7 @@ fn sync_custom_environment_path() -> Result<()> { // And, it can be outside the project let tempdir = tempdir_in(TestContext::test_bucket_dir())?; context = context.with_filtered_path(tempdir.path(), "OTHER_TEMPDIR"); - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, tempdir.path().join(".venv")), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, tempdir.path().join(".venv")), @r" success: true exit_code: 0 ----- stdout ----- @@ -4454,7 +4577,7 @@ fn sync_custom_environment_path() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); ChildPath::new(tempdir.path()) .child(".venv") @@ -4491,7 +4614,7 @@ fn sync_custom_environment_path() -> Result<()> { fs_err::write(context.temp_dir.join("foo").join("file"), b"")?; // We can delete and use it - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4503,7 +4626,7 @@ fn sync_custom_environment_path() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -4526,7 +4649,7 @@ fn sync_active_project_environment() -> Result<()> { )?; // Running `uv sync` with `VIRTUAL_ENV` should warn - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4539,7 +4662,7 @@ fn sync_active_project_environment() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4552,7 +4675,7 @@ fn sync_active_project_environment() -> Result<()> { .assert(predicate::path::missing()); // Using `--active` should create the environment - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4563,7 +4686,7 @@ fn sync_active_project_environment() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4571,7 +4694,7 @@ fn sync_active_project_environment() -> Result<()> { .assert(predicate::path::is_dir()); // A subsequent sync will re-use the environment - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4579,13 +4702,13 @@ fn sync_active_project_environment() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // Setting both the `VIRTUAL_ENV` and `UV_PROJECT_ENVIRONMENT` is fine if they agree uv_snapshot!(context.filters(), context.sync() .arg("--active") .env(EnvVars::VIRTUAL_ENV, "foo") - .env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + .env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4593,13 +4716,13 @@ fn sync_active_project_environment() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // If they disagree, we use `VIRTUAL_ENV` because of `--active` uv_snapshot!(context.filters(), context.sync() .arg("--active") .env(EnvVars::VIRTUAL_ENV, "foo") - .env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r###" + .env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4607,7 +4730,7 @@ fn sync_active_project_environment() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); context .temp_dir @@ -4616,7 +4739,7 @@ fn sync_active_project_environment() -> Result<()> { // Requesting another Python version will invalidate the environment uv_snapshot!(context.filters(), context.sync() - .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active").arg("-p").arg("3.12"), @r###" + .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active").arg("-p").arg("3.12"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4628,7 +4751,7 @@ fn sync_active_project_environment() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -4662,7 +4785,7 @@ fn sync_active_script_environment() -> Result<()> { .collect::>(); // Running `uv sync --script` with `VIRTUAL_ENV` should warn - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4676,7 +4799,7 @@ fn sync_active_script_environment() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); context .temp_dir @@ -4684,7 +4807,7 @@ fn sync_active_script_environment() -> Result<()> { .assert(predicate::path::missing()); // Using `--active` should create the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4696,7 +4819,7 @@ fn sync_active_script_environment() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); context .temp_dir @@ -4704,7 +4827,7 @@ fn sync_active_script_environment() -> Result<()> { .assert(predicate::path::is_dir()); // A subsequent sync will re-use the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4713,7 +4836,7 @@ fn sync_active_script_environment() -> Result<()> { Using script environment at: foo Resolved 3 packages in [TIME] Audited 3 packages in [TIME] - "###); + "); // Requesting another Python version will invalidate the environment uv_snapshot!(&filters, context.sync() @@ -4722,19 +4845,19 @@ fn sync_active_script_environment() -> Result<()> { .env(EnvVars::VIRTUAL_ENV, "foo") .arg("--active") .arg("-p") - .arg("3.12"), @r###" + .arg("3.12"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Recreating script environment at: foo + Updating script environment at: foo Resolved 3 packages in [TIME] Installed 3 packages in [TIME] + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -4774,23 +4897,32 @@ fn sync_active_script_environment_json() -> Result<()> { // Running `uv sync --script` with `VIRTUAL_ENV` should warn uv_snapshot!(&filters, context.sync() .arg("--script").arg("script.py") - .arg("--format").arg("json") + .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo"), @r#" success: true exit_code: 0 ----- stdout ----- { - "project_dir": "[TEMP_DIR]/", - "workspace_dir": null, + "schema": { + "version": "preview" + }, + "target": "script", + "script": { + "path": "[TEMP_DIR]/script.py" + }, "sync": { - "env_path": "[CACHE_DIR]/environments-v2/script-[HASH]", - "env_kind": "script", - "dry_run": false, - "action": "create", - "python_executable": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python", - "python_version": "3.11.[X]" + "environment": { + "path": "[CACHE_DIR]/environments-v2/script-[HASH]", + "python": { + "path": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python", + "version": "3.11.[X]", + "implementation": "cpython" + } + }, + "action": "create" }, - "lock": null + "lock": null, + "dry_run": false } ----- stderr ----- @@ -4811,23 +4943,32 @@ fn sync_active_script_environment_json() -> Result<()> { // Using `--active` should create the environment uv_snapshot!(&filters, context.sync() .arg("--script").arg("script.py") - .arg("--format").arg("json") + .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r#" success: true exit_code: 0 ----- stdout ----- { - "project_dir": "[TEMP_DIR]/", - "workspace_dir": null, + "schema": { + "version": "preview" + }, + "target": "script", + "script": { + "path": "[TEMP_DIR]/script.py" + }, "sync": { - "env_path": "[TEMP_DIR]/foo", - "env_kind": "script", - "dry_run": false, - "action": "create", - "python_executable": "[TEMP_DIR]/foo/[BIN]/python", - "python_version": "3.11.[X]" + "environment": { + "path": "[TEMP_DIR]/foo", + "python": { + "path": "[TEMP_DIR]/foo/[BIN]/python", + "version": "3.11.[X]", + "implementation": "cpython" + } + }, + "action": "create" }, - "lock": null + "lock": null, + "dry_run": false } ----- stderr ----- @@ -4844,7 +4985,7 @@ fn sync_active_script_environment_json() -> Result<()> { .assert(predicate::path::is_dir()); // A subsequent sync will re-use the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4853,12 +4994,12 @@ fn sync_active_script_environment_json() -> Result<()> { Using script environment at: foo Resolved 3 packages in [TIME] Audited 3 packages in [TIME] - "###); + "); // Requesting another Python version will invalidate the environment uv_snapshot!(&filters, context.sync() .arg("--script").arg("script.py") - .arg("--format").arg("json") + .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo") .arg("--active") .arg("-p") @@ -4867,17 +5008,26 @@ fn sync_active_script_environment_json() -> Result<()> { exit_code: 0 ----- stdout ----- { - "project_dir": "[TEMP_DIR]/", - "workspace_dir": null, + "schema": { + "version": "preview" + }, + "target": "script", + "script": { + "path": "[TEMP_DIR]/script.py" + }, "sync": { - "env_path": "[TEMP_DIR]/foo", - "env_kind": "script", - "dry_run": false, - "action": "update", - "python_executable": "[TEMP_DIR]/foo/[BIN]/python", - "python_version": "3.12.[X]" + "environment": { + "path": "[TEMP_DIR]/foo", + "python": { + "path": "[TEMP_DIR]/foo/[BIN]/python", + "version": "3.12.[X]", + "implementation": "cpython" + } + }, + "action": "update" }, - "lock": null + "lock": null, + "dry_run": false } ----- stderr ----- @@ -4911,7 +5061,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { context.init().arg("child").assert().success(); // Running `uv sync` should create `.venv` in the workspace root - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -4921,7 +5071,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4929,7 +5079,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { .assert(predicate::path::is_dir()); // Similarly, `uv sync` from the child project uses `.venv` in the workspace root - uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.join("child")), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.join("child")), @r" success: true exit_code: 0 ----- stdout ----- @@ -4938,7 +5088,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Resolved 3 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4952,7 +5102,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { .assert(predicate::path::missing()); // Running `uv sync` should create `foo` in the workspace root when customized - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -4963,7 +5113,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Resolved 3 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -4977,7 +5127,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { .assert(predicate::path::is_dir()); // Similarly, `uv sync` from the child project uses `foo` relative to the workspace root - uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(context.temp_dir.join("child")), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(context.temp_dir.join("child")), @r" success: true exit_code: 0 ----- stdout ----- @@ -4986,7 +5136,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { Resolved 3 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); context .temp_dir @@ -5000,7 +5150,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { .assert(predicate::path::missing()); // And, `uv sync --package child` uses `foo` relative to the workspace root - uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5008,7 +5158,7 @@ fn sync_workspace_custom_environment_path() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] Audited in [TIME] - "###); + "); context .temp_dir @@ -5043,7 +5193,7 @@ fn sync_empty_virtual_environment() -> Result<()> { )?; // Running `uv sync` should work - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5055,7 +5205,7 @@ fn sync_empty_virtual_environment() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -5077,7 +5227,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { )?; // We should not warn if it matches the project environment - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join(".venv")), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join(".venv")), @r" success: true exit_code: 0 ----- stdout ----- @@ -5087,10 +5237,10 @@ fn sync_legacy_non_project_warning() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // Including if it's a relative path that matches - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, ".venv"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, ".venv"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5098,7 +5248,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // Or, if it's a link that resolves to the same path #[cfg(unix)] @@ -5108,7 +5258,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { let link = context.temp_dir.join("link"); symlink(context.temp_dir.join(".venv"), &link)?; - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, link), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, link), @r" success: true exit_code: 0 ----- stdout ----- @@ -5116,11 +5266,11 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); } // But we should warn if it's a different path - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5129,10 +5279,10 @@ fn sync_legacy_non_project_warning() -> Result<()> { warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // Including absolute paths - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")), @r" success: true exit_code: 0 ----- stdout ----- @@ -5141,10 +5291,10 @@ fn sync_legacy_non_project_warning() -> Result<()> { warning: `VIRTUAL_ENV=foo` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // We should not warn if the project environment has been customized and matches - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5155,10 +5305,10 @@ fn sync_legacy_non_project_warning() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // But we should warn if they don't match still - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "bar"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5170,14 +5320,14 @@ fn sync_legacy_non_project_warning() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); let child = context.temp_dir.child("child"); child.create_dir_all()?; // And `VIRTUAL_ENV` is resolved relative to the project root so with relative paths we should // warn from a child too - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, "foo").env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r" success: true exit_code: 0 ----- stdout ----- @@ -5186,10 +5336,10 @@ fn sync_legacy_non_project_warning() -> Result<()> { warning: `VIRTUAL_ENV=foo` does not match the project environment path `[TEMP_DIR]/foo` and will be ignored; use `--active` to target the active environment instead Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); // But, a matching absolute path shouldn't warn - uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")).env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r###" + uv_snapshot!(context.filters(), context.sync().env(EnvVars::VIRTUAL_ENV, context.temp_dir.join("foo")).env(EnvVars::UV_PROJECT_ENVIRONMENT, "foo").current_dir(&child), @r" success: true exit_code: 0 ----- stdout ----- @@ -5197,7 +5347,7 @@ fn sync_legacy_non_project_warning() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] Audited 1 package in [TIME] - "###); + "); Ok(()) } @@ -5217,7 +5367,7 @@ fn sync_update_project() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5229,7 +5379,7 @@ fn sync_update_project() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // Bump the project version. pyproject_toml.write_str( @@ -5246,7 +5396,7 @@ fn sync_update_project() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5256,7 +5406,7 @@ fn sync_update_project() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + my-project==0.2.0 (from file://[TEMP_DIR]/) - "###); + "); Ok(()) } @@ -5277,7 +5427,7 @@ fn sync_environment_prompt() -> Result<()> { )?; // Running `uv sync` should create `.venv` - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5289,7 +5439,7 @@ fn sync_environment_prompt() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // The `pyvenv.cfg` should contain the prompt matching the project name let pyvenv_cfg = context.read(".venv/pyvenv.cfg"); @@ -5316,7 +5466,7 @@ fn no_binary() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("iniconfig"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("iniconfig"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5326,11 +5476,11 @@ fn no_binary() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--no-binary"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--no-binary"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5341,9 +5491,9 @@ fn no_binary() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] ~ iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BINARY_PACKAGE", "iniconfig"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BINARY_PACKAGE", "iniconfig"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5354,9 +5504,9 @@ fn no_binary() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] ~ iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BINARY", "1"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BINARY", "1"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5367,7 +5517,7 @@ fn no_binary() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] ~ iniconfig==2.0.0 - "###); + "); uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BINARY", "iniconfig"), @r###" success: false @@ -5400,7 +5550,7 @@ fn no_binary_error() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("odrive"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("odrive"), @r" success: false exit_code: 2 ----- stdout ----- @@ -5408,7 +5558,7 @@ fn no_binary_error() -> Result<()> { ----- stderr ----- Resolved 31 packages in [TIME] error: Distribution `odrive==0.6.8 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-binary` but has no source distribution - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); @@ -5432,7 +5582,7 @@ fn no_build() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("iniconfig"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("iniconfig"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5442,11 +5592,11 @@ fn no_build() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "iniconfig"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "iniconfig"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5457,7 +5607,7 @@ fn no_build() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] ~ iniconfig==2.0.0 - "###); + "); Ok(()) } @@ -5479,7 +5629,7 @@ fn no_build_error() -> Result<()> { context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("django-allauth"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("django-allauth"), @r" success: false exit_code: 2 ----- stdout ----- @@ -5487,7 +5637,7 @@ fn no_build_error() -> Result<()> { ----- stderr ----- Resolved 19 packages in [TIME] error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + "); uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r" success: false @@ -5509,7 +5659,7 @@ fn no_build_error() -> Result<()> { error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution "); - uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "django-allauth"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "django-allauth"), @r" success: false exit_code: 2 ----- stdout ----- @@ -5517,7 +5667,7 @@ fn no_build_error() -> Result<()> { ----- stderr ----- Resolved 19 packages in [TIME] error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution - "###); + "); uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD", "django-allauth"), @r###" success: false @@ -5561,7 +5711,7 @@ fn sync_wheel_url_source_error() -> Result<()> { Resolved 3 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 2 ----- stdout ----- @@ -5571,7 +5721,7 @@ fn sync_wheel_url_source_error() -> Result<()> { error: Distribution `cffi==1.17.1 @ direct+https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform hint: You're using CPython 3.12 (`cp312`), but `cffi` (v1.17.1) only has wheels with the following Python ABI tag: `cp310` - "###); + "); Ok(()) } @@ -5612,7 +5762,7 @@ fn sync_wheel_path_source_error() -> Result<()> { Resolved 3 packages in [TIME] "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 2 ----- stdout ----- @@ -5622,7 +5772,7 @@ fn sync_wheel_path_source_error() -> Result<()> { error: Distribution `cffi==1.17.1 @ path+cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl` can't be installed because the binary distribution is incompatible with the current platform hint: You're using CPython 3.12 (`cp312`), but `cffi` (v1.17.1) only has wheels with the following Python ABI tag: `cp310` - "###); + "); Ok(()) } @@ -5684,7 +5834,7 @@ fn sync_override_package() -> Result<()> { .touch()?; // Syncing the project should _not_ install `core`. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5694,7 +5844,7 @@ fn sync_override_package() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + project==0.0.0 (from file://[TEMP_DIR]/) - "###); + "); // Mark the source as `package = true`. let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -5716,7 +5866,7 @@ fn sync_override_package() -> Result<()> { )?; // Syncing the project _should_ install `core`. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5728,7 +5878,7 @@ fn sync_override_package() -> Result<()> { Installed 2 packages in [TIME] + core==0.1.0 (from file://[TEMP_DIR]/core) ~ project==0.0.0 (from file://[TEMP_DIR]/) - "###); + "); // Remove `package = false`. let pyproject_toml = context.temp_dir.child("core").child("pyproject.toml"); @@ -5746,7 +5896,7 @@ fn sync_override_package() -> Result<()> { )?; // Syncing the project _should_ install `core`. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5757,7 +5907,7 @@ fn sync_override_package() -> Result<()> { Uninstalled 1 package in [TIME] Installed 1 package in [TIME] ~ core==0.1.0 (from file://[TEMP_DIR]/core) - "###); + "); // Mark the source as `package = false`. let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -5779,7 +5929,7 @@ fn sync_override_package() -> Result<()> { )?; // Syncing the project should _not_ install `core`. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -5791,7 +5941,7 @@ fn sync_override_package() -> Result<()> { Installed 1 package in [TIME] - core==0.1.0 (from file://[TEMP_DIR]/core) ~ project==0.0.0 (from file://[TEMP_DIR]/) - "###); + "); Ok(()) } @@ -5853,7 +6003,7 @@ fn transitive_dev() -> Result<()> { let init = src.child("__init__.py"); init.touch()?; - uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5866,7 +6016,7 @@ fn transitive_dev() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -5926,7 +6076,7 @@ fn sync_no_editable() -> Result<()> { let init = src.child("__init__.py"); init.touch()?; - uv_snapshot!(context.filters(), context.sync().arg("--no-editable"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-editable"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5937,7 +6087,7 @@ fn sync_no_editable() -> Result<()> { Installed 2 packages in [TIME] + child==0.1.0 (from file://[TEMP_DIR]/child) + root==0.1.0 (from file://[TEMP_DIR]/) - "###); + "); uv_snapshot!(context.filters(), context.sync().env(EnvVars::UV_NO_EDITABLE, "1"), @r" success: true @@ -5992,7 +6142,7 @@ fn sync_scripts_without_build_system() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6001,7 +6151,7 @@ fn sync_scripts_without_build_system() -> Result<()> { warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system` Resolved 1 package in [TIME] Audited in [TIME] - "###); + "); Ok(()) } @@ -6041,7 +6191,7 @@ fn sync_scripts_project_not_packaged() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6050,7 +6200,7 @@ fn sync_scripts_project_not_packaged() -> Result<()> { warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system` Resolved 1 package in [TIME] Audited in [TIME] - "###); + "); Ok(()) } @@ -6083,7 +6233,7 @@ fn sync_dynamic_extra() -> Result<()> { .child("requirements-dev.txt") .write_str("typing-extensions")?; - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6095,7 +6245,7 @@ fn sync_dynamic_extra() -> Result<()> { + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/) + typing-extensions==4.10.0 - "###); + "); let lock = context.read("uv.lock"); @@ -6156,7 +6306,7 @@ fn sync_dynamic_extra() -> Result<()> { ); // Check that we can re-read the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--locked"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6165,7 +6315,7 @@ fn sync_dynamic_extra() -> Result<()> { Resolved 3 packages in [TIME] Uninstalled 1 package in [TIME] - typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -6235,7 +6385,7 @@ fn build_system_requires_workspace() -> Result<()> { ", })?; - uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r" success: true exit_code: 0 ----- stdout ----- @@ -6248,7 +6398,7 @@ fn build_system_requires_workspace() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/project) - "###); + "); Ok(()) } @@ -6315,7 +6465,7 @@ fn build_system_requires_path() -> Result<()> { ", })?; - uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(context.temp_dir.child("project")), @r" success: true exit_code: 0 ----- stdout ----- @@ -6328,7 +6478,7 @@ fn build_system_requires_path() -> Result<()> { Installed 2 packages in [TIME] + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/project) - "###); + "); Ok(()) } @@ -6380,7 +6530,7 @@ fn sync_invalid_environment() -> Result<()> { fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?; // We can delete and use it - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6393,7 +6543,7 @@ fn sync_invalid_environment() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); let bin = venv_bin_path(context.temp_dir.join(".venv")); @@ -6402,7 +6552,7 @@ fn sync_invalid_environment() -> Result<()> { { fs_err::remove_file(bin.join("python"))?; fs_err::os::unix::fs::symlink(context.temp_dir.join("does-not-exist"), bin.join("python"))?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6415,7 +6565,7 @@ fn sync_invalid_environment() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); } // But if the Python executable is missing entirely we should also fail @@ -6503,7 +6653,7 @@ fn sync_no_sources_missing_member() -> Result<()> { let init = src.child("__init__.py"); init.touch()?; - uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6515,7 +6665,7 @@ fn sync_no_sources_missing_member() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -6534,7 +6684,7 @@ fn sync_python_version() -> Result<()> { "#})?; // We should respect the project's required version, not the first on the path - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6548,7 +6698,7 @@ fn sync_python_version() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Unless explicitly requested... uv_snapshot!(context.filters(), context.sync().arg("--python").arg("3.10"), @r" @@ -6571,7 +6721,7 @@ fn sync_python_version() -> Result<()> { ----- stderr ----- "###); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6585,7 +6735,7 @@ fn sync_python_version() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Create a pin that's incompatible with the project uv_snapshot!(context.filters(), context.python_pin().arg("3.10").arg("--no-workspace"), @r###" @@ -6624,7 +6774,7 @@ fn sync_python_version() -> Result<()> { "#}) .unwrap(); - uv_snapshot!(context.filters(), context.sync().current_dir(&child_dir), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(&child_dir), @r" success: true exit_code: 0 ----- stdout ----- @@ -6637,7 +6787,7 @@ fn sync_python_version() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -6667,7 +6817,7 @@ fn sync_explicit() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6677,13 +6827,13 @@ fn sync_explicit() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + idna==2.7 - "###); + "); // Clear the environment. fs_err::remove_dir_all(&context.venv)?; // The package should be drawn from the cache. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -6694,7 +6844,7 @@ fn sync_explicit() -> Result<()> { Resolved 2 packages in [TIME] Installed 1 package in [TIME] + idna==2.7 - "###); + "); Ok(()) } @@ -6756,7 +6906,7 @@ fn sync_all() -> Result<()> { context.lock().assert().success(); // Sync all workspace members. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6771,7 +6921,7 @@ fn sync_all() -> Result<()> { + iniconfig==2.0.0 + project==0.1.0 (from file://[TEMP_DIR]/) + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -6837,7 +6987,7 @@ fn sync_all_extras() -> Result<()> { context.lock().assert().success(); // Sync an extra that exists in both the parent and child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6850,10 +7000,10 @@ fn sync_all_extras() -> Result<()> { + iniconfig==2.0.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); // Sync an extra that only exists in the child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("testing"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("testing"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6866,10 +7016,10 @@ fn sync_all_extras() -> Result<()> { + packaging==24.0 - sniffio==1.3.1 - typing-extensions==4.10.0 - "###); + "); // Sync all extras. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6882,10 +7032,10 @@ fn sync_all_extras() -> Result<()> { + idna==3.6 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); // Sync all extras excluding an extra that exists in both the parent and child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("types"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("types"), @r" success: true exit_code: 0 ----- stdout ----- @@ -6894,10 +7044,10 @@ fn sync_all_extras() -> Result<()> { Resolved 8 packages in [TIME] Uninstalled 1 package in [TIME] - typing-extensions==4.10.0 - "###); + "); // Sync an extra that doesn't exist. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r" success: false exit_code: 2 ----- stdout ----- @@ -6905,10 +7055,10 @@ fn sync_all_extras() -> Result<()> { ----- stderr ----- Resolved 8 packages in [TIME] error: Extra `foo` is not defined in any project's `optional-dependencies` table - "###); + "); // Sync all extras excluding an extra that doesn't exist. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras").arg("--no-extra").arg("foo"), @r" success: false exit_code: 2 ----- stdout ----- @@ -6916,7 +7066,7 @@ fn sync_all_extras() -> Result<()> { ----- stderr ----- Resolved 8 packages in [TIME] error: Extra `foo` is not defined in any project's `optional-dependencies` table - "###); + "); Ok(()) } @@ -6992,7 +7142,7 @@ fn sync_all_extras_dynamic() -> Result<()> { context.lock().assert().success(); // Sync an extra that exists in the parent. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7004,10 +7154,10 @@ fn sync_all_extras_dynamic() -> Result<()> { + child==0.1.0 (from file://[TEMP_DIR]/child) + project==0.1.0 (from file://[TEMP_DIR]/) + sniffio==1.3.1 - "###); + "); // Sync a dynamic extra that exists in the child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("dev"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("dev"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7019,10 +7169,10 @@ fn sync_all_extras_dynamic() -> Result<()> { Installed 1 package in [TIME] - sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); // Sync a dynamic extra that doesn't exist in the child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("foo"), @r" success: false exit_code: 2 ----- stdout ----- @@ -7030,7 +7180,7 @@ fn sync_all_extras_dynamic() -> Result<()> { ----- stderr ----- Resolved 6 packages in [TIME] error: Extra `foo` is not defined in any project's `optional-dependencies` table - "###); + "); Ok(()) } @@ -7097,7 +7247,7 @@ fn sync_all_groups() -> Result<()> { context.lock().assert().success(); // Sync a group that exists in both the parent and child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("types"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("types"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7110,10 +7260,10 @@ fn sync_all_groups() -> Result<()> { + iniconfig==2.0.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); // Sync a group that only exists in the child. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("testing"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("testing"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7126,10 +7276,10 @@ fn sync_all_groups() -> Result<()> { + packaging==24.0 - sniffio==1.3.1 - typing-extensions==4.10.0 - "###); + "); // Sync a group that doesn't exist. - uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("foo"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("foo"), @r" success: false exit_code: 2 ----- stdout ----- @@ -7137,10 +7287,10 @@ fn sync_all_groups() -> Result<()> { ----- stderr ----- Resolved 8 packages in [TIME] error: Group `foo` is not defined in any project's `dependency-groups` table - "###); + "); // Sync an empty group. - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("empty"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("empty"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7149,7 +7299,7 @@ fn sync_all_groups() -> Result<()> { Resolved 8 packages in [TIME] Uninstalled 1 package in [TIME] - packaging==24.0 - "###); + "); Ok(()) } @@ -7201,7 +7351,7 @@ fn sync_multiple_sources_index_disjoint_extras() -> Result<()> { // Generate a lockfile. context.lock().assert().success(); - uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("cu124"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("cu124"), @r" success: true exit_code: 0 ----- stdout ----- @@ -7212,7 +7362,7 @@ fn sync_multiple_sources_index_disjoint_extras() -> Result<()> { Installed 2 packages in [TIME] + jinja2==3.1.3 + markupsafe==2.1.5 - "###); + "); Ok(()) } @@ -7243,7 +7393,7 @@ fn sync_derivation_chain() -> Result<()> { .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); - uv_snapshot!(filters, context.sync(), @r###" + uv_snapshot!(filters, context.sync(), @r#" success: false exit_code: 1 ----- stdout ----- @@ -7274,7 +7424,7 @@ fn sync_derivation_chain() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref` - "###); + "#); Ok(()) } @@ -7306,7 +7456,7 @@ fn sync_derivation_chain_extra() -> Result<()> { .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); - uv_snapshot!(filters, context.sync().arg("--extra").arg("wsgi"), @r###" + uv_snapshot!(filters, context.sync().arg("--extra").arg("wsgi"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -7337,7 +7487,7 @@ fn sync_derivation_chain_extra() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project[wsgi]` (v0.1.0) depends on `wsgiref` - "###); + "#); Ok(()) } @@ -7371,7 +7521,7 @@ fn sync_derivation_chain_group() -> Result<()> { .chain([(r"/.*/src", "/[TMP]/src")]) .collect::>(); - uv_snapshot!(filters, context.sync().arg("--group").arg("wsgi"), @r###" + uv_snapshot!(filters, context.sync().arg("--group").arg("wsgi"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -7402,7 +7552,7 @@ fn sync_derivation_chain_group() -> Result<()> { hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project:wsgi` (v0.1.0) depends on `wsgiref` - "###); + "#); Ok(()) } @@ -7496,7 +7646,7 @@ fn sync_stale_egg_info() -> Result<()> { } ); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7508,7 +7658,7 @@ fn sync_stale_egg_info() -> Result<()> { + member==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee#subdirectory=member) + root==0.1.dev5+gfea1041 (from git+https://github.com/astral-sh/uv-stale-egg-info-test.git@fea10416b9c479ac88fb217e14e40249b63bfbee) + setuptools==69.2.0 - "###); + "); Ok(()) } @@ -7591,7 +7741,7 @@ fn sync_git_repeated_member_static_metadata() -> Result<()> { } ); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7602,7 +7752,7 @@ fn sync_git_repeated_member_static_metadata() -> Result<()> { Installed 2 packages in [TIME] + uv-git-workspace-in-root==0.1.0 (from git+https://github.com/astral-sh/workspace-in-root-test.git@d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68) + workspace-member-in-subdir==0.1.0 (from git+https://github.com/astral-sh/workspace-in-root-test.git@d3ab48d2338296d47e28dbb2fb327c5e2ac4ac68#subdirectory=workspace-member-in-subdir) - "###); + "); Ok(()) } @@ -7707,7 +7857,7 @@ fn sync_git_repeated_member_dynamic_metadata() -> Result<()> { } ); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7720,7 +7870,7 @@ fn sync_git_repeated_member_dynamic_metadata() -> Result<()> { + iniconfig==2.0.0 + package==0.1.0 (from git+https://github.com/astral-sh/uv-dynamic-metadata-test.git@6c5aa0a65db737c9e7e2e60dc865bd8087012e64) + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -7803,7 +7953,7 @@ fn sync_git_repeated_member_backwards_path() -> Result<()> { } ); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7814,7 +7964,7 @@ fn sync_git_repeated_member_backwards_path() -> Result<()> { Installed 2 packages in [TIME] + dependency==0.1.0 (from git+https://github.com/astral-sh/uv-backwards-path-test@4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9#subdirectory=dependency) + package==0.1.0 (from git+https://github.com/astral-sh/uv-backwards-path-test@4bcc7fcd2e548c2ab7ba6b97b1c4e3ababccc7a9#subdirectory=root) - "###); + "); Ok(()) } @@ -7839,7 +7989,7 @@ fn mismatched_name_self_editable() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -7849,7 +7999,7 @@ fn mismatched_name_self_editable() -> Result<()> { × Failed to build `foo @ file://[TEMP_DIR]/` ╰─▶ Package metadata name `project` does not match given name `foo` help: `foo` was included because `project` (v0.1.0) depends on `foo` - "###); + "); Ok(()) } @@ -7871,7 +8021,7 @@ fn mismatched_name_cached_wheel() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7881,7 +8031,7 @@ fn mismatched_name_cached_wheel() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz) - "###); + "); pyproject_toml.write_str( r#" @@ -7893,7 +8043,7 @@ fn mismatched_name_cached_wheel() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -7901,7 +8051,7 @@ fn mismatched_name_cached_wheel() -> Result<()> { ----- stderr ----- × Failed to download and build `foo @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz` ╰─▶ Package metadata name `iniconfig` does not match given name `foo` - "###); + "); Ok(()) } @@ -7981,7 +8131,7 @@ fn sync_git_path_dependency() -> Result<()> { } ); - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -7992,7 +8142,7 @@ fn sync_git_path_dependency() -> Result<()> { Installed 2 packages in [TIME] + package1==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package1) + package2==0.1.0 (from git+https://github.com/astral-sh/uv-path-dependency-test.git@28781b32cf1f260cdb2c8040628079eb265202bd#subdirectory=package2) - "###); + "); Ok(()) } @@ -8096,7 +8246,7 @@ fn sync_build_tag() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8105,7 +8255,7 @@ fn sync_build_tag() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + build-tag==1.0.0 - "###); + "); // Ensure that we choose the highest build tag (5). uv_snapshot!(context.filters(), context.run().arg("--no-sync").arg("python").arg("-c").arg("import build_tag; build_tag.main()"), @r###" @@ -8165,7 +8315,7 @@ fn url_hash_mismatch() -> Result<()> { "#})?; // Running `uv sync` should fail. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -8181,7 +8331,7 @@ fn url_hash_mismatch() -> Result<()> { Computed: sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 help: `iniconfig` was included because `project` (v0.1.0) depends on `iniconfig` - "###); + "); Ok(()) } @@ -8238,7 +8388,7 @@ fn path_hash_mismatch() -> Result<()> { "#})?; // Running `uv sync` should fail. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -8254,7 +8404,7 @@ fn path_hash_mismatch() -> Result<()> { Computed: sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 help: `iniconfig` was included because `project` (v0.1.0) depends on `iniconfig` - "###); + "); Ok(()) } @@ -8290,7 +8440,7 @@ fn find_links_relative_in_config_works_from_subdir() -> Result<()> { subdir.create_dir_all()?; // Run `uv sync --offline` from subdir. We expect it to find the local wheel in ../packages/. - uv_snapshot!(context.filters(), context.sync().current_dir(&subdir).arg("--offline"), @r###" + uv_snapshot!(context.filters(), context.sync().current_dir(&subdir).arg("--offline"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8300,7 +8450,7 @@ fn find_links_relative_in_config_works_from_subdir() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + ok==1.0.0 - "###); + "); Ok(()) } @@ -8321,23 +8471,23 @@ fn sync_dry_run() -> Result<()> { )?; // Perform a `--dry-run`. - uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] - Would create virtual environment at: .venv + Would create project 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 - "###); + "); // Perform a full sync. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -8349,7 +8499,7 @@ fn sync_dry_run() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // Update the requirements. pyproject_toml.write_str( @@ -8362,13 +8512,13 @@ fn sync_dry_run() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv + Would use project environment at: .venv Resolved 2 packages in [TIME] Would update lockfile at: uv.lock Would download 1 package @@ -8376,7 +8526,7 @@ fn sync_dry_run() -> Result<()> { Would install 1 package - iniconfig==2.0.0 + typing-extensions==4.10.0 - "###); + "); // Update the `requires-python`. pyproject_toml.write_str( @@ -8396,7 +8546,7 @@ fn sync_dry_run() -> Result<()> { ----- stderr ----- Using CPython 3.9.[X] interpreter at: [PYTHON-3.9] - Would replace existing virtual environment at: .venv + Would replace project environment at: .venv warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'` Resolved 2 packages in [TIME] Would update lockfile at: uv.lock @@ -8436,7 +8586,7 @@ fn sync_dry_run() -> Result<()> { ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv + Would use project environment at: .venv Resolved 2 packages in [TIME] Found up-to-date lockfile at: uv.lock Audited 1 package in [TIME] @@ -8484,7 +8634,7 @@ fn sync_dry_run_and_locked() -> Result<()> { ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv + Would use project environment at: .venv Resolved 2 packages in [TIME] Would download 1 package Would install 1 package @@ -8536,8 +8686,7 @@ fn sync_dry_run_and_frozen() -> Result<()> { ----- stdout ----- ----- stderr ----- - Discovered existing environment at: .venv - Found up-to-date lockfile at: uv.lock + Would use project environment at: .venv Would download 3 packages Would install 3 packages + anyio==3.7.0 @@ -8632,7 +8781,7 @@ fn sync_script() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8642,7 +8791,7 @@ fn sync_script() -> Result<()> { Resolved 3 packages in [TIME] Uninstalled 1 package in [TIME] - iniconfig==2.0.0 - "###); + "); // Modify the `requires-python`. script.write_str(indoc! { r#" @@ -8657,13 +8806,13 @@ fn sync_script() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Recreating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Updating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] Resolved 5 packages in [TIME] Prepared 2 packages in [TIME] Installed 5 packages in [TIME] @@ -8672,7 +8821,7 @@ fn sync_script() -> Result<()> { + idna==3.6 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); // `--locked` and `--frozen` should fail with helpful error messages. uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r" @@ -8923,7 +9072,7 @@ fn sync_locked_script() -> Result<()> { ----- stdout ----- ----- stderr ----- - Recreating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Updating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'` Resolved 6 packages in [TIME] error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. @@ -8983,7 +9132,7 @@ fn sync_script_with_compatible_build_constraints() -> Result<()> { )]) .collect::>(); - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8999,7 +9148,7 @@ fn sync_script_with_compatible_build_constraints() -> Result<()> { + requests==1.2.0 + sniffio==1.3.1 + typing-extensions==4.10.0 - "###); + "); Ok(()) } @@ -9035,7 +9184,7 @@ fn sync_script_with_incompatible_build_constraints() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" success: false exit_code: 1 ----- stdout ----- @@ -9046,7 +9195,7 @@ fn sync_script_with_incompatible_build_constraints() -> Result<()> { ├─▶ Failed to resolve requirements from `setup.py` build ├─▶ No solution found when resolving: `setuptools>=40.8.0` ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. - "###); + "); Ok(()) } @@ -9069,7 +9218,7 @@ fn unsupported_git_scheme() -> Result<()> { "#}, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: false exit_code: 1 ----- stdout ----- @@ -9080,7 +9229,7 @@ fn unsupported_git_scheme() -> Result<()> { × Failed to build `foo @ file://[TEMP_DIR]/` ├─▶ Failed to parse entry: `foo` ╰─▶ Unsupported Git URL scheme `c:` in `c:/home/ferris/projects/foo` (expected one of `https:`, `ssh:`, or `file:`) - "###); + "); Ok(()) } @@ -9119,7 +9268,7 @@ fn multiple_group_conflicts() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -9127,9 +9276,9 @@ fn multiple_group_conflicts() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] Audited in [TIME] - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9139,9 +9288,9 @@ fn multiple_group_conflicts() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("baz"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9149,9 +9298,9 @@ fn multiple_group_conflicts() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] Audited 1 package in [TIME] - "###); + "); - uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar").arg("--group").arg("baz"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar").arg("--group").arg("baz"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9163,7 +9312,7 @@ fn multiple_group_conflicts() -> Result<()> { Installed 1 package in [TIME] - iniconfig==2.0.0 + iniconfig==1.1.1 - "###); + "); uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r" success: false @@ -9577,7 +9726,7 @@ fn prune_cache_url_subdirectory() -> Result<()> { context.prune().arg("--ci").assert().success(); // Install the project. - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -9590,7 +9739,7 @@ fn prune_cache_url_subdirectory() -> Result<()> { + idna==3.6 + root==0.0.1 (from https://github.com/user-attachments/files/18216295/subdirectory-test.tar.gz#subdirectory=packages/root) + sniffio==1.3.1 - "###); + "); Ok(()) } @@ -10006,7 +10155,7 @@ fn sync_upload_time() -> Result<()> { "#)?; // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- @@ -10017,17 +10166,17 @@ fn sync_upload_time() -> Result<()> { + anyio==3.7.0 + idna==3.6 + sniffio==1.3.1 - "###); + "); // Re-install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Audited 3 packages in [TIME] - "###); + "); Ok(()) } @@ -10255,7 +10404,7 @@ fn read_only() -> Result<()> { "#, )?; - uv_snapshot!(context.filters(), context.sync(), @r###" + uv_snapshot!(context.filters(), context.sync(), @r" success: true exit_code: 0 ----- stdout ----- @@ -10265,7 +10414,7 @@ fn read_only() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); assert!(context.temp_dir.child("uv.lock").exists()); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a9c2f9dafe7d4..5ce325a6bfee9 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1019,11 +1019,6 @@ uv sync [OPTIONS]
    • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
    • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
    • -
--format format

Select the output format

-

[default: text]

Possible values:

-
    -
  • text: Display the result in a human-readable format
  • -
  • json: Display the result in JSON format
--frozen

Sync without updating the uv.lock file.

Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

May also be set with the UV_FROZEN environment variable.

--group group

Include dependencies from the specified dependency group.

@@ -1112,7 +1107,12 @@ uv sync [OPTIONS]
--only-group only-group

Only include dependencies from the specified dependency group.

The project and its dependencies will be omitted.

May be provided multiple times. Implies --no-default-groups.

-
--package package

Sync for a specific package in the workspace.

+
--output-format output-format

Select the output format

+

[default: text]

Possible values:

+
    +
  • text: Display the result in a human-readable format
  • +
  • json: Display the result in JSON format
  • +
--package package

Sync for a specific package in the workspace.

The workspace's environment (.venv) is updated to reflect the subset of dependencies declared by the specified workspace member package.

If the workspace member does not exist, uv will exit with an error.

--prerelease prerelease

The strategy to use when considering pre-release versions.

From 5b73350407e90a1c4a9b7facaac036f3af1fdcff Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 14 Jul 2025 09:42:04 -0500 Subject: [PATCH 22/22] Use `Self` in constructors Co-authored-by: John Mumm --- crates/uv/src/commands/project/sync.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index c3cfa75874937..8ccfac0c8e94e 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -839,7 +839,7 @@ struct WorkspaceReport { impl From<&Workspace> for WorkspaceReport { fn from(workspace: &Workspace) -> Self { - WorkspaceReport { + Self { path: workspace.install_path().as_path().into(), } } @@ -854,7 +854,7 @@ struct ProjectReport { impl From<&VirtualProject> for ProjectReport { fn from(project: &VirtualProject) -> Self { - ProjectReport { + Self { path: project.root().into(), workspace: WorkspaceReport::from(project.workspace()), } @@ -878,7 +878,7 @@ struct ScriptReport { impl From<&Pep723Script> for ScriptReport { fn from(script: &Pep723Script) -> Self { - ScriptReport { + Self { path: script.path.as_path().into(), } } @@ -1033,7 +1033,7 @@ struct PythonReport { impl From<&uv_python::Interpreter> for PythonReport { fn from(interpreter: &uv_python::Interpreter) -> Self { - PythonReport { + Self { path: interpreter.sys_executable().into(), version: interpreter.python_full_version().clone(), implementation: interpreter.implementation_name().to_string(), @@ -1060,7 +1060,7 @@ struct EnvironmentReport { impl From<&PythonEnvironment> for EnvironmentReport { fn from(env: &PythonEnvironment) -> Self { - EnvironmentReport { + Self { python: PythonReport::from(env.interpreter()), path: env.root().into(), } @@ -1157,7 +1157,7 @@ struct LockReport { impl From<(&LockTarget<'_>, &LockMode<'_>, &Outcome)> for LockReport { fn from((target, mode, outcome): (&LockTarget, &LockMode, &Outcome)) -> Self { - LockReport { + Self { path: target.lock_path().deref().into(), action: match outcome { Outcome::Success(result) => {