mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-19 11:35:36 +00:00
Warn on managed prerelease interpreters when a stable build is available (#16619)
Resolves https://github.com/astral-sh/uv/issues/16616 This PR detects managed prerelease interpreters during discovery and warns when a matching stable build is available, wiring the new helper into `PythonInstallation::find`, `find_best`, and `find_or_download`.
This commit is contained in:
parent
92230ba679
commit
1b38b47a3f
3 changed files with 364 additions and 9 deletions
|
|
@ -415,6 +415,86 @@ impl PythonDownloadRequest {
|
|||
self.version.take()
|
||||
}
|
||||
|
||||
/// Remove default implementation and platform details so the request only contains
|
||||
/// explicitly user-specified segments.
|
||||
#[must_use]
|
||||
pub fn unset_defaults(self) -> Self {
|
||||
let request = self.unset_non_platform_defaults();
|
||||
|
||||
if let Ok(host) = Platform::from_env() {
|
||||
request.unset_platform_defaults(&host)
|
||||
} else {
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fn unset_non_platform_defaults(mut self) -> Self {
|
||||
self.implementation = self
|
||||
.implementation
|
||||
.filter(|implementation_name| *implementation_name != ImplementationName::default());
|
||||
|
||||
self.version = self
|
||||
.version
|
||||
.filter(|version| !matches!(version, VersionRequest::Any | VersionRequest::Default));
|
||||
|
||||
// Drop implicit architecture derived from environment so only user overrides remain.
|
||||
self.arch = self
|
||||
.arch
|
||||
.filter(|arch| !matches!(arch, ArchRequest::Environment(_)));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn unset_defaults_for_host(self, host: &Platform) -> Self {
|
||||
self.unset_non_platform_defaults()
|
||||
.unset_platform_defaults(host)
|
||||
}
|
||||
|
||||
pub(crate) fn unset_platform_defaults(mut self, host: &Platform) -> Self {
|
||||
self.os = self.os.filter(|os| *os != host.os);
|
||||
|
||||
self.libc = self.libc.filter(|libc| *libc != host.libc);
|
||||
|
||||
self.arch = self
|
||||
.arch
|
||||
.filter(|arch| !matches!(arch, ArchRequest::Explicit(explicit_arch) if *explicit_arch == host.arch));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Drop patch and prerelease information so the request can be re-used for upgrades.
|
||||
#[must_use]
|
||||
pub fn without_patch(mut self) -> Self {
|
||||
self.version = self.version.take().map(VersionRequest::only_minor);
|
||||
self.prereleases = None;
|
||||
self.build = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a compact string representation suitable for user-facing display.
|
||||
///
|
||||
/// The resulting string only includes explicitly-set pieces of the request and returns
|
||||
/// [`None`] when no segments are explicitly set.
|
||||
pub fn simplified_display(self) -> Option<String> {
|
||||
let parts = [
|
||||
self.implementation
|
||||
.map(|implementation| implementation.to_string()),
|
||||
self.version.map(|version| version.to_string()),
|
||||
self.os.map(|os| os.to_string()),
|
||||
self.arch.map(|arch| arch.to_string()),
|
||||
self.libc.map(|libc| libc.to_string()),
|
||||
];
|
||||
|
||||
let joined = parts.into_iter().flatten().collect::<Vec<_>>().join("-");
|
||||
|
||||
if joined.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(joined)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all [`PythonDownload`]'s that match this request.
|
||||
pub fn iter_downloads<'a>(
|
||||
&'a self,
|
||||
|
|
@ -554,6 +634,30 @@ impl PythonDownloadRequest {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&PythonInstallationKey> for PythonDownloadRequest {
|
||||
type Error = LenientImplementationName;
|
||||
|
||||
fn try_from(key: &PythonInstallationKey) -> Result<Self, Self::Error> {
|
||||
let implementation = match key.implementation().into_owned() {
|
||||
LenientImplementationName::Known(name) => name,
|
||||
unknown @ LenientImplementationName::Unknown(_) => return Err(unknown),
|
||||
};
|
||||
|
||||
Ok(Self::new(
|
||||
Some(VersionRequest::MajorMinor(
|
||||
key.major(),
|
||||
key.minor(),
|
||||
*key.variant(),
|
||||
)),
|
||||
Some(implementation),
|
||||
Some(ArchRequest::Explicit(*key.arch())),
|
||||
Some(*key.os()),
|
||||
Some(*key.libc()),
|
||||
Some(key.prerelease().is_some()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
|
||||
fn from(installation: &ManagedPythonInstallation) -> Self {
|
||||
let key = installation.key();
|
||||
|
|
@ -1601,6 +1705,7 @@ async fn read_url(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::PythonVariant;
|
||||
use crate::implementation::LenientImplementationName;
|
||||
use crate::installation::PythonInstallationKey;
|
||||
use uv_platform::{Arch, Libc, Os, Platform};
|
||||
|
|
@ -1859,6 +1964,172 @@ mod tests {
|
|||
assert_eq!(downloads.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_request_native_defaults() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_version(VersionRequest::MajorMinorPatch(
|
||||
3,
|
||||
13,
|
||||
1,
|
||||
PythonVariant::Default,
|
||||
))
|
||||
.with_os(Os::from_str("linux").unwrap())
|
||||
.with_arch(Arch::from_str("x86_64").unwrap())
|
||||
.with_libc(Libc::from_str("gnu").unwrap())
|
||||
.with_prereleases(false);
|
||||
|
||||
let host = Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.clone()
|
||||
.unset_defaults_for_host(&host)
|
||||
.without_patch()
|
||||
.simplified_display()
|
||||
.as_deref(),
|
||||
Some("3.13")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_request_preserves_variant() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_version(VersionRequest::MajorMinorPatch(
|
||||
3,
|
||||
13,
|
||||
0,
|
||||
PythonVariant::Freethreaded,
|
||||
))
|
||||
.with_os(Os::from_str("linux").unwrap())
|
||||
.with_arch(Arch::from_str("x86_64").unwrap())
|
||||
.with_libc(Libc::from_str("gnu").unwrap())
|
||||
.with_prereleases(false);
|
||||
|
||||
let host = Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.clone()
|
||||
.unset_defaults_for_host(&host)
|
||||
.without_patch()
|
||||
.simplified_display()
|
||||
.as_deref(),
|
||||
Some("3.13+freethreaded")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_request_preserves_non_default_platform() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_version(VersionRequest::MajorMinorPatch(
|
||||
3,
|
||||
12,
|
||||
4,
|
||||
PythonVariant::Default,
|
||||
))
|
||||
.with_os(Os::from_str("linux").unwrap())
|
||||
.with_arch(Arch::from_str("aarch64").unwrap())
|
||||
.with_libc(Libc::from_str("gnu").unwrap())
|
||||
.with_prereleases(false);
|
||||
|
||||
let host = Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.clone()
|
||||
.unset_defaults_for_host(&host)
|
||||
.without_patch()
|
||||
.simplified_display()
|
||||
.as_deref(),
|
||||
Some("3.12-aarch64")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upgrade_request_preserves_custom_implementation() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_implementation(ImplementationName::PyPy)
|
||||
.with_version(VersionRequest::MajorMinorPatch(
|
||||
3,
|
||||
10,
|
||||
5,
|
||||
PythonVariant::Default,
|
||||
))
|
||||
.with_os(Os::from_str("linux").unwrap())
|
||||
.with_arch(Arch::from_str("x86_64").unwrap())
|
||||
.with_libc(Libc::from_str("gnu").unwrap())
|
||||
.with_prereleases(false);
|
||||
|
||||
let host = Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.clone()
|
||||
.unset_defaults_for_host(&host)
|
||||
.without_patch()
|
||||
.simplified_display()
|
||||
.as_deref(),
|
||||
Some("pypy-3.10")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplified_display_returns_none_when_empty() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.fill_platform()
|
||||
.expect("should populate defaults");
|
||||
|
||||
let host = Platform::from_env().expect("host platform");
|
||||
|
||||
assert_eq!(
|
||||
request.unset_defaults_for_host(&host).simplified_display(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplified_display_omits_environment_arch() {
|
||||
let mut request = PythonDownloadRequest::default()
|
||||
.with_version(VersionRequest::MajorMinor(3, 12, PythonVariant::Default))
|
||||
.with_os(Os::from_str("linux").unwrap())
|
||||
.with_libc(Libc::from_str("gnu").unwrap());
|
||||
|
||||
request.arch = Some(ArchRequest::Environment(Arch::from_str("x86_64").unwrap()));
|
||||
|
||||
let host = Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("aarch64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request
|
||||
.unset_defaults_for_host(&host)
|
||||
.simplified_display()
|
||||
.as_deref(),
|
||||
Some("3.12")
|
||||
);
|
||||
}
|
||||
|
||||
/// Test build display
|
||||
#[test]
|
||||
fn test_managed_python_download_build_display() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use std::str::FromStr;
|
|||
use indexmap::IndexMap;
|
||||
use ref_cast::RefCast;
|
||||
use tracing::{debug, info};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
|
|
@ -63,6 +64,7 @@ impl PythonInstallation {
|
|||
) -> Result<Self, Error> {
|
||||
let installation =
|
||||
find_python_installation(request, environments, preference, cache, preview)??;
|
||||
installation.warn_if_outdated_prerelease(request, None);
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
|
|
@ -75,13 +77,10 @@ impl PythonInstallation {
|
|||
cache: &Cache,
|
||||
preview: Preview,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(find_best_python_installation(
|
||||
request,
|
||||
environments,
|
||||
preference,
|
||||
cache,
|
||||
preview,
|
||||
)??)
|
||||
let installation =
|
||||
find_best_python_installation(request, environments, preference, cache, preview)??;
|
||||
installation.warn_if_outdated_prerelease(request, None);
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Find or fetch a [`PythonInstallation`].
|
||||
|
|
@ -201,7 +200,7 @@ impl PythonInstallation {
|
|||
return Err(err);
|
||||
}
|
||||
|
||||
Self::fetch(
|
||||
let installation = Self::fetch(
|
||||
download,
|
||||
client_builder,
|
||||
cache,
|
||||
|
|
@ -210,7 +209,11 @@ impl PythonInstallation {
|
|||
pypy_install_mirror,
|
||||
preview,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
installation.warn_if_outdated_prerelease(request, python_downloads_json_url);
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Download and install the requested installation.
|
||||
|
|
@ -343,6 +346,81 @@ impl PythonInstallation {
|
|||
pub fn into_interpreter(self) -> Interpreter {
|
||||
self.interpreter
|
||||
}
|
||||
|
||||
/// Emit a warning when the interpreter is a managed prerelease and a matching stable
|
||||
/// build can be installed via `uv python upgrade`.
|
||||
pub(crate) fn warn_if_outdated_prerelease(
|
||||
&self,
|
||||
request: &PythonRequest,
|
||||
python_downloads_json_url: Option<&str>,
|
||||
) {
|
||||
if request.allows_prereleases() {
|
||||
return;
|
||||
}
|
||||
|
||||
let interpreter = self.interpreter();
|
||||
let version = interpreter.python_version();
|
||||
|
||||
if version.pre().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !interpreter.is_managed() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transparent upgrades only exist for CPython, so skip the warning for other
|
||||
// managed implementations.
|
||||
//
|
||||
// See: https://github.com/astral-sh/uv/issues/16675
|
||||
if !interpreter
|
||||
.implementation_name()
|
||||
.eq_ignore_ascii_case("cpython")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let release = version.only_release();
|
||||
|
||||
let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let download_request = download_request.with_prereleases(false);
|
||||
|
||||
let has_stable_download = {
|
||||
let Ok(mut downloads) = download_request.iter_downloads(python_downloads_json_url)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
downloads.any(|download| {
|
||||
let download_version = download.key().version().into_version();
|
||||
download_version.pre().is_none() && download_version.only_release() >= release
|
||||
})
|
||||
};
|
||||
|
||||
if !has_stable_download {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(upgrade_request) = download_request
|
||||
.unset_defaults()
|
||||
.without_patch()
|
||||
.simplified_display()
|
||||
{
|
||||
warn_user!(
|
||||
"You're using a pre-release version of Python ({}) but a stable version is available. Use `uv python upgrade {}` to upgrade.",
|
||||
version,
|
||||
upgrade_request
|
||||
);
|
||||
} else {
|
||||
warn_user!(
|
||||
"You're using a pre-release version of Python ({}) but a stable version is available. Run `uv python upgrade` to update your managed interpreters.",
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
@ -434,6 +512,10 @@ impl PythonInstallationKey {
|
|||
self.minor
|
||||
}
|
||||
|
||||
pub fn prerelease(&self) -> Option<Prerelease> {
|
||||
self.prerelease
|
||||
}
|
||||
|
||||
pub fn platform(&self) -> &Platform {
|
||||
&self.platform
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1396,6 +1396,7 @@ fn python_find_prerelease_version_specifiers() {
|
|||
[TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
|
||||
|
||||
----- stderr -----
|
||||
warning: You're using a pre-release version of Python (3.14.0rc3) but a stable version is available. Use `uv python upgrade 3.14` to upgrade.
|
||||
");
|
||||
|
||||
// `>3.14rc2` should not match rc2
|
||||
|
|
@ -1494,6 +1495,7 @@ fn python_find_prerelease_with_patch_request() {
|
|||
[TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
|
||||
|
||||
----- stderr -----
|
||||
warning: You're using a pre-release version of Python (3.14.0rc3) but a stable version is available. Use `uv python upgrade 3.14` to upgrade.
|
||||
");
|
||||
|
||||
// When `.0` is explicitly included, we will require a stable release
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue