Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3734,6 +3734,10 @@ pub struct PythonListArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct PythonInstallArgs {
/// The directory to store the Python installation in.
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
pub install_dir: Option<PathBuf>,

/// The Python version(s) to install.
///
/// If not provided, the requested Python version(s) will be read from the
Expand All @@ -3755,6 +3759,11 @@ pub struct PythonInstallArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct PythonUninstallArgs {
/// The directory where Python is installed.
///
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
pub install_dir: Option<PathBuf>,

/// The Python version(s) to uninstall.
///
/// See `uv help python` to view supported request formats.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ fn python_executables_from_installed<'a>(
preference: PythonPreference,
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
let from_managed_installations = std::iter::once_with(move || {
ManagedPythonInstallations::from_settings()
ManagedPythonInstallations::from_settings(None)
.map_err(Error::from)
.and_then(|installed_installations| {
debug!(
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl PythonInstallation {
cache: &Cache,
reporter: Option<&dyn Reporter>,
) -> Result<Self, Error> {
let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
let installations_dir = installations.root();
let cache_dir = installations.cache();
let _lock = installations.lock().await?;
Expand Down
15 changes: 9 additions & 6 deletions crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,14 @@ impl ManagedPythonInstallations {
}

/// Prefer, in order:
/// 1. The specific Python directory specified by the user, i.e., `UV_PYTHON_INSTALL_DIR`
/// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`
/// 3. A directory in the local data directory, e.g., `./.uv/python`
pub fn from_settings() -> Result<Self, Error> {
if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
/// 1. The specific Python directory directly passed to the `install_dir` argument
/// 2. The specific Python directory specified with the `UV_PYTHON_INSTALL_DIR` environment variable
/// 3. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`
/// 4. A directory in the local data directory, e.g., `./.uv/python`
pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
if install_dir.is_some() {
Ok(Self::from_path(install_dir.unwrap()))
} else if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
Ok(Self::from_path(install_dir))
} else {
Ok(Self::from_path(
Expand Down Expand Up @@ -197,7 +200,7 @@ impl ManagedPythonInstallations {
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation>, Error> {
let platform_key = platform_key_from_env()?;

let iter = ManagedPythonInstallations::from_settings()?
let iter = ManagedPythonInstallations::from_settings(None)?
.find_all()?
.filter(move |installation| {
installation
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/python/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use uv_python::managed::ManagedPythonInstallations;

/// Show the toolchain directory.
pub(crate) fn dir() -> anyhow::Result<()> {
let installed_toolchains = ManagedPythonInstallations::from_settings()
let installed_toolchains = ManagedPythonInstallations::from_settings(None)
.context("Failed to initialize toolchain settings")?;
println!(
"{}",
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::printer::Printer;
/// Download and install Python versions.
pub(crate) async fn install(
project_dir: &Path,
install_dir: Option<&Path>,
targets: Vec<String>,
reinstall: bool,
python_downloads: PythonDownloads,
Expand All @@ -32,7 +33,8 @@ pub(crate) async fn install(
) -> Result<ExitStatus> {
let start = std::time::Instant::now();

let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations =
ManagedPythonInstallations::from_settings(install_dir.map(|p| p.to_path_buf()))?.init()?;
let installations_dir = installations.root();
let cache_dir = installations.cache();
let _lock = installations.lock().await?;
Expand Down
7 changes: 6 additions & 1 deletion crates/uv/src/commands/python/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ use uv_python::PythonRequest;
use crate::commands::python::{ChangeEvent, ChangeEventKind};
use crate::commands::{elapsed, ExitStatus};
use crate::printer::Printer;
use std::path::Path;

/// Uninstall managed Python versions.
pub(crate) async fn uninstall(
install_dir: Option<&Path>,
targets: Vec<String>,
all: bool,

printer: Printer,
) -> Result<ExitStatus> {
let installations = ManagedPythonInstallations::from_settings()?.init()?;
// need to convert install_dir to Option<PathBuf> to match the function signature
let installations =
ManagedPythonInstallations::from_settings(install_dir.map(|p| p.to_path_buf()))?.init()?;

let _lock = installations.lock().await?;

// Perform the uninstallation.
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {

commands::python_install(
&project_dir,
args.install_dir.as_deref(),
args.targets,
args.reinstall,
globals.python_downloads,
Expand All @@ -1053,7 +1054,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
let args = settings::PythonUninstallSettings::resolve(args, filesystem);
show_settings!(args);

commands::python_uninstall(args.targets, args.all, printer).await
commands::python_uninstall(args.install_dir.as_deref(), args.targets, args.all, printer)
.await
}
Commands::Python(PythonNamespace {
command: PythonCommand::Find(args),
Expand Down
26 changes: 22 additions & 4 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ impl PythonListSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct PythonInstallSettings {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) targets: Vec<String>,
pub(crate) reinstall: bool,
}
Expand All @@ -600,16 +601,25 @@ impl PythonInstallSettings {
/// Resolve the [`PythonInstallSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonInstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonInstallArgs { targets, reinstall } = args;
let PythonInstallArgs {
install_dir,
targets,
reinstall,
} = args;

Self { targets, reinstall }
Self {
install_dir,
targets,
reinstall,
}
}
}

/// The resolved settings to use for a `python uninstall` invocation.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct PythonUninstallSettings {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) targets: Vec<String>,
pub(crate) all: bool,
}
Expand All @@ -621,9 +631,17 @@ impl PythonUninstallSettings {
args: PythonUninstallArgs,
_filesystem: Option<FilesystemOptions>,
) -> Self {
let PythonUninstallArgs { targets, all } = args;
let PythonUninstallArgs {
install_dir,
targets,
all,
} = args;

Self { targets, all }
Self {
install_dir,
targets,
all,
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,7 @@ pub fn venv_to_interpreter(venv: &Path) -> PathBuf {

/// Get the path to the python interpreter for a specific python version.
pub fn get_python(version: &PythonVersion) -> PathBuf {
ManagedPythonInstallations::from_settings()
ManagedPythonInstallations::from_settings(None)
.map(|installed_pythons| {
installed_pythons
.find_version(version)
Expand Down
13 changes: 10 additions & 3 deletions crates/uv/tests/it/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,11 @@ fn help_subsubcommand() {
See `uv help python` to view supported request formats.

Options:
-i, --install-dir <INSTALL_DIR>
The directory where Python will be installed

[env: UV_PYTHON_INSTALL_DIR=]

-r, --reinstall
Reinstall the requested Python version, if it's already installed.

Expand Down Expand Up @@ -620,7 +625,7 @@ fn help_flag_subcommand() {
fn help_flag_subsubcommand() {
let context = TestContext::new_with_versions(&[]);

uv_snapshot!(context.filters(), context.command().arg("python").arg("install").arg("--help"), @r###"
uv_snapshot!(context.filters(), context.command().arg("python").arg("install").arg("--help"), @r#"
success: true
exit_code: 0
----- stdout -----
Expand All @@ -632,7 +637,9 @@ fn help_flag_subsubcommand() {
[TARGETS]... The Python version(s) to install

Options:
-r, --reinstall Reinstall the requested Python version, if it's already installed
-i, --install-dir <INSTALL_DIR> The directory where Python will be installed [env:
UV_PYTHON_INSTALL_DIR=]
-r, --reinstall Reinstall the requested Python version, if it's already installed

Cache options:
-n, --no-cache Avoid reading from or writing to the cache, instead using a temporary
Expand Down Expand Up @@ -665,7 +672,7 @@ fn help_flag_subsubcommand() {
-V, --version Display the uv version

----- stderr -----
"###);
"#);
}

#[test]
Expand Down
14 changes: 9 additions & 5 deletions crates/uv/tests/it/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,18 @@ fn missing_venv() -> Result<()> {
requirements.write_str("anyio")?;
fs::remove_dir_all(&context.venv)?;

uv_snapshot!(context.filters(), context.pip_sync().arg("requirements.txt"), @r###"
success: false
exit_code: 2
uv_snapshot!(context.filters(), context.pip_sync().arg("requirements.txt"), @r#"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment
"###);
Using Python 3.12.[X] environment at /home/dan/.venv
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this expected? it has my absolute path here...

Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ anyio==4.3.0
"#);

assert!(predicates::path::missing().eval(&context.venv));

Expand Down
6 changes: 6 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4200,6 +4200,9 @@ uv python install [OPTIONS] [TARGETS]...

</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>

</dd><dt><code>--install-dir</code>, <code>-i</code> <i>install-dir</i></dt><dd><p>The directory where Python will be installed</p>

<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;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>
Expand Down Expand Up @@ -4677,6 +4680,9 @@ uv python uninstall [OPTIONS] <TARGETS>...

</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>

</dd><dt><code>--install-dir</code>, <code>-i</code> <i>install-dir</i></dt><dd><p>The directory where Python is installed</p>

<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;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>
Expand Down