Suggest uv self update if required version is newer (#13305)

## Summary

Closes #13253 

## Test Plan

```sh
❯ cat pyproject.toml | rg required
required-version = ">=0.7.3, <0.8"
❯ cargo run -q --features self-update --manifest-path ~/uv/Cargo.toml add black
error: Required uv version `>=0.7.3, <0.8` does not match the running version `0.7.2`.
hint: Update `uv` by running `uv self update`.
❯ cat pyproject.toml | rg required
required-version = ">=0.7.3"
❯ cargo run -q --features self-update --manifest-path ~/uv/Cargo.toml add black
error: Required uv version `>=0.7.3` does not match the running version `0.7.2`. 
hint: Update `uv` by running `uv self update`.
❯ cat pyproject.toml | rg required
required-version = "<0.7"
❯ cargo run -q --features self-update --manifest-path ~/uv/Cargo.toml add black
error: Required uv version `<0.7` does not match the running version `0.7.2`.
❯ cat pyproject.toml | rg required
required-version = ">=0.4,<0.7"
❯ cargo run -q --features self-update --manifest-path ~/uv/Cargo.toml add black
error: Required uv version `>=0.4, <0.7` does not match the running version `0.7.2`.
```

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Ahmed Ilyas 2025-05-08 02:09:29 +02:00 committed by GitHub
parent d242c47821
commit 3eba70cf09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 1 deletions

View file

@ -12,6 +12,11 @@ impl RequiredVersion {
pub fn contains(&self, version: &Version) -> bool {
self.0.contains(version)
}
/// Returns the underlying [`VersionSpecifiers`].
pub fn specifiers(&self) -> &VersionSpecifiers {
&self.0
}
}
impl FromStr for RequiredVersion {

View file

@ -3,6 +3,8 @@ use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
#[cfg(feature = "self-update")]
use std::ops::Bound;
use std::path::Path;
use std::process::ExitCode;
use std::str::FromStr;
@ -17,6 +19,7 @@ use owo_colors::OwoColorize;
use settings::PipTreeSettings;
use tokio::task::spawn_blocking;
use tracing::{debug, instrument};
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
#[cfg(feature = "self-update")]
@ -28,6 +31,8 @@ use uv_cli::{
};
use uv_configuration::min_stack_size;
use uv_fs::{Simplified, CWD};
#[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges;
use uv_pep508::VersionOrUrl;
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
use uv_requirements::RequirementsSource;
@ -297,8 +302,37 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
if let Some(required_version) = globals.required_version.as_ref() {
let package_version = uv_pep440::Version::from_str(uv_version::version())?;
if !required_version.contains(&package_version) {
#[cfg(feature = "self-update")]
let hint = {
// If the required version range includes a lower bound that's higher than
// the current version, suggest `uv self update`.
let ranges = release_specifiers_to_ranges(required_version.specifiers().clone());
if let Some(singleton) = ranges.as_singleton() {
// E.g., `==1.0.0`
format!(
". Update `uv` by running `{}`.",
format!("uv self update {singleton}").green()
)
} else if ranges
.bounding_range()
.iter()
.any(|(lowest, _highest)| match lowest {
Bound::Included(version) => **version > package_version,
Bound::Excluded(version) => **version > package_version,
Bound::Unbounded => false,
})
{
// E.g., `>=1.0.0`
format!(". Update `uv` by running `{}`.", "uv self update".cyan())
} else {
String::new()
}
};
#[cfg(not(feature = "self-update"))]
let hint = "";
return Err(anyhow::anyhow!(
"Required uv version `{required_version}` does not match the running version `{package_version}`",
"Required uv version `{required_version}` does not match the running version `{package_version}`{hint}",
));
}
}