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
This commit is contained in:
Zanie Blue
2026-02-05 11:26:54 -06:00
parent f3d50129c6
commit d2ab2d0208
35 changed files with 221 additions and 239 deletions
Generated
+1 -3
View File
@@ -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",
-1
View File
@@ -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 }
-3
View File
@@ -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<Self, Error> {
let temp_dir = build_context.cache().venv_dir()?;
@@ -365,7 +363,6 @@ impl SourceBuild {
false,
false,
false,
preview,
)?
};
+6
View File
@@ -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<String>,
+1 -1
View File
@@ -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 }
-1
View File
@@ -491,7 +491,6 @@ impl BuildContext for BuildDispatch<'_> {
build_output,
self.concurrency.builds,
self.client.credentials_cache(),
self.preview,
)
.boxed_local()
.await?;
+1
View File
@@ -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 }
+1 -4
View File
@@ -356,7 +356,6 @@ fn python_executables_from_installed<'a>(
implementation: Option<&'a ImplementationName>,
platform: PlatformRequest,
preference: PythonPreference,
preview: Preview,
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + '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),
+1 -3
View File
@@ -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<Self, Error> {
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() {
+68 -42
View File
@@ -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<Self, Error> {
pub(crate) fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
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<Self> {
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<Self> {
fn from_executable(executable: &Path, key: &PythonInstallationKey) -> Option<Self> {
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> {
Self::from_executable(
installation.executable(false).as_path(),
installation.key(),
preview,
)
pub fn from_installation(installation: &ManagedPythonInstallation) -> Option<Self> {
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<PathBuf> {
#[cfg(unix)]
{
self.symlink_directory.read_link().ok()
}
#[cfg(windows)]
{
junction::get_target(&self.symlink_directory).ok()
}
}
}
-1
View File
@@ -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 }
-3
View File
@@ -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<PythonEnvironment, Error> {
let environment_path = self.tool_dir(name);
@@ -342,7 +340,6 @@ impl InstalledTools {
false,
false,
false,
preview,
)?;
Ok(venv)
-1
View File
@@ -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 }
-3
View File
@@ -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<PythonEnvironment, Error> {
// 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`.
+9 -10
View File
@@ -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<VirtualEnvironment, Error> {
// 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
+16 -2
View File
@@ -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(());
};
@@ -191,7 +191,6 @@ impl CachedEnvironment {
true,
false,
false,
preview,
)?;
sync_environment(
-4
View File
@@ -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 {
-4
View File
@@ -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()?
+14 -10
View File
@@ -28,6 +28,7 @@ pub(crate) async fn find(
project_dir: &Path,
request: Option<String>,
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)
+2 -20
View File
@@ -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 {
+3 -6
View File
@@ -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<String>,
all: bool,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
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<String>,
all: bool,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
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) {
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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,
+4 -6
View File
@@ -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)?;
+3 -8
View File
@@ -1733,14 +1733,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
&project_dir,
args.request,
args.show_version,
args.resolve_links,
args.no_project,
cli.top_level.no_config,
args.system,
+3
View File
@@ -1457,6 +1457,7 @@ impl PythonUninstallSettings {
pub(crate) struct PythonFindSettings {
pub(crate) request: Option<String>,
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<String>,
@@ -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,
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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(&[])
+8 -8
View File
@@ -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.
+36 -35
View File
@@ -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 <download available>
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 <download available>
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 -----
");
+1 -1
View File
@@ -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] <download available>
graalpy-3.10.0-[PLATFORM] <download available>
+33 -35
View File
@@ -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 -----
-7
View File
@@ -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).
+3 -12
View File
@@ -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.