Skip to content

Commit 0077f23

Browse files
committed
Stabilize addition of Python executables to the bin (#14626)
Closes #14296 As mentioned in #14681, this does not stabilize the `--default` behavior.
1 parent ff30f14 commit 0077f23

File tree

8 files changed

+592
-112
lines changed

8 files changed

+592
-112
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4810,10 +4810,9 @@ pub enum PythonCommand {
48104810
/// Python versions are installed into the uv Python directory, which can be retrieved with `uv
48114811
/// python dir`.
48124812
///
4813-
/// A `python` executable is not made globally available, managed Python versions are only used
4814-
/// in uv commands or in active virtual environments. There is experimental support for adding
4815-
/// Python executables to a directory on the path — use the `--preview` flag to enable this
4816-
/// behavior and `uv python dir --bin` to retrieve the target directory.
4813+
/// By default, Python executables are added to a directory on the path with a minor version
4814+
/// suffix, e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use
4815+
/// `uv python dir --bin` to see the target directory.
48174816
///
48184817
/// Multiple Python versions may be requested.
48194818
///

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

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,14 @@ pub(crate) async fn install(
166166
) -> Result<ExitStatus> {
167167
let start = std::time::Instant::now();
168168

169+
// TODO(zanieb): We should consider marking the Python installation as the default when
170+
// `--default` is used. It's not clear how this overlaps with a global Python pin, but I'd be
171+
// surprised if `uv python find` returned the "newest" Python version rather than the one I just
172+
// installed with the `--default` flag.
169173
if default && !preview.is_enabled() {
170-
writeln!(
171-
printer.stderr(),
172-
"The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default`"
173-
)?;
174-
return Ok(ExitStatus::Failure);
174+
warn_user!(
175+
"The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning"
176+
);
175177
}
176178

177179
if upgrade && preview.is_disabled() {
@@ -222,6 +224,8 @@ pub(crate) async fn install(
222224
.map(PythonVersionFile::into_versions)
223225
.unwrap_or_else(|| {
224226
// If no version file is found and no requests were made
227+
// TODO(zanieb): We should consider differentiating between a global Python version
228+
// file here, allowing a request from there to enable `is_default_install`.
225229
is_default_install = true;
226230
vec![if reinstall {
227231
// On bare `--reinstall`, reinstall all Python versions
@@ -451,10 +455,10 @@ pub(crate) async fn install(
451455
}
452456
}
453457

454-
let bin_dir = if matches!(bin, Some(true)) || preview.is_enabled() {
455-
Some(python_executable_dir()?)
456-
} else {
458+
let bin_dir = if matches!(bin, Some(false)) {
457459
None
460+
} else {
461+
Some(python_executable_dir()?)
458462
};
459463

460464
let installations: Vec<_> = downloaded.iter().chain(satisfied.iter().copied()).collect();
@@ -469,20 +473,10 @@ pub(crate) async fn install(
469473
e.warn_user(installation);
470474
}
471475

472-
if preview.is_disabled() {
473-
debug!("Skipping installation of Python executables, use `--preview` to enable.");
474-
continue;
475-
}
476-
477-
let bin_dir = bin_dir
478-
.as_ref()
479-
.expect("We should have a bin directory with preview enabled")
480-
.as_path();
481-
482476
let upgradeable = (default || is_default_install)
483477
|| requested_minor_versions.contains(&installation.key().version().python_version());
484478

485-
if !matches!(bin, Some(false)) {
479+
if let Some(bin_dir) = bin_dir.as_ref() {
486480
create_bin_links(
487481
installation,
488482
bin_dir,
@@ -661,11 +655,7 @@ pub(crate) async fn install(
661655
}
662656
}
663657

664-
if preview.is_enabled() && !matches!(bin, Some(false)) {
665-
let bin_dir = bin_dir
666-
.as_ref()
667-
.expect("We should have a bin directory with preview enabled")
668-
.as_path();
658+
if let Some(bin_dir) = bin_dir.as_ref() {
669659
warn_if_not_on_path(bin_dir);
670660
}
671661
}
@@ -749,16 +739,20 @@ fn create_bin_links(
749739
errors: &mut Vec<(InstallErrorKind, PythonInstallationKey, Error)>,
750740
preview: PreviewMode,
751741
) {
752-
let targets =
753-
if (default || is_default_install) && first_request.matches_installation(installation) {
754-
vec![
755-
installation.key().executable_name_minor(),
756-
installation.key().executable_name_major(),
757-
installation.key().executable_name(),
758-
]
759-
} else {
760-
vec![installation.key().executable_name_minor()]
761-
};
742+
// TODO(zanieb): We want more feedback on the `is_default_install` behavior before stabilizing
743+
// it. In particular, it may be confusing because it does not apply when versions are loaded
744+
// from a `.python-version` file.
745+
let targets = if (default || (is_default_install && preview.is_enabled()))
746+
&& first_request.matches_installation(installation)
747+
{
748+
vec![
749+
installation.key().executable_name_minor(),
750+
installation.key().executable_name_major(),
751+
installation.key().executable_name(),
752+
]
753+
} else {
754+
vec![installation.key().executable_name_minor()]
755+
};
762756

763757
for target in targets {
764758
let target = bin.join(target);

crates/uv/tests/it/common/mod.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,30 @@ impl TestContext {
220220
/// and `.exe` suffixes.
221221
#[must_use]
222222
pub fn with_filtered_python_names(mut self) -> Self {
223+
use env::consts::EXE_SUFFIX;
224+
let exe_suffix = regex::escape(EXE_SUFFIX);
225+
226+
self.filters.push((
227+
format!(r"python\d.\d\d{exe_suffix}"),
228+
"[PYTHON]".to_string(),
229+
));
230+
self.filters
231+
.push((format!(r"python\d{exe_suffix}"), "[PYTHON]".to_string()));
232+
223233
if cfg!(windows) {
234+
// On Windows, we want to filter out all `python.exe` instances
224235
self.filters
225-
.push((r"python\.exe".to_string(), "[PYTHON]".to_string()));
226-
} else {
227-
self.filters
228-
.push((r"python\d.\d\d".to_string(), "[PYTHON]".to_string()));
236+
.push((format!(r"python{exe_suffix}"), "[PYTHON]".to_string()));
237+
// Including ones where we'd already stripped the `.exe` in another filter
229238
self.filters
230-
.push((r"python\d".to_string(), "[PYTHON]".to_string()));
239+
.push((r"[\\/]python".to_string(), "/[PYTHON]".to_string()));
240+
} else {
241+
// On Unix, it's a little trickier — we don't want to clobber use of `python` in the
242+
// middle of something else, e.g., `cpython`. For this reason, we require a leading `/`.
231243
self.filters
232-
.push((r"/python".to_string(), "/[PYTHON]".to_string()));
244+
.push((format!(r"/python{exe_suffix}"), "/[PYTHON]".to_string()));
233245
}
246+
234247
self
235248
}
236249

crates/uv/tests/it/help.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,9 @@ fn help_subsubcommand() {
469469
Python versions are installed into the uv Python directory, which can be retrieved with `uv python
470470
dir`.
471471
472-
A `python` executable is not made globally available, managed Python versions are only used in uv
473-
commands or in active virtual environments. There is experimental support for adding Python
474-
executables to a directory on the path — use the `--preview` flag to enable this behavior and `uv
475-
python dir --bin` to retrieve the target directory.
472+
By default, Python executables are added to a directory on the path with a minor version suffix,
473+
e.g., `python3.13`. To install `python3` and `python`, use the `--default` flag. Use `uv python dir
474+
--bin` to see the target directory.
476475
477476
Multiple Python versions may be requested.
478477

0 commit comments

Comments
 (0)