From 38884da9b902e6dcd553beb4b79aeef7d066c8d2 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 21 May 2025 09:46:09 -0400 Subject: [PATCH] make `uv version` lock and sync (#13317) This adopts the logic from `uv remove` for locking and syncing, as the scope of the changes made are ultimately similar. Unlike `uv remove` there is no support for modifying PEP723 scripts, as these are not versioned. In doing this the `version` command gains a truckload of args for configuring lock/sync behaviour. Presumably most of these are passed via settings or env files, and not of particular concern. The most interesting additions are: * `--frozen`: makes `uv version` work ~exactly as it did before this PR * `--locked`: errors if the lockfile is out of date * `--no-sync`: updates the lockfile, but doesn't run the equivalent of `uv sync` * `--package name`: a convenience for referring to a package in the workspace Note that the existing `--dry-run` flag effectively implies `--frozen` for sets and bumps. Fixes #13254 Fixes #13548 --- crates/uv-cli/src/lib.rs | 68 ++- crates/uv/src/commands/mod.rs | 3 +- crates/uv/src/commands/project/mod.rs | 1 + crates/uv/src/commands/project/version.rs | 595 ++++++++++++++++++++ crates/uv/src/lib.rs | 78 ++- crates/uv/src/settings.rs | 70 +++ crates/uv/tests/it/help.rs | 14 +- crates/uv/tests/it/version.rs | 633 +++++++++++++++++++++- docs/reference/cli.md | 496 ++++++++++++----- 9 files changed, 1776 insertions(+), 182 deletions(-) create mode 100644 crates/uv/src/commands/project/version.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 039d9ccf0..df4a19919 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -493,8 +493,6 @@ pub enum Commands { /// Clear the cache, removing all entries or those linked to specific packages. #[command(hide = true)] Clean(CleanArgs), - /// Read or update the project's version. - Version(VersionArgs), /// Generate shell completion #[command(alias = "--generate-shell-completion", hide = true)] GenerateShellCompletion(GenerateShellCompletionArgs), @@ -525,29 +523,91 @@ pub struct HelpArgs { pub command: Option>, } -#[derive(Args, Debug)] +#[derive(Args)] #[command(group = clap::ArgGroup::new("operation"))] +#[allow(clippy::struct_excessive_bools)] pub struct VersionArgs { /// Set the project version to this value /// /// To update the project using semantic versioning components instead, use `--bump`. #[arg(group = "operation")] pub value: Option, + /// Update the project version using the given semantics #[arg(group = "operation", long)] pub bump: Option, + /// Don't write a new version to the `pyproject.toml` /// /// Instead, the version will be displayed. #[arg(long)] pub dry_run: bool, + /// Only show the version /// /// By default, uv will show the project name before the version. #[arg(long)] pub short: bool, + + /// The format of the output #[arg(long, value_enum, default_value = "text")] pub output_format: VersionFormat, + + /// Avoid syncing the virtual environment after re-locking the project. + #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] + pub no_sync: bool, + + /// Prefer the active virtual environment over the project's virtual environment. + /// + /// If the project virtual environment is active or no virtual environment is active, this has + /// no effect. + #[arg(long, overrides_with = "no_active")] + pub active: bool, + + /// Prefer project's virtual environment over an active environment. + /// + /// This is the default behavior. + #[arg(long, overrides_with = "active", hide = true)] + pub no_active: bool, + + /// Assert that the `uv.lock` will remain unchanged. + /// + /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, + /// uv will exit with an error. + #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + pub locked: bool, + + /// Update the version without re-locking the project. + /// + /// The project environment will not be synced. + #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + pub frozen: bool, + + #[command(flatten)] + pub installer: ResolverInstallerArgs, + + #[command(flatten)] + pub build: BuildOptionsArgs, + + #[command(flatten)] + pub refresh: RefreshArgs, + + /// Update the version of a specific package in the workspace. + #[arg(long, conflicts_with = "isolated")] + pub package: Option, + + /// The Python interpreter to use for resolving and syncing. + /// + /// See `uv help python` for details on Python discovery and supported request formats. + #[arg( + long, + short, + env = EnvVars::UV_PYTHON, + verbatim_doc_comment, + help_heading = "Python options", + value_parser = parse_maybe_string, + )] + pub python: Option>, } #[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)] @@ -819,6 +879,8 @@ pub enum ProjectCommand { after_long_help = "" )] Remove(RemoveArgs), + /// Read or update the project's version. + Version(VersionArgs), /// Update the project's environment. /// /// Syncing ensures that all project dependencies are installed and up-to-date with the diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 8e3c9ef37..0203d4dd5 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -29,6 +29,7 @@ pub(crate) use project::remove::remove; pub(crate) use project::run::{RunCommand, run}; pub(crate) use project::sync::sync; pub(crate) use project::tree::tree; +pub(crate) use project::version::{project_version, self_version}; pub(crate) use publish::publish; pub(crate) use python::dir::dir as python_dir; pub(crate) use python::find::find as python_find; @@ -56,7 +57,6 @@ use uv_normalize::PackageName; use uv_python::PythonEnvironment; use uv_scripts::Pep723Script; pub(crate) use venv::venv; -pub(crate) use version::{project_version, self_version}; use crate::printer::Printer; @@ -77,7 +77,6 @@ mod run; mod self_update; mod tool; mod venv; -mod version; #[derive(Copy, Clone)] pub(crate) enum ExitStatus { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 9adeb7216..2569b962f 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -67,6 +67,7 @@ pub(crate) mod remove; pub(crate) mod run; pub(crate) mod sync; pub(crate) mod tree; +pub(crate) mod version; #[derive(thiserror::Error, Debug)] pub(crate) enum ProjectError { diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs new file mode 100644 index 000000000..0e50c2ac0 --- /dev/null +++ b/crates/uv/src/commands/project/version.rs @@ -0,0 +1,595 @@ +use std::fmt::Write; +use std::str::FromStr; +use std::{cmp::Ordering, path::Path}; + +use anyhow::{Context, Result, anyhow}; +use owo_colors::OwoColorize; + +use tracing::debug; +use uv_cache::Cache; +use uv_cli::version::VersionInfo; +use uv_cli::{VersionBump, VersionFormat}; +use uv_configuration::{ + Concurrency, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, InstallOptions, + PreviewMode, +}; +use uv_fs::Simplified; +use uv_normalize::DefaultExtras; +use uv_pep440::Version; +use uv_pep508::PackageName; +use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; +use uv_settings::PythonInstallMirrors; +use uv_warnings::warn_user; +use uv_workspace::pyproject_mut::Error; +use uv_workspace::{ + DiscoveryOptions, WorkspaceCache, + pyproject_mut::{DependencyTarget, PyProjectTomlMut}, +}; +use uv_workspace::{VirtualProject, Workspace}; + +use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; +use crate::commands::pip::operations::Modifications; +use crate::commands::project::add::{AddTarget, PythonTarget}; +use crate::commands::project::install_target::InstallTarget; +use crate::commands::project::lock::LockMode; +use crate::commands::project::{ + ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, default_dependency_groups, +}; +use crate::commands::{ExitStatus, diagnostics, project}; +use crate::printer::Printer; +use crate::settings::{NetworkSettings, ResolverInstallerSettings}; + +/// Display version information for uv itself (`uv self version`) +pub(crate) fn self_version( + short: bool, + output_format: VersionFormat, + printer: Printer, +) -> Result { + let version_info = uv_cli::version::uv_self_version(); + print_version(version_info, None, short, output_format, printer)?; + + Ok(ExitStatus::Success) +} + +/// Read or update project version (`uv version`) +#[allow(clippy::fn_params_excessive_bools)] +pub(crate) async fn project_version( + value: Option, + bump: Option, + short: bool, + output_format: VersionFormat, + strict: bool, + project_dir: &Path, + package: Option, + dry_run: bool, + locked: bool, + frozen: bool, + active: Option, + no_sync: bool, + python: Option, + install_mirrors: PythonInstallMirrors, + settings: ResolverInstallerSettings, + network_settings: NetworkSettings, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + installer_metadata: bool, + concurrency: Concurrency, + no_config: bool, + cache: &Cache, + printer: Printer, + preview: PreviewMode, +) -> Result { + // Read the metadata + let project = match find_target(project_dir, package.as_ref()).await { + Ok(target) => target, + Err(err) => { + // If strict, hard bail on failing to find the pyproject.toml + if strict { + return Err(err)?; + } + // Otherwise, warn and provide fallback to the old `uv version` from before 0.7.0 + warn_user!( + "Failed to read project metadata ({err}). Running `{}` for compatibility. This fallback will be removed in the future; pass `--preview` to force an error.", + "uv self version".green() + ); + return self_version(short, output_format, printer); + } + }; + + let pyproject_path = project.root().join("pyproject.toml"); + let Some(name) = project.project_name().cloned() else { + return Err(anyhow!( + "Missing `project.name` field in: {}", + pyproject_path.user_display() + )); + }; + + // Short-circuit early for a frozen read + let is_read_only = value.is_none() && bump.is_none(); + if frozen && is_read_only { + return Box::pin(print_frozen_version( + project, + &name, + project_dir, + active, + python, + install_mirrors, + &settings, + network_settings, + python_preference, + python_downloads, + concurrency, + no_config, + cache, + short, + output_format, + printer, + preview, + )) + .await; + } + + let mut toml = PyProjectTomlMut::from_toml( + project.pyproject_toml().raw.as_ref(), + DependencyTarget::PyProjectToml, + )?; + + let old_version = toml.version().map_err(|err| match err { + Error::MalformedWorkspace => { + if toml.has_dynamic_version() { + anyhow!( + "We cannot get or set dynamic project versions in: {}", + pyproject_path.user_display() + ) + } else { + anyhow!( + "There is no 'project.version' field in: {}", + pyproject_path.user_display() + ) + } + } + err => { + anyhow!("{err}: {}", pyproject_path.user_display()) + } + })?; + + // Figure out new metadata + let new_version = if let Some(value) = value { + match Version::from_str(&value) { + Ok(version) => Some(version), + Err(err) => match &*value { + "major" | "minor" | "patch" => { + return Err(anyhow!( + "Invalid version `{value}`, did you mean to pass `--bump {value}`?" + )); + } + _ => { + return Err(err)?; + } + }, + } + } else if let Some(bump) = bump { + Some(bumped_version(&old_version, bump, printer)?) + } else { + None + }; + + // Update the toml and lock + let status = if dry_run { + ExitStatus::Success + } else if let Some(new_version) = &new_version { + let project = update_project(project, new_version, &mut toml, &pyproject_path)?; + Box::pin(lock_and_sync( + project, + project_dir, + locked, + frozen, + active, + no_sync, + python, + install_mirrors, + &settings, + network_settings, + python_preference, + python_downloads, + installer_metadata, + concurrency, + no_config, + cache, + printer, + preview, + )) + .await? + } else { + debug!("No changes to version; skipping update"); + ExitStatus::Success + }; + + // Report the results + let old_version = VersionInfo::new(Some(&name), &old_version); + let new_version = new_version.map(|version| VersionInfo::new(Some(&name), &version)); + print_version(old_version, new_version, short, output_format, printer)?; + + Ok(status) +} + +/// Find the pyproject.toml we're modifying +/// +/// Note that `uv version` never needs to support PEP 723 scripts, as those are unversioned. +async fn find_target(project_dir: &Path, package: Option<&PackageName>) -> Result { + // Find the project in the workspace. + // No workspace caching since `uv version` changes the workspace definition. + let project = if let Some(package) = package { + VirtualProject::Project( + Workspace::discover( + project_dir, + &DiscoveryOptions::default(), + &WorkspaceCache::default(), + ) + .await? + .with_current_project(package.clone()) + .with_context(|| format!("Package `{package}` not found in workspace"))?, + ) + } else { + VirtualProject::discover( + project_dir, + &DiscoveryOptions::default(), + &WorkspaceCache::default(), + ) + .await? + }; + Ok(project) +} + +/// Update the pyproject.toml on-disk and in-memory with a new version +fn update_project( + project: VirtualProject, + new_version: &Version, + toml: &mut PyProjectTomlMut, + pyproject_path: &Path, +) -> Result { + // Save to disk + toml.set_version(new_version)?; + let content = toml.to_string(); + fs_err::write(pyproject_path, &content)?; + + // Update the `pyproject.toml` in-memory. + let project = project + .with_pyproject_toml(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?) + .ok_or(ProjectError::PyprojectTomlUpdate)?; + + Ok(project) +} + +/// Do the minimal work to try to find the package in the lockfile and print its version +async fn print_frozen_version( + project: VirtualProject, + name: &PackageName, + project_dir: &Path, + active: Option, + python: Option, + install_mirrors: PythonInstallMirrors, + settings: &ResolverInstallerSettings, + network_settings: NetworkSettings, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + concurrency: Concurrency, + no_config: bool, + cache: &Cache, + short: bool, + output_format: VersionFormat, + printer: Printer, + preview: PreviewMode, +) -> Result { + // Discover the interpreter (this is the same interpreter --no-sync uses). + let interpreter = ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + &network_settings, + python_preference, + python_downloads, + &install_mirrors, + false, + no_config, + active, + cache, + printer, + ) + .await? + .into_interpreter(); + + let target = AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter))); + + // Initialize any shared state. + let state = UniversalState::default(); + + // Lock and sync the environment, if necessary. + let lock = match project::lock::LockOperation::new( + LockMode::Frozen, + &settings.resolver, + &network_settings, + &state, + Box::new(DefaultResolveLogger), + concurrency, + cache, + printer, + preview, + ) + .execute((&target).into()) + .await + { + Ok(result) => result.into_lock(), + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); + } + Err(err) => return Err(err.into()), + }; + + // Try to find the package of interest in the lock + let Some(package) = lock + .packages() + .iter() + .find(|package| package.name() == name) + else { + return Err(anyhow!( + "Failed to find the {name}'s version in the frozen lockfile" + )); + }; + let Some(version) = package.version() else { + return Err(anyhow!( + "Failed to find the {name}'s version in the frozen lockfile" + )); + }; + + // Finally, print! + let old_version = VersionInfo::new(Some(name), version); + print_version(old_version, None, short, output_format, printer)?; + + Ok(ExitStatus::Success) +} + +/// Re-lock and re-sync the project after a series of edits. +#[allow(clippy::fn_params_excessive_bools)] +async fn lock_and_sync( + project: VirtualProject, + project_dir: &Path, + locked: bool, + frozen: bool, + active: Option, + no_sync: bool, + python: Option, + install_mirrors: PythonInstallMirrors, + settings: &ResolverInstallerSettings, + network_settings: NetworkSettings, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + installer_metadata: bool, + concurrency: Concurrency, + no_config: bool, + cache: &Cache, + printer: Printer, + preview: PreviewMode, +) -> Result { + // If frozen, don't touch the lock or sync at all + if frozen { + return Ok(ExitStatus::Success); + } + + // Convert to an `AddTarget` by attaching the appropriate interpreter or environment. + let target = if no_sync { + // Discover the interpreter. + let interpreter = ProjectInterpreter::discover( + project.workspace(), + project_dir, + python.as_deref().map(PythonRequest::parse), + &network_settings, + python_preference, + python_downloads, + &install_mirrors, + false, + no_config, + active, + cache, + printer, + ) + .await? + .into_interpreter(); + + AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter))) + } else { + // Discover or create the virtual environment. + let environment = ProjectEnvironment::get_or_init( + project.workspace(), + python.as_deref().map(PythonRequest::parse), + &install_mirrors, + &network_settings, + python_preference, + python_downloads, + no_sync, + no_config, + active, + cache, + DryRun::Disabled, + printer, + ) + .await? + .into_environment()?; + + AddTarget::Project(project, Box::new(PythonTarget::Environment(environment))) + }; + + // Determine the lock mode. + let mode = if locked { + LockMode::Locked(target.interpreter()) + } else { + LockMode::Write(target.interpreter()) + }; + + // Initialize any shared state. + let state = UniversalState::default(); + + // Lock and sync the environment, if necessary. + let lock = match project::lock::LockOperation::new( + mode, + &settings.resolver, + &network_settings, + &state, + Box::new(DefaultResolveLogger), + concurrency, + cache, + printer, + preview, + ) + .execute((&target).into()) + .await + { + Ok(result) => result.into_lock(), + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); + } + Err(err) => return Err(err.into()), + }; + + let AddTarget::Project(project, environment) = target else { + // If we're not adding to a project, exit early. + return Ok(ExitStatus::Success); + }; + + let PythonTarget::Environment(venv) = &*environment else { + // If we're not syncing, exit early. + return Ok(ExitStatus::Success); + }; + + // Perform a full sync, because we don't know what exactly is affected by the version. + // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? + let extras = ExtrasSpecification::from_all_extras(); + let install_options = InstallOptions::default(); + + // Determine the default groups to include. + let default_groups = default_dependency_groups(project.pyproject_toml())?; + + // Determine the default extras to include. + let default_extras = DefaultExtras::default(); + + // Identify the installation target. + let target = match &project { + VirtualProject::Project(project) => InstallTarget::Project { + workspace: project.workspace(), + name: project.project_name(), + lock: &lock, + }, + VirtualProject::NonProject(workspace) => InstallTarget::NonProjectWorkspace { + workspace, + lock: &lock, + }, + }; + + let state = state.fork(); + + match project::sync::do_sync( + target, + venv, + &extras.with_defaults(default_extras), + &DependencyGroups::default().with_defaults(default_groups), + EditableMode::Editable, + install_options, + Modifications::Sufficient, + settings.into(), + &network_settings, + &state, + Box::new(DefaultInstallLogger), + installer_metadata, + concurrency, + cache, + DryRun::Disabled, + printer, + preview, + ) + .await + { + Ok(()) => {} + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); + } + Err(err) => return Err(err.into()), + } + + Ok(ExitStatus::Success) +} + +fn print_version( + old_version: VersionInfo, + new_version: Option, + short: bool, + output_format: VersionFormat, + printer: Printer, +) -> Result<()> { + match output_format { + VersionFormat::Text => { + if let Some(name) = &old_version.package_name { + if !short { + write!(printer.stdout(), "{name} ")?; + } + } + if let Some(new_version) = new_version { + if short { + writeln!(printer.stdout(), "{}", new_version.cyan())?; + } else { + writeln!( + printer.stdout(), + "{} => {}", + old_version.cyan(), + new_version.cyan() + )?; + } + } else { + writeln!(printer.stdout(), "{}", old_version.cyan())?; + } + } + VersionFormat::Json => { + let final_version = new_version.unwrap_or(old_version); + let string = serde_json::to_string_pretty(&final_version)?; + writeln!(printer.stdout(), "{string}")?; + } + } + Ok(()) +} + +fn bumped_version(from: &Version, bump: VersionBump, printer: Printer) -> Result { + // All prereleasey details "carry to 0" with every currently supported mode of `--bump` + // We could go out of our way to preserve epoch information but no one uses those... + if from.any_prerelease() || from.is_post() || from.is_local() || from.epoch() > 0 { + writeln!( + printer.stderr(), + "warning: prerelease information will be cleared as part of the version bump" + )?; + } + + let index = match bump { + VersionBump::Major => 0, + VersionBump::Minor => 1, + VersionBump::Patch => 2, + }; + + // Use `max` here to try to do 0.2 => 0.3 instead of 0.2 => 0.3.0 + let old_parts = from.release(); + let len = old_parts.len().max(index + 1); + let new_release_vec = (0..len) + .map(|i| match i.cmp(&index) { + // Everything before the bumped value is preserved (or is an implicit 0) + Ordering::Less => old_parts.get(i).copied().unwrap_or(0), + // This is the value to bump (could be implicit 0) + Ordering::Equal => old_parts.get(i).copied().unwrap_or(0) + 1, + // Everything after the bumped value becomes 0 + Ordering::Greater => 0, + }) + .collect::>(); + Ok(Version::new(new_release_vec)) +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 05174a947..b2c1fb690 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -27,7 +27,7 @@ use uv_cli::SelfUpdateArgs; use uv_cli::{ BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand, - ToolNamespace, TopLevelArgs, VersionArgs, compat::CompatArgs, + ToolNamespace, TopLevelArgs, compat::CompatArgs, }; use uv_configuration::min_stack_size; use uv_fs::{CWD, Simplified}; @@ -1046,6 +1046,7 @@ async fn run(mut cli: Cli) -> Result { } Commands::Project(project) => { Box::pin(run_project( + cli.top_level.global_args.project.is_some(), project, &project_dir, run_command, @@ -1084,33 +1085,6 @@ async fn run(mut cli: Cli) -> Result { is not available. Please use your package manager to update uv." ); } - Commands::Version(VersionArgs { - value, - bump, - dry_run, - short, - output_format, - }) => { - // If they specified any of these flags, they probably don't mean `uv self version` - let strict = cli.top_level.global_args.project.is_some() - || globals.preview.is_enabled() - || dry_run - || bump.is_some() - || value.is_some(); - commands::project_version( - &project_dir, - value, - bump, - dry_run, - short, - output_format, - strict, - &workspace_cache, - printer, - ) - .await - } - Commands::GenerateShellCompletion(args) => { args.shell.generate(&mut Cli::command(), &mut stdout()); Ok(ExitStatus::Success) @@ -1614,6 +1588,7 @@ async fn run(mut cli: Cli) -> Result { /// Run a [`ProjectCommand`]. async fn run_project( + project_was_explicit: bool, project_command: Box, project_dir: &Path, command: Option, @@ -1989,6 +1964,53 @@ async fn run_project( )) .await } + ProjectCommand::Version(args) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::VersionSettings::resolve(args, filesystem); + show_settings!(args); + + // Initialize the cache. + let cache = cache.init()?.with_refresh( + args.refresh + .combine(Refresh::from(args.settings.reinstall.clone())) + .combine(Refresh::from(args.settings.resolver.upgrade.clone())), + ); + + // If they specified any of these flags, they probably don't mean `uv self version` + let strict = project_was_explicit + || globals.preview.is_enabled() + || args.dry_run + || args.bump.is_some() + || args.value.is_some() + || args.package.is_some(); + Box::pin(commands::project_version( + args.value, + args.bump, + args.short, + args.output_format, + strict, + project_dir, + args.package, + args.dry_run, + args.locked, + args.frozen, + args.active, + args.no_sync, + args.python, + args.install_mirrors, + args.settings, + globals.network_settings, + globals.python_preference, + globals.python_downloads, + globals.installer_metadata, + globals.concurrency, + no_config, + &cache, + printer, + globals.preview, + )) + .await + } ProjectCommand::Tree(args) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = settings::TreeSettings::resolve(args, filesystem); diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index f11ca7834..dce7c58f0 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -14,6 +14,7 @@ use uv_cli::{ PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, PythonPinArgs, PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs, + VersionArgs, VersionBump, VersionFormat, }; use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, @@ -1487,6 +1488,75 @@ impl RemoveSettings { } } +/// The resolved settings to use for a `version` invocation. +#[allow(clippy::struct_excessive_bools, dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct VersionSettings { + pub(crate) value: Option, + pub(crate) bump: Option, + pub(crate) short: bool, + pub(crate) output_format: VersionFormat, + pub(crate) dry_run: bool, + pub(crate) locked: bool, + pub(crate) frozen: bool, + pub(crate) active: Option, + pub(crate) no_sync: bool, + pub(crate) package: Option, + pub(crate) python: Option, + pub(crate) install_mirrors: PythonInstallMirrors, + pub(crate) refresh: Refresh, + pub(crate) settings: ResolverInstallerSettings, +} + +impl VersionSettings { + /// Resolve the [`RemoveSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: VersionArgs, filesystem: Option) -> Self { + let VersionArgs { + value, + bump, + short, + output_format, + dry_run, + no_sync, + locked, + frozen, + active, + no_active, + installer, + build, + refresh, + package, + python, + } = args; + + let install_mirrors = filesystem + .clone() + .map(|fs| fs.install_mirrors.clone()) + .unwrap_or_default(); + + Self { + value, + bump, + short, + output_format, + dry_run, + locked, + frozen, + active: flag(active, no_active), + no_sync, + package, + python: python.and_then(Maybe::into_option), + refresh: Refresh::from(refresh), + settings: ResolverInstallerSettings::combine( + resolver_installer_options(installer, build), + filesystem, + ), + install_mirrors, + } + } +} + /// The resolved settings to use for a `tree` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index bd9f87d72..932991859 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -20,6 +20,7 @@ fn help() { init Create a new project add Add dependencies to the project remove Remove dependencies from the project + version Read or update the project's version sync Update the project's environment lock Update the project's lockfile export Export the project's lockfile to an alternate format @@ -32,7 +33,6 @@ fn help() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Read or update the project's version generate-shell-completion Generate shell completion help Display documentation for a command @@ -100,6 +100,7 @@ fn help_flag() { init Create a new project add Add dependencies to the project remove Remove dependencies from the project + version Read or update the project's version sync Update the project's environment lock Update the project's lockfile export Export the project's lockfile to an alternate format @@ -112,7 +113,6 @@ fn help_flag() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Read or update the project's version help Display documentation for a command Cache options: @@ -178,6 +178,7 @@ fn help_short_flag() { init Create a new project add Add dependencies to the project remove Remove dependencies from the project + version Read or update the project's version sync Update the project's environment lock Update the project's lockfile export Export the project's lockfile to an alternate format @@ -190,7 +191,6 @@ fn help_short_flag() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Read or update the project's version help Display documentation for a command Cache options: @@ -857,6 +857,7 @@ fn help_unknown_subcommand() { init add remove + version sync lock export @@ -869,7 +870,6 @@ fn help_unknown_subcommand() { publish cache self - version generate-shell-completion "); @@ -884,6 +884,7 @@ fn help_unknown_subcommand() { init add remove + version sync lock export @@ -896,7 +897,6 @@ fn help_unknown_subcommand() { publish cache self - version generate-shell-completion "); } @@ -938,6 +938,7 @@ fn help_with_global_option() { init Create a new project add Add dependencies to the project remove Remove dependencies from the project + version Read or update the project's version sync Update the project's environment lock Update the project's lockfile export Export the project's lockfile to an alternate format @@ -950,7 +951,6 @@ fn help_with_global_option() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Read or update the project's version generate-shell-completion Generate shell completion help Display documentation for a command @@ -1059,6 +1059,7 @@ fn help_with_no_pager() { init Create a new project add Add dependencies to the project remove Remove dependencies from the project + version Read or update the project's version sync Update the project's environment lock Update the project's lockfile export Export the project's lockfile to an alternate format @@ -1071,7 +1072,6 @@ fn help_with_no_pager() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Read or update the project's version generate-shell-completion Generate shell completion help Display documentation for a command diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 51682a59b..e8eb05279 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -1,6 +1,7 @@ use anyhow::{Ok, Result}; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; +use indoc::indoc; use insta::assert_snapshot; use crate::common::TestContext; @@ -147,6 +148,8 @@ requires-python = ">=3.12" myproject 1.10.31 => 1.1.1 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -187,6 +190,8 @@ requires-python = ">=3.12" 1.1.1 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -226,6 +231,8 @@ requires-python = ">=3.12" myproject 1.10.31 => 1.10.32 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -265,6 +272,8 @@ requires-python = ">=3.12" 1.10.32 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -303,6 +312,8 @@ requires-python = ">=3.12" myproject 1.10.31 => 1.11.0 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -341,6 +352,8 @@ requires-python = ">=3.12" myproject 1.10.31 => 2.0.0 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -379,6 +392,8 @@ requires-python = ">=3.12" myproject 0.1 => 0.1.1 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -417,6 +432,8 @@ requires-python = ">=3.12" myproject 0.1 => 0.2 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -455,6 +472,8 @@ requires-python = ">=3.12" myproject 0.1 => 1.0 ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -494,6 +513,8 @@ requires-python = ">=3.12" ----- stderr ----- warning: prerelease information will be cleared as part of the version bump + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -533,6 +554,8 @@ requires-python = ">=3.12" ----- stderr ----- warning: prerelease information will be cleared as part of the version bump + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -572,6 +595,8 @@ requires-python = ">=3.12" ----- stderr ----- warning: prerelease information will be cleared as part of the version bump + Resolved 1 package in [TIME] + Audited in [TIME] "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -1248,6 +1273,25 @@ fn version_get_workspace() -> Result<()> { ----- stderr ----- "); + // Check that --directory also works + uv_snapshot!(context.filters(), context.version().arg("--directory").arg(context.temp_dir.as_ref()), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 + + ----- stderr ----- + "); + + uv_snapshot!(context.filters(), context.version().arg("--directory").arg(context.temp_dir.join("workspace-member")), @r" + success: true + exit_code: 0 + ----- stdout ----- + workspace-member 0.1.0 + + ----- stderr ----- + "); + pyproject_toml.write_str( r#" [tool.uv.workspace] @@ -1264,7 +1308,594 @@ fn version_get_workspace() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: No `project` table found in: `[TEMP_DIR]/pyproject.toml` + error: Missing `project.name` field in: pyproject.toml + "); + + Ok(()) +} + +/// Edit the version of a workspace member +/// +/// Also check that --locked/--frozen/--no-sync do what they say +#[test] +fn version_set_workspace() -> Result<()> { + let context = TestContext::new("3.12"); + + let workspace = context.temp_dir.child("pyproject.toml"); + workspace.write_str(indoc! {r#" + [tool.uv.workspace] + members = ["child1", "child2"] + "#})?; + + let pyproject_toml = context.temp_dir.child("child1/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "child2", + ] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv.sources] + child2 = { workspace = true } + "#})?; + context + .temp_dir + .child("child1") + .child("src") + .child("child1") + .child("__init__.py") + .touch()?; + + let pyproject_toml = context.temp_dir.child("child2/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "child2" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#})?; + context + .temp_dir + .child("child2") + .child("src") + .child("child2") + .child("__init__.py") + .touch()?; + + // Set one child's version, creating the lock and initial sync + let mut version_cmd = context.version(); + version_cmd + .arg("--package") + .arg("child2") + .arg("1.1.1") + .current_dir(&context.temp_dir); + + uv_snapshot!(context.filters(), version_cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + child2 0.1.0 => 1.1.1 + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + child2==1.1.1 (from file://[TEMP_DIR]/child2) + "); + + // `uv version` implies a full lock and sync, including development dependencies. + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "child1", + "child2", + ] + + [[package]] + name = "child1" + version = "0.1.0" + source = { editable = "child1" } + dependencies = [ + { name = "child2" }, + ] + + [package.metadata] + requires-dist = [{ name = "child2", editable = "child2" }] + + [[package]] + name = "child2" + version = "1.1.1" + source = { editable = "child2" } + "# + ); + }); + + // Set the other child's version, refereshing the lock and sync + let mut version_cmd = context.version(); + version_cmd + .arg("--package") + .arg("child1") + .arg("1.2.3") + .current_dir(&context.temp_dir); + + uv_snapshot!(context.filters(), version_cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + child1 0.1.0 => 1.2.3 + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + child1==1.2.3 (from file://[TEMP_DIR]/child1) + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "child1", + "child2", + ] + + [[package]] + name = "child1" + version = "1.2.3" + source = { editable = "child1" } + dependencies = [ + { name = "child2" }, + ] + + [package.metadata] + requires-dist = [{ name = "child2", editable = "child2" }] + + [[package]] + name = "child2" + version = "1.1.1" + source = { editable = "child2" } + "# + ); + }); + + // Confirm --locked get works fine + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child1") + .arg("--locked"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child1 1.2.3 + + ----- stderr ----- + "); + + // Confirm --frozen get works fine + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child2") + .arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child2 1.1.1 + + ----- stderr ----- + "); + + // Confirm --no-sync get works fine + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child1") + .arg("--no-sync"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child1 1.2.3 + + ----- stderr ----- + "); + + // Confirm --frozen set works + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child2") + .arg("--frozen") + .arg("2.0.0"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child2 1.1.1 => 2.0.0 + + ----- stderr ----- + "); + + // Confirm --frozen --bump works, sees the previous set + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child2") + .arg("--frozen") + .arg("--bump").arg("patch"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child2 2.0.0 => 2.0.1 + + ----- stderr ----- + "); + + // Confirm --frozen get doesn't see the --frozen set or bump + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child2") + .arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child2 1.1.1 + + ----- stderr ----- + "); + + // Confirm --no-sync set does a lock but no sync + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child1") + .arg("--no-sync") + .arg("3.0.0"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child1 1.2.3 => 3.0.0 + + ----- stderr ----- + Resolved 2 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "child1", + "child2", + ] + + [[package]] + name = "child1" + version = "3.0.0" + source = { editable = "child1" } + dependencies = [ + { name = "child2" }, + ] + + [package.metadata] + requires-dist = [{ name = "child2", editable = "child2" }] + + [[package]] + name = "child2" + version = "2.0.1" + source = { editable = "child2" } + "# + ); + }); + + // Confirm --locked set works if it's a noop (can sync) + uv_snapshot!(context.filters(), context.version() + .arg("--package").arg("child1") + .arg("--locked") + .arg("3.0.0"), @r" + success: true + exit_code: 0 + ----- stdout ----- + child1 3.0.0 => 3.0.0 + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Uninstalled 2 packages in [TIME] + Installed 2 packages in [TIME] + - child1==1.2.3 (from file://[TEMP_DIR]/child1) + + child1==3.0.0 (from file://[TEMP_DIR]/child1) + - child2==1.1.1 (from file://[TEMP_DIR]/child2) + + child2==2.0.1 (from file://[TEMP_DIR]/child2) + "); + Ok(()) +} + +/// Edit the version of a workspace member in a way that breaks a version +/// constraint, forcing the lockfile to be updated non-trivially. +/// +/// The idea here is that: +/// +/// * "myproj" depends on the registry package "anyio" +/// * "anyio" depends on the registry package "idna" +/// * our workspace defines "idna", forcing "anyio" to use whatever version we have +/// +/// The result is that `uv version --package idna x.y.z` can force the re-evaluation +/// of the version of "anyio" we select. In particular we *shrink* the version of "idna", +/// forcing "anyio" to massively revert all the way back to before it depended on "idna". +/// +/// It would be nice to have a case where we still get a package dependency, but +/// this still demonstrates the non-trivial "hazard" of a version change. +#[test] +fn version_set_evil_constraints() -> Result<()> { + let context = TestContext::new("3.12"); + + let workspace = context.temp_dir.child("pyproject.toml"); + workspace.write_str(indoc! {r#" + [tool.uv.workspace] + members = ["idna", "myproj"] + "#})?; + + let pyproject_toml = context.temp_dir.child("idna/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "idna" + version = "3.10.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#})?; + context + .temp_dir + .child("idna") + .child("src") + .child("idna") + .child("__init__.py") + .touch()?; + + let pyproject_toml = context.temp_dir.child("myproj/pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "myproj" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "anyio", + ] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#})?; + context + .temp_dir + .child("myproj") + .child("src") + .child("myproj") + .child("__init__.py") + .touch()?; + + // sync all, creating the lock and initial sync + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.10.0 (from file://[TEMP_DIR]/idna) + + myproj==0.1.0 (from file://[TEMP_DIR]/myproj) + + sniffio==1.3.1 + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "idna", + "myproj", + ] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642, upload-time = "2024-02-19T08:36:28.641Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584, upload-time = "2024-02-19T08:36:26.842Z" }, + ] + + [[package]] + name = "idna" + version = "3.10.0" + source = { editable = "idna" } + + [[package]] + name = "myproj" + version = "0.1.0" + source = { editable = "myproj" } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + ); + }); + + // Reduce idna's version, forcing a downgrade of anyio (used by myproj) + // This will not appear in the sync, but it will show up in the lock, + // because we use "sufficient" sync semantics + let mut version_cmd = context.version(); + version_cmd + .arg("--project") + .arg("idna") + .arg("2.0.0") + .current_dir(&context.temp_dir); + + uv_snapshot!(context.filters(), version_cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + idna 3.10.0 => 2.0.0 + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - idna==3.10.0 (from file://[TEMP_DIR]/idna) + + idna==2.0.0 (from file://[TEMP_DIR]/idna) + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "idna", + "myproj", + ] + + [[package]] + name = "anyio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "async-generator" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/44/eb/c5f29a8c854cf454cb995dc791152c641eb8948b2d71cb30e233eb262c53/anyio-1.3.1.tar.gz", hash = "sha256:a46bb2b7743455434afd9adea848a3c4e0b7321aee3e9d08844b11d348d3b5a0", size = 56763, upload-time = "2020-05-31T11:50:54.61Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/c2/17b5c64a1a92c5dbd7ab3fd24c4db4332aa78ebe132e54136d1bc5eb1bd5/anyio-1.3.1-py3-none-any.whl", hash = "sha256:f21b4fafeec1b7db81e09a907e44e374a1e39718d782a488fdfcdcf949c8950c", size = 35056, upload-time = "2020-05-31T11:50:53.646Z" }, + ] + + [[package]] + name = "async-generator" + version = "1.10" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" }, + ] + + [[package]] + name = "idna" + version = "2.0.0" + source = { editable = "idna" } + + [[package]] + name = "myproj" + version = "0.1.0" + source = { editable = "myproj" } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio" }] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + ] + "# + ); + }); + + // however once we explicitly sync the change will go into effect + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 2 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 2 packages in [TIME] + - anyio==4.3.0 + + anyio==1.3.1 + + async-generator==1.10 "); Ok(()) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index bbdb5da55..ee0e0ad57 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -20,6 +20,8 @@ uv [OPTIONS]
uv remove

Remove dependencies from the project

+
uv version

Read or update the project’s version

+
uv sync

Update the project’s environment

uv lock

Update the project’s lockfile

@@ -44,8 +46,6 @@ uv [OPTIONS]
uv self

Manage the uv executable

-
uv version

Read or update the project’s version

-
uv help

Display documentation for a command

@@ -1462,6 +1462,359 @@ uv remove [OPTIONS] ... +## uv version + +Read or update the project's version + +

Usage

+ +``` +uv version [OPTIONS] [VALUE] +``` + +

Arguments

+ +
VALUE

Set the project version to this value

+ +

To update the project using semantic versioning components instead, use --bump.

+ +
+ +

Options

+ +
--active

Prefer the active virtual environment over the project’s virtual environment.

+ +

If the project virtual environment is active or no virtual environment is active, this has no effect.

+ +
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

+ +

Can be provided multiple times.

+ +

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+ +

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+ +

May also be set with the UV_INSECURE_HOST environment variable.

+
--bump bump

Update the project version using the given semantics

+ +

Possible values:

+ +
    +
  • major: Increase the major version (1.2.3 => 2.0.0)
  • + +
  • minor: Increase the minor version (1.2.3 => 1.3.0)
  • + +
  • patch: Increase the patch version (1.2.3 => 1.2.4)
  • +
+
--cache-dir cache-dir

Path to the cache directory.

+ +

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+ +

To view the location of the cache directory, run uv cache dir.

+ +

May also be set with the UV_CACHE_DIR environment variable.

+
--color color-choice

Control the use of color in output.

+ +

By default, uv will automatically detect support for colors when writing to a terminal.

+ +

Possible values:

+ +
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • + +
  • always: Enables colored output regardless of the detected environment
  • + +
  • never: Disables colored output
  • +
+
--compile-bytecode, --compile

Compile Python files to bytecode after installation.

+ +

By default, uv does not compile Python (.py) files to bytecode (__pycache__/*.pyc); instead, compilation is performed lazily the first time a module is imported. For use-cases in which start time is critical, such as CLI applications and Docker containers, this option can be enabled to trade longer installation times for faster start times.

+ +

When enabled, uv will process the entire site-packages directory (including packages that are not being modified by the current operation) for consistency. Like pip, it will also ignore errors.

+ +

May also be set with the UV_COMPILE_BYTECODE environment variable.

+
--config-file config-file

The path to a uv.toml file to use for configuration.

+ +

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+ +

May also be set with the UV_CONFIG_FILE environment variable.

+
--config-setting, --config-settings, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+ +
--default-index default-index

The URL of the default package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --index flag.

+ +

May also be set with the UV_DEFAULT_INDEX environment variable.

+
--directory directory

Change to the given directory prior to running the command.

+ +

Relative paths are resolved with the given directory as the base.

+ +

See --project to only change the project root directory.

+ +
--dry-run

Don’t write a new version to the pyproject.toml

+ +

Instead, the version will be displayed.

+ +
--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

+ +

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

+ +

May also be set with the UV_EXCLUDE_NEWER environment variable.

+
--extra-index-url extra-index-url

(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+ +

May also be set with the UV_EXTRA_INDEX_URL environment variable.

+

Locations to search for candidate distributions, in addition to those found in the registry indexes.

+ +

If a path, the target must be a directory that contains packages as wheel files (.whl) or source distributions (e.g., .tar.gz or .zip) at the top level.

+ +

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

+ +

May also be set with the UV_FIND_LINKS environment variable.

+
--fork-strategy fork-strategy

The strategy to use when selecting multiple versions of a given package across Python versions and platforms.

+ +

By default, uv will optimize for selecting the latest version of each package for each supported Python version (requires-python), while minimizing the number of selected versions across platforms.

+ +

Under fewest, uv will minimize the number of selected versions for each package, preferring older versions that are compatible with a wider range of supported Python versions or platforms.

+ +

May also be set with the UV_FORK_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • 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
  • +
+
--frozen

Update the version without re-locking the project.

+ +

The project environment will not be synced.

+ +

May also be set with the UV_FROZEN environment variable.

+
--help, -h

Display the concise help for this command

+ +
--index index

The URLs to use when resolving dependencies, in addition to the default index.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+ +

May also be set with the UV_INDEX environment variable.

+
--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

+ +

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

+ +

May also be set with the UV_INDEX_STRATEGY environment variable.

+

Possible values:

+ +
    +
  • first-index: Only use results from the first index that returns a match for a given package name
  • + +
  • unsafe-first-match: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next
  • + +
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • +
+
--index-url, -i index-url

(Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+ +

May also be set with the UV_INDEX_URL environment variable.

+
--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

+ +

At present, only --keyring-provider subprocess is supported, which configures uv to use the keyring CLI to handle authentication.

+ +

Defaults to disabled.

+ +

May also be set with the UV_KEYRING_PROVIDER environment variable.

+

Possible values:

+ +
    +
  • disabled: Do not use keyring for credential lookup
  • + +
  • subprocess: Use the keyring command for credential lookup
  • +
+

The method to use when installing packages from the global cache.

+ +

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

+ +

May also be set with the UV_LINK_MODE environment variable.

+

Possible values:

+ +
    +
  • clone: Clone (i.e., copy-on-write) packages from the wheel into the site-packages directory
  • + +
  • copy: Copy packages from the wheel into the site-packages directory
  • + +
  • hardlink: Hard link packages from the wheel into the site-packages directory
  • + +
  • symlink: Symbolically link packages from the wheel into the site-packages directory
  • +
+
--locked

Assert that the uv.lock will remain unchanged.

+ +

Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, uv will exit with an error.

+ +

May also be set with the UV_LOCKED environment variable.

+
--managed-python

Require use of uv-managed Python versions.

+ +

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

+ +

May also be set with the UV_MANAGED_PYTHON environment variable.

+
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

+ +

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+ +

However, in some cases, you may want to use the platform’s native certificate store, especially if you’re relying on a corporate trust root (e.g., for a mandatory proxy) that’s included in your system’s certificate store.

+ +

May also be set with the UV_NATIVE_TLS environment variable.

+
--no-binary

Don’t install pre-built wheels.

+ +

The given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.

+ +

May also be set with the UV_NO_BINARY environment variable.

+
--no-binary-package no-binary-package

Don’t install pre-built wheels for a specific package

+ +

May also be set with the UV_NO_BINARY_PACKAGE environment variable.

+
--no-build

Don’t build source distributions.

+ +

When enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.

+ +

May also be set with the UV_NO_BUILD environment variable.

+
--no-build-isolation

Disable isolation when building source distributions.

+ +

Assumes that build dependencies specified by PEP 518 are already installed.

+ +

May also be set with the UV_NO_BUILD_ISOLATION environment variable.

+
--no-build-isolation-package no-build-isolation-package

Disable isolation when building source distributions for a specific package.

+ +

Assumes that the packages’ build dependencies specified by PEP 518 are already installed.

+ +
--no-build-package no-build-package

Don’t build source distributions for a specific package

+ +

May also be set with the UV_NO_BUILD_PACKAGE environment variable.

+
--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+ +

May also be set with the UV_NO_CACHE environment variable.

+
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+ +

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

+ +

May also be set with the UV_NO_CONFIG environment variable.

+
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

+ +
--no-managed-python

Disable use of uv-managed Python versions.

+ +

Instead, uv will search for a suitable Python version on the system.

+ +

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

+
--no-progress

Hide all progress outputs.

+ +

For example, spinners or progress bars.

+ +

May also be set with the UV_NO_PROGRESS environment variable.

+
--no-python-downloads

Disable automatic downloads of Python.

+ +
--no-sources

Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any workspace, Git, URL, or local path sources

+ +
--no-sync

Avoid syncing the virtual environment after re-locking the project

+ +

May also be set with the UV_NO_SYNC environment variable.

+
--offline

Disable network access.

+ +

When disabled, uv will only use locally cached data and locally available files.

+ +

May also be set with the UV_OFFLINE environment variable.

+
--output-format output-format

The format of the output

+ +

[default: text]

+

Possible values:

+ +
    +
  • text: Display the version as plain text
  • + +
  • json: Display the version as JSON
  • +
+
--package package

Update the version of a specific package in the workspace

+ +
--prerelease prerelease

The strategy to use when considering pre-release versions.

+ +

By default, uv will accept pre-releases for packages that only publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (if-necessary-or-explicit).

+ +

May also be set with the UV_PRERELEASE environment variable.

+

Possible values:

+ +
    +
  • disallow: Disallow all pre-release versions
  • + +
  • allow: Allow all pre-release versions
  • + +
  • if-necessary: Allow pre-release versions if all versions of a package are pre-release
  • + +
  • explicit: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements
  • + +
  • if-necessary-or-explicit: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements
  • +
+
--project project

Run the command within the given project directory.

+ +

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project’s virtual environment (.venv).

+ +

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

+ +

See --directory to change the working directory entirely.

+ +

This setting has no effect when used in the uv pip interface.

+ +

May also be set with the UV_PROJECT environment variable.

+
--python, -p python

The Python interpreter to use for resolving and syncing.

+ +

See uv python for details on Python discovery and supported request formats.

+ +

May also be set with the UV_PYTHON environment variable.

+
--quiet, -q

Use quiet output.

+ +

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

+ +
--refresh

Refresh all cached data

+ +
--refresh-package refresh-package

Refresh cached data for a specific package

+ +
--reinstall, --force-reinstall

Reinstall all packages, regardless of whether they’re already installed. Implies --refresh

+ +
--reinstall-package reinstall-package

Reinstall a specific package, regardless of whether it’s already installed. Implies --refresh-package

+ +
--resolution resolution

The strategy to use when selecting between the different compatible versions for a given package requirement.

+ +

By default, uv will use the latest compatible version of each package (highest).

+ +

May also be set with the UV_RESOLUTION environment variable.

+

Possible values:

+ +
    +
  • highest: Resolve the highest compatible version of each package
  • + +
  • lowest: Resolve the lowest compatible version of each package
  • + +
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
+
--short

Only show the version

+ +

By default, uv will show the project name before the version.

+ +
--upgrade, -U

Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

+ +
--upgrade-package, -P upgrade-package

Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

+ +
--verbose, -v

Use verbose output.

+ +

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)

+ +
+ ## uv sync Update the project's environment. @@ -9554,145 +9907,6 @@ uv self version [OPTIONS] -## uv version - -Read or update the project's version - -

Usage

- -``` -uv version [OPTIONS] [VALUE] -``` - -

Arguments

- -
VALUE

Set the project version to this value

- -

To update the project using semantic versioning components instead, use --bump.

- -
- -

Options

- -
--allow-insecure-host, --trusted-host allow-insecure-host

Allow insecure connections to a host.

- -

Can be provided multiple times.

- -

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

- -

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

- -

May also be set with the UV_INSECURE_HOST environment variable.

-
--bump bump

Update the project version using the given semantics

- -

Possible values:

- -
    -
  • major: Increase the major version (1.2.3 => 2.0.0)
  • - -
  • minor: Increase the minor version (1.2.3 => 1.3.0)
  • - -
  • patch: Increase the patch version (1.2.3 => 1.2.4)
  • -
-
--cache-dir cache-dir

Path to the cache directory.

- -

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

- -

To view the location of the cache directory, run uv cache dir.

- -

May also be set with the UV_CACHE_DIR environment variable.

-
--color color-choice

Control the use of color in output.

- -

By default, uv will automatically detect support for colors when writing to a terminal.

- -

Possible values:

- -
    -
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • - -
  • always: Enables colored output regardless of the detected environment
  • - -
  • never: Disables colored output
  • -
-
--config-file config-file

The path to a uv.toml file to use for configuration.

- -

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

- -

May also be set with the UV_CONFIG_FILE environment variable.

-
--directory directory

Change to the given directory prior to running the command.

- -

Relative paths are resolved with the given directory as the base.

- -

See --project to only change the project root directory.

- -
--dry-run

Don’t write a new version to the pyproject.toml

- -

Instead, the version will be displayed.

- -
--help, -h

Display the concise help for this command

- -
--managed-python

Require use of uv-managed Python versions.

- -

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

- -

May also be set with the UV_MANAGED_PYTHON environment variable.

-
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

- -

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

- -

However, in some cases, you may want to use the platform’s native certificate store, especially if you’re relying on a corporate trust root (e.g., for a mandatory proxy) that’s included in your system’s certificate store.

- -

May also be set with the UV_NATIVE_TLS environment variable.

-
--no-cache, --no-cache-dir, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

- -

May also be set with the UV_NO_CACHE environment variable.

-
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

- -

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

- -

May also be set with the UV_NO_CONFIG environment variable.

-
--no-managed-python

Disable use of uv-managed Python versions.

- -

Instead, uv will search for a suitable Python version on the system.

- -

May also be set with the UV_NO_MANAGED_PYTHON environment variable.

-
--no-progress

Hide all progress outputs.

- -

For example, spinners or progress bars.

- -

May also be set with the UV_NO_PROGRESS environment variable.

-
--no-python-downloads

Disable automatic downloads of Python.

- -
--offline

Disable network access.

- -

When disabled, uv will only use locally cached data and locally available files.

- -

May also be set with the UV_OFFLINE environment variable.

-
--output-format output-format
--project project

Run the command within the given project directory.

- -

All pyproject.toml, uv.toml, and .python-version files will be discovered by walking up the directory tree from the project root, as will the project’s virtual environment (.venv).

- -

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

- -

See --directory to change the working directory entirely.

- -

This setting has no effect when used in the uv pip interface.

- -

May also be set with the UV_PROJECT environment variable.

-
--quiet, -q

Use quiet output.

- -

Repeating this option, e.g., -qq, will enable a silent mode in which uv will write no output to stdout.

- -
--short

Only show the version

- -

By default, uv will show the project name before the version.

- -
--verbose, -v

Use verbose output.

- -

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)

- -
- ## uv generate-shell-completion Generate shell completion