Stabilize addition of Python executables to the bin (#14626)

Closes https://github.com/astral-sh/uv/issues/14296

As mentioned in #14681, this does not stabilize the `--default`
behavior.
This commit is contained in:
Zanie Blue 2025-07-17 11:09:13 -05:00
parent ff30f14d50
commit 0077f2357f
8 changed files with 593 additions and 113 deletions

View file

@ -4810,10 +4810,9 @@ pub enum PythonCommand {
/// Python versions are installed into the uv Python directory, which can be retrieved with `uv /// Python versions are installed into the uv Python directory, which can be retrieved with `uv
/// python dir`. /// python dir`.
/// ///
/// A `python` executable is not made globally available, managed Python versions are only used /// By default, Python executables are added to a directory on the path with a minor version
/// in uv commands or in active virtual environments. There is experimental support for adding /// suffix, e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use
/// Python executables to a directory on the path — use the `--preview` flag to enable this /// `uv python dir --bin` to see the target directory.
/// behavior and `uv python dir --bin` to retrieve the target directory.
/// ///
/// Multiple Python versions may be requested. /// Multiple Python versions may be requested.
/// ///

View file

@ -166,12 +166,14 @@ pub(crate) async fn install(
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
// TODO(zanieb): We should consider marking the Python installation as the default when
// `--default` is used. It's not clear how this overlaps with a global Python pin, but I'd be
// surprised if `uv python find` returned the "newest" Python version rather than the one I just
// installed with the `--default` flag.
if default && !preview.is_enabled() { if default && !preview.is_enabled() {
writeln!( warn_user!(
printer.stderr(), "The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning"
"The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default`" );
)?;
return Ok(ExitStatus::Failure);
} }
if upgrade && preview.is_disabled() { if upgrade && preview.is_disabled() {
@ -222,6 +224,8 @@ pub(crate) async fn install(
.map(PythonVersionFile::into_versions) .map(PythonVersionFile::into_versions)
.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
// file here, allowing a request from there to enable `is_default_install`.
is_default_install = true; is_default_install = true;
vec![if reinstall { vec![if reinstall {
// On bare `--reinstall`, reinstall all Python versions // On bare `--reinstall`, reinstall all Python versions
@ -451,10 +455,10 @@ pub(crate) async fn install(
} }
} }
let bin_dir = if matches!(bin, Some(true)) || preview.is_enabled() { let bin_dir = if matches!(bin, Some(false)) {
Some(python_executable_dir()?)
} else {
None None
} else {
Some(python_executable_dir()?)
}; };
let installations: Vec<_> = downloaded.iter().chain(satisfied.iter().copied()).collect(); let installations: Vec<_> = downloaded.iter().chain(satisfied.iter().copied()).collect();
@ -469,20 +473,10 @@ pub(crate) async fn install(
e.warn_user(installation); e.warn_user(installation);
} }
if preview.is_disabled() {
debug!("Skipping installation of Python executables, use `--preview` to enable.");
continue;
}
let bin_dir = bin_dir
.as_ref()
.expect("We should have a bin directory with preview enabled")
.as_path();
let upgradeable = (default || is_default_install) let upgradeable = (default || is_default_install)
|| requested_minor_versions.contains(&installation.key().version().python_version()); || requested_minor_versions.contains(&installation.key().version().python_version());
if !matches!(bin, Some(false)) { if let Some(bin_dir) = bin_dir.as_ref() {
create_bin_links( create_bin_links(
installation, installation,
bin_dir, bin_dir,
@ -661,11 +655,7 @@ pub(crate) async fn install(
} }
} }
if preview.is_enabled() && !matches!(bin, Some(false)) { if let Some(bin_dir) = bin_dir.as_ref() {
let bin_dir = bin_dir
.as_ref()
.expect("We should have a bin directory with preview enabled")
.as_path();
warn_if_not_on_path(bin_dir); warn_if_not_on_path(bin_dir);
} }
} }
@ -749,8 +739,12 @@ fn create_bin_links(
errors: &mut Vec<(InstallErrorKind, PythonInstallationKey, Error)>, errors: &mut Vec<(InstallErrorKind, PythonInstallationKey, Error)>,
preview: PreviewMode, preview: PreviewMode,
) { ) {
let targets = // TODO(zanieb): We want more feedback on the `is_default_install` behavior before stabilizing
if (default || is_default_install) && first_request.matches_installation(installation) { // it. In particular, it may be confusing because it does not apply when versions are loaded
// from a `.python-version` file.
let targets = if (default || (is_default_install && preview.is_enabled()))
&& first_request.matches_installation(installation)
{
vec![ vec![
installation.key().executable_name_minor(), installation.key().executable_name_minor(),
installation.key().executable_name_major(), installation.key().executable_name_major(),

View file

@ -220,17 +220,30 @@ impl TestContext {
/// and `.exe` suffixes. /// and `.exe` suffixes.
#[must_use] #[must_use]
pub fn with_filtered_python_names(mut self) -> Self { pub fn with_filtered_python_names(mut self) -> Self {
use env::consts::EXE_SUFFIX;
let exe_suffix = regex::escape(EXE_SUFFIX);
self.filters.push((
format!(r"python\d.\d\d{exe_suffix}"),
"[PYTHON]".to_string(),
));
self.filters
.push((format!(r"python\d{exe_suffix}"), "[PYTHON]".to_string()));
if cfg!(windows) { if cfg!(windows) {
// On Windows, we want to filter out all `python.exe` instances
self.filters self.filters
.push((r"python\.exe".to_string(), "[PYTHON]".to_string())); .push((format!(r"python{exe_suffix}"), "[PYTHON]".to_string()));
// Including ones where we'd already stripped the `.exe` in another filter
self.filters
.push((r"[\\/]python".to_string(), "/[PYTHON]".to_string()));
} else { } else {
// On Unix, it's a little trickier — we don't want to clobber use of `python` in the
// middle of something else, e.g., `cpython`. For this reason, we require a leading `/`.
self.filters self.filters
.push((r"python\d.\d\d".to_string(), "[PYTHON]".to_string())); .push((format!(r"/python{exe_suffix}"), "/[PYTHON]".to_string()));
self.filters
.push((r"python\d".to_string(), "[PYTHON]".to_string()));
self.filters
.push((r"/python".to_string(), "/[PYTHON]".to_string()));
} }
self self
} }

View file

@ -469,10 +469,9 @@ fn help_subsubcommand() {
Python versions are installed into the uv Python directory, which can be retrieved with `uv python Python versions are installed into the uv Python directory, which can be retrieved with `uv python
dir`. dir`.
A `python` executable is not made globally available, managed Python versions are only used in uv By default, Python executables are added to a directory on the path with a minor version suffix,
commands or in active virtual environments. There is experimental support for adding Python e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use `uv python dir
executables to a directory on the path use the `--preview` flag to enable this behavior and `uv --bin` to see the target directory.
python dir --bin` to retrieve the target directory.
Multiple Python versions may be requested. Multiple Python versions may be requested.

View file

@ -30,15 +30,49 @@ fn python_install() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
let bin_python = context let bin_python = context
.bin_dir .bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX)); .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
// The executable should not be installed in the bin directory (requires preview) // The executable should be installed in the bin directory
bin_python.assert(predicate::path::missing()); bin_python.assert(predicate::path::exists());
// On Unix, it should be a link
#[cfg(unix)]
bin_python.assert(predicate::path::is_symlink());
// The link should be a path to the binary
if cfg!(unix) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13"
);
});
} else if cfg!(windows) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python"
);
});
}
// The executable should "work"
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
success: true
exit_code: 0
----- stdout -----
hello world
----- stderr -----
"###);
// Should be a no-op when already installed // Should be a no-op when already installed
uv_snapshot!(context.filters(), context.python_install(), @r###" uv_snapshot!(context.filters(), context.python_install(), @r###"
@ -67,9 +101,12 @@ fn python_install() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
~ cpython-3.13.5-[PLATFORM] ~ cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// The executable should still be present in the bin directory
bin_python.assert(predicate::path::exists());
// Uninstallation requires an argument // Uninstallation requires an argument
uv_snapshot!(context.filters(), context.python_uninstall(), @r###" uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
success: false success: false
@ -93,8 +130,11 @@ fn python_install() {
----- stderr ----- ----- stderr -----
Searching for Python versions matching: Python 3.13 Searching for Python versions matching: Python 3.13
Uninstalled Python 3.13.5 in [TIME] Uninstalled Python 3.13.5 in [TIME]
- cpython-3.13.5-[PLATFORM] - cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// The executable should be removed
bin_python.assert(predicate::path::missing());
} }
#[test] #[test]
@ -112,8 +152,8 @@ fn python_reinstall() {
----- stderr ----- ----- stderr -----
Installed 2 versions in [TIME] Installed 2 versions in [TIME]
+ cpython-3.12.11-[PLATFORM] + cpython-3.12.11-[PLATFORM] (python3.12)
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// Reinstall a single version // Reinstall a single version
@ -124,7 +164,7 @@ fn python_reinstall() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
~ cpython-3.13.5-[PLATFORM] ~ cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// Reinstall multiple versions // Reinstall multiple versions
@ -135,8 +175,8 @@ fn python_reinstall() {
----- stderr ----- ----- stderr -----
Installed 2 versions in [TIME] Installed 2 versions in [TIME]
~ cpython-3.12.11-[PLATFORM] ~ cpython-3.12.11-[PLATFORM] (python3.12)
~ cpython-3.13.5-[PLATFORM] ~ cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// Reinstalling a version that is not installed should also work // Reinstalling a version that is not installed should also work
@ -147,7 +187,7 @@ fn python_reinstall() {
----- stderr ----- ----- stderr -----
Installed Python 3.11.13 in [TIME] Installed Python 3.11.13 in [TIME]
+ cpython-3.11.13-[PLATFORM] + cpython-3.11.13-[PLATFORM] (python3.11)
"); ");
} }
@ -167,7 +207,7 @@ fn python_reinstall_patch() {
----- stderr ----- ----- stderr -----
Installed 2 versions in [TIME] Installed 2 versions in [TIME]
+ cpython-3.12.6-[PLATFORM] + cpython-3.12.6-[PLATFORM]
+ cpython-3.12.7-[PLATFORM] + cpython-3.12.7-[PLATFORM] (python3.12)
"); ");
// Reinstall all "3.12" versions // Reinstall all "3.12" versions
@ -180,7 +220,7 @@ fn python_reinstall_patch() {
----- stderr ----- ----- stderr -----
Installed Python 3.12.11 in [TIME] Installed Python 3.12.11 in [TIME]
+ cpython-3.12.11-[PLATFORM] + cpython-3.12.11-[PLATFORM] (python3.12)
"); ");
} }
@ -328,6 +368,208 @@ fn regression_cpython() {
"###); "###);
} }
#[test]
fn python_install_force() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install the latest version
uv_snapshot!(context.filters(), context.python_install(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] (python3.13)
");
let bin_python = context
.bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
// You can force replacement of the executables
uv_snapshot!(context.filters(), context.python_install().arg("--force"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] (python3.13)
");
// The executable should still be present in the bin directory
bin_python.assert(predicate::path::exists());
// If an unmanaged executable is present, `--force` is required
fs_err::remove_file(bin_python.path()).unwrap();
bin_python.touch().unwrap();
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Failed to install executable for cpython-3.13.5-[PLATFORM]
Caused by: Executable already exists at `[BIN]/python3.13` but is not managed by uv; use `--force` to replace it
");
uv_snapshot!(context.filters(), context.python_install().arg("--force").arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] (python3.13)
");
bin_python.assert(predicate::path::exists());
}
#[test]
fn python_install_minor() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install a minor version
uv_snapshot!(context.filters(), context.python_install().arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed Python 3.11.13 in [TIME]
+ cpython-3.11.13-[PLATFORM] (python3.11)
");
let bin_python = context
.bin_dir
.child(format!("python3.11{}", std::env::consts::EXE_SUFFIX));
// The executable should be installed in the bin directory
bin_python.assert(predicate::path::exists());
// It should be a link to the minor version
if cfg!(unix) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11.13-[PLATFORM]/bin/python3.11"
);
});
} else if cfg!(windows) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.11.13-[PLATFORM]/python"
);
});
}
uv_snapshot!(context.filters(), context.python_uninstall().arg("3.11"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python versions matching: Python 3.11
Uninstalled Python 3.11.13 in [TIME]
- cpython-3.11.13-[PLATFORM] (python3.11)
");
// The executable should be removed
bin_python.assert(predicate::path::missing());
}
#[test]
fn python_install_multiple_patch() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
// Install multiple patch versions
uv_snapshot!(context.filters(), context.python_install().arg("3.12.8").arg("3.12.6"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Installed 2 versions in [TIME]
+ cpython-3.12.6-[PLATFORM]
+ cpython-3.12.8-[PLATFORM] (python3.12)
");
let bin_python = context
.bin_dir
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
// The executable should be installed in the bin directory
bin_python.assert(predicate::path::exists());
// The link should resolve to the newer patch version
if cfg!(unix) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/bin/python3.12"
);
});
} else if cfg!(windows) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.8-[PLATFORM]/python"
);
});
}
uv_snapshot!(context.filters(), context.python_uninstall().arg("3.12.8"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python versions matching: Python 3.12.8
Uninstalled Python 3.12.8 in [TIME]
- cpython-3.12.8-[PLATFORM] (python3.12)
");
// TODO(zanieb): This behavior is not implemented yet
// // The executable should be installed in the bin directory
// bin_python.assert(predicate::path::exists());
// // When the version is removed, the link should point to the other patch version
// if cfg!(unix) {
// insta::with_settings!({
// filters => context.filters(),
// }, {
// insta::assert_snapshot!(
// canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/bin/python3.12"
// );
// });
// } else if cfg!(windows) {
// insta::with_settings!({
// filters => context.filters(),
// }, {
// insta::assert_snapshot!(
// canonicalize_link_path(&bin_python), @"[TEMP_DIR]/managed/cpython-3.12.6-[PLATFORM]/python"
// );
// });
// }
}
#[test] #[test]
fn python_install_preview() { fn python_install_preview() {
let context: TestContext = TestContext::new_with_versions(&[]) let context: TestContext = TestContext::new_with_versions(&[])
@ -853,7 +1095,7 @@ fn python_install_freethreaded() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// Should not work with older Python versions // Should not work with older Python versions
@ -875,7 +1117,7 @@ fn python_install_freethreaded() {
Searching for Python installations Searching for Python installations
Uninstalled 2 versions in [TIME] Uninstalled 2 versions in [TIME]
- cpython-3.13.5+freethreaded-[PLATFORM] (python3.13t) - cpython-3.13.5+freethreaded-[PLATFORM] (python3.13t)
- cpython-3.13.5-[PLATFORM] - cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
} }
@ -936,15 +1178,243 @@ fn python_install_default() {
.bin_dir .bin_dir
.child(format!("python{}", std::env::consts::EXE_SUFFIX)); .child(format!("python{}", std::env::consts::EXE_SUFFIX));
// `--preview` is required for `--default` // Install a specific version
uv_snapshot!(context.filters(), context.python_install().arg("--default"), @r###" uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
success: false success: true
exit_code: 1 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default` Installed Python 3.13.5 in [TIME]
"###); + cpython-3.13.5-[PLATFORM] (python3.13)
");
// Only the minor versioned executable should be installed
bin_python_minor_13.assert(predicate::path::exists());
bin_python_major.assert(predicate::path::missing());
bin_python_default.assert(predicate::path::missing());
// Install again, with `--default`
uv_snapshot!(context.filters(), context.python_install().arg("--default").arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning
Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] (python, python3)
");
// Now all the executables should be installed
bin_python_minor_13.assert(predicate::path::exists());
bin_python_major.assert(predicate::path::exists());
bin_python_default.assert(predicate::path::exists());
// Uninstall
uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python installations
Uninstalled Python 3.13.5 in [TIME]
- cpython-3.13.5-[PLATFORM] (python, python3, python3.13)
");
// The executables should be removed
bin_python_minor_13.assert(predicate::path::missing());
bin_python_major.assert(predicate::path::missing());
bin_python_default.assert(predicate::path::missing());
// Install the latest version, i.e., a "default install"
uv_snapshot!(context.filters(), context.python_install().arg("--default"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning
Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] (python, python3, python3.13)
");
// Since it's a default install, we should include all of the executables
bin_python_minor_13.assert(predicate::path::exists());
bin_python_major.assert(predicate::path::exists());
bin_python_default.assert(predicate::path::exists());
// And 3.13 should be the default
if cfg!(unix) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/bin/python3.13"
);
});
} else if cfg!(windows) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.5-[PLATFORM]/python"
);
});
}
// Uninstall again
uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Searching for Python versions matching: Python 3.13
Uninstalled Python 3.13.5 in [TIME]
- cpython-3.13.5-[PLATFORM] (python, python3, python3.13)
");
// We should remove all the executables
bin_python_minor_13.assert(predicate::path::missing());
bin_python_major.assert(predicate::path::missing());
bin_python_default.assert(predicate::path::missing());
// Install multiple versions, with the `--default` flag
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("3.13").arg("--default"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning
error: The `--default` flag cannot be used with multiple targets
");
// Install 3.12 as a new default
uv_snapshot!(context.filters(), context.python_install().arg("3.12").arg("--default"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning
Installed Python 3.12.11 in [TIME]
+ cpython-3.12.11-[PLATFORM] (python, python3, python3.12)
");
let bin_python_minor_12 = context
.bin_dir
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
// All the executables should exist
bin_python_minor_12.assert(predicate::path::exists());
bin_python_major.assert(predicate::path::exists());
bin_python_default.assert(predicate::path::exists());
// And 3.12 should be the default
if cfg!(unix) {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/bin/python3.12"
);
});
} else {
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python"
);
});
insta::with_settings!({
filters => context.filters(),
}, {
insta::assert_snapshot!(
read_link(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.11-[PLATFORM]/python"
);
});
}
}
#[test]
fn python_install_default_preview() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_managed_python_dirs();
let bin_python_minor_13 = context
.bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
let bin_python_major = context
.bin_dir
.child(format!("python3{}", std::env::consts::EXE_SUFFIX));
let bin_python_default = context
.bin_dir
.child(format!("python{}", std::env::consts::EXE_SUFFIX));
// Install a specific version // Install a specific version
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r" uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r"
@ -1342,7 +1812,7 @@ fn python_install_unknown() {
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn python_install_preview_broken_link() { fn python_install_broken_link() {
use assert_fs::prelude::PathCreateDir; use assert_fs::prelude::PathCreateDir;
use fs_err::os::unix::fs::symlink; use fs_err::os::unix::fs::symlink;
@ -1358,7 +1828,7 @@ fn python_install_preview_broken_link() {
symlink(context.temp_dir.join("does-not-exist"), &bin_python).unwrap(); symlink(context.temp_dir.join("does-not-exist"), &bin_python).unwrap();
// Install // Install
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r" uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -1393,7 +1863,7 @@ fn python_install_default_from_env() {
----- stderr ----- ----- stderr -----
Installed Python 3.12.11 in [TIME] Installed Python 3.12.11 in [TIME]
+ cpython-3.12.11-[PLATFORM] + cpython-3.12.11-[PLATFORM] (python3.12)
"); ");
// But prefer explicit requests // But prefer explicit requests
@ -1404,7 +1874,7 @@ fn python_install_default_from_env() {
----- stderr ----- ----- stderr -----
Installed Python 3.11.13 in [TIME] Installed Python 3.11.13 in [TIME]
+ cpython-3.11.13-[PLATFORM] + cpython-3.11.13-[PLATFORM] (python3.11)
"); ");
// We should ignore `UV_PYTHON` here and complain there is not a target // We should ignore `UV_PYTHON` here and complain there is not a target
@ -1431,8 +1901,8 @@ fn python_install_default_from_env() {
----- stderr ----- ----- stderr -----
Searching for Python installations Searching for Python installations
Uninstalled 2 versions in [TIME] Uninstalled 2 versions in [TIME]
- cpython-3.11.13-[PLATFORM] - cpython-3.11.13-[PLATFORM] (python3.11)
- cpython-3.12.11-[PLATFORM] - cpython-3.12.11-[PLATFORM] (python3.12)
"); ");
// Uninstall with no targets should error // Uninstall with no targets should error
@ -1516,8 +1986,6 @@ fn python_install_314() {
let context: TestContext = TestContext::new_with_versions(&[]) let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys() .with_filtered_python_keys()
.with_managed_python_dirs() .with_managed_python_dirs()
.with_filtered_python_install_bin()
.with_filtered_python_names()
.with_filtered_exe_suffix(); .with_filtered_exe_suffix();
// Install 3.14 // Install 3.14
@ -1529,7 +1997,7 @@ fn python_install_314() {
----- stderr ----- ----- stderr -----
Installed Python 3.14.0b4 in [TIME] Installed Python 3.14.0b4 in [TIME]
+ cpython-3.14.0b4-[PLATFORM] + cpython-3.14.0b4-[PLATFORM] (python3.14)
"); ");
// Install a specific pre-release // Install a specific pre-release
@ -1543,6 +2011,17 @@ fn python_install_314() {
+ cpython-3.14.0a4-[PLATFORM] + cpython-3.14.0a4-[PLATFORM]
"); ");
// Add name filtering for the `find` tests, we avoid it in `install` tests because it clobbers
// the version suffixes which matter in the install logs
let filters = context
.filters()
.iter()
.map(|(a, b)| ((*a).to_string(), (*b).to_string()))
.collect::<Vec<_>>();
let context = context
.with_filtered_python_install_bin()
.with_filtered_python_names();
// We should be able to find this version without opt-in, because there is no stable release // We should be able to find this version without opt-in, because there is no stable release
// installed // installed
uv_snapshot!(context.filters(), context.python_find().arg("3.14"), @r" uv_snapshot!(context.filters(), context.python_find().arg("3.14"), @r"
@ -1574,14 +2053,14 @@ fn python_install_314() {
"); ");
// If we install a stable version, that should be preferred though // If we install a stable version, that should be preferred though
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r" uv_snapshot!(filters, context.python_install().arg("3.13"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
uv_snapshot!(context.filters(), context.python_find().arg("3"), @r" uv_snapshot!(context.filters(), context.python_find().arg("3"), @r"
@ -1621,15 +2100,15 @@ fn python_install_cached() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
let bin_python = context let bin_python = context
.bin_dir .bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX)); .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
// The executable should not be installed in the bin directory (requires preview) // The executable should be installed in the bin directory
bin_python.assert(predicate::path::missing()); bin_python.assert(predicate::path::exists());
// Should be a no-op when already installed // Should be a no-op when already installed
uv_snapshot!(context.filters(), context uv_snapshot!(context.filters(), context
@ -1651,7 +2130,7 @@ fn python_install_cached() {
----- stderr ----- ----- stderr -----
Searching for Python versions matching: Python 3.13 Searching for Python versions matching: Python 3.13
Uninstalled Python 3.13.5 in [TIME] Uninstalled Python 3.13.5 in [TIME]
- cpython-3.13.5-[PLATFORM] - cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// The cached archive can be installed offline // The cached archive can be installed offline
@ -1665,7 +2144,7 @@ fn python_install_cached() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-[PLATFORM] + cpython-3.13.5-[PLATFORM] (python3.13)
"); ");
// 3.12 isn't cached, so it can't be installed // 3.12 isn't cached, so it can't be installed
@ -1714,7 +2193,7 @@ fn python_install_emulated_macos() {
----- stderr ----- ----- stderr -----
Installed Python 3.13.5 in [TIME] Installed Python 3.13.5 in [TIME]
+ cpython-3.13.5-macos-x86_64-none + cpython-3.13.5-macos-x86_64-none (python3.13)
"); ");
// It should be discoverable with `uv python find` // It should be discoverable with `uv python find`

View file

@ -121,28 +121,17 @@ present, uv will install all the Python versions listed in the file.
### Installing Python executables ### Installing Python executables
!!! important uv installs Python executables into your `PATH` by default, e.g., `uv python install 3.12` will
install a Python executable into `~/.local/bin`, e.g., as `python3.12`.
Support for installing Python executables is in _preview_. This means the behavior is experimental
and subject to change.
To install Python executables into your `PATH`, provide the `--preview` option:
```console
$ uv python install 3.12 --preview
```
This will install a Python executable for the requested version into `~/.local/bin`, e.g., as
`python3.12`.
!!! tip !!! tip
If `~/.local/bin` is not in your `PATH`, you can add it with `uv tool update-shell`. If `~/.local/bin` is not in your `PATH`, you can add it with `uv tool update-shell`.
To install `python` and `python3` executables, include the `--default` option: To install `python` and `python3` executables, include the experimental `--default` option:
```console ```console
$ uv python install 3.12 --default --preview $ uv python install 3.12 --default
``` ```
When installing Python executables, uv will only overwrite an existing executable if it is managed When installing Python executables, uv will only overwrite an existing executable if it is managed
@ -153,9 +142,9 @@ uv will update executables that it manages. However, it will prefer the latest p
Python minor version by default. For example: Python minor version by default. For example:
```console ```console
$ uv python install 3.12.7 --preview # Adds `python3.12` to `~/.local/bin` $ uv python install 3.12.7 # Adds `python3.12` to `~/.local/bin`
$ uv python install 3.12.6 --preview # Does not update `python3.12` $ uv python install 3.12.6 # Does not update `python3.12`
$ uv python install 3.12.8 --preview # Updates `python3.12` to point to 3.12.8 $ uv python install 3.12.8 # Updates `python3.12` to point to 3.12.8
``` ```
## Upgrading Python versions ## Upgrading Python versions

View file

@ -24,17 +24,24 @@ $ uv python install
Python does not publish official distributable binaries. As such, uv uses distributions from the Astral [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project. See the [Python distributions](../concepts/python-versions.md#managed-python-distributions) documentation for more details. Python does not publish official distributable binaries. As such, uv uses distributions from the Astral [`python-build-standalone`](https://github.com/astral-sh/python-build-standalone) project. See the [Python distributions](../concepts/python-versions.md#managed-python-distributions) documentation for more details.
Once Python is installed, it will be used by `uv` commands automatically. Once Python is installed, it will be used by `uv` commands automatically. uv also adds the installed
version to your `PATH`:
!!! important ```console
$ python3.13
```
When Python is installed by uv, it will not be available globally (i.e. via the `python` command). uv only installs a _versioned_ executable by default. To install `python` and `python3` executables,
Support for this feature is in _preview_. See [Installing Python executables](../concepts/python-versions.md#installing-python-executables) include the experimental `--default` option:
for details.
You can still use ```console
[`uv run`](../guides/scripts.md#using-different-python-versions) or $ uv python install --default
[create and activate a virtual environment](../pip/environments.md) to use `python` directly. ```
!!! tip
See the documentation on [installing Python executables](../concepts/python-versions.md#installing-python-executables)
for more details.
## Installing a specific version ## Installing a specific version

View file

@ -2739,7 +2739,7 @@ Supports CPython and PyPy. CPython distributions are downloaded from the Astral
Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`. Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`.
A `python` executable is not made globally available, managed Python versions are only used in uv commands or in active virtual environments. There is experimental support for adding Python executables to a directory on the path — use the `--preview` flag to enable this behavior and `uv python dir --bin` to retrieve the target directory. By default, Python executables are added to a directory on the path with a minor version suffix, e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use `uv python dir --bin` to see the target directory.
Multiple Python versions may be requested. Multiple Python versions may be requested.