mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 20:19:08 +00:00
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:
parent
ff30f14d50
commit
0077f2357f
8 changed files with 593 additions and 113 deletions
|
@ -4810,10 +4810,9 @@ pub enum PythonCommand {
|
|||
/// 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.
|
||||
///
|
||||
|
|
|
@ -166,12 +166,14 @@ pub(crate) async fn install(
|
|||
) -> Result<ExitStatus> {
|
||||
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() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default`"
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
warn_user!(
|
||||
"The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning"
|
||||
);
|
||||
}
|
||||
|
||||
if upgrade && preview.is_disabled() {
|
||||
|
@ -222,6 +224,8 @@ pub(crate) async fn install(
|
|||
.map(PythonVersionFile::into_versions)
|
||||
.unwrap_or_else(|| {
|
||||
// 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;
|
||||
vec![if reinstall {
|
||||
// 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() {
|
||||
Some(python_executable_dir()?)
|
||||
} else {
|
||||
let bin_dir = if matches!(bin, Some(false)) {
|
||||
None
|
||||
} else {
|
||||
Some(python_executable_dir()?)
|
||||
};
|
||||
|
||||
let installations: Vec<_> = downloaded.iter().chain(satisfied.iter().copied()).collect();
|
||||
|
@ -469,20 +473,10 @@ pub(crate) async fn install(
|
|||
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)
|
||||
|| 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(
|
||||
installation,
|
||||
bin_dir,
|
||||
|
@ -661,11 +655,7 @@ pub(crate) async fn install(
|
|||
}
|
||||
}
|
||||
|
||||
if preview.is_enabled() && !matches!(bin, Some(false)) {
|
||||
let bin_dir = bin_dir
|
||||
.as_ref()
|
||||
.expect("We should have a bin directory with preview enabled")
|
||||
.as_path();
|
||||
if let Some(bin_dir) = bin_dir.as_ref() {
|
||||
warn_if_not_on_path(bin_dir);
|
||||
}
|
||||
}
|
||||
|
@ -749,16 +739,20 @@ fn create_bin_links(
|
|||
errors: &mut Vec<(InstallErrorKind, PythonInstallationKey, Error)>,
|
||||
preview: PreviewMode,
|
||||
) {
|
||||
let targets =
|
||||
if (default || is_default_install) && first_request.matches_installation(installation) {
|
||||
vec![
|
||||
installation.key().executable_name_minor(),
|
||||
installation.key().executable_name_major(),
|
||||
installation.key().executable_name(),
|
||||
]
|
||||
} else {
|
||||
vec![installation.key().executable_name_minor()]
|
||||
};
|
||||
// TODO(zanieb): We want more feedback on the `is_default_install` behavior before stabilizing
|
||||
// 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![
|
||||
installation.key().executable_name_minor(),
|
||||
installation.key().executable_name_major(),
|
||||
installation.key().executable_name(),
|
||||
]
|
||||
} else {
|
||||
vec![installation.key().executable_name_minor()]
|
||||
};
|
||||
|
||||
for target in targets {
|
||||
let target = bin.join(target);
|
||||
|
|
|
@ -220,17 +220,30 @@ impl TestContext {
|
|||
/// and `.exe` suffixes.
|
||||
#[must_use]
|
||||
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) {
|
||||
// On Windows, we want to filter out all `python.exe` instances
|
||||
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 {
|
||||
// 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
|
||||
.push((r"python\d.\d\d".to_string(), "[PYTHON]".to_string()));
|
||||
self.filters
|
||||
.push((r"python\d".to_string(), "[PYTHON]".to_string()));
|
||||
self.filters
|
||||
.push((r"/python".to_string(), "/[PYTHON]".to_string()));
|
||||
.push((format!(r"/python{exe_suffix}"), "/[PYTHON]".to_string()));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
@ -469,10 +469,9 @@ fn help_subsubcommand() {
|
|||
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.
|
||||
|
||||
|
|
|
@ -30,15 +30,49 @@ fn python_install() {
|
|||
|
||||
----- stderr -----
|
||||
Installed Python 3.13.5 in [TIME]
|
||||
+ cpython-3.13.5-[PLATFORM]
|
||||
+ cpython-3.13.5-[PLATFORM] (python3.13)
|
||||
");
|
||||
|
||||
let bin_python = context
|
||||
.bin_dir
|
||||
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
|
||||
|
||||
// The executable should not be installed in the bin directory (requires preview)
|
||||
bin_python.assert(predicate::path::missing());
|
||||
// The executable should be installed in the bin directory
|
||||
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
|
||||
uv_snapshot!(context.filters(), context.python_install(), @r###"
|
||||
|
@ -67,9 +101,12 @@ fn python_install() {
|
|||
|
||||
----- stderr -----
|
||||
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
|
||||
uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
|
||||
success: false
|
||||
|
@ -93,8 +130,11 @@ fn python_install() {
|
|||
----- stderr -----
|
||||
Searching for Python versions matching: Python 3.13
|
||||
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]
|
||||
|
@ -112,8 +152,8 @@ fn python_reinstall() {
|
|||
|
||||
----- stderr -----
|
||||
Installed 2 versions in [TIME]
|
||||
+ cpython-3.12.11-[PLATFORM]
|
||||
+ cpython-3.13.5-[PLATFORM]
|
||||
+ cpython-3.12.11-[PLATFORM] (python3.12)
|
||||
+ cpython-3.13.5-[PLATFORM] (python3.13)
|
||||
");
|
||||
|
||||
// Reinstall a single version
|
||||
|
@ -124,7 +164,7 @@ fn python_reinstall() {
|
|||
|
||||
----- stderr -----
|
||||
Installed Python 3.13.5 in [TIME]
|
||||
~ cpython-3.13.5-[PLATFORM]
|
||||
~ cpython-3.13.5-[PLATFORM] (python3.13)
|
||||
");
|
||||
|
||||
// Reinstall multiple versions
|
||||
|
@ -135,8 +175,8 @@ fn python_reinstall() {
|
|||
|
||||
----- stderr -----
|
||||
Installed 2 versions in [TIME]
|
||||
~ cpython-3.12.11-[PLATFORM]
|
||||
~ cpython-3.13.5-[PLATFORM]
|
||||
~ cpython-3.12.11-[PLATFORM] (python3.12)
|
||||
~ cpython-3.13.5-[PLATFORM] (python3.13)
|
||||
");
|
||||
|
||||
// Reinstalling a version that is not installed should also work
|
||||
|
@ -147,7 +187,7 @@ fn python_reinstall() {
|
|||
|
||||
----- stderr -----
|
||||
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 -----
|
||||
Installed 2 versions in [TIME]
|
||||
+ cpython-3.12.6-[PLATFORM]
|
||||
+ cpython-3.12.7-[PLATFORM]
|
||||
+ cpython-3.12.7-[PLATFORM] (python3.12)
|
||||
");
|
||||
|
||||
// Reinstall all "3.12" versions
|
||||
|
@ -180,7 +220,7 @@ fn python_reinstall_patch() {
|
|||
|
||||
----- stderr -----
|
||||
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]
|
||||
fn python_install_preview() {
|
||||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
|
@ -853,7 +1095,7 @@ fn python_install_freethreaded() {
|
|||
|
||||
----- stderr -----
|
||||
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
|
||||
|
@ -875,7 +1117,7 @@ fn python_install_freethreaded() {
|
|||
Searching for Python installations
|
||||
Uninstalled 2 versions in [TIME]
|
||||
- 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
|
||||
.child(format!("python{}", std::env::consts::EXE_SUFFIX));
|
||||
|
||||
// `--preview` is required for `--default`
|
||||
uv_snapshot!(context.filters(), context.python_install().arg("--default"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
// Install a specific version
|
||||
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- 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
|
||||
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r"
|
||||
|
@ -1342,7 +1812,7 @@ fn python_install_unknown() {
|
|||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn python_install_preview_broken_link() {
|
||||
fn python_install_broken_link() {
|
||||
use assert_fs::prelude::PathCreateDir;
|
||||
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();
|
||||
|
||||
// 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
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -1393,7 +1863,7 @@ fn python_install_default_from_env() {
|
|||
|
||||
----- stderr -----
|
||||
Installed Python 3.12.11 in [TIME]
|
||||
+ cpython-3.12.11-[PLATFORM]
|
||||
+ cpython-3.12.11-[PLATFORM] (python3.12)
|
||||
");
|
||||
|
||||
// But prefer explicit requests
|
||||
|
@ -1404,7 +1874,7 @@ fn python_install_default_from_env() {
|
|||
|
||||
----- stderr -----
|
||||
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
|
||||
|
@ -1431,8 +1901,8 @@ fn python_install_default_from_env() {
|
|||
----- stderr -----
|
||||
Searching for Python installations
|
||||
Uninstalled 2 versions in [TIME]
|
||||
- cpython-3.11.13-[PLATFORM]
|
||||
- cpython-3.12.11-[PLATFORM]
|
||||
- cpython-3.11.13-[PLATFORM] (python3.11)
|
||||
- cpython-3.12.11-[PLATFORM] (python3.12)
|
||||
");
|
||||
|
||||
// Uninstall with no targets should error
|
||||
|
@ -1516,8 +1986,6 @@ fn python_install_314() {
|
|||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
.with_filtered_python_keys()
|
||||
.with_managed_python_dirs()
|
||||
.with_filtered_python_install_bin()
|
||||
.with_filtered_python_names()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
// Install 3.14
|
||||
|
@ -1529,7 +1997,7 @@ fn python_install_314() {
|
|||
|
||||
----- stderr -----
|
||||
Installed Python 3.14.0b4 in [TIME]
|
||||
+ cpython-3.14.0b4-[PLATFORM]
|
||||
+ cpython-3.14.0b4-[PLATFORM] (python3.14)
|
||||
");
|
||||
|
||||
// Install a specific pre-release
|
||||
|
@ -1543,6 +2011,17 @@ fn python_install_314() {
|
|||
+ 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
|
||||
// installed
|
||||
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
|
||||
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
|
||||
uv_snapshot!(filters, context.python_install().arg("3.13"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
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"
|
||||
|
@ -1621,15 +2100,15 @@ fn python_install_cached() {
|
|||
|
||||
----- stderr -----
|
||||
Installed Python 3.13.5 in [TIME]
|
||||
+ cpython-3.13.5-[PLATFORM]
|
||||
+ cpython-3.13.5-[PLATFORM] (python3.13)
|
||||
");
|
||||
|
||||
let bin_python = context
|
||||
.bin_dir
|
||||
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
|
||||
|
||||
// The executable should not be installed in the bin directory (requires preview)
|
||||
bin_python.assert(predicate::path::missing());
|
||||
// The executable should be installed in the bin directory
|
||||
bin_python.assert(predicate::path::exists());
|
||||
|
||||
// Should be a no-op when already installed
|
||||
uv_snapshot!(context.filters(), context
|
||||
|
@ -1651,7 +2130,7 @@ fn python_install_cached() {
|
|||
----- stderr -----
|
||||
Searching for Python versions matching: Python 3.13
|
||||
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
|
||||
|
@ -1665,7 +2144,7 @@ fn python_install_cached() {
|
|||
|
||||
----- stderr -----
|
||||
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
|
||||
|
@ -1714,7 +2193,7 @@ fn python_install_emulated_macos() {
|
|||
|
||||
----- stderr -----
|
||||
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`
|
||||
|
|
|
@ -121,28 +121,17 @@ present, uv will install all the Python versions listed in the file.
|
|||
|
||||
### Installing Python executables
|
||||
|
||||
!!! important
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
!!! tip
|
||||
|
||||
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
|
||||
$ 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
|
||||
|
@ -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:
|
||||
|
||||
```console
|
||||
$ uv python install 3.12.7 --preview # Adds `python3.12` to `~/.local/bin`
|
||||
$ uv python install 3.12.6 --preview # 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.7 # Adds `python3.12` to `~/.local/bin`
|
||||
$ uv python install 3.12.6 # Does not update `python3.12`
|
||||
$ uv python install 3.12.8 # Updates `python3.12` to point to 3.12.8
|
||||
```
|
||||
|
||||
## Upgrading Python versions
|
||||
|
|
|
@ -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.
|
||||
|
||||
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).
|
||||
Support for this feature is in _preview_. See [Installing Python executables](../concepts/python-versions.md#installing-python-executables)
|
||||
for details.
|
||||
uv only installs a _versioned_ executable by default. To install `python` and `python3` executables,
|
||||
include the experimental `--default` option:
|
||||
|
||||
You can still use
|
||||
[`uv run`](../guides/scripts.md#using-different-python-versions) or
|
||||
[create and activate a virtual environment](../pip/environments.md) to use `python` directly.
|
||||
```console
|
||||
$ uv python install --default
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
See the documentation on [installing Python executables](../concepts/python-versions.md#installing-python-executables)
|
||||
for more details.
|
||||
|
||||
## Installing a specific version
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
||||
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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue