diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 31f66388a..ba6076ed0 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -74,7 +74,7 @@ const STYLES: Styles = Styles::styled() .placeholder(AnsiColor::Cyan.on_default()); #[derive(Parser)] -#[command(name = "uv", author, long_version = crate::version::version())] +#[command(name = "uv", author, long_version = crate::version::uv_self_version())] #[command(about = "An extremely fast Python package manager.")] #[command(propagate_version = true)] #[command( @@ -494,11 +494,8 @@ pub enum Commands { /// Clear the cache, removing all entries or those linked to specific packages. #[command(hide = true)] Clean(CleanArgs), - /// Display uv's version - Version { - #[arg(long, value_enum, default_value = "text")] - output_format: VersionFormat, - }, + /// Read or update the project's version. + Version(VersionArgs), /// Generate shell completion #[command(alias = "--generate-shell-completion", hide = true)] GenerateShellCompletion(GenerateShellCompletionArgs), @@ -529,6 +526,41 @@ pub struct HelpArgs { pub command: Option>, } +#[derive(Args, Debug)] +#[command(group = clap::ArgGroup::new("operation"))] +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, + #[arg(long, value_enum, default_value = "text")] + pub output_format: VersionFormat, +} + +#[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)] +pub enum VersionBump { + /// Increase the major version (1.2.3 => 2.0.0) + Major, + /// Increase the minor version (1.2.3 => 1.3.0) + Minor, + /// Increase the patch version (1.2.3 => 1.2.4) + Patch, +} + #[derive(Args)] pub struct SelfNamespace { #[command(subcommand)] @@ -539,6 +571,14 @@ pub struct SelfNamespace { pub enum SelfCommand { /// Update uv. Update(SelfUpdateArgs), + /// Display uv's version + Version { + /// Only print the version + #[arg(long)] + short: bool, + #[arg(long, value_enum, default_value = "text")] + output_format: VersionFormat, + }, } #[derive(Args, Debug)] diff --git a/crates/uv-cli/src/version.rs b/crates/uv-cli/src/version.rs index 076d6cf8b..ee4dd43e4 100644 --- a/crates/uv-cli/src/version.rs +++ b/crates/uv-cli/src/version.rs @@ -3,6 +3,7 @@ use std::fmt; use serde::Serialize; +use uv_pep508::{uv_pep440::Version, PackageName}; /// Information about the git repository where uv was built from. #[derive(Serialize)] @@ -17,7 +18,9 @@ pub(crate) struct CommitInfo { /// uv's version. #[derive(Serialize)] pub struct VersionInfo { - /// uv's version, such as "0.5.1" + /// Name of the package (or "uv" if printing uv's own version) + pub package_name: Option, + /// version, such as "0.5.1" version: String, /// Information about the git commit we may have been built from. /// @@ -25,18 +28,36 @@ pub struct VersionInfo { commit_info: Option, } +impl VersionInfo { + pub fn new(package_name: Option<&PackageName>, version: &Version) -> Self { + Self { + package_name: package_name.map(ToString::to_string), + version: version.to_string(), + commit_info: None, + } + } +} + impl fmt::Display for VersionInfo { /// Formatted version information: "[+] ( )" + /// + /// This is intended for consumption by `clap` to provide `uv --version`, + /// and intentionally omits the name of the package fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.version)?; - - if let Some(ref ci) = self.commit_info { - if ci.commits_since_last_tag > 0 { - write!(f, "+{}", ci.commits_since_last_tag)?; - } - write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?; + if let Some(ci) = &self.commit_info { + write!(f, "{ci}")?; } + Ok(()) + } +} +impl fmt::Display for CommitInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.commits_since_last_tag > 0 { + write!(f, "+{}", self.commits_since_last_tag)?; + } + write!(f, " ({} {})", self.short_commit_hash, self.commit_date)?; Ok(()) } } @@ -48,7 +69,7 @@ impl From for clap::builder::Str { } /// Returns information about uv's version. -pub fn version() -> VersionInfo { +pub fn uv_self_version() -> VersionInfo { // Environment variables are only read at compile-time macro_rules! option_env_str { ($name:expr) => { @@ -71,6 +92,7 @@ pub fn version() -> VersionInfo { }); VersionInfo { + package_name: Some("uv".to_owned()), version, commit_info, } @@ -85,6 +107,7 @@ mod tests { #[test] fn version_formatting() { let version = VersionInfo { + package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: None, }; @@ -94,6 +117,7 @@ mod tests { #[test] fn version_formatting_with_commit_info() { let version = VersionInfo { + package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), @@ -109,6 +133,7 @@ mod tests { #[test] fn version_formatting_with_commits_since_last_tag() { let version = VersionInfo { + package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), @@ -124,6 +149,7 @@ mod tests { #[test] fn version_serializable() { let version = VersionInfo { + package_name: Some("uv".to_string()), version: "0.0.0".to_string(), commit_info: Some(CommitInfo { short_commit_hash: "53b0f5d92".to_string(), @@ -135,6 +161,7 @@ mod tests { }; assert_json_snapshot!(version, @r#" { + "package_name": "uv", "version": "0.0.0", "commit_info": { "short_commit_hash": "53b0f5d92", diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index e741a1d6c..dbfecf140 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -14,7 +14,7 @@ use uv_cache_key::CanonicalUrl; use uv_distribution_types::Index; use uv_fs::PortablePath; use uv_normalize::GroupName; -use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; +use uv_pep440::{Version, VersionParseError, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl}; use crate::pyproject::{DependencyType, Source}; @@ -44,6 +44,8 @@ pub enum Error { MalformedWorkspace, #[error("Expected a dependency at index {0}")] MissingDependency(usize), + #[error("Failed to parse `version` field of `pyproject.toml`")] + VersionParse(#[from] VersionParseError), #[error("Cannot perform ambiguous update; found multiple entries for `{}`:\n{}", package_name, requirements.iter().map(|requirement| format!("- `{requirement}`")).join("\n"))] Ambiguous { package_name: PackageName, @@ -942,6 +944,46 @@ impl PyProjectTomlMut { types } + + pub fn version(&mut self) -> Result { + let version = self + .doc + .get("project") + .and_then(Item::as_table) + .and_then(|project| project.get("version")) + .and_then(Item::as_str) + .ok_or(Error::MalformedWorkspace)?; + + Ok(Version::from_str(version)?) + } + + pub fn has_dynamic_version(&mut self) -> bool { + let Some(dynamic) = self + .doc + .get("project") + .and_then(Item::as_table) + .and_then(|project| project.get("dynamic")) + .and_then(Item::as_array) + else { + return false; + }; + + dynamic.iter().any(|val| val.as_str() == Some("version")) + } + + pub fn set_version(&mut self, version: &Version) -> Result<(), Error> { + let project = self + .doc + .get_mut("project") + .and_then(Item::as_table_mut) + .ok_or(Error::MalformedWorkspace)?; + project.insert( + "version", + Item::Value(Value::String(Formatted::new(version.to_string()))), + ); + + Ok(()) + } } /// Returns an implicit table. diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 83f48b7c2..27874ac10 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -56,7 +56,7 @@ use uv_normalize::PackageName; use uv_python::PythonEnvironment; use uv_scripts::Pep723Script; pub(crate) use venv::venv; -pub(crate) use version::version; +pub(crate) use version::{project_version, self_version}; use crate::printer::Printer; diff --git a/crates/uv/src/commands/version.rs b/crates/uv/src/commands/version.rs index 71da73d77..07418306c 100644 --- a/crates/uv/src/commands/version.rs +++ b/crates/uv/src/commands/version.rs @@ -1,20 +1,199 @@ -use anyhow::Result; +use std::fmt::Write; +use std::str::FromStr; +use std::{cmp::Ordering, path::Path}; -use uv_cli::VersionFormat; +use anyhow::{anyhow, Result}; +use owo_colors::OwoColorize; -/// Display version information -pub(crate) fn version(output_format: VersionFormat, buffer: &mut dyn std::io::Write) -> Result<()> { - let version_info = uv_cli::version::version(); +use uv_cli::version::VersionInfo; +use uv_cli::{VersionBump, VersionFormat}; +use uv_fs::Simplified; +use uv_pep440::Version; +use uv_workspace::pyproject_mut::Error; +use uv_workspace::{ + pyproject_mut::{DependencyTarget, PyProjectTomlMut}, + DiscoveryOptions, Workspace, WorkspaceCache, +}; +use crate::{commands::ExitStatus, printer::Printer}; + +/// 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`) +pub(crate) async fn project_version( + project_dir: &Path, + value: Option, + bump: Option, + dry_run: bool, + short: bool, + output_format: VersionFormat, + strict: bool, + cache: &WorkspaceCache, + printer: Printer, +) -> Result { + // Read the metadata + let workspace = + match Workspace::discover(project_dir, &DiscoveryOptions::default(), cache).await { + Ok(workspace) => workspace, + Err(err) => { + // If strict, hard bail on missing workspace + if strict { + return Err(err)?; + } + // Otherwise, warn and provide fallback + writeln!( + printer.stderr(), + r"warning: failed to read project: {err} + running `uv self version` for compatibility with old `uv version` command. + this fallback will be removed soon, pass `--preview` to make this an error. +" + )?; + return self_version(short, output_format, printer); + } + }; + + let mut pyproject = PyProjectTomlMut::from_toml( + &workspace.pyproject_toml().raw, + DependencyTarget::PyProjectToml, + )?; + let pyproject_path = workspace.install_path().join("pyproject.toml"); + let name = workspace + .pyproject_toml() + .project + .as_ref() + .map(|project| &project.name); + let old_version = pyproject.version().map_err(|err| match err { + Error::MalformedWorkspace => { + if pyproject.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 + }; + + // Apply the metadata + if let Some(new_version) = &new_version { + if !dry_run { + pyproject.set_version(new_version)?; + fs_err::write(pyproject_path, pyproject.to_string())?; + } + } + + // Report the results + let old_version = VersionInfo::new(name, &old_version); + let new_version = new_version.map(|version| VersionInfo::new(name, &version)); + print_version(old_version, new_version, short, output_format, printer)?; + + 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 => { - writeln!(buffer, "uv {}", &version_info)?; + 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 => { - serde_json::to_writer_pretty(&mut *buffer, &version_info)?; - // Add a trailing newline - writeln!(buffer)?; + 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 333bb7526..bdef0be80 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -19,13 +19,13 @@ use tokio::task::spawn_blocking; use tracing::{debug, instrument}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; +#[cfg(feature = "self-update")] +use uv_cli::SelfUpdateArgs; use uv_cli::{ compat::CompatArgs, BuildBackendCommand, CacheCommand, CacheNamespace, Cli, Commands, - PipCommand, PipNamespace, ProjectCommand, + PipCommand, PipNamespace, ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, + SelfNamespace, ToolCommand, ToolNamespace, TopLevelArgs, VersionArgs, }; -use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLevelArgs}; -#[cfg(feature = "self-update")] -use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs}; use uv_configuration::min_stack_size; use uv_fs::{Simplified, CWD}; use uv_pep508::VersionOrUrl; @@ -359,7 +359,7 @@ async fn run(mut cli: Cli) -> Result { // Don't initialize the rayon threadpool yet, this is too costly when we're doing a noop sync. uv_configuration::RAYON_PARALLELISM.store(globals.concurrency.installs, Ordering::SeqCst); - debug!("uv {}", uv_cli::version::version()); + debug!("uv {}", uv_cli::version::uv_self_version()); // Write out any resolved settings. macro_rules! show_settings { @@ -1018,6 +1018,16 @@ async fn run(mut cli: Cli) -> Result { token, }), }) => commands::self_update(target_version, token, printer).await, + Commands::Self_(SelfNamespace { + command: + SelfCommand::Version { + short, + output_format, + }, + }) => { + commands::self_version(short, output_format, printer)?; + Ok(ExitStatus::Success) + } #[cfg(not(feature = "self-update"))] Commands::Self_(_) => { anyhow::bail!( @@ -1025,10 +1035,30 @@ async fn run(mut cli: Cli) -> Result { is not available. Please use your package manager to update uv." ); } - Commands::Version { output_format } => { - commands::version(output_format, &mut stdout())?; - Ok(ExitStatus::Success) + Commands::Version(VersionArgs { + value, + bump, + dry_run, + short, + output_format, + }) => { + // If they specified a project, they probably don't want `uv --version` semantics + let strict = + cli.top_level.global_args.project.is_some() || globals.preview.is_enabled(); + 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) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4fa0c43f5..8951616a9 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -851,6 +851,20 @@ impl TestContext { command } + pub fn version(&self) -> Command { + let mut command = self.new_command(); + command.arg("version"); + self.add_shared_options(&mut command, false); + command + } + + pub fn self_version(&self) -> Command { + let mut command = self.new_command(); + command.arg("self").arg("version"); + self.add_shared_options(&mut command, false); + command + } + /// Create a `uv publish` command with options shared across scenarios. pub fn publish(&self) -> Command { let mut command = self.new_command(); diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index a9ccacd78..b1f883f49 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -32,7 +32,7 @@ fn help() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Display uv's version + version Read or update the project's version generate-shell-completion Generate shell completion help Display documentation for a command @@ -111,7 +111,7 @@ fn help_flag() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Display uv's version + version Read or update the project's version help Display documentation for a command Cache options: @@ -188,7 +188,7 @@ fn help_short_flag() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Display uv's version + version Read or update the project's version help Display documentation for a command Cache options: @@ -837,7 +837,7 @@ fn help_flag_subsubcommand() { fn help_unknown_subcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("foobar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foobar"), @r" success: false exit_code: 2 ----- stdout ----- @@ -862,9 +862,9 @@ fn help_unknown_subcommand() { self version generate-shell-completion - "###); + "); - uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r" success: false exit_code: 2 ----- stdout ----- @@ -889,7 +889,7 @@ fn help_unknown_subcommand() { self version generate-shell-completion - "###); + "); } #[test] @@ -941,7 +941,7 @@ fn help_with_global_option() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Display uv's version + version Read or update the project's version generate-shell-completion Generate shell completion help Display documentation for a command @@ -1055,7 +1055,7 @@ fn help_with_no_pager() { publish Upload distributions to an index cache Manage uv's cache self Manage the uv executable - version Display uv's version + 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/main.rs b/crates/uv/tests/it/main.rs index 9632c9a3b..926a3a790 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -39,6 +39,8 @@ mod lock_conflict; mod lock_scenarios; +mod version; + mod pip_check; #[cfg(all(feature = "python", feature = "pypi"))] diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs new file mode 100644 index 000000000..a192de951 --- /dev/null +++ b/crates/uv/tests/it/version.rs @@ -0,0 +1,1214 @@ +use anyhow::{Ok, Result}; +use assert_fs::prelude::*; +use insta::assert_snapshot; + +use crate::common::uv_snapshot; +use crate::common::TestContext; + +// Print the version +#[test] +fn version_get() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.version(), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Print the version (json format) +#[test] +fn version_get_json() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--output-format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "package_name": "myproject", + "version": "1.10.31", + "commit_info": null + } + + ----- stderr ----- + "#); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Print the version (--short) +#[test] +fn version_get_short() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--short"), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1.10.31 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + + Ok(()) +} + +// Set the version +#[test] +fn version_set_value() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("1.1.1"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 1.1.1 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r###" + [project] + name = "myproject" + version = "1.1.1" + requires-python = ">=3.12" + "### + ); + + Ok(()) +} + +// Set the version (--short) +#[test] +fn version_set_value_short() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("1.1.1") + .arg("--short"), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1.1.1 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r###" + [project] + name = "myproject" + version = "1.1.1" + requires-python = ">=3.12" + "### + ); + + Ok(()) +} + +// Bump patch version +#[test] +fn version_bump_patch() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("patch"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 1.10.32 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.32" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump patch version (--short) +#[test] +fn version_bump_patch_short() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("patch") + .arg("--short"), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1.10.32 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.32" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump minor version +#[test] +fn version_bump_minor() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("minor"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 1.11.0 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.11.0" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// bump major version +#[test] +fn version_major_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 2.0.0 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "2.0.0" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump patch but the input version is missing a component +#[test] +fn version_patch_uncompleted() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "0.1" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("patch"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 0.1 => 0.1.1 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "0.1.1" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump minor but the input version is missing a component +#[test] +fn version_minor_uncompleted() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "0.1" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("minor"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 0.1 => 0.2 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "0.2" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump major but the input version is missing a component +#[test] +fn version_major_uncompleted() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "0.1" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 0.1 => 1.0 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.0" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump major but the input version is .dev +#[test] +fn version_major_dev() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31.dev10" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31.dev10 => 2.0.0 + + ----- stderr ----- + warning: prerelease information will be cleared as part of the version bump + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "2.0.0" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump major but the input version is a complex mess +#[test] +fn version_major_complex_mess() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1!2a3.post4.dev5+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1!2a3.post4.dev5+deadbeef6 => 3 + + ----- stderr ----- + warning: prerelease information will be cleared as part of the version bump + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "3" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump major but the input version is .post +#[test] +fn version_major_post() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31.post10" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31.post10 => 2.0.0 + + ----- stderr ----- + warning: prerelease information will be cleared as part of the version bump + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "2.0.0" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Set version --dry-run +#[test] +fn version_set_dry() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("1.2.3") + .arg("--dry-run"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 1.2.3 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Bump version --dry-run +#[test] +fn version_major_dry() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--dry-run"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 1.10.31 => 2.0.0 + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Set version invalid +#[test] +fn version_set_invalid() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("abcd"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: expected version to start with a number, but no leading ASCII digits were found + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// forget --bump but pass a valid bump name +#[test] +fn version_missing_bump() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "myproject" +version = "1.10.31" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("minor"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Invalid version `minor`, did you mean to pass `--bump minor`? + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "1.10.31" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Dynamic version should error on read +#[test] +fn version_get_dynamic() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + dynamic = ["version"] + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.version(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: We cannot get or set dynamic project versions in: pyproject.toml + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + dynamic = ["version"] + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Dynamic version should error on write +#[test] +fn version_set_dynamic() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myproject" + dynamic = ["version"] + requires-python = ">=3.12" + "#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("0.1.2"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: We cannot get or set dynamic project versions in: pyproject.toml + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + dynamic = ["version"] + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Should fallback to `uv --version` if this pyproject.toml isn't usable for whatever reason +// (In this case, because tool.uv.managed = false) +#[test] +fn version_get_fallback_unmanaged() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "#, + )?; + + uv_snapshot!(context.filters(), context.version(), @r" + success: true + exit_code: 0 + ----- stdout ----- + uv [VERSION] ([COMMIT] DATE) + + ----- stderr ----- + warning: failed to read project: The project is marked as unmanaged: `[TEMP_DIR]/` + running `uv self version` for compatibility with old `uv version` command. + this fallback will be removed soon, pass `--preview` to make this an error. + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "# + ); + Ok(()) +} + +// version_get_fallback with `--short` +#[test] +fn version_get_fallback_unmanaged_short() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "#, + )?; + + let filters = context + .filters() + .into_iter() + .chain([( + r"\d+\.\d+\.\d+(\+\d+)?( \(.*\))?", + r"[VERSION] ([COMMIT] DATE)", + )]) + .collect::>(); + uv_snapshot!(filters, context.version() + .arg("--short"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [VERSION] ([COMMIT] DATE) + + ----- stderr ----- + warning: failed to read project: The project is marked as unmanaged: `[TEMP_DIR]/` + running `uv self version` for compatibility with old `uv version` command. + this fallback will be removed soon, pass `--preview` to make this an error. + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "# + ); + Ok(()) +} + +// version_get_fallback with `--json` +#[test] +fn version_get_fallback_unmanaged_json() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "#, + )?; + + let filters = context + .filters() + .into_iter() + .chain([ + (r#"version": "\d+.\d+.\d+""#, r#"version": "[VERSION]""#), + ( + r#"short_commit_hash": ".*""#, + r#"short_commit_hash": "[HASH]""#, + ), + (r#"commit_hash": ".*""#, r#"commit_hash": "[LONGHASH]""#), + (r#"commit_date": ".*""#, r#"commit_date": "[DATE]""#), + (r#"last_tag": (".*"|null)"#, r#"last_tag": "[TAG]""#), + ( + r#"commits_since_last_tag": .*"#, + r#"commits_since_last_tag": [COUNT]"#, + ), + ]) + .collect::>(); + uv_snapshot!(filters, context.version() + .arg("--output-format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "package_name": "uv", + "version": "[VERSION]", + "commit_info": { + "short_commit_hash": "[LONGHASH]", + "commit_hash": "[LONGHASH]", + "commit_date": "[DATE]", + "last_tag": "[TAG]", + "commits_since_last_tag": [COUNT] + } + } + + ----- stderr ----- + warning: failed to read project: The project is marked as unmanaged: `[TEMP_DIR]/` + running `uv self version` for compatibility with old `uv version` command. + this fallback will be removed soon, pass `--preview` to make this an error. + "#); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "# + ); + Ok(()) +} + +// Should error if this pyproject.toml isn't usable for whatever reason +// and --project was passed explicitly. +#[test] +fn version_get_fallback_unmanaged_strict() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--project").arg("."), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: The project is marked as unmanaged: `[TEMP_DIR]/` + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + + [tool.uv] + managed = false + "# + ); + Ok(()) +} + +// Should error if this pyproject.toml is missing +// and --project was passed explicitly. +#[test] +fn version_get_fallback_missing_strict() -> Result<()> { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.version() + .arg("--project").arg("."), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No `pyproject.toml` found in current directory or any parent directory + "); + + Ok(()) +} + +// Should error if this pyproject.toml is missing +// and --preview was passed explicitly. +#[test] +fn version_get_fallback_missing_strict_preview() -> Result<()> { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.version() + .arg("--preview"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No `pyproject.toml` found in current directory or any parent directory + "); + + Ok(()) +} + +// `uv self version` +// (also setup a honeypot project and make sure it's not used) +#[test] +fn self_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + "#, + )?; + + uv_snapshot!(context.filters(), context.self_version(), @r" + success: true + exit_code: 0 + ----- stdout ----- + uv [VERSION] ([COMMIT] DATE) + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + "# + ); + Ok(()) +} + +// `uv self version --short` +// (also setup a honeypot project and make sure it's not used) +#[test] +fn self_version_short() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + "#, + )?; + + let filters = context + .filters() + .into_iter() + .chain([( + r"\d+\.\d+\.\d+(\+\d+)?( \(.*\))?", + r"[VERSION] ([COMMIT] DATE)", + )]) + .collect::>(); + uv_snapshot!(filters, context.self_version() + .arg("--short"), @r" + success: true + exit_code: 0 + ----- stdout ----- + [VERSION] ([COMMIT] DATE) + + ----- stderr ----- + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + "# + ); + Ok(()) +} + +// `uv self version --output-format json` +// (also setup a honeypot project and make sure it's not used) +#[test] +fn self_version_json() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "myapp" + version = "0.1.2" + "#, + )?; + + let filters = context + .filters() + .into_iter() + .chain([ + (r#"version": "\d+.\d+.\d+""#, r#"version": "[VERSION]""#), + ( + r#"short_commit_hash": ".*""#, + r#"short_commit_hash": "[HASH]""#, + ), + (r#"commit_hash": ".*""#, r#"commit_hash": "[LONGHASH]""#), + (r#"commit_date": ".*""#, r#"commit_date": "[DATE]""#), + (r#"last_tag": (".*"|null)"#, r#"last_tag": "[TAG]""#), + ( + r#"commits_since_last_tag": .*"#, + r#"commits_since_last_tag": [COUNT]"#, + ), + ]) + .collect::>(); + uv_snapshot!(filters, context.self_version() + .arg("--output-format").arg("json"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "package_name": "uv", + "version": "[VERSION]", + "commit_info": { + "short_commit_hash": "[LONGHASH]", + "commit_hash": "[LONGHASH]", + "commit_date": "[DATE]", + "last_tag": "[TAG]", + "commits_since_last_tag": [COUNT] + } + } + + ----- stderr ----- + "#); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myapp" + version = "0.1.2" + "# + ); + Ok(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 332ae95cf..a6d10b21c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -44,7 +44,7 @@ uv [OPTIONS]
uv self

Manage the uv executable

-
uv version

Display uv’s version

+
uv version

Read or update the project’s version

uv help

Display documentation for a command

@@ -9371,6 +9371,8 @@ uv self [OPTIONS]
uv self update

Update uv

+
uv self version

Display uv’s version

+
### uv self update @@ -9496,18 +9498,142 @@ uv self update [OPTIONS] [TARGET_VERSION] -## uv version +### uv self version Display uv's version

Usage

``` -uv version [OPTIONS] +uv self version [OPTIONS] ```

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.

+
--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.

+ +
--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 print 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>)

+ +
--version, -V

Display the uv version

+ +
+ +## 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.

@@ -9517,6 +9643,17 @@ uv version [OPTIONS]

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.

@@ -9548,6 +9685,10 @@ uv version [OPTIONS]

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.

@@ -9602,6 +9743,10 @@ uv version [OPTIONS]

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>)