Skip to content

Commit

Permalink
Surface the EXTERNALLY-MANAGED message to users (#2032)
Browse files Browse the repository at this point in the history
## Summary

Per the
[spec](https://packaging.python.org/en/latest/specifications/externally-managed-environments/),
this message should be surfaced to users:

![Screenshot 2024-02-27 at 10 42
52 PM](https://github.com/astral-sh/uv/assets/1309177/dac3bd6b-dd05-4146-8faa-f046492e8a26)
  • Loading branch information
charliermarsh authored Feb 28, 2024
1 parent 3116c37 commit 995fba8
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 30 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/uv-interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ uv-cache = { path = "../uv-cache" }
uv-fs = { path = "../uv-fs" }
install-wheel-rs = { path = "../install-wheel-rs" }

configparser = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
once_cell = { workspace = true }
regex = { workspace = true }
Expand Down
53 changes: 45 additions & 8 deletions crates/uv-interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

use configparser::ini::Ini;
use fs_err as fs;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -267,20 +268,41 @@ impl Interpreter {
self.prefix != self.base_prefix
}

/// Returns `true` if the environment is externally managed.
/// Returns `Some` if the environment is externally managed, optionally including an error
/// message from the `EXTERNALLY-MANAGED` file.
///
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
pub fn externally_managed(&self) -> bool {
pub fn is_externally_managed(&self) -> Option<ExternallyManaged> {
// Per the spec, a virtual environment is never externally managed.
if self.is_virtualenv() {
return false;
return None;
}

// Per the spec, the existence of the file is the only requirement.
self.sysconfig_paths
.stdlib
.join("EXTERNALLY-MANAGED")
.is_file()
let Ok(contents) =
fs::read_to_string(self.sysconfig_paths.stdlib.join("EXTERNALLY-MANAGED"))
else {
return None;
};

let Ok(mut ini) = Ini::new_cs().read(contents) else {
// If a file exists but is not a valid INI file, we assume the environment is
// externally managed.
return Some(ExternallyManaged::default());
};

let Some(section) = ini.get_mut("externally-managed") else {
// If the file exists but does not contain an "externally-managed" section, we assume
// the environment is externally managed.
return Some(ExternallyManaged::default());
};

let Some(error) = section.remove("Error") else {
// If the file exists but does not contain an "Error" key, we assume the environment is
// externally managed.
return Some(ExternallyManaged::default());
};

Some(ExternallyManaged { error })
}

/// Returns the Python version.
Expand Down Expand Up @@ -403,6 +425,21 @@ impl Interpreter {
}
}

/// The `EXTERNALLY-MANAGED` file in a Python installation.
///
/// See: <https://packaging.python.org/en/latest/specifications/externally-managed-environments/>
#[derive(Debug, Default, Clone)]
pub struct ExternallyManaged {
error: Option<String>,
}

impl ExternallyManaged {
/// Return the `EXTERNALLY-MANAGED` error message, if any.
pub fn into_error(self) -> Option<String> {
self.error
}
}

/// The installation paths returned by `sysconfig.get_paths()`.
///
/// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>
Expand Down
6 changes: 2 additions & 4 deletions crates/uv-interpreter/src/python_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@ fn find_python(
}
Err(Error::PyList(error)) => {
if error.kind() == std::io::ErrorKind::NotFound {
tracing::debug!(
"`py` is not installed. Falling back to searching Python on the path"
);
debug!("`py` is not installed. Falling back to searching Python on the path");
// Continue searching for python installations on the path.
}
}
Expand Down Expand Up @@ -156,7 +154,7 @@ fn find_python(
return Err(Error::Python2OrOlder);
}
// Skip over Python 2 or older installation when querying for a recent python installation.
tracing::debug!("Found a Python 2 installation that isn't supported by uv, skipping.");
debug!("Found a Python 2 installation that isn't supported by uv, skipping.");
continue;
}
Err(error) => return Err(error),
Expand Down
19 changes: 13 additions & 6 deletions crates/uv/src/commands/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,19 @@ pub(crate) async fn pip_install(
);

// If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
if venv.interpreter().externally_managed() {
return Err(anyhow::anyhow!(
"The environment at {} is externally managed",
venv.root().normalized_display()
));
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan()
))
};
}

let _lock = venv.lock()?;
Expand Down
19 changes: 13 additions & 6 deletions crates/uv/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,19 @@ pub(crate) async fn pip_sync(
);

// If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
if venv.interpreter().externally_managed() {
return Err(anyhow::anyhow!(
"The environment at {} is externally managed",
venv.root().normalized_display()
));
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan()
))
};
}

let _lock = venv.lock()?;
Expand Down
19 changes: 13 additions & 6 deletions crates/uv/src/commands/pip_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ pub(crate) async fn pip_uninstall(
);

// If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file.
if venv.interpreter().externally_managed() {
return Err(anyhow::anyhow!(
"The environment at {} is externally managed",
venv.root().normalized_display()
));
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().normalized_display().cyan()
))
};
}

let _lock = venv.lock()?;
Expand Down

0 comments on commit 995fba8

Please sign in to comment.