Add support for --upgrade in uv python install (#16676)
Some checks are pending
zizmor / Run zizmor (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | windows python install manager (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 10 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions

This allows us to suggest `uv python install --upgrade 3.14` as the
canonical way to get the latest patch version of a given Python
regardless of whether it is installed already. Currently, you can do `uv
python upgrade 3.14` and it will install it, but I'd like to remove that
behavior as I find it very surprising.
This commit is contained in:
Zanie Blue 2025-11-13 09:55:48 -06:00 committed by GitHub
parent e28dc62358
commit f5ce5b47c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 341 additions and 49 deletions

View file

@ -5868,6 +5868,19 @@ pub struct PythonInstallArgs {
#[arg(long, short)] #[arg(long, short)]
pub force: bool, pub force: bool,
/// Upgrade existing Python installations to the latest patch version.
///
/// By default, uv will not upgrade already-installed Python versions to newer patch releases.
/// With `--upgrade`, uv will upgrade to the latest available patch version for the specified
/// minor version(s).
///
/// If the requested versions are not yet installed, uv will install them.
///
/// This option is only supported for minor version requests, e.g., `3.12`; uv will exit with an
/// error if a patch version, e.g., `3.12.2`, is requested.
#[arg(long, short = 'U')]
pub upgrade: bool,
/// Use as the default Python version. /// Use as the default Python version.
/// ///
/// By default, only a `python{major}.{minor}` executable is installed, e.g., `python3.10`. When /// By default, only a `python{major}.{minor}` executable is installed, e.g., `python3.10`. When

View file

@ -43,6 +43,7 @@ pub(crate) use python::dir::dir as python_dir;
pub(crate) use python::find::find as python_find; pub(crate) use python::find::find as python_find;
pub(crate) use python::find::find_script as python_find_script; pub(crate) use python::find::find_script as python_find_script;
pub(crate) use python::install::install as python_install; pub(crate) use python::install::install as python_install;
pub(crate) use python::install::{PythonUpgrade, PythonUpgradeSource};
pub(crate) use python::list::list as python_list; pub(crate) use python::list::list as python_list;
pub(crate) use python::pin::pin as python_pin; pub(crate) use python::pin::pin as python_pin;
pub(crate) use python::uninstall::uninstall as python_uninstall; pub(crate) use python::uninstall::uninstall as python_uninstall;

View file

@ -149,6 +149,31 @@ enum InstallErrorKind {
Registry, Registry,
} }
#[derive(Debug, Clone, Copy)]
pub(crate) enum PythonUpgradeSource {
/// The user invoked `uv python install --upgrade`
Install,
/// The user invoked `uv python upgrade`
Upgrade,
}
impl std::fmt::Display for PythonUpgradeSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Install => write!(f, "uv python install --upgrade"),
Self::Upgrade => write!(f, "uv python upgrade"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum PythonUpgrade {
/// Python upgrades are enabled.
Enabled(PythonUpgradeSource),
/// Python upgrades are disabled.
Disabled,
}
/// Download and install Python versions. /// Download and install Python versions.
#[allow(clippy::fn_params_excessive_bools)] #[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn install( pub(crate) async fn install(
@ -156,7 +181,7 @@ pub(crate) async fn install(
install_dir: Option<PathBuf>, install_dir: Option<PathBuf>,
targets: Vec<String>, targets: Vec<String>,
reinstall: bool, reinstall: bool,
upgrade: bool, upgrade: PythonUpgrade,
bin: Option<bool>, bin: Option<bool>,
registry: Option<bool>, registry: Option<bool>,
force: bool, force: bool,
@ -183,11 +208,13 @@ pub(crate) async fn install(
); );
} }
if upgrade && !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) { if let PythonUpgrade::Enabled(source @ PythonUpgradeSource::Upgrade) = upgrade {
warn_user!( if !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) {
"`uv python upgrade` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning", warn_user!(
PreviewFeatures::PYTHON_UPGRADE "`{source}` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning",
); PreviewFeatures::PYTHON_UPGRADE
);
}
} }
if default && targets.len() > 1 { if default && targets.len() > 1 {
@ -207,8 +234,14 @@ pub(crate) async fn install(
// Resolve the requests // Resolve the requests
let mut is_default_install = false; let mut is_default_install = false;
let mut is_unspecified_upgrade = false; let mut is_unspecified_upgrade = false;
// TODO(zanieb): We use this variable to special-case .python-version files, but it'd be nice to
// have generalized request source tracking instead
let mut is_from_python_version_file = false;
let requests: Vec<_> = if targets.is_empty() { let requests: Vec<_> = if targets.is_empty() {
if upgrade { if matches!(
upgrade,
PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade)
) {
is_unspecified_upgrade = true; is_unspecified_upgrade = true;
// On upgrade, derive requests for all of the existing installations // On upgrade, derive requests for all of the existing installations
let mut minor_version_requests = IndexSet::<InstallRequest>::default(); let mut minor_version_requests = IndexSet::<InstallRequest>::default();
@ -240,6 +273,7 @@ pub(crate) async fn install(
); );
}) })
.map(PythonVersionFile::into_versions) .map(PythonVersionFile::into_versions)
.inspect(|_| is_from_python_version_file = true)
.unwrap_or_else(|| { .unwrap_or_else(|| {
// If no version file is found and no requests were made // If no version file is found and no requests were made
// TODO(zanieb): We should consider differentiating between a global Python version // TODO(zanieb): We should consider differentiating between a global Python version
@ -265,11 +299,20 @@ pub(crate) async fn install(
}; };
if requests.is_empty() { if requests.is_empty() {
if upgrade { match upgrade {
writeln!( PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade) => {
printer.stderr(), writeln!(
"There are no installed versions to upgrade" printer.stderr(),
)?; "There are no installed versions to upgrade"
)?;
}
PythonUpgrade::Enabled(PythonUpgradeSource::Install) => {
writeln!(
printer.stderr(),
"No Python versions specified for upgrade; did you mean `uv python upgrade`?"
)?;
}
PythonUpgrade::Disabled => {}
} }
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
@ -287,17 +330,25 @@ pub(crate) async fn install(
}) })
.collect::<IndexSet<_>>(); .collect::<IndexSet<_>>();
if upgrade if let PythonUpgrade::Enabled(source) = upgrade {
&& let Some(request) = requests.iter().find(|request| { if let Some(request) = requests.iter().find(|request| {
request.request.includes_patch() || request.request.includes_prerelease() request.request.includes_patch() || request.request.includes_prerelease()
}) }) {
{ writeln!(
writeln!( printer.stderr(),
printer.stderr(), "error: `{source}` only accepts minor versions, got: {}",
"error: `uv python upgrade` only accepts minor versions, got: {}", request.request.to_canonical_string()
request.request.to_canonical_string() )?;
)?; if is_from_python_version_file {
return Ok(ExitStatus::Failure); writeln!(
printer.stderr(),
"\n{}{} The version request came from a `.python-version` file; change the patch version in the file to upgrade instead",
"hint".bold().cyan(),
":".bold(),
)?;
}
return Ok(ExitStatus::Failure);
}
} }
// Find requests that are already satisfied // Find requests that are already satisfied
@ -361,10 +412,10 @@ pub(crate) async fn install(
// If we can find one existing installation that matches the request, it is satisfied // If we can find one existing installation that matches the request, it is satisfied
requests.iter().partition_map(|request| { requests.iter().partition_map(|request| {
if let Some(installation) = existing_installations.iter().find(|installation| { if let Some(installation) = existing_installations.iter().find(|installation| {
if upgrade { if matches!(upgrade, PythonUpgrade::Enabled(_)) {
// If this is an upgrade, the requested version is a minor version // If this is an upgrade, the requested version is a minor version but the
// but the requested download is the highest patch for that minor // requested download is the highest patch for that minor version. We need to
// version. We need to install it unless an exact match is found. // install it unless an exact match is found.
request.download.key() == installation.key() request.download.key() == installation.key()
} else { } else {
request.matches_installation(installation) request.matches_installation(installation)
@ -498,7 +549,10 @@ pub(crate) async fn install(
force, force,
default, default,
upgradeable, upgradeable,
upgrade, matches!(
upgrade,
PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade)
),
is_default_install, is_default_install,
&existing_installations, &existing_installations,
&installations, &installations,
@ -534,7 +588,10 @@ pub(crate) async fn install(
); );
for installation in minor_versions.values() { for installation in minor_versions.values() {
if upgrade { if matches!(
upgrade,
PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade)
) {
// During an upgrade, update existing symlinks but avoid // During an upgrade, update existing symlinks but avoid
// creating new ones. // creating new ones.
installation.update_minor_version_link(preview)?; installation.update_minor_version_link(preview)?;
@ -545,24 +602,38 @@ pub(crate) async fn install(
if changelog.installed.is_empty() && errors.is_empty() { if changelog.installed.is_empty() && errors.is_empty() {
if is_default_install { if is_default_install {
writeln!( if matches!(
printer.stderr(), upgrade,
"Python is already installed. Use `uv python install <request>` to install another version.", PythonUpgrade::Enabled(PythonUpgradeSource::Install)
)?; ) {
} else if upgrade && requests.is_empty() { writeln!(
printer.stderr(),
"The default Python installation is already on the latest supported patch release. Use `uv python install <request>` to install another version.",
)?;
} else {
writeln!(
printer.stderr(),
"Python is already installed. Use `uv python install <request>` to install another version.",
)?;
}
} else if matches!(
upgrade,
PythonUpgrade::Enabled(PythonUpgradeSource::Upgrade)
) && requests.is_empty()
{
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"There are no installed versions to upgrade" "There are no installed versions to upgrade"
)?; )?;
} else if upgrade && is_unspecified_upgrade {
writeln!(
printer.stderr(),
"All versions already on latest supported patch release"
)?;
} else if let [request] = requests.as_slice() { } else if let [request] = requests.as_slice() {
// Convert to the inner request // Convert to the inner request
let request = &request.request; let request = &request.request;
if upgrade { if is_unspecified_upgrade {
writeln!(
printer.stderr(),
"All versions already on latest supported patch release"
)?;
} else if matches!(upgrade, PythonUpgrade::Enabled(_)) {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{request} is already on the latest supported patch release" "{request} is already on the latest supported patch release"
@ -571,11 +642,18 @@ pub(crate) async fn install(
writeln!(printer.stderr(), "{request} is already installed")?; writeln!(printer.stderr(), "{request} is already installed")?;
} }
} else { } else {
if upgrade { if matches!(upgrade, PythonUpgrade::Enabled(_)) {
writeln!( if is_unspecified_upgrade {
printer.stderr(), writeln!(
"All requested versions already on latest supported patch release" printer.stderr(),
)?; "All versions already on latest supported patch release"
)?;
} else {
writeln!(
printer.stderr(),
"All requested versions already on latest supported patch release"
)?;
}
} else { } else {
writeln!(printer.stderr(), "All requested versions already installed")?; writeln!(printer.stderr(), "All requested versions already installed")?;
} }

View file

@ -1543,15 +1543,13 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration. // Resolve the settings from the command-line arguments and workspace configuration.
let args = settings::PythonInstallSettings::resolve(args, filesystem, environment); let args = settings::PythonInstallSettings::resolve(args, filesystem, environment);
show_settings!(args); show_settings!(args);
// TODO(john): If we later want to support `--upgrade`, we need to replace this.
let upgrade = false;
commands::python_install( commands::python_install(
&project_dir, &project_dir,
args.install_dir, args.install_dir,
args.targets, args.targets,
args.reinstall, args.reinstall,
upgrade, args.upgrade,
args.bin, args.bin,
args.registry, args.registry,
args.force, args.force,
@ -1573,7 +1571,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the settings from the command-line arguments and workspace configuration. // Resolve the settings from the command-line arguments and workspace configuration.
let args = settings::PythonUpgradeSettings::resolve(args, filesystem, environment); let args = settings::PythonUpgradeSettings::resolve(args, filesystem, environment);
show_settings!(args); show_settings!(args);
let upgrade = true; let upgrade = commands::PythonUpgrade::Enabled(commands::PythonUpgradeSource::Upgrade);
commands::python_install( commands::python_install(
&project_dir, &project_dir,

View file

@ -5,6 +5,7 @@ use std::process;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use crate::commands::{PythonUpgrade, PythonUpgradeSource};
use uv_auth::Service; use uv_auth::Service;
use uv_cache::{CacheArgs, Refresh}; use uv_cache::{CacheArgs, Refresh};
use uv_cli::comma::CommaSeparatedRequirements; use uv_cli::comma::CommaSeparatedRequirements;
@ -1061,6 +1062,7 @@ pub(crate) struct PythonInstallSettings {
pub(crate) targets: Vec<String>, pub(crate) targets: Vec<String>,
pub(crate) reinstall: bool, pub(crate) reinstall: bool,
pub(crate) force: bool, pub(crate) force: bool,
pub(crate) upgrade: PythonUpgrade,
pub(crate) bin: Option<bool>, pub(crate) bin: Option<bool>,
pub(crate) registry: Option<bool>, pub(crate) registry: Option<bool>,
pub(crate) python_install_mirror: Option<String>, pub(crate) python_install_mirror: Option<String>,
@ -1101,6 +1103,7 @@ impl PythonInstallSettings {
registry, registry,
no_registry, no_registry,
force, force,
upgrade,
mirror: _, mirror: _,
pypy_mirror: _, pypy_mirror: _,
python_downloads_json_url: _, python_downloads_json_url: _,
@ -1112,6 +1115,11 @@ impl PythonInstallSettings {
targets, targets,
reinstall, reinstall,
force, force,
upgrade: if upgrade {
PythonUpgrade::Enabled(PythonUpgradeSource::Install)
} else {
PythonUpgrade::Disabled
},
bin: flag(bin, no_bin, "bin").or(environment.python_install_bin), bin: flag(bin, no_bin, "bin").or(environment.python_install_bin),
registry: flag(registry, no_registry, "registry") registry: flag(registry, no_registry, "registry")
.or(environment.python_install_registry), .or(environment.python_install_registry),

View file

@ -557,6 +557,18 @@ fn help_subsubcommand() {
Implies `--reinstall`. Implies `--reinstall`.
-U, --upgrade
Upgrade existing Python installations to the latest patch version.
By default, uv will not upgrade already-installed Python versions to newer patch releases.
With `--upgrade`, uv will upgrade to the latest available patch version for the specified
minor version(s).
If the requested versions are not yet installed, uv will install them.
This option is only supported for minor version requests, e.g., `3.12`; uv will exit with
an error if a patch version, e.g., `3.12.2`, is requested.
--default --default
Use as the default Python version. Use as the default Python version.
@ -819,6 +831,8 @@ fn help_flag_subsubcommand() {
Reinstall the requested Python version, if it's already installed Reinstall the requested Python version, if it's already installed
-f, --force -f, --force
Replace existing Python executables during installation Replace existing Python executables during installation
-U, --upgrade
Upgrade existing Python installations to the latest patch version
--default --default
Use as the default Python version Use as the default Python version

View file

@ -1379,7 +1379,8 @@ fn python_install_debug_freethreaded() {
let context: TestContext = TestContext::new_with_versions(&[]) let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys() .with_filtered_python_keys()
.with_filtered_exe_suffix() .with_filtered_exe_suffix()
.with_managed_python_dirs(); .with_managed_python_dirs()
.with_python_download_cache();
// Install the latest version // Install the latest version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13td"), @r" uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13td"), @r"
@ -2748,6 +2749,7 @@ fn python_install_emulated_macos() {
if !arch_status.is_ok_and(|x| x.success()) { if !arch_status.is_ok_and(|x| x.success()) {
// Rosetta is not available to run the x86_64 interpreter // Rosetta is not available to run the x86_64 interpreter
// fail the test in CI, otherwise skip it // fail the test in CI, otherwise skip it
#[allow(clippy::manual_assert)]
if env::var("CI").is_ok() { if env::var("CI").is_ok() {
panic!("x86_64 emulation is not available on this CI runner"); panic!("x86_64 emulation is not available on this CI runner");
} }
@ -3774,3 +3776,177 @@ fn python_install_build_version_pypy() {
error: No download found for request: pypy-3.10-[PLATFORM] error: No download found for request: pypy-3.10-[PLATFORM]
"); ");
} }
#[test]
fn python_install_upgrade() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_python_download_cache()
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Provide `--upgrade` as an `install` option without any versions
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.14.0 in [TIME]
+ cpython-3.14.0-[PLATFORM] (python3.14)
");
// Provide `--upgrade` as an `install` option without any versions again!
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
The default Python installation is already on the latest supported patch release. Use `uv python install <request>` to install another version.
");
// Install an earlier patch version
uv_snapshot!(context.filters(), context.python_install().arg("3.10.17"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.10.17 in [TIME]
+ cpython-3.10.17-[PLATFORM] (python3.10)
");
// Ask for an `--upgrade`
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.10"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.10.19 in [TIME]
+ cpython-3.10.19-[PLATFORM] (python3.10)
");
// Request a patch version with `--upgrade`
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.11.4"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: `uv python install --upgrade` only accepts minor versions, got: 3.11.4
");
// Request a version that isn't installed yet
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.11.14 in [TIME]
+ cpython-3.11.14-[PLATFORM] (python3.11)
");
// Ask for it again
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Python 3.11 is already on the latest supported patch release
");
// Install an outdated version
uv_snapshot!(context.filters(), context.python_install().arg("3.9.5"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.9.5 in [TIME]
+ cpython-3.9.5-[PLATFORM] (python3.9)
");
// We shouldn't update it when not relevant
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Python 3.11 is already on the latest supported patch release
");
// Ask for multiple already satisfied versions
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.10").arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
All requested versions already on latest supported patch release
");
// Mix in an unsatisfied version and a missing one
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade").arg("3.9").arg("3.10").arg("3.11").arg("3.12"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 2 versions in [TIME]
+ cpython-3.9.25-[PLATFORM] (python3.9)
+ cpython-3.12.12-[PLATFORM] (python3.12)
");
}
#[test]
fn python_install_upgrade_version_file() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_python_download_cache()
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Pin to a minor version
context.python_pin().arg("3.13").assert().success();
// Provide `--upgrade` as an `install` option without any versions
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.9 in [TIME]
+ cpython-3.13.9-[PLATFORM] (python3.13)
");
// Provide `--upgrade` as an `install` option without any versions again!
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Python 3.13 is already on the latest supported patch release
");
// Pin to a patch version
context.python_pin().arg("3.12.4").assert().success();
// Provide `--upgrade` as an `install` option without any versions
uv_snapshot!(context.filters(), context.python_install().arg("--upgrade"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: `uv python install --upgrade` only accepts minor versions, got: 3.12.4
hint: The version request came from a `.python-version` file; change the patch version in the file to upgrade instead
");
}

View file

@ -3525,6 +3525,10 @@ uv python install [OPTIONS] [TARGETS]...
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p> <p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
</dd><dt id="uv-python-install--reinstall"><a href="#uv-python-install--reinstall"><code>--reinstall</code></a>, <code>-r</code></dt><dd><p>Reinstall the requested Python version, if it's already installed.</p> </dd><dt id="uv-python-install--reinstall"><a href="#uv-python-install--reinstall"><code>--reinstall</code></a>, <code>-r</code></dt><dd><p>Reinstall the requested Python version, if it's already installed.</p>
<p>By default, uv will exit successfully if the version is already installed.</p> <p>By default, uv will exit successfully if the version is already installed.</p>
</dd><dt id="uv-python-install--upgrade"><a href="#uv-python-install--upgrade"><code>--upgrade</code></a>, <code>-U</code></dt><dd><p>Upgrade existing Python installations to the latest patch version.</p>
<p>By default, uv will not upgrade already-installed Python versions to newer patch releases. With <code>--upgrade</code>, uv will upgrade to the latest available patch version for the specified minor version(s).</p>
<p>If the requested versions are not yet installed, uv will install them.</p>
<p>This option is only supported for minor version requests, e.g., <code>3.12</code>; uv will exit with an error if a patch version, e.g., <code>3.12.2</code>, is requested.</p>
</dd><dt id="uv-python-install--verbose"><a href="#uv-python-install--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p> </dd><dt id="uv-python-install--verbose"><a href="#uv-python-install--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p> <p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl> </dd></dl>