Add `uv python update-shell` (#14627)

Part of #14296 

This is the same as `uv tool update-shell` but handles the case where
the Python bin directory is configured to a different path.

```
❯ UV_PYTHON_BIN_DIR=/tmp/foo cargo run -q -- python install --preview 3.13.3
Installed Python 3.13.3 in 1.75s
 + cpython-3.13.3-macos-aarch64-none
warning: `/tmp/foo` is not on your PATH. To use installed Python executables, run `export PATH="/tmp/foo:$PATH"` or `uv python update-shell`.
❯ UV_PYTHON_BIN_DIR=/tmp/foo cargo run -q -- python update-shell
Created configuration file: /Users/zb/.zshenv
Restart your shell to apply changes
❯ cat /Users/zb/.zshenv
# uv
export PATH="/tmp/foo:$PATH"
❯ UV_TOOL_BIN_DIR=/tmp/bar cargo run -q -- tool update-shell
Updated configuration file: /Users/zb/.zshenv
Restart your shell to apply changes
❯ cat /Users/zb/.zshenv
# uv
export PATH="/tmp/foo:$PATH"

# uv
export PATH="/tmp/bar:$PATH"
```
This commit is contained in:
Zanie Blue 2025-07-15 13:47:02 -05:00 committed by GitHub
parent c226d66f35
commit d525720266
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 274 additions and 23 deletions

View File

@ -4856,6 +4856,19 @@ pub enum PythonCommand {
/// Uninstall Python versions.
Uninstall(PythonUninstallArgs),
/// Ensure that the Python executable directory is on the `PATH`.
///
/// If the Python executable directory is not present on the `PATH`, uv will attempt to add it to
/// the relevant shell configuration files.
///
/// If the shell configuration files already include a blurb to add the executable directory to
/// the path, but the directory is not present on the `PATH`, uv will exit with an error.
///
/// The Python executable directory is determined according to the XDG standard and can be
/// retrieved with `uv python dir --bin`.
#[command(alias = "ensurepath")]
UpdateShell,
}
#[derive(Args)]

View File

@ -38,6 +38,7 @@ pub(crate) use python::install::install as python_install;
pub(crate) use python::list::list as python_list;
pub(crate) use python::pin::pin as python_pin;
pub(crate) use python::uninstall::uninstall as python_uninstall;
pub(crate) use python::update_shell::update_shell as python_update_shell;
#[cfg(feature = "self-update")]
pub(crate) use self_update::self_update;
pub(crate) use tool::dir::dir as tool_dir;

View File

@ -993,20 +993,29 @@ fn warn_if_not_on_path(bin: &Path) {
if !Shell::contains_path(bin) {
if let Some(shell) = Shell::from_env() {
if let Some(command) = shell.prepend_path(bin) {
warn_user!(
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
bin.simplified_display().cyan(),
command.green(),
);
if shell.supports_update() {
warn_user!(
"`{}` is not on your PATH. To use installed Python executables, run `{}` or `{}`.",
bin.simplified_display().cyan(),
command.green(),
"uv python update-shell".green()
);
} else {
warn_user!(
"`{}` is not on your PATH. To use installed Python executables, run `{}`.",
bin.simplified_display().cyan(),
command.green()
);
}
} else {
warn_user!(
"`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.",
"`{}` is not on your PATH. To use installed Python executables, add the directory to your PATH.",
bin.simplified_display().cyan(),
);
}
} else {
warn_user!(
"`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.",
"`{}` is not on your PATH. To use installed Python executables, add the directory to your PATH.",
bin.simplified_display().cyan(),
);
}

View File

@ -4,6 +4,7 @@ pub(crate) mod install;
pub(crate) mod list;
pub(crate) mod pin;
pub(crate) mod uninstall;
pub(crate) mod update_shell;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(super) enum ChangeEventKind {

View File

@ -0,0 +1,153 @@
#![cfg_attr(windows, allow(unreachable_code))]
use std::fmt::Write;
use anyhow::Result;
use owo_colors::OwoColorize;
use tokio::io::AsyncWriteExt;
use tracing::debug;
use uv_fs::Simplified;
use uv_python::managed::python_executable_dir;
use uv_shell::Shell;
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Ensure that the executable directory is in PATH.
pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
let executable_directory = python_executable_dir()?;
debug!(
"Ensuring that the executable directory is in PATH: {}",
executable_directory.simplified_display()
);
#[cfg(windows)]
{
if uv_shell::windows::prepend_path(&executable_directory)? {
writeln!(
printer.stderr(),
"Updated PATH to include executable directory {}",
executable_directory.simplified_display().cyan()
)?;
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
} else {
writeln!(
printer.stderr(),
"Executable directory {} is already in PATH",
executable_directory.simplified_display().cyan()
)?;
}
return Ok(ExitStatus::Success);
}
if Shell::contains_path(&executable_directory) {
writeln!(
printer.stderr(),
"Executable directory {} is already in PATH",
executable_directory.simplified_display().cyan()
)?;
return Ok(ExitStatus::Success);
}
// Determine the current shell.
let Some(shell) = Shell::from_env() else {
return Err(anyhow::anyhow!(
"The executable directory {} is not in PATH, but the current shell could not be determined",
executable_directory.simplified_display().cyan()
));
};
// Look up the configuration files (e.g., `.bashrc`, `.zshrc`) for the shell.
let files = shell.configuration_files();
if files.is_empty() {
return Err(anyhow::anyhow!(
"The executable directory {} is not in PATH, but updating {shell} is currently unsupported",
executable_directory.simplified_display().cyan()
));
}
// Prepare the command (e.g., `export PATH="$HOME/.cargo/bin:$PATH"`).
let Some(command) = shell.prepend_path(&executable_directory) else {
return Err(anyhow::anyhow!(
"The executable directory {} is not in PATH, but the necessary command to update {shell} could not be determined",
executable_directory.simplified_display().cyan()
));
};
// Update each file, as necessary.
let mut updated = false;
for file in files {
// Search for the command in the file, to avoid redundant updates.
match fs_err::tokio::read_to_string(&file).await {
Ok(contents) => {
if contents
.lines()
.map(str::trim)
.filter(|line| !line.starts_with('#'))
.any(|line| line.contains(&command))
{
debug!(
"Skipping already-updated configuration file: {}",
file.simplified_display()
);
continue;
}
// Append the command to the file.
fs_err::tokio::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&file)
.await?
.write_all(format!("{contents}\n# uv\n{command}\n").as_bytes())
.await?;
writeln!(
printer.stderr(),
"Updated configuration file: {}",
file.simplified_display().cyan()
)?;
updated = true;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
// Ensure that the directory containing the file exists.
if let Some(parent) = file.parent() {
fs_err::tokio::create_dir_all(&parent).await?;
}
// Append the command to the file.
fs_err::tokio::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&file)
.await?
.write_all(format!("# uv\n{command}\n").as_bytes())
.await?;
writeln!(
printer.stderr(),
"Created configuration file: {}",
file.simplified_display().cyan()
)?;
updated = true;
}
Err(err) => {
return Err(err.into());
}
}
}
if updated {
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
Ok(ExitStatus::Success)
} else {
Err(anyhow::anyhow!(
"The executable directory {} is not in PATH, but the {shell} configuration files are already up-to-date",
executable_directory.simplified_display().cyan()
))
}
}

View File

@ -1537,6 +1537,12 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
commands::python_dir(args.bin)?;
Ok(ExitStatus::Success)
}
Commands::Python(PythonNamespace {
command: PythonCommand::UpdateShell,
}) => {
commands::python_update_shell(printer).await?;
Ok(ExitStatus::Success)
}
Commands::Publish(args) => {
show_settings!(args);

View File

@ -290,14 +290,15 @@ fn help_subcommand() {
Usage: uv python [OPTIONS] <COMMAND>
Commands:
list List the available Python installations
install Download and install Python versions
upgrade Upgrade installed Python versions to the latest supported patch release (requires the
`--preview` flag)
find Search for a Python installation
pin Pin to a specific Python version
dir Show the uv Python installation directory
uninstall Uninstall Python versions
list List the available Python installations
install Download and install Python versions
upgrade Upgrade installed Python versions to the latest supported patch release (requires
the `--preview` flag)
find Search for a Python installation
pin Pin to a specific Python version
dir Show the uv Python installation directory
uninstall Uninstall Python versions
update-shell Ensure that the Python executable directory is on the `PATH`
Cache options:
-n, --no-cache
@ -725,14 +726,15 @@ fn help_flag_subcommand() {
Usage: uv python [OPTIONS] <COMMAND>
Commands:
list List the available Python installations
install Download and install Python versions
upgrade Upgrade installed Python versions to the latest supported patch release (requires the
`--preview` flag)
find Search for a Python installation
pin Pin to a specific Python version
dir Show the uv Python installation directory
uninstall Uninstall Python versions
list List the available Python installations
install Download and install Python versions
upgrade Upgrade installed Python versions to the latest supported patch release (requires
the `--preview` flag)
find Search for a Python installation
pin Pin to a specific Python version
dir Show the uv Python installation directory
uninstall Uninstall Python versions
update-shell Ensure that the Python executable directory is on the `PATH`
Cache options:
-n, --no-cache Avoid reading from or writing to the cache, instead using a temporary
@ -934,6 +936,7 @@ fn help_unknown_subsubcommand() {
pin
dir
uninstall
update-shell
");
}

View File

@ -2633,6 +2633,7 @@ uv python [OPTIONS] <COMMAND>
<dt><a href="#uv-python-pin"><code>uv python pin</code></a></dt><dd><p>Pin to a specific Python version</p></dd>
<dt><a href="#uv-python-dir"><code>uv python dir</code></a></dt><dd><p>Show the uv Python installation directory</p></dd>
<dt><a href="#uv-python-uninstall"><code>uv python uninstall</code></a></dt><dd><p>Uninstall Python versions</p></dd>
<dt><a href="#uv-python-update-shell"><code>uv python update-shell</code></a></dt><dd><p>Ensure that the Python executable directory is on the <code>PATH</code></p></dd>
</dl>
### uv python list
@ -3206,6 +3207,70 @@ uv python uninstall [OPTIONS] <TARGETS>...
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>
### uv python update-shell
Ensure that the Python executable directory is on the `PATH`.
If the Python executable directory is not present on the `PATH`, uv will attempt to add it to the relevant shell configuration files.
If the shell configuration files already include a blurb to add the executable directory to the path, but the directory is not present on the `PATH`, uv will exit with an error.
The Python executable directory is determined according to the XDG standard and can be retrieved with `uv python dir --bin`.
<h3 class="cli-reference">Usage</h3>
```
uv python update-shell [OPTIONS]
```
<h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-python-update-shell--allow-insecure-host"><a href="#uv-python-update-shell--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
<p>WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-python-update-shell--cache-dir"><a href="#uv-python-update-shell--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p></dd><dt id="uv-python-update-shell--color"><a href="#uv-python-update-shell--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
<p>By default, uv will automatically detect support for colors when writing to a terminal.</p>
<p>Possible values:</p>
<ul>
<li><code>auto</code>: Enables colored output only when the output is going to a terminal or TTY with support</li>
<li><code>always</code>: Enables colored output regardless of the detected environment</li>
<li><code>never</code>: Disables colored output</li>
</ul></dd><dt id="uv-python-update-shell--config-file"><a href="#uv-python-update-shell--config-file"><code>--config-file</code></a> <i>config-file</i></dt><dd><p>The path to a <code>uv.toml</code> file to use for configuration.</p>
<p>While uv configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>
<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p></dd><dt id="uv-python-update-shell--directory"><a href="#uv-python-update-shell--directory"><code>--directory</code></a> <i>directory</i></dt><dd><p>Change to the given directory prior to running the command.</p>
<p>Relative paths are resolved with the given directory as the base.</p>
<p>See <code>--project</code> to only change the project root directory.</p>
</dd><dt id="uv-python-update-shell--help"><a href="#uv-python-update-shell--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt id="uv-python-update-shell--managed-python"><a href="#uv-python-update-shell--managed-python"><code>--managed-python</code></a></dt><dd><p>Require use of uv-managed Python versions.</p>
<p>By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.</p>
<p>May also be set with the <code>UV_MANAGED_PYTHON</code> environment variable.</p></dd><dt id="uv-python-update-shell--native-tls"><a href="#uv-python-update-shell--native-tls"><code>--native-tls</code></a></dt><dd><p>Whether to load TLS certificates from the platform's native certificate store.</p>
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
<p>However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.</p>
<p>May also be set with the <code>UV_NATIVE_TLS</code> environment variable.</p></dd><dt id="uv-python-update-shell--no-cache"><a href="#uv-python-update-shell--no-cache"><code>--no-cache</code></a>, <code>--no-cache-dir</code>, <code>-n</code></dt><dd><p>Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation</p>
<p>May also be set with the <code>UV_NO_CACHE</code> environment variable.</p></dd><dt id="uv-python-update-shell--no-config"><a href="#uv-python-update-shell--no-config"><code>--no-config</code></a></dt><dd><p>Avoid discovering configuration files (<code>pyproject.toml</code>, <code>uv.toml</code>).</p>
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p></dd><dt id="uv-python-update-shell--no-managed-python"><a href="#uv-python-update-shell--no-managed-python"><code>--no-managed-python</code></a></dt><dd><p>Disable use of uv-managed Python versions.</p>
<p>Instead, uv will search for a suitable Python version on the system.</p>
<p>May also be set with the <code>UV_NO_MANAGED_PYTHON</code> environment variable.</p></dd><dt id="uv-python-update-shell--no-progress"><a href="#uv-python-update-shell--no-progress"><code>--no-progress</code></a></dt><dd><p>Hide all progress outputs.</p>
<p>For example, spinners or progress bars.</p>
<p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-python-update-shell--no-python-downloads"><a href="#uv-python-update-shell--no-python-downloads"><code>--no-python-downloads</code></a></dt><dd><p>Disable automatic downloads of Python.</p>
</dd><dt id="uv-python-update-shell--offline"><a href="#uv-python-update-shell--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
<p>When disabled, uv will only use locally cached data and locally available files.</p>
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-python-update-shell--project"><a href="#uv-python-update-shell--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
<p>See <code>--directory</code> to change the working directory entirely.</p>
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
<p>May also be set with the <code>UV_PROJECT</code> environment variable.</p></dd><dt id="uv-python-update-shell--quiet"><a href="#uv-python-update-shell--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Use quiet output.</p>
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
</dd><dt id="uv-python-update-shell--verbose"><a href="#uv-python-update-shell--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
</dd></dl>
## uv pip
Manage Python packages with a pip-compatible interface