From 2fddfa7088e07a91eb941a82b69cb336f2e7af8a Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 22 May 2025 10:23:16 -0400 Subject: [PATCH 1/3] improve --bump flag --- crates/uv-cli/src/lib.rs | 29 +- crates/uv-pep440/src/lib.rs | 4 +- crates/uv-pep440/src/version.rs | 452 ++++++++++++++++++++++ crates/uv/src/commands/project/version.rs | 81 ++-- crates/uv/src/lib.rs | 3 +- crates/uv/src/settings.rs | 5 +- crates/uv/tests/it/version.rs | 408 ++++++++++++++++++- docs/guides/package.md | 77 ++++ docs/guides/projects.md | 37 ++ docs/reference/cli.md | 9 + 10 files changed, 1055 insertions(+), 50 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index df4a19919..90dfb6102 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -534,8 +534,13 @@ pub struct VersionArgs { pub value: Option, /// Update the project version using the given semantics + /// + /// This flag can be passed multiple times, and the bumps will be applied in the + /// following order that prevents bumps from being undone by other bumps: + /// + /// major > minor > patch > stable > alpha > beta > rc > post > dev #[arg(group = "operation", long)] - pub bump: Option, + pub bump: Vec, /// Don't write a new version to the `pyproject.toml` /// @@ -549,6 +554,10 @@ pub struct VersionArgs { #[arg(long)] pub short: bool, + /// Permit the result of `--bump` to be a version decrease + #[arg(long)] + pub allow_decreases: bool, + /// The format of the output #[arg(long, value_enum, default_value = "text")] pub output_format: VersionFormat, @@ -610,7 +619,9 @@ pub struct VersionArgs { pub python: Option>, } -#[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)] +// Note that the ordering of the variants is significant, as when given a list of operations +// to perform, we sort them and apply them in order, so users don't have to think too hard about it. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] pub enum VersionBump { /// Increase the major version (1.2.3 => 2.0.0) Major, @@ -618,6 +629,20 @@ pub enum VersionBump { Minor, /// Increase the patch version (1.2.3 => 1.2.4) Patch, + /// Make the version stable (1.2.3b4.post5.dev6 => 1.2.3) + /// + /// This intentionally clears `.postN` and preserves `+local` + Stable, + /// Increase the alpha version (1.2.3a4 => 1.2.3a5) + Alpha, + /// Increase the beta version (1.2.3b4 => 1.2.3b5) + Beta, + /// Increase the rc version (1.2.3rc4 => 1.2.3rc5) + Rc, + /// Increase the post version (1.2.3.post5 => 1.2.3.post6) + Post, + /// Increase the dev version (1.2.3a4.dev6 => 1.2.3.dev7) + Dev, } #[derive(Args)] diff --git a/crates/uv-pep440/src/lib.rs b/crates/uv-pep440/src/lib.rs index 3d2e256ae..fbfd08956 100644 --- a/crates/uv-pep440/src/lib.rs +++ b/crates/uv-pep440/src/lib.rs @@ -29,8 +29,8 @@ pub use version_ranges::{ }; pub use { version::{ - LocalSegment, LocalVersion, LocalVersionSlice, MIN_VERSION, Operator, OperatorParseError, - Prerelease, PrereleaseKind, Version, VersionParseError, VersionPattern, + BumpCommand, LocalSegment, LocalVersion, LocalVersionSlice, MIN_VERSION, Operator, + OperatorParseError, Prerelease, PrereleaseKind, Version, VersionParseError, VersionPattern, VersionPatternParseError, }, version_specifier::{ diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 59f090927..54c066bc3 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -625,6 +625,90 @@ impl Version { self.with_release(release) } + /// Various "increment the version" operations + pub fn bump(&mut self, bump: BumpCommand) { + // This code operates on the understanding that the components of a version form + // the following hierarchy: + // + // major > minor > patch > stable > pre > post > dev + // + // Any updates to something earlier in the hierarchy should clear all values lower + // in the hierarchy. So for instance: + // + // if you bump `minor`, then clear: patch, pre, post, dev + // if you bump `pre`, then clear: post, dev + // + // ...and so on. + // + // If you bump a value that doesn't exist, it will be set to "1". + // + // The special "stable" mode has no value, bumping it clears: pre, post, dev. + let full = self.make_full(); + + match bump { + BumpCommand::BumpRelease { index } => { + // Clear all sub-release items + full.pre = None; + full.post = None; + full.dev = None; + + // Use `max` here to try to do 0.2 => 0.3 instead of 0.2 => 0.3.0 + let old_parts = &full.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::>(); + full.release = new_release_vec; + } + BumpCommand::MakeStable => { + // Clear all sub-release items + full.pre = None; + full.post = None; + full.dev = None; + } + BumpCommand::BumpPrerelease { kind } => { + // Clear all sub-prerelease items + full.post = None; + full.dev = None; + + // Either bump the matching kind or set to 1 + if let Some(prerelease) = &mut full.pre { + if prerelease.kind == kind { + prerelease.number += 1; + return; + } + } + full.pre = Some(Prerelease { kind, number: 1 }); + } + BumpCommand::BumpPost => { + // Clear sub-post items + full.dev = None; + + // Either bump or set to 1 + if let Some(post) = &mut full.post { + *post += 1; + } else { + full.post = Some(1); + } + } + BumpCommand::BumpDev => { + // Either bump or set to 1 + if let Some(dev) = &mut full.dev { + *dev += 1; + } else { + full.dev = Some(1); + } + } + } + } + /// Set the min-release component and return the updated version. /// /// The "min" component is internal-only, and does not exist in PEP 440. @@ -864,6 +948,27 @@ impl FromStr for Version { } } +/// Various ways to "bump" a version +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum BumpCommand { + /// Bump the release component + BumpRelease { + /// The release component to bump (0 is major, 1 is minor, 2 is patch) + index: usize, + }, + /// Bump the prerelease component + BumpPrerelease { + /// prerelease component to bump + kind: PrereleaseKind, + }, + /// Bump to the associated stable release + MakeStable, + /// Bump the post component + BumpPost, + /// Bump the dev component + BumpDev, +} + /// A small representation of a version. /// /// This representation is used for a (very common) subset of versions: the @@ -4028,4 +4133,351 @@ mod tests { assert_eq!(size_of::(), size_of::() * 2); assert_eq!(size_of::(), size_of::() * 2); } + + /// Test major bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_major() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "2.0"); + + // three digit (zero major) + let mut version = "0.1.2".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "1.0.0"); + + // three digit (non-zero major) + let mut version = "1.2.3".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "2.0.0"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "2.0.0.0"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "5!2.0.0.0+local"); + version.bump(BumpCommand::BumpRelease { index: 0 }); + assert_eq!(version.to_string().as_str(), "5!3.0.0.0+local"); + } + + /// Test minor bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_minor() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "0.1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "1.6"); + + // three digit (non-zero major) + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "5.4.0"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "1.3.0.0"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "5!1.8.0.0+local"); + version.bump(BumpCommand::BumpRelease { index: 1 }); + assert_eq!(version.to_string().as_str(), "5!1.9.0.0+local"); + } + + /// Test patch bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_patch() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "0.0.1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "1.5.1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "5.3.7"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "1.2.4.0"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "5!1.7.4.0+local"); + version.bump(BumpCommand::BumpRelease { index: 2 }); + assert_eq!(version.to_string().as_str(), "5!1.7.5.0+local"); + } + + /// Test alpha bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_alpha() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "0a1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "1.5a1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "5.3.6a1"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "1.2.3.4a1"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5a1+local"); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5a2+local"); + } + + /// Test beta bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_beta() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "0b1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "1.5b1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "5.3.6b1"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "1.2.3.4b1"); + + // All the version junk + let mut version = "5!1.7.3.5a2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5b1+local"); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2+local"); + } + + /// Test rc bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_rc() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "0rc1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "1.5rc1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "5.3.6rc1"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "1.2.3.4rc1"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345.dev456+local" + .parse::() + .unwrap(); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5rc1+local"); + version.bump(BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5rc2+local"); + } + + /// Test post bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_post() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "0.post1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "1.5.post1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "5.3.6.post1"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "1.2.3.4.post1"); + + // All the version junk + let mut version = "5!1.7.3.5b2.dev123+local".parse::().unwrap(); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2.post1+local"); + version.bump(BumpCommand::BumpPost); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5b2.post2+local"); + } + + /// Test dev bumping + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn bump_dev() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::BumpDev); + assert_eq!(version.to_string().as_str(), "0.dev1"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::BumpDev); + assert_eq!(version.to_string().as_str(), "1.5.dev1"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::BumpDev); + assert_eq!(version.to_string().as_str(), "5.3.6.dev1"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::BumpDev); + assert_eq!(version.to_string().as_str(), "1.2.3.4.dev1"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345+local".parse::().unwrap(); + version.bump(BumpCommand::BumpDev); + assert_eq!( + version.to_string().as_str(), + "5!1.7.3.5b2.post345.dev1+local" + ); + version.bump(BumpCommand::BumpDev); + assert_eq!( + version.to_string().as_str(), + "5!1.7.3.5b2.post345.dev2+local" + ); + } + + /// Test stable setting + /// Explicitly using the string display because we want to preserve formatting where possible! + #[test] + fn make_stable() { + // one digit + let mut version = "0".parse::().unwrap(); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "0"); + + // two digit + let mut version = "1.5".parse::().unwrap(); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "1.5"); + + // three digit + let mut version = "5.3.6".parse::().unwrap(); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "5.3.6"); + + // four digit + let mut version = "1.2.3.4".parse::().unwrap(); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "1.2.3.4"); + + // All the version junk + let mut version = "5!1.7.3.5b2.post345+local".parse::().unwrap(); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5+local"); + version.bump(BumpCommand::MakeStable); + assert_eq!(version.to_string().as_str(), "5!1.7.3.5+local"); + } } diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 0e50c2ac0..81bd8823b 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -1,6 +1,6 @@ use std::fmt::Write; +use std::path::Path; use std::str::FromStr; -use std::{cmp::Ordering, path::Path}; use anyhow::{Context, Result, anyhow}; use owo_colors::OwoColorize; @@ -15,7 +15,7 @@ use uv_configuration::{ }; use uv_fs::Simplified; use uv_normalize::DefaultExtras; -use uv_pep440::Version; +use uv_pep440::{BumpCommand, PrereleaseKind, Version}; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_settings::PythonInstallMirrors; @@ -55,10 +55,11 @@ pub(crate) fn self_version( #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn project_version( value: Option, - bump: Option, + mut bump: Vec, short: bool, output_format: VersionFormat, strict: bool, + allow_decreases: bool, project_dir: &Path, package: Option, dry_run: bool, @@ -105,7 +106,7 @@ pub(crate) async fn project_version( }; // Short-circuit early for a frozen read - let is_read_only = value.is_none() && bump.is_none(); + let is_read_only = value.is_none() && bump.is_empty(); if frozen && is_read_only { return Box::pin(print_frozen_version( project, @@ -158,7 +159,8 @@ pub(crate) async fn project_version( match Version::from_str(&value) { Ok(version) => Some(version), Err(err) => match &*value { - "major" | "minor" | "patch" => { + "major" | "minor" | "patch" | "alpha" | "beta" | "rc" | "dev" | "post" + | "stable" => { return Err(anyhow!( "Invalid version `{value}`, did you mean to pass `--bump {value}`?" )); @@ -168,8 +170,41 @@ pub(crate) async fn project_version( } }, } - } else if let Some(bump) = bump { - Some(bumped_version(&old_version, bump, printer)?) + } else if !bump.is_empty() { + // Sort the given commands so the user doesn't have to care about + // the ordering of `--bump minor --bump major` (only one ordering is ever useful) + bump.sort(); + + // Apply all the bumps + let mut new_version = old_version.clone(); + for bump in &bump { + let command = match *bump { + VersionBump::Major => BumpCommand::BumpRelease { index: 0 }, + VersionBump::Minor => BumpCommand::BumpRelease { index: 1 }, + VersionBump::Patch => BumpCommand::BumpRelease { index: 2 }, + VersionBump::Alpha => BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Alpha, + }, + VersionBump::Beta => BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Beta, + }, + VersionBump::Rc => BumpCommand::BumpPrerelease { + kind: PrereleaseKind::Rc, + }, + VersionBump::Post => BumpCommand::BumpPost, + VersionBump::Dev => BumpCommand::BumpDev, + VersionBump::Stable => BumpCommand::MakeStable, + }; + new_version.bump(command); + } + + if !allow_decreases && new_version < old_version { + return Err(anyhow!( + "{old_version} => {new_version} was a version decrease, use `--allow-decreases` if this is desired" + )); + } + + Some(new_version) } else { None }; @@ -561,35 +596,3 @@ fn print_version( } 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 b2c1fb690..da779a35d 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1980,7 +1980,7 @@ async fn run_project( let strict = project_was_explicit || globals.preview.is_enabled() || args.dry_run - || args.bump.is_some() + || !args.bump.is_empty() || args.value.is_some() || args.package.is_some(); Box::pin(commands::project_version( @@ -1989,6 +1989,7 @@ async fn run_project( args.short, args.output_format, strict, + args.allow_decreases, project_dir, args.package, args.dry_run, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index dce7c58f0..368b7e216 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1493,8 +1493,9 @@ impl RemoveSettings { #[derive(Debug, Clone)] pub(crate) struct VersionSettings { pub(crate) value: Option, - pub(crate) bump: Option, + pub(crate) bump: Vec, pub(crate) short: bool, + pub(crate) allow_decreases: bool, pub(crate) output_format: VersionFormat, pub(crate) dry_run: bool, pub(crate) locked: bool, @@ -1516,6 +1517,7 @@ impl VersionSettings { value, bump, short, + allow_decreases, output_format, dry_run, no_sync, @@ -1539,6 +1541,7 @@ impl VersionSettings { value, bump, short, + allow_decreases, output_format, dry_run, locked, diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 6ecc38aa3..5033aa3b1 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -512,7 +512,6 @@ requires-python = ">=3.12" myproject 1.10.31.dev10 => 2.0.0 ----- stderr ----- - warning: prerelease information will be cleared as part of the version bump Resolved 1 package in [TIME] Audited in [TIME] "); @@ -550,10 +549,9 @@ requires-python = ">=3.12" success: true exit_code: 0 ----- stdout ----- - myproject 1!2a3.post4.dev5+deadbeef6 => 3 + myproject 1!2a3.post4.dev5+deadbeef6 => 1!3+deadbeef6 ----- stderr ----- - warning: prerelease information will be cleared as part of the version bump Resolved 1 package in [TIME] Audited in [TIME] "); @@ -564,7 +562,296 @@ requires-python = ">=3.12" @r#" [project] name = "myproject" - version = "3" + version = "1!3+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// Pass a ton of bump flags to a complex version +// The flags are in a messy order and some are duplicated, +// they should get sorted and the duplicates respected (i.e. 2 minor bumps) +#[test] +fn many_bump_complex() -> 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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("patch") + .arg("--bump").arg("alpha") + .arg("--bump").arg("minor") + .arg("--bump").arg("dev") + .arg("--bump").arg("minor") + .arg("--bump").arg("post") + .arg("--bump").arg("post"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!3.2.1a1.post2.dev1+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!3.2.1a1.post2.dev1+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump stable +#[test] +fn bump_stable() -> 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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("stable"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump alpha +#[test] +fn bump_alpha() -> 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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4a6+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4a6+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump beta +#[test] +fn bump_beta() -> 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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("beta"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4b1+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4b1+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump rc +#[test] +fn bump_rc() -> 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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("rc"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4rc1+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4rc1+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump post +#[test] +fn bump_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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("post"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4a5.post7+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4a5.post7+deadbeef6" + requires-python = ">=3.12" + "# + ); + Ok(()) +} + +// --bump dev +#[test] +fn bump_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 = "9!2.3.4a5.post6.dev7+deadbeef6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!2.3.4a5.post6.dev8+deadbeef6 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + assert_snapshot!( + pyproject, + @r#" + [project] + name = "myproject" + version = "9!2.3.4a5.post6.dev8+deadbeef6" requires-python = ">=3.12" "# ); @@ -594,7 +881,6 @@ requires-python = ">=3.12" myproject 1.10.31.post10 => 2.0.0 ----- stderr ----- - warning: prerelease information will be cleared as part of the version bump Resolved 1 package in [TIME] Audited in [TIME] "); @@ -612,6 +898,118 @@ requires-python = ">=3.12" Ok(()) } +// --bump stable but it decreases the version +#[test] +fn bump_decrease_stable() -> 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 = "2.3.4.post6" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("stable"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: 2.3.4.post6 => 2.3.4 was a version decrease, use `--allow-decreases` if this is desired + "); + Ok(()) +} + +// --bump alpha but it decreases the version by reverting beta +#[test] +fn bump_decrease_alpha_beta() -> 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 = "2.3.4b5" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("alpha"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: 2.3.4b5 => 2.3.4a1 was a version decrease, use `--allow-decreases` if this is desired + "); + Ok(()) +} + +// --bump alpha but it decreases the version from a stable +#[test] +fn bump_decrease_alpha_stable() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("alpha"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: 2.3.4 => 2.3.4a1 was a version decrease, use `--allow-decreases` if this is desired + "); + Ok(()) +} + +// --bump alpha but it decreases the version from a stable +// and --allow-decreases is passed +#[test] +fn bump_decrease_allow() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("alpha") + .arg("--allow-decreases"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 2.3.4 => 2.3.4a1 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + Ok(()) +} + // Set version --dry-run #[test] fn version_set_dry() -> Result<()> { diff --git a/docs/guides/package.md b/docs/guides/package.md index a5437769a..0ddeb29a2 100644 --- a/docs/guides/package.md +++ b/docs/guides/package.md @@ -55,6 +55,83 @@ Alternatively, `uv build ` will build the package in the specified director running `uv build --no-sources` to ensure that the package builds correctly when `tool.uv.sources` is disabled, as is the case when using other build tools, like [`pypa/build`](https://github.com/pypa/build). +## Updating your version + +The `uv version` command provides conveniences for updating the version of your package before you +publish it. +[See the project docs for reading your package's version](./projects.md#managing-version). + +To set the the exact version of your package, just pass that version: + +```console +$ uv version 1.0.0 +hello-world 0.7.0 => 1.0.0 +``` + +If you want to preview the change without actually applying it, use the `--dry-run` flag: + +```console +$ uv version 2.0.0 --dry-run +hello-world 1.0.0 => 2.0.0 +$ uv version +hello-world 1.0.0 +``` + +If you want to change the version of a particular package, use the `--package` flag: + +```console +$ uv version --package hello-world 1.2.3 +hello-world 1.0.0 => 1.2.3 +``` + +To increase the version of your package, use the `--bump` flag: + +```console +$ uv version --bump minor +hello-world 1.2.3 => 1.3.0 +``` + +The `--bump` flag can be passed multiple times, and uv will run them in the following order that +prevents bumps from clobbering eachother: + +```text + major > minor > patch > stable > alpha > beta > rc > post > dev +``` + +When you're on a stable version and want to start shipping prereleases, you'll want to bump the +release and the prerelease: + +```console +$ uv version --bump patch --bump beta +hello-world 1.3.0 => 1.3.1b1 +``` + +!!! Note + + If you only bump the prerelease here it will actually decrease the current version. + `uv version` will error if that ever happens. If you intended to do that, you can pass + `--allow-decreases` to disable the check. + +When you're on a prerelease and want to ship another, you can just bump the prerelease: + +```console +uv version --bump beta +hello-world 1.3.0b1 => 1.3.1b2 +``` + +When you're on a prerelease and want to ship a stable version, you can bump to stable: + +```console +uv version --bump stable +hello-world 1.3.1b2 => 1.3.1 +``` + +!!! info + + By default, when `uv version` modifies your package it will lock and sync your project to + ensure everything sees the change. To prevent locking and syncing, pass `--frozen`. To just + prevent syncing, pass `--no-sync`. + ## Publishing your package Publish your package with `uv publish`: diff --git a/docs/guides/projects.md b/docs/guides/projects.md index efea187db..e7575a06f 100644 --- a/docs/guides/projects.md +++ b/docs/guides/projects.md @@ -160,6 +160,43 @@ version, while keeping the rest of the lockfile intact. See the documentation on [managing dependencies](../concepts/projects/dependencies.md) for more details. +## Managing version + +The `uv version` command can be used to read your package's version. +[See the publishing docs for updating your package's version](./package.md#updating-your-version). + +To get the version of your package, run `uv version` with no other arguments: + +```console +$ uv version +hello-world 0.7.0 +``` + +To get the version of a particular package, pass `--package`: + +```console +$ uv version --package myapp +myapp 1.2.3 +``` + +To just get the version with no other output, pass `--short`: + +```console +$ uv version --short +0.7.0 +``` + +To get the version as json, pass `--output-format json`: + +```console +$ uv version --output-format json +{ + "package_name": "hello-world", + "version": "0.7.0", + "commit_info": null +} +``` + ## Running commands `uv run` can be used to run arbitrary scripts or commands in your project environment. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 583db76eb..d63e12707 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -769,16 +769,25 @@ uv version [OPTIONS] [VALUE]
--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-decreases

Permit the result of --bump to be a version decrease

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

+

This flag can be passed multiple times, and the bumps will be applied in the following order that prevents bumps from being undone by other bumps:

+

major > minor > patch > stable > alpha > beta > rc > post > dev

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)
  • +
  • stable: Make the version stable (1.2.3b4.post5.dev6 => 1.2.3)
  • +
  • alpha: Increase the alpha version (1.2.3a4 => 1.2.3a5)
  • +
  • beta: Increase the beta version (1.2.3b4 => 1.2.3b5)
  • +
  • rc: Increase the rc version (1.2.3rc4 => 1.2.3rc5)
  • +
  • post: Increase the post version (1.2.3.post5 => 1.2.3.post6)
  • +
  • dev: Increase the dev version (1.2.3a4.dev6 => 1.2.3.dev7)
--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.

From fb525a6ed45cbf865a106055b69e9de52e1e960d Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 22 May 2025 12:58:32 -0400 Subject: [PATCH 2/3] remove --allow-decreases and be extremely strict about args --- crates/uv-cli/src/lib.rs | 27 ++++--- crates/uv/src/commands/project/version.rs | 86 +++++++++++++++++++++-- crates/uv/src/lib.rs | 1 - crates/uv/src/settings.rs | 3 - docs/concepts/projects/config.md | 2 + docs/reference/cli.md | 4 +- 6 files changed, 104 insertions(+), 19 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 90dfb6102..9ea52a0b7 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -535,10 +535,8 @@ pub struct VersionArgs { /// Update the project version using the given semantics /// - /// This flag can be passed multiple times, and the bumps will be applied in the - /// following order that prevents bumps from being undone by other bumps: - /// - /// major > minor > patch > stable > alpha > beta > rc > post > dev + /// This flag can be passed multiple times to allow going to a new release and entering + /// a prerelease: `--bump patch --bump beta` #[arg(group = "operation", long)] pub bump: Vec, @@ -554,10 +552,6 @@ pub struct VersionArgs { #[arg(long)] pub short: bool, - /// Permit the result of `--bump` to be a version decrease - #[arg(long)] - pub allow_decreases: bool, - /// The format of the output #[arg(long, value_enum, default_value = "text")] pub output_format: VersionFormat, @@ -645,6 +639,23 @@ pub enum VersionBump { Dev, } +impl std::fmt::Display for VersionBump { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string = match self { + VersionBump::Major => "major", + VersionBump::Minor => "minor", + VersionBump::Patch => "patch", + VersionBump::Stable => "stable", + VersionBump::Alpha => "alpha", + VersionBump::Beta => "beta", + VersionBump::Rc => "rc", + VersionBump::Post => "post", + VersionBump::Dev => "dev", + }; + string.fmt(f) + } +} + #[derive(Args)] pub struct SelfNamespace { #[command(subcommand)] diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 81bd8823b..4c31150cc 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -59,7 +59,6 @@ pub(crate) async fn project_version( short: bool, output_format: VersionFormat, strict: bool, - allow_decreases: bool, project_dir: &Path, package: Option, dry_run: bool, @@ -171,8 +170,82 @@ pub(crate) async fn project_version( }, } } else if !bump.is_empty() { + // While we can rationalize many of these combinations of operations together, + // we want to conservatively refuse to support any of them until users demand it. + // + // The most complex thing we *do* allow is `--bump major --bump beta --bump dev` + // because that makes perfect sense and is reasonable to do. + let release_components: Vec<_> = bump + .iter() + .filter(|bump| { + matches!( + bump, + VersionBump::Major | VersionBump::Minor | VersionBump::Patch + ) + }) + .collect(); + let prerelease_components: Vec<_> = bump + .iter() + .filter(|bump| { + matches!( + bump, + VersionBump::Alpha | VersionBump::Beta | VersionBump::Rc | VersionBump::Dev + ) + }) + .collect(); + let post_count = bump + .iter() + .filter(|bump| *bump == &VersionBump::Post) + .count(); + let stable_count = bump + .iter() + .filter(|bump| *bump == &VersionBump::Stable) + .count(); + + // Very little reason to do "bump to stable" and then do other things, + // even if we can make sense of it. + if stable_count > 0 && bump.len() > 1 { + if let Some(component) = release_components.first() { + return Err(anyhow!( + "`--bump stable` isn't needed if you're already passing `--bump {component}`" + )); + } + return Err(anyhow!( + "`--bump stable` cannot be combined with any other `--bump`" + )); + } + + // Very little reason to "bump to post" and then do other things, + // how is it a post-release otherwise? + if post_count > 0 && bump.len() > 1 { + return Err(anyhow!( + "`--bump post` cannot be combined with any other `--bump`" + )); + } + + // `--bump major --bump minor` makes perfect sense (1.2.3 => 2.1.0) + // ...but it's weird and probably a mistake? + // `--bump major --bump major` perfect sense (1.2.3 => 3.0.0) + // ...but it's weird and probably a mistake? + if release_components.len() > 1 { + return Err(anyhow!( + "`--bump` can only take one of `major`, `minor`, `patch`" + )); + } + + // `--bump alpha --bump beta` is basically completely incoherent + // `--bump beta --bump beta` makes perfect sense (1.2.3b4 => 1.2.3b6) + // ...but it's weird and probably a mistake? + // `--bump beta --bump dev` makes perfect sense (1.2.3 => 1.2.3b1.dev1) + // ...but we want to discourage mixing `dev` with prereleases + if prerelease_components.len() > 1 { + return Err(anyhow!( + "`--bump` can only take one of `alpha`, `beta`, `rc`, `dev`" + )); + } + // Sort the given commands so the user doesn't have to care about - // the ordering of `--bump minor --bump major` (only one ordering is ever useful) + // the ordering of `--bump minor --bump beta` (only one ordering is ever useful) bump.sort(); // Apply all the bumps @@ -198,9 +271,14 @@ pub(crate) async fn project_version( new_version.bump(command); } - if !allow_decreases && new_version < old_version { + if new_version <= old_version { + if old_version.is_stable() && new_version.is_pre() { + return Err(anyhow!( + "{old_version} => {new_version} didn't increase the version; when moving to a prerelease you also need to increase the release `--bump patch`?" + )); + } return Err(anyhow!( - "{old_version} => {new_version} was a version decrease, use `--allow-decreases` if this is desired" + "{old_version} => {new_version} didn't increase the version" )); } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index da779a35d..315f018d2 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1989,7 +1989,6 @@ async fn run_project( args.short, args.output_format, strict, - args.allow_decreases, project_dir, args.package, args.dry_run, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 368b7e216..d436d9d66 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1495,7 +1495,6 @@ pub(crate) struct VersionSettings { pub(crate) value: Option, pub(crate) bump: Vec, pub(crate) short: bool, - pub(crate) allow_decreases: bool, pub(crate) output_format: VersionFormat, pub(crate) dry_run: bool, pub(crate) locked: bool, @@ -1517,7 +1516,6 @@ impl VersionSettings { value, bump, short, - allow_decreases, output_format, dry_run, no_sync, @@ -1541,7 +1539,6 @@ impl VersionSettings { value, bump, short, - allow_decreases, output_format, dry_run, locked, diff --git a/docs/concepts/projects/config.md b/docs/concepts/projects/config.md index de281bee3..193170ed3 100644 --- a/docs/concepts/projects/config.md +++ b/docs/concepts/projects/config.md @@ -160,6 +160,8 @@ Setting `tool.uv.package = false` will force a project package _not_ to be built the project environment. uv will ignore a declared build system when interacting with the project; however, uv will still respect explicit attempts to build the project such as invoking `uv build`. +## Project versioning + ## Project environment path The `UV_PROJECT_ENVIRONMENT` environment variable can be used to configure the project virtual diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d63e12707..ebf77fd1d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -769,14 +769,12 @@ uv version [OPTIONS] [VALUE]
--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-decreases

Permit the result of --bump to be a version decrease

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

-

This flag can be passed multiple times, and the bumps will be applied in the following order that prevents bumps from being undone by other bumps:

-

major > minor > patch > stable > alpha > beta > rc > post > dev

+

This flag can be passed multiple times to allow going to a new release and entering a prerelease: --bump patch --bump beta

Possible values:

  • major: Increase the major version (1.2.3 => 2.0.0)
  • From b0879af032fd62b0cb289b0d800b88b1843d14d8 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 22 May 2025 18:00:01 -0400 Subject: [PATCH 3/3] update tests --- crates/uv/tests/it/version.rs | 228 +++++++++++++++++++++++++++++++--- 1 file changed, 213 insertions(+), 15 deletions(-) diff --git a/crates/uv/tests/it/version.rs b/crates/uv/tests/it/version.rs index 5033aa3b1..0207ade28 100644 --- a/crates/uv/tests/it/version.rs +++ b/crates/uv/tests/it/version.rs @@ -571,7 +571,8 @@ requires-python = ">=3.12" // Pass a ton of bump flags to a complex version // The flags are in a messy order and some are duplicated, -// they should get sorted and the duplicates respected (i.e. 2 minor bumps) +// Under extremely permissive semantics this could be allowed, but right +// now it fails for a dozen reasons! #[test] fn many_bump_complex() -> Result<()> { let context = TestContext::new("3.12"); @@ -595,14 +596,12 @@ requires-python = ">=3.12" .arg("--bump").arg("minor") .arg("--bump").arg("post") .arg("--bump").arg("post"), @r" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- - myproject 9!2.3.4a5.post6.dev7+deadbeef6 => 9!3.2.1a1.post2.dev1+deadbeef6 ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + error: `--bump post` cannot be combined with any other `--bump` "); let pyproject = fs_err::read_to_string(&pyproject_toml)?; @@ -611,7 +610,7 @@ requires-python = ">=3.12" @r#" [project] name = "myproject" - version = "9!3.2.1a1.post2.dev1+deadbeef6" + version = "9!2.3.4a5.post6.dev7+deadbeef6" requires-python = ">=3.12" "# ); @@ -920,7 +919,7 @@ requires-python = ">=3.12" ----- stdout ----- ----- stderr ----- - error: 2.3.4.post6 => 2.3.4 was a version decrease, use `--allow-decreases` if this is desired + error: 2.3.4.post6 => 2.3.4 didn't increase the version "); Ok(()) } @@ -947,7 +946,7 @@ requires-python = ">=3.12" ----- stdout ----- ----- stderr ----- - error: 2.3.4b5 => 2.3.4a1 was a version decrease, use `--allow-decreases` if this is desired + error: 2.3.4b5 => 2.3.4a1 didn't increase the version "); Ok(()) } @@ -974,15 +973,42 @@ requires-python = ">=3.12" ----- stdout ----- ----- stderr ----- - error: 2.3.4 => 2.3.4a1 was a version decrease, use `--allow-decreases` if this is desired + error: 2.3.4 => 2.3.4a1 didn't increase the version; when moving to a prerelease you also need to increase the release `--bump patch`? "); Ok(()) } -// --bump alpha but it decreases the version from a stable -// and --allow-decreases is passed +// --bump major twice #[test] -fn bump_decrease_allow() -> Result<()> { +fn bump_double_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("major"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--bump` can only take one of `major`, `minor`, `patch` + "); + Ok(()) +} + +// --bump alpha twice +#[test] +fn bump_double_alpha() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -997,11 +1023,67 @@ requires-python = ">=3.12" uv_snapshot!(context.filters(), context.version() .arg("--bump").arg("alpha") - .arg("--allow-decreases"), @r" + .arg("--bump").arg("alpha"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--bump` can only take one of `alpha`, `beta`, `rc`, `dev` + "); + Ok(()) +} + +// --bump stable --bump major +#[test] +fn bump_stable_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("stable") + .arg("--bump").arg("major"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--bump stable` isn't needed if you're already passing `--bump major` + "); + Ok(()) +} + +// --bump major --bump alpha +#[test] +fn bump_alpha_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("alpha"), @r" success: true exit_code: 0 ----- stdout ----- - myproject 2.3.4 => 2.3.4a1 + myproject 2.3.4 => 3.0.0a1 ----- stderr ----- Resolved 1 package in [TIME] @@ -1010,6 +1092,122 @@ requires-python = ">=3.12" Ok(()) } +// --bump major --bump minor +#[test] +fn bump_minor_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("alpha"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 2.3.4 => 3.0.0a1 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + Ok(()) +} + +// --bump alpha --bump dev +#[test] +fn bump_alpha_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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("alpha") + .arg("--bump").arg("dev"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--bump` can only take one of `alpha`, `beta`, `rc`, `dev` + "); + Ok(()) +} + +// --bump major --bump dev +#[test] +fn bump_dev_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + myproject 2.3.4 => 3.0.0.dev1 + + ----- stderr ----- + Resolved 1 package in [TIME] + Audited in [TIME] + "); + Ok(()) +} + +// --bump major --bump post +#[test] +fn bump_post_major() -> 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 = "2.3.4" +requires-python = ">=3.12" +"#, + )?; + + uv_snapshot!(context.filters(), context.version() + .arg("--bump").arg("major") + .arg("--bump").arg("post"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--bump post` cannot be combined with any other `--bump` + "); + Ok(()) +} + // Set version --dry-run #[test] fn version_set_dry() -> Result<()> {