Skip to content

Commit 7d2cf99

Browse files
committed
Gate with preview mode
1 parent 6b7d517 commit 7d2cf99

File tree

4 files changed

+145
-48
lines changed

4 files changed

+145
-48
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3803,14 +3803,15 @@ pub enum PythonCommand {
38033803
///
38043804
/// Multiple Python versions may be requested.
38053805
///
3806-
/// Supports CPython and PyPy.
3806+
/// Supports CPython and PyPy. CPython distributions are downloaded from the
3807+
/// `python-build-standalone` project. PyPy distributions are downloaded from `python.org`.
38073808
///
3808-
/// CPython distributions are downloaded from the `python-build-standalone` project.
3809+
/// Python versions are installed into the uv Python directory, which can be retrieved with `uv
3810+
/// python dir`.
38093811
///
3810-
/// Python versions are installed into the uv Python directory, which can be
3811-
/// retrieved with `uv python dir`. A `python` executable is not made
3812-
/// globally available, managed Python versions are only used in uv
3813-
/// commands or in active virtual environments.
3812+
/// A `python` executable is not made globally available, managed Python versions are only used
3813+
/// in uv commands or in active virtual environments. There is experimental support for
3814+
/// adding Python executables to the `PATH` — use the `--preview` flag to enable this behavior.
38143815
///
38153816
/// See `uv help python` to view supported request formats.
38163817
Install(PythonInstallArgs),
@@ -3873,6 +3874,8 @@ pub struct PythonListArgs {
38733874
pub struct PythonDirArgs {
38743875
/// Show the directory into which `uv python` will install Python executables.
38753876
///
3877+
/// Note this directory is only used when installing with preview mode enabled.
3878+
///
38763879
/// By default, `uv python dir` shows the directory into which the Python distributions
38773880
/// themselves are installed, rather than the directory containing the linked executables.
38783881
///

crates/uv/src/commands/python/install.rs

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use same_file::is_same_file;
1313
use tracing::debug;
1414

1515
use uv_client::Connectivity;
16+
use uv_configuration::PreviewMode;
1617
use uv_fs::Simplified;
1718
use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest};
1819
use uv_python::managed::{
@@ -36,6 +37,7 @@ pub(crate) async fn install(
3637
native_tls: bool,
3738
connectivity: Connectivity,
3839
no_config: bool,
40+
preview: PreviewMode,
3941
printer: Printer,
4042
) -> Result<ExitStatus> {
4143
let start = std::time::Instant::now();
@@ -164,7 +166,11 @@ pub(crate) async fn install(
164166
});
165167
}
166168

167-
let bin = python_executable_dir()?;
169+
let bin = if preview.is_enabled() {
170+
Some(python_executable_dir()?)
171+
} else {
172+
None
173+
};
168174

169175
let mut installed = FxHashSet::default();
170176
let mut errors = vec![];
@@ -183,35 +189,42 @@ pub(crate) async fn install(
183189
let managed = ManagedPythonInstallation::new(path.clone())?;
184190
managed.ensure_externally_managed()?;
185191
managed.ensure_canonical_executables()?;
186-
match managed.create_bin_link(&bin) {
187-
Ok(executable) => {
188-
debug!("Installed {} executable to {}", key, executable.display());
189-
}
190-
Err(uv_python::managed::Error::LinkExecutable { from, to, err })
191-
if err.kind() == ErrorKind::AlreadyExists =>
192-
{
193-
// TODO(zanieb): Add `--force`
194-
if reinstall {
195-
fs_err::remove_file(&to)?;
196-
let executable = managed.create_bin_link(&bin)?;
197-
debug!(
198-
"Replaced {} executable at {}",
199-
key,
200-
executable.user_display()
201-
);
202-
} else {
203-
if !is_same_file(&to, &from).unwrap_or_default() {
204-
errors.push((
192+
193+
if preview.is_enabled() && cfg!(unix) {
194+
let bin = bin
195+
.as_ref()
196+
.expect("We should have a bin directory with preview enabled")
197+
.as_path();
198+
match managed.create_bin_link(&bin) {
199+
Ok(executable) => {
200+
debug!("Installed {} executable to {}", key, executable.display());
201+
}
202+
Err(uv_python::managed::Error::LinkExecutable { from, to, err })
203+
if err.kind() == ErrorKind::AlreadyExists =>
204+
{
205+
// TODO(zanieb): Add `--force`
206+
if reinstall {
207+
fs_err::remove_file(&to)?;
208+
let executable = managed.create_bin_link(&bin)?;
209+
debug!(
210+
"Replaced {} executable at {}",
205211
key,
206-
anyhow::anyhow!(
207-
"Executable already exists at `{}`. Use `--reinstall` to force replacement.",
208-
to.user_display()
209-
),
210-
));
212+
executable.user_display()
213+
);
214+
} else {
215+
if !is_same_file(&to, &from).unwrap_or_default() {
216+
errors.push((
217+
key,
218+
anyhow::anyhow!(
219+
"Executable already exists at `{}`. Use `--reinstall` to force replacement.",
220+
to.user_display()
221+
),
222+
));
223+
}
211224
}
212225
}
226+
Err(err) => return Err(err.into()),
213227
}
214-
Err(err) => return Err(err.into()),
215228
}
216229
}
217230
Err(err) => {
@@ -301,7 +314,13 @@ pub(crate) async fn install(
301314
}
302315
}
303316

304-
warn_if_not_on_path(&bin);
317+
if preview.is_enabled() && cfg!(unix) {
318+
let bin = bin
319+
.as_ref()
320+
.expect("We should have a bin directory with preview enabled")
321+
.as_path();
322+
warn_if_not_on_path(&bin);
323+
}
305324
}
306325

307326
if !errors.is_empty() {

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
10571057
globals.native_tls,
10581058
globals.connectivity,
10591059
cli.top_level.no_config,
1060+
globals.preview,
10601061
printer,
10611062
)
10621063
.await

crates/uv/tests/it/python_install.rs

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,92 @@ fn python_install() {
1515
exit_code: 0
1616
----- stdout -----
1717
18+
----- stderr -----
19+
Searching for Python installations
20+
Installed Python 3.13.0 in [TIME]
21+
+ cpython-3.13.0-[PLATFORM]
22+
"###);
23+
24+
let bin_python = context
25+
.temp_dir
26+
.child("bin")
27+
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
28+
29+
// The executable should not be installed in the bin directory (requires preview)
30+
bin_python.assert(predicate::path::missing());
31+
32+
// Should be a no-op when already installed
33+
uv_snapshot!(context.filters(), context.python_install(), @r###"
34+
success: true
35+
exit_code: 0
36+
----- stdout -----
37+
38+
----- stderr -----
39+
Searching for Python installations
40+
Found: cpython-3.13.0-[PLATFORM]
41+
Python is already available. Use `uv python install <request>` to install a specific version.
42+
"###);
43+
44+
// Similarly, when a requested version is already installed
45+
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###"
46+
success: true
47+
exit_code: 0
48+
----- stdout -----
49+
50+
----- stderr -----
51+
Searching for Python versions matching: Python 3.13
52+
Found existing installation for Python 3.13: cpython-3.13.0-[PLATFORM]
53+
"###);
54+
55+
// You can opt-in to a reinstall
56+
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
57+
success: true
58+
exit_code: 0
59+
----- stdout -----
60+
61+
----- stderr -----
62+
Searching for Python installations
63+
Found: cpython-3.13.0-[PLATFORM]
64+
Installed Python 3.13.0 in [TIME]
65+
~ cpython-3.13.0-[PLATFORM]
66+
"###);
67+
68+
// Uninstallation requires an argument
69+
uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
70+
success: false
71+
exit_code: 2
72+
----- stdout -----
73+
74+
----- stderr -----
75+
error: the following required arguments were not provided:
76+
<TARGETS>...
77+
78+
Usage: uv python uninstall <TARGETS>...
79+
80+
For more information, try '--help'.
81+
"###);
82+
83+
uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r###"
84+
success: false
85+
exit_code: 2
86+
----- stdout -----
87+
88+
----- stderr -----
89+
Searching for Python versions matching: Python 3.13
90+
error: No such file or directory (os error 2)
91+
"###);
92+
}
93+
94+
#[test]
95+
fn python_install_preview() {
96+
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
97+
98+
// Install the latest version
99+
uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###"
100+
success: true
101+
exit_code: 0
102+
----- stdout -----
103+
18104
----- stderr -----
19105
Searching for Python installations
20106
Installed Python 3.13.0 in [TIME]
@@ -46,7 +132,7 @@ fn python_install() {
46132
"###);
47133

48134
// Should be a no-op when already installed
49-
uv_snapshot!(context.filters(), context.python_install(), @r###"
135+
uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###"
50136
success: true
51137
exit_code: 0
52138
----- stdout -----
@@ -57,19 +143,8 @@ fn python_install() {
57143
Python is already available. Use `uv python install <request>` to install a specific version.
58144
"###);
59145

60-
// Similarly, when a requested version is already installed
61-
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r###"
62-
success: true
63-
exit_code: 0
64-
----- stdout -----
65-
66-
----- stderr -----
67-
Searching for Python versions matching: Python 3.13
68-
Found existing installation for Python 3.13: cpython-3.13.0-[PLATFORM]
69-
"###);
70-
71146
// You can opt-in to a reinstall
72-
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
147+
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--reinstall"), @r###"
73148
success: true
74149
exit_code: 0
75150
----- stdout -----
@@ -120,7 +195,7 @@ fn python_install_freethreaded() {
120195
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
121196

122197
// Install the latest version
123-
uv_snapshot!(context.filters(), context.python_install().arg("3.13t"), @r###"
198+
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r###"
124199
success: true
125200
exit_code: 0
126201
----- stdout -----
@@ -165,7 +240,6 @@ fn python_install_freethreaded() {
165240
Searching for Python versions matching: Python 3.13
166241
Installed Python 3.13.0 in [TIME]
167242
+ cpython-3.13.0-[PLATFORM]
168-
warning: `[TEMP_DIR]/bin` is not on your PATH. To use the installed Python executable, run `export PATH="[TEMP_DIR]/bin:$PATH"`.
169243
"###);
170244

171245
// Should not work with older Python versions

0 commit comments

Comments
 (0)