Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
62 changes: 28 additions & 34 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 19 additions & 6 deletions crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,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()));
} else {
self.filters
.push((r"python\d.\d\d".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\d".to_string(), "[PYTHON]".to_string()));
.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".to_string(), "/[PYTHON]".to_string()));
.push((format!(r"/python{exe_suffix}"), "/[PYTHON]".to_string()));
}

self
}

Expand Down
7 changes: 3 additions & 4 deletions crates/uv/tests/it/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading
Loading