use std::fmt::Write;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Result, anyhow};
use owo_colors::OwoColorize;
use tracing::debug;
use uv_cache::Cache;
use uv_cli::version::ProjectVersionInfo;
use uv_cli::{VersionBump, VersionBumpSpec, VersionFormat};
use uv_client::BaseClientBuilder;
use uv_configuration::{
Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification,
InstallOptions,
};
use uv_fs::Simplified;
use uv_normalize::DefaultExtras;
use uv_normalize::PackageName;
use uv_pep440::{BumpCommand, PrereleaseKind, Version};
use uv_preview::Preview;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_settings::PythonInstallMirrors;
use uv_workspace::VirtualProject;
use uv_workspace::pyproject_mut::Error;
use uv_workspace::{
DiscoveryOptions, WorkspaceCache, WorkspaceError,
pyproject_mut::{DependencyTarget, PyProjectTomlMut},
};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::add::{AddTarget, PythonTarget};
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::{
ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, WorkspacePython,
default_dependency_groups,
};
use crate::commands::{ExitStatus, diagnostics, project};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings};
pub(crate) fn self_version(
short: bool,
output_format: VersionFormat,
printer: Printer,
) -> Result<ExitStatus> {
let version_info = uv_cli::version::uv_self_version();
match output_format {
VersionFormat::Text => {
if short {
writeln!(printer.stdout(), "{}", version_info.version().cyan())?;
} else {
writeln!(printer.stdout(), "uv {}", version_info.cyan())?;
}
}
VersionFormat::Json => {
let string = serde_json::to_string_pretty(&version_info)?;
writeln!(printer.stdout(), "{string}")?;
}
}
Ok(ExitStatus::Success)
}
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn project_version(
value: Option<String>,
mut bump: Vec<VersionBumpSpec>,
short: bool,
output_format: VersionFormat,
project_dir: &Path,
package: Option<PackageName>,
explicit_project: bool,
dry_run: bool,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
active: Option<bool>,
no_sync: bool,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: Concurrency,
no_config: bool,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let project = find_target(
project_dir,
package.as_ref(),
explicit_project,
workspace_cache,
)
.await?;
let pyproject_path = project.root().join("pyproject.toml");
let Some(name) = project.project_name().cloned() else {
return Err(anyhow!(
"Missing `project.name` field in: {}",
pyproject_path.user_display()
));
};
let is_read_only = value.is_none() && bump.is_empty();
if let Some(frozen_source) = frozen {
if is_read_only {
return Box::pin(print_frozen_version(
project,
&name,
project_dir,
frozen_source,
active,
python,
install_mirrors,
&settings,
client_builder,
python_preference,
python_downloads,
&concurrency,
no_config,
cache,
workspace_cache,
short,
output_format,
printer,
preview,
))
.await;
}
}
let mut toml = PyProjectTomlMut::from_toml(
project.pyproject_toml().raw.as_ref(),
DependencyTarget::PyProjectToml,
)?;
let old_version = toml.version().map_err(|err| match err {
Error::MalformedWorkspace => {
if toml.has_dynamic_version() {
anyhow!(
"We cannot get or set dynamic project versions in: {}",
pyproject_path.user_display()
)
} else {
anyhow!(
"There is no 'project.version' field in: {}",
pyproject_path.user_display()
)
}
}
err => {
anyhow!("{err}: {}", pyproject_path.user_display())
}
})?;
let new_version = if let Some(value) = value {
match Version::from_str(&value) {
Ok(version) => Some(version),
Err(err) => match &*value {
"major" | "minor" | "patch" | "alpha" | "beta" | "rc" | "dev" | "post"
| "stable" => {
return Err(anyhow!(
"Invalid version `{value}`, did you mean to pass `--bump {value}`?"
));
}
_ => {
return Err(err)?;
}
},
}
} else if !bump.is_empty() {
let release_components: Vec<_> = bump
.iter()
.filter(|spec| {
matches!(
spec.bump,
VersionBump::Major | VersionBump::Minor | VersionBump::Patch
)
})
.collect();
let prerelease_components: Vec<_> = bump
.iter()
.filter(|spec| {
matches!(
spec.bump,
VersionBump::Alpha | VersionBump::Beta | VersionBump::Rc | VersionBump::Dev
)
})
.collect();
let post_count = bump
.iter()
.filter(|spec| spec.bump == VersionBump::Post)
.count();
let stable_count = bump
.iter()
.filter(|spec| spec.bump == VersionBump::Stable)
.count();
if stable_count > 0 && bump.len() > 1 {
let components = bump
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow!(
"`--bump stable` cannot be used with another `--bump` value, got: {components}"
));
}
if post_count > 0 && bump.len() > 1 {
let components = bump
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow!(
"`--bump post` cannot be used with another `--bump` value, got: {components}"
));
}
if release_components.len() > 1 {
let components = release_components
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow!(
"Only one release version component can be provided to `--bump`, got: {components}"
));
}
if prerelease_components.len() > 1 {
let components = prerelease_components
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
return Err(anyhow!(
"Only one pre-release version component can be provided to `--bump`, got: {components}"
));
}
bump.sort();
let mut new_version = old_version.clone();
for spec in &bump {
match spec.bump {
VersionBump::Major => new_version.bump(BumpCommand::BumpRelease {
index: 0,
value: spec.value,
}),
VersionBump::Minor => new_version.bump(BumpCommand::BumpRelease {
index: 1,
value: spec.value,
}),
VersionBump::Patch => new_version.bump(BumpCommand::BumpRelease {
index: 2,
value: spec.value,
}),
VersionBump::Stable => new_version.bump(BumpCommand::MakeStable),
VersionBump::Alpha => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Alpha,
value: spec.value,
}),
VersionBump::Beta => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Beta,
value: spec.value,
}),
VersionBump::Rc => new_version.bump(BumpCommand::BumpPrerelease {
kind: PrereleaseKind::Rc,
value: spec.value,
}),
VersionBump::Post => new_version.bump(BumpCommand::BumpPost { value: spec.value }),
VersionBump::Dev => new_version.bump(BumpCommand::BumpDev { value: spec.value }),
}
}
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 bumping to a pre-release version you also need to increase a release version component, e.g., with `--bump <major|minor|patch>`"
));
}
if new_version.is_dev() && !old_version.is_dev() {
return Err(anyhow!(
"{old_version} => {new_version} didn't increase the version; when bumping to a dev version you also need to increase another version component, e.g., with `--bump <major|minor|patch|alpha|beta|rc>`"
));
}
return Err(anyhow!(
"{old_version} => {new_version} didn't increase the version; provide the exact version to force an update"
));
}
Some(new_version)
} else {
None
};
let status = if dry_run {
ExitStatus::Success
} else if let Some(new_version) = &new_version {
let project = update_project(project, new_version, &mut toml, &pyproject_path)?;
Box::pin(lock_and_sync(
project,
project_dir,
lock_check,
frozen,
active,
no_sync,
python,
install_mirrors,
&settings,
client_builder,
python_preference,
python_downloads,
installer_metadata,
&concurrency,
no_config,
cache,
printer,
preview,
))
.await?
} else {
debug!("No changes to version; skipping update");
ExitStatus::Success
};
let old_version = ProjectVersionInfo::new(Some(&name), &old_version);
let new_version = new_version.map(|version| ProjectVersionInfo::new(Some(&name), &version));
print_version(old_version, new_version, short, output_format, printer)?;
Ok(status)
}
fn hint_uv_self_version(err: WorkspaceError, explicit_project: bool) -> anyhow::Error {
if matches!(err, WorkspaceError::MissingPyprojectToml) && !explicit_project {
anyhow!(
"{}\n\n{}{} If you meant to view uv's version, use `{}` instead",
err,
"hint".bold().cyan(),
":".bold(),
"uv self version".green()
)
} else {
err.into()
}
}
async fn find_target(
project_dir: &Path,
package: Option<&PackageName>,
explicit_project: bool,
workspace_cache: &WorkspaceCache,
) -> Result<VirtualProject> {
let project = if let Some(package) = package {
VirtualProject::discover_with_package(
project_dir,
&DiscoveryOptions {
project: uv_workspace::ProjectDiscovery::Required,
..DiscoveryOptions::default()
},
workspace_cache,
package.clone(),
)
.await
.map_err(|err| hint_uv_self_version(err, explicit_project))?
} else {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
project: uv_workspace::ProjectDiscovery::Required,
..DiscoveryOptions::default()
},
workspace_cache,
)
.await
.map_err(|err| hint_uv_self_version(err, explicit_project))?
};
Ok(project)
}
fn update_project(
project: VirtualProject,
new_version: &Version,
toml: &mut PyProjectTomlMut,
pyproject_path: &Path,
) -> Result<VirtualProject> {
toml.set_version(new_version)?;
let content = toml.to_string();
fs_err::write(pyproject_path, &content)?;
let project = project
.update_member(toml::from_str(&content).map_err(ProjectError::PyprojectTomlParse)?)?
.ok_or(ProjectError::PyprojectTomlUpdate)?;
Ok(project)
}
async fn print_frozen_version(
project: VirtualProject,
name: &PackageName,
project_dir: &Path,
frozen_source: FrozenSource,
active: Option<bool>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: &ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
concurrency: &Concurrency,
no_config: bool,
cache: &Cache,
workspace_cache: &WorkspaceCache,
short: bool,
output_format: VersionFormat,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let groups = DependencyGroupsWithDefaults::none();
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
let interpreter = ProjectInterpreter::discover(
project.workspace(),
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
active,
cache,
printer,
preview,
)
.await?
.into_interpreter();
let target = AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter)));
let state = UniversalState::default();
let lock = match Box::pin(
project::lock::LockOperation::new(
LockMode::Frozen(frozen_source.into()),
&settings.resolver,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
concurrency,
cache,
workspace_cache,
printer,
preview,
)
.execute((&target).into()),
)
.await
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
let Some(package) = lock
.packages()
.iter()
.find(|package| package.name() == name)
else {
return Err(anyhow!(
"Failed to find the {name}'s version in the frozen lockfile"
));
};
let Some(version) = package.version() else {
return Err(anyhow!(
"Failed to find the {name}'s version in the frozen lockfile"
));
};
let old_version = ProjectVersionInfo::new(Some(name), version);
print_version(old_version, None, short, output_format, printer)?;
Ok(ExitStatus::Success)
}
async fn lock_and_sync(
project: VirtualProject,
project_dir: &Path,
lock_check: LockCheck,
frozen: Option<FrozenSource>,
active: Option<bool>,
no_sync: bool,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: &ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: &Concurrency,
no_config: bool,
cache: &Cache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
if frozen.is_some() {
return Ok(ExitStatus::Success);
}
let default_groups = default_dependency_groups(project.pyproject_toml())?;
let default_extras = DefaultExtras::default();
let groups = DependencyGroups::default().with_defaults(default_groups);
let extras = ExtrasSpecification::default().with_defaults(default_extras);
let install_options = InstallOptions::default();
let target = if no_sync {
let workspace_python = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
let interpreter = ProjectInterpreter::discover(
project.workspace(),
&groups,
workspace_python,
&client_builder,
python_preference,
python_downloads,
&install_mirrors,
false,
active,
cache,
printer,
preview,
)
.await?
.into_interpreter();
AddTarget::Project(project, Box::new(PythonTarget::Interpreter(interpreter)))
} else {
let environment = ProjectEnvironment::get_or_init(
project.workspace(),
&groups,
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
&client_builder,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
DryRun::Disabled,
printer,
preview,
)
.await?
.into_environment()?;
AddTarget::Project(project, Box::new(PythonTarget::Environment(environment)))
};
let mode = if let LockCheck::Enabled(lock_check) = lock_check {
LockMode::Locked(target.interpreter(), lock_check)
} else {
LockMode::Write(target.interpreter())
};
let state = UniversalState::default();
let workspace_cache = WorkspaceCache::default();
let lock = match Box::pin(
project::lock::LockOperation::new(
mode,
&settings.resolver,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
concurrency,
cache,
&workspace_cache,
printer,
preview,
)
.execute((&target).into()),
)
.await
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
let AddTarget::Project(project, environment) = target else {
return Ok(ExitStatus::Success);
};
let PythonTarget::Environment(venv) = &*environment else {
return Ok(ExitStatus::Success);
};
let target = match &project {
VirtualProject::Project(project) => InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
},
VirtualProject::NonProject(workspace) => InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
},
};
let state = state.fork();
match project::sync::do_sync(
target,
venv,
&extras,
&groups,
None,
install_options,
Modifications::Sufficient,
None,
settings.into(),
&client_builder,
&state,
Box::new(DefaultInstallLogger),
installer_metadata,
concurrency,
cache,
&workspace_cache,
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
Ok(ExitStatus::Success)
}
fn print_version(
old_version: ProjectVersionInfo,
new_version: Option<ProjectVersionInfo>,
short: bool,
output_format: VersionFormat,
printer: Printer,
) -> Result<()> {
match output_format {
VersionFormat::Text => {
if let Some(name) = &old_version.package_name {
if !short {
write!(printer.stdout(), "{name} ")?;
}
}
if let Some(new_version) = new_version {
if short {
writeln!(printer.stdout(), "{}", new_version.cyan())?;
} else {
writeln!(
printer.stdout(),
"{} => {}",
old_version.cyan(),
new_version.cyan()
)?;
}
} else {
writeln!(printer.stdout(), "{}", old_version.cyan())?;
}
}
VersionFormat::Json => {
let final_version = new_version.unwrap_or(old_version);
let string = serde_json::to_string_pretty(&final_version)?;
writeln!(printer.stdout(), "{string}")?;
}
}
Ok(())
}