From d2ab2d02085fbf7e25a5f27d144b2d533654288d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 5 Feb 2026 11:26:54 -0600 Subject: [PATCH] Stabilize Python upgrades (#17766) Includes a few things... - Drops preview warnings for use of `uv python upgrade` and `uv python install --upgrade` - Adds `--resolve-links` to `uv python find`, which I needed in test cases to retain existing snapshots - Fixes issues in our "Using environment ..." messages on Windows which were incorrect - Refactors `from_executable` for the `PythonMinorVersionLink` type (https://github.com/astral-sh/uv/pull/17842/commits/28b2ed2525327d94fdf5372a29bbbc476d74680f) to use the type system to prevent incorrect construction (for above) - Removes special casing where we only upgrade links if they already exist, which existed so preview wasn't needed on every invocation - Fixes a bug with `PythonMinorVersionLink::exists` which returned `true` even if the link pointed to the wrong Python installation leading to discovery failures --- Cargo.lock | 4 +- crates/uv-build-frontend/Cargo.toml | 1 - crates/uv-build-frontend/src/lib.rs | 3 - crates/uv-cli/src/lib.rs | 6 + crates/uv-dev/Cargo.toml | 2 +- crates/uv-dispatch/src/lib.rs | 1 - crates/uv-python/Cargo.toml | 1 + crates/uv-python/src/discovery.rs | 5 +- crates/uv-python/src/installation.rs | 4 +- crates/uv-python/src/managed.rs | 110 +++++++++++------- crates/uv-tool/Cargo.toml | 1 - crates/uv-tool/src/lib.rs | 3 - crates/uv-virtualenv/Cargo.toml | 1 - crates/uv-virtualenv/src/lib.rs | 3 - crates/uv-virtualenv/src/virtualenv.rs | 19 ++- crates/uv/src/commands/pip/operations.rs | 18 ++- crates/uv/src/commands/project/environment.rs | 1 - crates/uv/src/commands/project/mod.rs | 4 - crates/uv/src/commands/project/run.rs | 4 - crates/uv/src/commands/python/find.rs | 24 ++-- crates/uv/src/commands/python/install.rs | 22 +--- crates/uv/src/commands/python/uninstall.rs | 9 +- crates/uv/src/commands/tool/install.rs | 2 +- crates/uv/src/commands/tool/upgrade.rs | 2 +- crates/uv/src/commands/venv.rs | 10 +- crates/uv/src/lib.rs | 11 +- crates/uv/src/settings.rs | 3 + crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/pip_install.rs | 5 +- crates/uv/tests/it/python_find.rs | 16 +-- crates/uv/tests/it/python_install.rs | 71 +++++------ crates/uv/tests/it/python_list.rs | 2 +- crates/uv/tests/it/python_upgrade.rs | 68 ++++++----- docs/concepts/preview.md | 7 -- docs/concepts/python-versions.md | 15 +-- 35 files changed, 221 insertions(+), 239 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3390012a9..6433f0f89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5920,7 +5920,6 @@ dependencies = [ "uv-normalize", "uv-pep440", "uv-pep508", - "uv-preview", "uv-pypi-types", "uv-python", "uv-static", @@ -6782,6 +6781,7 @@ dependencies = [ "indoc", "insta", "itertools 0.14.0", + "junction", "owo-colors", "ref-cast", "regex", @@ -7094,7 +7094,6 @@ dependencies = [ "uv-normalize", "uv-pep440", "uv-pep508", - "uv-preview", "uv-pypi-types", "uv-python", "uv-settings", @@ -7189,7 +7188,6 @@ dependencies = [ "uv-console", "uv-fs", "uv-platform-tags", - "uv-preview", "uv-pypi-types", "uv-python", "uv-shell", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index 507cb6bec..b417326b9 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -25,7 +25,6 @@ uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } -uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-static = { workspace = true } diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index b85067aee..5e5eedf97 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -40,7 +40,6 @@ use uv_fs::{LockedFile, LockedFileMode}; use uv_fs::{PythonExt, Simplified}; use uv_normalize::PackageName; use uv_pep440::Version; -use uv_preview::Preview; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{Interpreter, PythonEnvironment}; use uv_static::EnvVars; @@ -293,7 +292,6 @@ impl SourceBuild { level: BuildOutput, concurrent_builds: usize, credentials_cache: &CredentialsCache, - preview: Preview, ) -> Result { let temp_dir = build_context.cache().venv_dir()?; @@ -365,7 +363,6 @@ impl SourceBuild { false, false, false, - preview, )? }; diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 26c2d3cd8..1feba99e4 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -6426,6 +6426,12 @@ pub struct PythonFindArgs { #[arg(long)] pub show_version: bool, + /// Resolve symlinks in the output path. + /// + /// When enabled, the output path will be canonicalized, resolving any symlinks. + #[arg(long)] + pub resolve_links: bool, + /// URL pointing to JSON of custom Python installations. #[arg(long, value_hint = ValueHint::Other)] pub python_downloads_json_url: Option, diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 6e9e757bf..9717268d2 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -26,8 +26,8 @@ uv-installer = { workspace = true } uv-macros = { workspace = true } uv-options-metadata = { workspace = true } uv-pep508 = { workspace = true } -uv-preview = { workspace = true } uv-pypi-types = { workspace = true } +uv-preview = { workspace = true } uv-python = { workspace = true } uv-settings = { workspace = true, features = ["schemars"] } uv-static = { workspace = true } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index bec85b45c..5da12e35d 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -491,7 +491,6 @@ impl BuildContext for BuildDispatch<'_> { build_output, self.concurrency.builds, self.client.credentials_cache(), - self.preview, ) .boxed_local() .await?; diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 3a0bb7572..cd2b63389 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -68,6 +68,7 @@ url = { workspace = true } which = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +junction = { workspace = true } windows-registry = { workspace = true } windows = { workspace = true } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 873453d79..c36317162 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -356,7 +356,6 @@ fn python_executables_from_installed<'a>( implementation: Option<&'a ImplementationName>, platform: PlatformRequest, preference: PythonPreference, - preview: Preview, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { ManagedPythonInstallations::from_settings(None) @@ -411,7 +410,6 @@ fn python_executables_from_installed<'a>( .then(|| { PythonMinorVersionLink::from_installation( &installation, - preview, ) .filter(PythonMinorVersionLink::exists) .map( @@ -547,7 +545,7 @@ fn python_executables<'a>( let from_virtual_environments = python_executables_from_virtual_environments(preview); let from_installed = - python_executables_from_installed(version, implementation, platform, preference, preview); + python_executables_from_installed(version, implementation, platform, preference); // Limit the search to the relevant environment preference; this avoids unnecessary work like // traversal of the file system. Subsequent filtering should be done by the caller with @@ -1542,7 +1540,6 @@ pub(crate) async fn find_best_python_installation( reporter, python_install_mirror, pypy_install_mirror, - preview, ) .await .map(Some), diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index e818d4961..adbcbd326 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -260,7 +260,6 @@ impl PythonInstallation { reporter, python_install_mirror, pypy_install_mirror, - preview, ) .await?; @@ -278,7 +277,6 @@ impl PythonInstallation { reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, - preview: Preview, ) -> Result { let installations = ManagedPythonInstallations::from_settings(None)?.init()?; let installations_dir = installations.root(); @@ -321,7 +319,7 @@ impl PythonInstallation { .patch() .is_some_and(|p| p >= highest_patch) { - installed.ensure_minor_version_link(preview)?; + installed.ensure_minor_version_link()?; } if let Err(e) = installed.ensure_dylib_patched() { diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index a18a7e038..6d715bd05 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -12,7 +12,6 @@ use fs_err as fs; use itertools::Itertools; use thiserror::Error; use tracing::{debug, warn}; -use uv_preview::{Preview, PreviewFeature}; #[cfg(windows)] use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; @@ -30,6 +29,7 @@ use crate::implementation::{ Error as ImplementationError, ImplementationName, LenientImplementationName, }; use crate::installation::{self, PythonInstallationKey}; +use crate::interpreter::Interpreter; use crate::python_version::PythonVersion; use crate::{ PythonInstallationMinorVersionKey, PythonRequest, PythonVariant, macos_dylib, sysconfig, @@ -357,7 +357,9 @@ impl ManagedPythonInstallation { } } - pub(crate) fn from_path(path: PathBuf) -> Result { + pub(crate) fn from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let key = PythonInstallationKey::from_str( path.file_name() .ok_or(Error::NameError("name is empty".to_string()))? @@ -365,7 +367,8 @@ impl ManagedPythonInstallation { .ok_or(Error::NameError("not a valid string".to_string()))?, )?; - let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?; + let path = std::path::absolute(path) + .map_err(|err| Error::AbsolutePath(path.to_path_buf(), err))?; // Try to read the BUILD file if it exists let build = match fs::read_to_string(path.join("BUILD")) { @@ -383,6 +386,34 @@ impl ManagedPythonInstallation { }) } + /// Try to create a [`ManagedPythonInstallation`] from an [`Interpreter`]. + /// + /// Returns `None` if the interpreter is not a managed installation. + pub fn try_from_interpreter(interpreter: &Interpreter) -> Option { + let managed_root = ManagedPythonInstallations::from_settings(None).ok()?; + + // Canonicalize both paths to handle Windows path format differences + // (e.g., \\?\ prefix, different casing, junction vs actual path). + // Fall back to the original path if canonicalization fails (e.g., target doesn't exist). + let sys_base_prefix = dunce::canonicalize(interpreter.sys_base_prefix()) + .unwrap_or_else(|_| interpreter.sys_base_prefix().to_path_buf()); + let root = dunce::canonicalize(managed_root.root()) + .unwrap_or_else(|_| managed_root.root().to_path_buf()); + + // Verify the interpreter's base prefix is within the managed root + let suffix = sys_base_prefix.strip_prefix(&root).ok()?; + + let first_component = suffix.components().next()?; + let name = first_component.as_os_str().to_str()?; + + // Verify it's a valid installation key + PythonInstallationKey::from_str(name).ok()?; + + // Construct the installation from the path within the managed root + let path = managed_root.root().join(name); + Self::from_path(path).ok() + } + /// The path to this managed installation's Python executable. /// /// If the installation has multiple executables i.e., `python`, `python3`, etc., this will @@ -560,23 +591,8 @@ impl ManagedPythonInstallation { /// Ensure the environment contains the symlink directory (or junction on Windows) /// pointing to the patch directory for this minor version. - pub fn ensure_minor_version_link(&self, preview: Preview) -> Result<(), Error> { - if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { - minor_version_link.create_directory()?; - } - Ok(()) - } - - /// If the environment contains a symlink directory (or junction on Windows), - /// update it to the latest patch directory for this minor version. - /// - /// Unlike [`ensure_minor_version_link`], will not create a new symlink directory - /// if one doesn't already exist, - pub fn update_minor_version_link(&self, preview: Preview) -> Result<(), Error> { - if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { - if !minor_version_link.exists() { - return Ok(()); - } + pub fn ensure_minor_version_link(&self) -> Result<(), Error> { + if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self) { minor_version_link.create_directory()?; } Ok(()) @@ -774,11 +790,7 @@ impl PythonMinorVersionLink { /// For a Python 3.10.8 installation in `C:\path\to\uv\python\cpython-3.10.8-windows-x86_64-none\python.exe`, /// the junction would be `C:\path\to\uv\python\cpython-3.10-windows-x86_64-none` and the executable path including the /// junction would be `C:\path\to\uv\python\cpython-3.10-windows-x86_64-none\python.exe`. - pub fn from_executable( - executable: &Path, - key: &PythonInstallationKey, - preview: Preview, - ) -> Option { + fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option { let implementation = key.implementation(); if !matches!( implementation.as_ref(), @@ -828,24 +840,11 @@ impl PythonMinorVersionLink { symlink_executable, target_directory, }; - // If preview mode is disabled, still return a `MinorVersionSymlink` for - // existing symlinks, allowing continued operations without the `--preview` - // flag after initial symlink directory installation. - if !preview.is_enabled(PreviewFeature::PythonUpgrade) && !minor_version_link.exists() { - return None; - } Some(minor_version_link) } - pub fn from_installation( - installation: &ManagedPythonInstallation, - preview: Preview, - ) -> Option { - Self::from_executable( - installation.executable(false).as_path(), - installation.key(), - preview, - ) + pub fn from_installation(installation: &ManagedPythonInstallation) -> Option { + Self::from_executable(installation.executable(false).as_path(), installation.key()) } pub fn create_directory(&self) -> Result<(), Error> { @@ -877,13 +876,21 @@ impl PythonMinorVersionLink { Ok(()) } + /// Check if the minor version link exists and points to the expected target directory. + /// + /// This verifies both that the symlink/junction exists AND that it points to the + /// `target_directory` specified in this struct. This is important because the link + /// may exist but point to a different installation (e.g., after an upgrade), in which + /// case we should not use the link for the current installation. pub fn exists(&self) -> bool { #[cfg(unix)] { self.symlink_directory .symlink_metadata() - .map(|metadata| metadata.file_type().is_symlink()) - .unwrap_or(false) + .is_ok_and(|metadata| metadata.file_type().is_symlink()) + && self + .read_target() + .is_some_and(|target| target == self.target_directory) } #[cfg(windows)] { @@ -894,6 +901,25 @@ impl PythonMinorVersionLink { // is a symlink or junction. (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0 }) + && self + .read_target() + .is_some_and(|target| target == self.target_directory) + } + } + + /// Read the target of the minor version link. + /// + /// On Unix, this reads the symlink target. On Windows, this reads the junction target + /// using the `junction` crate which properly handles the `\??\` prefix that Windows + /// uses internally for junction targets. + pub fn read_target(&self) -> Option { + #[cfg(unix)] + { + self.symlink_directory.read_link().ok() + } + #[cfg(windows)] + { + junction::get_target(&self.symlink_directory).ok() } } } diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index d324ee0f0..f7be69814 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -25,7 +25,6 @@ uv-installer = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } -uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-settings = { workspace = true } diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 8c6f0a6a8..b649fbad2 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -14,7 +14,6 @@ use uv_install_wheel::read_record_file; use uv_installer::SitePackages; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::Version; -use uv_preview::Preview; use uv_python::{Interpreter, PythonEnvironment}; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; @@ -311,7 +310,6 @@ impl InstalledTools { &self, name: &PackageName, interpreter: Interpreter, - preview: Preview, ) -> Result { let environment_path = self.tool_dir(name); @@ -342,7 +340,6 @@ impl InstalledTools { false, false, false, - preview, )?; Ok(venv) diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 1f9d17c1d..de256d811 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -21,7 +21,6 @@ workspace = true uv-console = { workspace = true } uv-fs = { workspace = true } uv-platform-tags = { workspace = true } -uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-shell = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index 44a39ead9..79f1168b7 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf}; use thiserror::Error; -use uv_preview::Preview; use uv_python::{Interpreter, PythonEnvironment}; pub use virtualenv::{OnExisting, RemovalReason, remove_virtualenv}; @@ -63,7 +62,6 @@ pub fn create_venv( relocatable: bool, seed: bool, upgradeable: bool, - preview: Preview, ) -> Result { // Create the virtualenv at the given location. let virtualenv = virtualenv::create( @@ -75,7 +73,6 @@ pub fn create_venv( relocatable, seed, upgradeable, - preview, )?; // Create the corresponding `PythonEnvironment`. diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 75282f41e..18b356f35 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -14,9 +14,10 @@ use tracing::{debug, trace}; use crate::{Error, Prompt}; use uv_fs::{CWD, Simplified, cachedir}; use uv_platform_tags::Os; -use uv_preview::Preview; use uv_pypi_types::Scheme; -use uv_python::managed::{PythonMinorVersionLink, create_link_to_executable}; +use uv_python::managed::{ + ManagedPythonInstallation, PythonMinorVersionLink, create_link_to_executable, +}; use uv_python::{Interpreter, VirtualEnvironment}; use uv_shell::escape_posix_for_single_quotes; use uv_version::version; @@ -57,7 +58,6 @@ pub(crate) fn create( relocatable: bool, seed: bool, upgradeable: bool, - preview: Preview, ) -> Result { // Determine the base Python executable; that is, the Python executable that should be // considered the "base" for the virtual environment. @@ -205,12 +205,11 @@ pub(crate) fn create( fs_err::write(location.join(".gitignore"), "*")?; let mut using_minor_version_link = false; - let executable_target = if upgradeable && interpreter.is_standalone() { - if let Some(minor_version_link) = PythonMinorVersionLink::from_executable( - base_python.as_path(), - &interpreter.key(), - preview, - ) { + let executable_target = if upgradeable { + if let Some(minor_version_link) = + ManagedPythonInstallation::try_from_interpreter(interpreter) + .and_then(|installation| PythonMinorVersionLink::from_installation(&installation)) + { if !minor_version_link.exists() { base_python.clone() } else { @@ -236,7 +235,7 @@ pub(crate) fn create( }; // Per PEP 405, the Python `home` is the parent directory of the interpreter. - // In preview mode, for standalone interpreters, this `home` value will include a + // For standalone interpreters, this `home` value will include a // symlink directory on Unix or junction on Windows to enable transparent Python patch // upgrades. let python_home = executable_target diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index d9404504a..870daa6c0 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -33,6 +33,7 @@ use uv_pep508::{MarkerEnvironment, RequirementOrigin, VerbatimUrl}; use uv_platform_tags::Tags; use uv_preview::Preview; use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; +use uv_python::managed::{ManagedPythonInstallation, PythonMinorVersionLink}; use uv_python::{PythonEnvironment, PythonInstallation}; use uv_requirements::{ GroupsSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource, @@ -900,13 +901,26 @@ pub(crate) fn report_target_environment( cache: &Cache, printer: Printer, ) -> Result<(), Error> { + // Resolve minor-version link directories (e.g., `cpython-3.12` → `cpython-3.12.12`). + // On Windows, junction points aren't resolved by the interpreter's `sys.prefix`, so we + // use the target directory from the minor-version link to display the actual installation. + // This only applies to managed installations, not virtual environments. + let root = if env.interpreter().is_virtualenv() { + env.root().to_path_buf() + } else { + ManagedPythonInstallation::try_from_interpreter(env.interpreter()) + .and_then(|installation| PythonMinorVersionLink::from_installation(&installation)) + .map(|link| link.target_directory) + .unwrap_or_else(|| env.root().to_path_buf()) + }; + let message = format!( "Using Python {} environment at: {}", env.interpreter().python_version(), - env.root().user_display() + root.user_display() ); - let Ok(target) = std::path::absolute(env.root()) else { + let Ok(target) = std::path::absolute(&root) else { debug!("{}", message); return Ok(()); }; diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 5b47768f9..8d9d21b7c 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -191,7 +191,6 @@ impl CachedEnvironment { true, false, false, - preview, )?; sync_environment( diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 4b8132580..5f79fefe5 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1454,7 +1454,6 @@ impl ProjectEnvironment { false, false, upgradeable, - preview, )?; return Ok(if replace { Self::WouldReplace(root, environment, temp_dir) @@ -1496,7 +1495,6 @@ impl ProjectEnvironment { false, false, upgradeable, - preview, )?; if replace { @@ -1650,7 +1648,6 @@ impl ScriptEnvironment { false, false, upgradeable, - preview, )?; return Ok(if root.exists() { Self::WouldReplace(root, environment, temp_dir) @@ -1692,7 +1689,6 @@ impl ScriptEnvironment { false, false, upgradeable, - preview, )?; Ok(if replaced { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 9561ca92a..12f82e4d7 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -498,7 +498,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, - preview, )?; Some(environment.into_interpreter()) @@ -717,7 +716,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, - preview, )? } else { // If we're not isolating the environment, reuse the base environment for the @@ -954,7 +952,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, - preview, )?; venv.into_interpreter() } else { @@ -1082,7 +1079,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl false, false, false, - preview, ) }) .transpose()? diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 094235284..33a902102 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -28,6 +28,7 @@ pub(crate) async fn find( project_dir: &Path, request: Option, show_version: bool, + resolve_links: bool, no_project: bool, no_config: bool, system: bool, @@ -112,11 +113,12 @@ pub(crate) async fn find( python.interpreter().python_version() )?; } else { - writeln!( - printer.stdout(), - "{}", - std::path::absolute(python.interpreter().sys_executable())?.simplified_display() - )?; + let path = if resolve_links { + dunce::canonicalize(python.interpreter().sys_executable())? + } else { + std::path::absolute(python.interpreter().sys_executable())? + }; + writeln!(printer.stdout(), "{}", path.simplified_display())?; } Ok(ExitStatus::Success) @@ -125,6 +127,7 @@ pub(crate) async fn find( pub(crate) async fn find_script( script: Pep723ItemRef<'_>, show_version: bool, + resolve_links: bool, client_builder: &BaseClientBuilder<'_>, python_preference: PythonPreference, python_downloads: PythonDownloads, @@ -160,11 +163,12 @@ pub(crate) async fn find_script( if show_version { writeln!(printer.stdout(), "{}", interpreter.python_version())?; } else { - writeln!( - printer.stdout(), - "{}", - std::path::absolute(interpreter.sys_executable())?.simplified_display() - )?; + let path = if resolve_links { + dunce::canonicalize(interpreter.sys_executable())? + } else { + std::path::absolute(interpreter.sys_executable())? + }; + writeln!(printer.stdout(), "{}", path.simplified_display())?; } Ok(ExitStatus::Success) diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index c1dd35c2b..f18dd6fc0 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -318,15 +318,6 @@ async fn perform_install( ); } - if let PythonUpgrade::Enabled(source @ PythonUpgradeSource::Upgrade) = upgrade { - if !preview.is_enabled(PreviewFeature::PythonUpgrade) { - warn_user!( - "`{source}` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning", - PreviewFeature::PythonUpgrade - ); - } - } - if default && targets.len() > 1 { anyhow::bail!("The `--default` flag cannot be used with multiple targets"); } @@ -733,16 +724,7 @@ async fn perform_install( ); for installation in minor_versions.values() { - if matches!( - upgrade, - PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade) - ) { - // During an upgrade, update existing symlinks but avoid - // creating new ones. - installation.update_minor_version_link(preview)?; - } else { - installation.ensure_minor_version_link(preview)?; - } + installation.ensure_minor_version_link()?; } if changelog.installed.is_empty() && errors.is_empty() { @@ -1015,7 +997,7 @@ fn create_bin_links( } let executable = if upgradeable { if let Some(minor_version_link) = - PythonMinorVersionLink::from_installation(installation, preview) + PythonMinorVersionLink::from_installation(installation) { minor_version_link.symlink_executable.clone() } else { diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index bb4bc5a15..ca2df3139 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -12,7 +12,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, warn}; use uv_fs::Simplified; -use uv_preview::Preview; use uv_python::downloads::PythonDownloadRequest; use uv_python::managed::{ ManagedPythonInstallations, PythonMinorVersionLink, python_executable_dir, @@ -30,14 +29,13 @@ pub(crate) async fn uninstall( targets: Vec, all: bool, printer: Printer, - preview: Preview, ) -> Result { let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?; let _lock = installations.lock().await?; // Perform the uninstallation. - do_uninstall(&installations, targets, all, printer, preview).await?; + do_uninstall(&installations, targets, all, printer).await?; // Clean up any empty directories. if uv_fs::directories(installations.root())?.all(|path| uv_fs::is_temporary(&path)) { @@ -66,7 +64,6 @@ async fn do_uninstall( targets: Vec, all: bool, printer: Printer, - preview: Preview, ) -> Result { let start = std::time::Instant::now(); @@ -241,7 +238,7 @@ async fn do_uninstall( .iter() .filter(|(minor_version, _)| uninstalled_minor_versions.contains(minor_version)) { - installation.update_minor_version_link(preview)?; + installation.ensure_minor_version_link()?; } // For each uninstalled installation, check if there are no remaining installations // for its minor version. If there are none remaining, remove the symlink directory @@ -249,7 +246,7 @@ async fn do_uninstall( for installation in &matching_installations { if !remaining_minor_versions.contains_key(installation.minor_version_key()) { if let Some(minor_version_link) = - PythonMinorVersionLink::from_installation(installation, preview) + PythonMinorVersionLink::from_installation(installation) { if minor_version_link.exists() { let result = if cfg!(windows) { diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 482a8580c..7885d5437 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -696,7 +696,7 @@ pub(crate) async fn install( }, }; - let environment = installed_tools.create_environment(package_name, interpreter, preview)?; + let environment = installed_tools.create_environment(package_name, interpreter)?; // At this point, we removed any existing environment, so we should remove any of its // executables. diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 11cec23f3..8e843cc41 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -360,7 +360,7 @@ async fn upgrade_tool( ) .await?; - let environment = installed_tools.create_environment(name, interpreter.clone(), preview)?; + let environment = installed_tools.create_environment(name, interpreter.clone())?; let environment = sync_environment( environment, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index aa6f734e4..d38c5851e 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,7 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_install_wheel::LinkMode; use uv_normalize::DefaultGroups; -use uv_preview::{Preview, PreviewFeature}; +use uv_preview::Preview; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; @@ -190,10 +190,9 @@ pub(crate) async fn venv( path.user_display().cyan() )?; - let upgradeable = preview.is_enabled(PreviewFeature::PythonUpgrade) - && python_request - .as_ref() - .is_none_or(|request| !request.includes_patch()); + let upgradeable = python_request + .as_ref() + .is_none_or(|request| !request.includes_patch()); // Create the virtual environment. let venv = uv_virtualenv::create_venv( @@ -205,7 +204,6 @@ pub(crate) async fn venv( relocatable, seed, upgradeable, - preview, ) .map_err(VenvError::Creation)?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 7ddf71901..0a78dab58 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1733,14 +1733,7 @@ async fn run(mut cli: Cli) -> Result { let args = settings::PythonUninstallSettings::resolve(args, filesystem); show_settings!(args); - commands::python_uninstall( - args.install_dir, - args.targets, - args.all, - printer, - globals.preview, - ) - .await + commands::python_uninstall(args.install_dir, args.targets, args.all, printer).await } Commands::Python(PythonNamespace { command: PythonCommand::Find(args), @@ -1755,6 +1748,7 @@ async fn run(mut cli: Cli) -> Result { commands::python_find_script( (&script).into(), args.show_version, + args.resolve_links, // TODO(zsol): is this the right thing to do here? &client_builder.subcommand(vec!["python".to_owned(), "find".to_owned()]), globals.python_preference, @@ -1770,6 +1764,7 @@ async fn run(mut cli: Cli) -> Result { &project_dir, args.request, args.show_version, + args.resolve_links, args.no_project, cli.top_level.no_config, args.system, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 65c7079c7..b7a3caa53 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1457,6 +1457,7 @@ impl PythonUninstallSettings { pub(crate) struct PythonFindSettings { pub(crate) request: Option, pub(crate) show_version: bool, + pub(crate) resolve_links: bool, pub(crate) no_project: bool, pub(crate) system: bool, pub(crate) python_downloads_json_url: Option, @@ -1472,6 +1473,7 @@ impl PythonFindSettings { let PythonFindArgs { request, show_version, + resolve_links, no_project, system, no_system, @@ -1499,6 +1501,7 @@ impl PythonFindSettings { Self { request, show_version, + resolve_links, no_project, system: flag(system, no_system, "system").unwrap_or_default(), python_downloads_json_url, diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 8da198e30..97b05913b 100755 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Output, Stdio}; use std::str::FromStr; use std::{env, io}; +use uv_preview::Preview; use uv_python::downloads::ManagedPythonDownloadList; use assert_cmd::assert::{Assert, OutputAssertExt}; @@ -26,7 +27,6 @@ use tokio::io::AsyncWriteExt; use uv_cache::{Cache, CacheBucket}; use uv_fs::Simplified; -use uv_preview::Preview; use uv_python::managed::ManagedPythonInstallations; use uv_python::{ EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersion, diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index e91882885..cbd961c3d 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -13671,7 +13671,10 @@ fn pip_install_no_sources_editable_to_registry_switch() -> Result<()> { Ok(()) } -#[cfg(feature = "test-python-managed")] +// TODO(zb): On Windows, this test shows the minor version symlink path instead of the +// actual installation path. The `report_target_environment` fix only handles the "Using Python" +// message but not the "externally managed" error path which uses `env.root()` directly. +#[cfg(all(feature = "test-python-managed", not(windows)))] #[test] fn install_with_system_interpreter() { let context = TestContext::new_with_versions(&[]) diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 8eda5baa6..27c972ee7 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -1381,7 +1381,7 @@ fn python_find_prerelease_version_specifiers() { context.python_install().arg("3.14.0rc3").assert().success(); // `>=3.14` should allow pre-release versions - uv_snapshot!(context.filters(), context.python_find().arg(">=3.14"), @" + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1392,7 +1392,7 @@ fn python_find_prerelease_version_specifiers() { "); // `>3.14rc2` should not match rc2 - uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2"), @" + uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1412,7 +1412,7 @@ fn python_find_prerelease_version_specifiers() { "); // `>=3.14.0rc3` should match rc3 - uv_snapshot!(context.filters(), context.python_find().arg(">=3.14.0rc3"), @" + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14.0rc3").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1422,7 +1422,7 @@ fn python_find_prerelease_version_specifiers() { "); // `<3.14.0rc3` should match rc2 - uv_snapshot!(context.filters(), context.python_find().arg("<3.14.0rc3"), @" + uv_snapshot!(context.filters(), context.python_find().arg("<3.14.0rc3").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1432,7 +1432,7 @@ fn python_find_prerelease_version_specifiers() { "); // `<=3.14.0rc3` should match rc3 - uv_snapshot!(context.filters(), context.python_find().arg("<=3.14.0rc3"), @" + uv_snapshot!(context.filters(), context.python_find().arg("<=3.14.0rc3").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1445,7 +1445,7 @@ fn python_find_prerelease_version_specifiers() { context.python_install().arg("3.14.0").assert().success(); // `>=3.14` should prefer stable - uv_snapshot!(context.filters(), context.python_find().arg(">=3.14"), @" + uv_snapshot!(context.filters(), context.python_find().arg(">=3.14").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1455,7 +1455,7 @@ fn python_find_prerelease_version_specifiers() { "); // `>3.14rc2` should prefer stable - uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2"), @" + uv_snapshot!(context.filters(), context.python_find().arg(">3.14.0rc2").arg("--resolve-links"), @" success: true exit_code: 0 ----- stdout ----- @@ -1485,7 +1485,7 @@ fn python_find_prerelease_with_patch_request() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.14.0rc3-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.14-[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. diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 9fceac5f5..f8d68ca92 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -57,7 +57,7 @@ fn python_install() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/bin/python3.14" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/bin/python3.14" ); }); } else if cfg!(windows) { @@ -65,7 +65,7 @@ fn python_install() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/python" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/python" ); }); } @@ -476,7 +476,7 @@ fn python_install_minor() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11.[LATEST]-[PLATFORM]/bin/python3.11" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/bin/python3.11" ); }); } else if cfg!(windows) { @@ -484,7 +484,7 @@ fn python_install_minor() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11.[LATEST]-[PLATFORM]/python" + read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11-[PLATFORM]/python" ); }); } @@ -1332,7 +1332,7 @@ fn python_install_debug() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]-[PLATFORM]/bin/python3.13 + [TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13 ----- stderr ----- "); @@ -1495,7 +1495,7 @@ fn python_install_debug_freethreaded() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]-[PLATFORM]/bin/python3.13 + [TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/bin/python3.13 ----- stderr ----- "); @@ -1504,7 +1504,7 @@ fn python_install_debug_freethreaded() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]+freethreaded-[PLATFORM]/bin/python3.13t + [TEMP_DIR]/managed/cpython-3.13+freethreaded-[PLATFORM]/bin/python3.13t ----- stderr ----- "); @@ -1667,7 +1667,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/bin/python3.14" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/bin/python3.14" ); }); @@ -1675,7 +1675,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_minor_14), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/bin/python3.14" + read_link(&bin_python_minor_14), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/bin/python3.14" ); }); @@ -1683,7 +1683,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/bin/python3.14" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/bin/python3.14" ); }); } else if cfg!(windows) { @@ -1691,7 +1691,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/python" ); }); @@ -1699,7 +1699,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_minor_14), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_minor_14), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/python" ); }); @@ -1707,7 +1707,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/python" ); }); } @@ -1767,7 +1767,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/bin/python3.12" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" ); }); @@ -1775,7 +1775,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/bin/python3.12" + read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" ); }); @@ -1783,7 +1783,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/bin/python3.12" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/bin/python3.12" ); }); } else { @@ -1791,7 +1791,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" ); }); @@ -1799,7 +1799,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" ); }); @@ -1807,7 +1807,7 @@ fn python_install_default() { filters => context.filters(), }, { insta::assert_snapshot!( - read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.[LATEST]-[PLATFORM]/python" + read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/python" ); }); } @@ -2500,7 +2500,7 @@ fn python_find_prerelease() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.15.[LATEST]-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.15-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); @@ -2510,7 +2510,7 @@ fn python_find_prerelease() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.15.[LATEST]-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.15-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); @@ -2519,7 +2519,7 @@ fn python_find_prerelease() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.15.[LATEST]-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.15-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); @@ -2539,7 +2539,7 @@ fn python_find_prerelease() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.13-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); @@ -2800,7 +2800,7 @@ fn python_install_emulated_macos() { "); // It should be discoverable with `uv python find` - uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r" + uv_snapshot!(context.filters(), context.python_find().arg("3.13").arg("--resolve-links"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2815,7 +2815,7 @@ fn python_install_emulated_macos() { exit_code: 0 ----- stdout ----- cpython-3.13.[LATEST]-macos-aarch64-none - cpython-3.13.[LATEST]-macos-x86_64-none managed/cpython-3.13.[LATEST]-macos-x86_64-none/bin/python3.13 + cpython-3.13.[LATEST]-macos-x86_64-none managed/cpython-3.13-macos-x86_64-none/bin/python3.13 ----- stderr ----- "); @@ -2831,7 +2831,7 @@ fn python_install_emulated_macos() { "); // Once we've installed the native version, it should be preferred over x86_64 - uv_snapshot!(context.filters(), context.python_find().arg("3.13"), @r" + uv_snapshot!(context.filters(), context.python_find().arg("3.13").arg("--resolve-links"), @r" success: true exit_code: 0 ----- stdout ----- @@ -2876,7 +2876,7 @@ fn python_install_emulated_windows_x86_on_x64() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]-windows-x86-none/python + [TEMP_DIR]/managed/cpython-3.13-windows-x86-none/python ----- stderr ----- "); @@ -2887,7 +2887,7 @@ fn python_install_emulated_windows_x86_on_x64() { exit_code: 0 ----- stdout ----- cpython-3.13.[LATEST]-windows-x86_64-none - cpython-3.13.[LATEST]-windows-x86-none managed/cpython-3.13.[LATEST]-windows-x86-none/python + cpython-3.13.[LATEST]-windows-x86-none managed/cpython-3.13-windows-x86-none/python ----- stderr ----- "); @@ -2907,7 +2907,7 @@ fn python_install_emulated_windows_x86_on_x64() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.13.[LATEST]-windows-x86_64-none/python + [TEMP_DIR]/managed/cpython-3.13-windows-x86_64-none/python ----- stderr ----- "); @@ -3463,14 +3463,15 @@ fn uninstall_last_patch() { ); #[cfg(windows)] - uv_snapshot!(filters, context.run().arg("python").arg("--version"), @r#" + uv_snapshot!(filters, context.run().arg("python").arg("--version"), @r" success: false - exit_code: 103 + exit_code: 2 ----- stdout ----- ----- stderr ----- - No Python at '"[TEMP_DIR]/managed/cpython-3.10-[PLATFORM]/python' - "# + error: Failed to inspect Python interpreter from active virtual environment at `.venv/[BIN]/python` + Caused by: Python interpreter not found at `[VENV]/[BIN]/python` + " ); } @@ -3630,7 +3631,7 @@ fn python_install_pyodide() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.14.[LATEST]-[PLATFORM]/bin/python3.14 + [TEMP_DIR]/managed/cpython-3.14-[PLATFORM]/bin/python3.14 ----- stderr ----- "); @@ -3687,7 +3688,7 @@ fn python_install_build_version() { success: true exit_code: 0 ----- stdout ----- - [TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + [TEMP_DIR]/managed/cpython-3.12-[PLATFORM]/[INSTALL-BIN]/[PYTHON] ----- stderr ----- "); diff --git a/crates/uv/tests/it/python_list.rs b/crates/uv/tests/it/python_list.rs index bc54ce874..5c4b6216b 100644 --- a/crates/uv/tests/it/python_list.rs +++ b/crates/uv/tests/it/python_list.rs @@ -456,7 +456,7 @@ fn python_list_downloads_installed() { success: true exit_code: 0 ----- stdout ----- - cpython-3.10.19-[PLATFORM] managed/cpython-3.10.19-[PLATFORM]/[INSTALL-BIN]/[PYTHON] + cpython-3.10.19-[PLATFORM] managed/cpython-3.10-[PLATFORM]/[INSTALL-BIN]/[PYTHON] pypy-3.10.16-[PLATFORM] graalpy-3.10.0-[PLATFORM] diff --git a/crates/uv/tests/it/python_upgrade.rs b/crates/uv/tests/it/python_upgrade.rs index f1eba76fc..2ca8419e4 100644 --- a/crates/uv/tests/it/python_upgrade.rs +++ b/crates/uv/tests/it/python_upgrade.rs @@ -16,7 +16,7 @@ fn python_upgrade() { .with_filtered_latest_python_versions(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -27,7 +27,7 @@ fn python_upgrade() { "); // Don't accept patch version as argument to upgrade command - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10.17"), @" success: false exit_code: 1 ----- stdout ----- @@ -37,7 +37,7 @@ fn python_upgrade() { "); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -48,7 +48,7 @@ fn python_upgrade() { "); // Should be a no-op when already upgraded - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -58,7 +58,7 @@ fn python_upgrade() { "); // Should reinstall on `--reinstall` - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10").arg("--reinstall"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10").arg("--reinstall"), @" success: true exit_code: 0 ----- stdout ----- @@ -86,7 +86,6 @@ fn python_upgrade() { ----- stdout ----- ----- stderr ----- - warning: `uv python upgrade` is experimental and may change without warning. Pass `--preview-features python-upgrade` to disable this warning Installed Python 3.14.[LATEST] in [TIME] + cpython-3.14.[LATEST]-[PLATFORM] (python3.14) "); @@ -101,7 +100,7 @@ fn python_upgrade_without_version() { .with_managed_python_dirs(); // Should be a no-op when no versions have been installed - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @" + uv_snapshot!(context.filters(), context.python_upgrade(), @" success: true exit_code: 0 ----- stdout ----- @@ -111,7 +110,7 @@ fn python_upgrade_without_version() { "); // Install earlier patch versions for different minor versions - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.11.8").arg("3.12.8").arg("3.13.1"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.11.8").arg("3.12.8").arg("3.13.1"), @" success: true exit_code: 0 ----- stdout ----- @@ -127,7 +126,7 @@ fn python_upgrade_without_version() { filters.push((r"3.13.\d+", "3.13.[X]")); // Upgrade one patch version - uv_snapshot!(filters, context.python_upgrade().arg("--preview").arg("3.13"), @" + uv_snapshot!(filters, context.python_upgrade().arg("3.13"), @" success: true exit_code: 0 ----- stdout ----- @@ -139,7 +138,7 @@ fn python_upgrade_without_version() { // Providing no minor version to `uv python upgrade` should upgrade the rest // of the patch versions - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @" + uv_snapshot!(context.filters(), context.python_upgrade(), @" success: true exit_code: 0 ----- stdout ----- @@ -151,7 +150,7 @@ fn python_upgrade_without_version() { "); // Should be a no-op when every version is already upgraded - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview"), @" + uv_snapshot!(context.filters(), context.python_upgrade(), @" success: true exit_code: 0 ----- stdout ----- @@ -170,7 +169,7 @@ fn python_upgrade_transparent_from_venv() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -228,7 +227,7 @@ fn python_upgrade_transparent_from_venv() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -262,8 +261,8 @@ fn python_upgrade_transparent_from_venv() { ); } -// Installing Python in preview mode should not prevent virtual environments -// from transparently upgrading. +// Installing Python should not prevent virtual environments from transparently +// upgrading. #[test] fn python_upgrade_transparent_from_venv_preview() { let context: TestContext = TestContext::new_with_versions(&["3.13"]) @@ -272,8 +271,8 @@ fn python_upgrade_transparent_from_venv_preview() { .with_filtered_exe_suffix() .with_managed_python_dirs(); - // Install an earlier patch version using `--preview` - uv_snapshot!(context.filters(), context.python_install().arg("3.10.17").arg("--preview"), @" + // Install an earlier patch version + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -306,7 +305,7 @@ fn python_upgrade_transparent_from_venv_preview() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -337,7 +336,7 @@ fn python_upgrade_ignored_with_python_pin() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -370,7 +369,7 @@ fn python_upgrade_ignored_with_python_pin() { "); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -403,7 +402,7 @@ fn python_no_transparent_upgrade_with_venv_patch_specification() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -436,7 +435,7 @@ fn python_no_transparent_upgrade_with_venv_patch_specification() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -470,7 +469,7 @@ fn python_transparent_upgrade_venv_venv() { .with_managed_python_dirs(); // Install an earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -528,7 +527,7 @@ fn python_transparent_upgrade_venv_venv() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -566,7 +565,7 @@ fn python_upgrade_transparent_from_venv_module() { let bin_dir = context.temp_dir.child("bin"); // Install earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12.9"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.12.9"), @" success: true exit_code: 0 ----- stdout ----- @@ -597,7 +596,7 @@ fn python_upgrade_transparent_from_venv_module() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -634,7 +633,7 @@ fn python_upgrade_transparent_from_venv_module_in_venv() { let bin_dir = context.temp_dir.child("bin"); // Install earlier patch version - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.10.17"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @" success: true exit_code: 0 ----- stdout ----- @@ -683,7 +682,7 @@ fn python_upgrade_transparent_from_venv_module_in_venv() { ); // Upgrade patch version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.10"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.10"), @" success: true exit_code: 0 ----- stdout ----- @@ -725,7 +724,7 @@ fn python_upgrade_force_install() -> Result<()> { .touch()?; // Try to upgrade with a non-managed interpreter installed in `bin`. - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -737,7 +736,7 @@ fn python_upgrade_force_install() -> Result<()> { "); // Force the `bin` install. - uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--force").arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--force").arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -769,7 +768,6 @@ fn python_upgrade_implementation() { ----- stdout ----- ----- stderr ----- - warning: `uv python upgrade` is experimental and may change without warning. Pass `--preview-features python-upgrade` to disable this warning All versions already on latest supported patch release "); } @@ -783,7 +781,7 @@ fn python_upgrade_build_version() { .with_managed_python_dirs(); // Install Python 3.12 - uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_install().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -794,7 +792,7 @@ fn python_upgrade_build_version() { "); // Should be a no-op when already installed at latest version - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -812,7 +810,7 @@ fn python_upgrade_build_version() { fs_err::write(&build_file, "19000101").unwrap(); // Now upgrade should detect the outdated build version and reinstall - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- @@ -823,7 +821,7 @@ fn python_upgrade_build_version() { "); // Should be a no-op again after upgrade - uv_snapshot!(context.filters(), context.python_upgrade().arg("--preview").arg("3.12"), @" + uv_snapshot!(context.filters(), context.python_upgrade().arg("3.12"), @" success: true exit_code: 0 ----- stdout ----- diff --git a/docs/concepts/preview.md b/docs/concepts/preview.md index 1e411670f..183bc5c63 100644 --- a/docs/concepts/preview.md +++ b/docs/concepts/preview.md @@ -51,11 +51,6 @@ can use `uv pip install` with a `pylock.toml` file without additional configurat specifying the `pylock.toml` file indicates you want to use the feature. However, a warning will be displayed that the feature is in preview. The preview feature can be enabled to silence the warning. -Other preview features change behavior without changes to your use of uv. For example, when the -`python-upgrade` feature is enabled, the default behavior of `uv python install` changes to allow uv -to upgrade Python versions transparently. This feature requires enabling the preview flag for proper -usage. - ## Available preview features The following preview features are available: @@ -67,8 +62,6 @@ The following preview features are available: - `pylock`: Allows installing from `pylock.toml` files. - `python-install-default`: Allows [installing `python` and `python3` executables](./python-versions.md#installing-python-executables). -- `python-upgrade`: Allows - [transparent Python version upgrades](./python-versions.md#upgrading-python-versions). - `format`: Allows using `uv format`. - `native-auth`: Enables storage of credentials in a [system-native location](../concepts/authentication/http.md#the-uv-credentials-store). diff --git a/docs/concepts/python-versions.md b/docs/concepts/python-versions.md index 11e58abef..2c73ac12e 100644 --- a/docs/concepts/python-versions.md +++ b/docs/concepts/python-versions.md @@ -158,12 +158,9 @@ $ uv python install 3.12.8 # Updates `python3.12` to point to 3.12.8 !!! important - Support for upgrading Python versions is in _preview_. This means the behavior is experimental - and subject to change. - Upgrades are only supported for uv-managed Python versions. - Upgrades are not currently supported for PyPy and GraalPy. + Upgrades are not currently supported for PyPy, GraalPy, and Pyodide. uv allows transparently upgrading Python versions to the latest patch release, e.g., 3.13.4 to 3.13.5. uv does not allow transparently upgrading across minor Python versions, e.g., 3.12 to 3.13, @@ -187,14 +184,8 @@ $ uv python upgrade After an upgrade, uv will prefer the new version, but will retain the existing version as it may still be used by virtual environments. -If the Python version was installed with the `python-upgrade` [preview feature](./preview.md) -enabled, e.g., `uv python install 3.12 --preview-features python-upgrade`, virtual environments -using the Python version will be automatically upgraded to the new patch version. - -!!! note - - If the virtual environment was created _before_ opting in to the preview mode, it will not be - included in the automatic upgrades. +Virtual environments using the Python version will be automatically upgraded to the new patch +version. If a virtual environment was created with an explicitly requested patch version, e.g., `uv venv -p 3.10.8`, it will not be transparently upgraded to a new version.