Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change uv version to be an interface for project version reads and edits #12349

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
52 changes: 46 additions & 6 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -529,6 +526,41 @@ pub struct HelpArgs {
pub command: Option<Vec<String>>,
}

#[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<String>,
/// Update the project version using the given semantics
#[arg(group = "operation", long)]
pub bump: Option<VersionBump>,
/// 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)]
Expand All @@ -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)]
Expand Down
43 changes: 35 additions & 8 deletions crates/uv-cli/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -17,26 +18,46 @@ 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<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not make this a PackageName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a write-only format and the version was already being flattened to a String, so I kept it consistent.

/// version, such as "0.5.1"
version: String,
/// Information about the git commit we may have been built from.
///
/// `None` if not built from a git repo or if retrieval failed.
commit_info: Option<CommitInfo>,
}

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: "<version>[+<commits>] (<commit> <date>)"
///
/// 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(())
}
}
Expand All @@ -48,7 +69,7 @@ impl From<VersionInfo> 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) => {
Expand All @@ -71,6 +92,7 @@ pub fn version() -> VersionInfo {
});

VersionInfo {
package_name: Some("uv".to_owned()),
version,
commit_info,
}
Expand All @@ -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,
};
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -135,6 +161,7 @@ mod tests {
};
assert_json_snapshot!(version, @r#"
{
"package_name": "uv",
"version": "0.0.0",
"commit_info": {
"short_commit_hash": "53b0f5d92",
Expand Down
44 changes: 43 additions & 1 deletion crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -976,6 +978,46 @@ impl PyProjectTomlMut {

types
}

pub fn version(&mut self) -> Result<Version, Error> {
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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading