Surface the EXTERNALLY-MANAGED message to users (#2032)

## 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](dac3bd6b-dd05-4146-8faa-f046492e8a26)
This commit is contained in:
Charlie Marsh 2024-02-27 23:18:45 -05:00 committed by GitHub
parent 3116c371a7
commit 995fba8fec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 88 additions and 30 deletions

1
Cargo.lock generated
View file

@ -4524,6 +4524,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cache-key", "cache-key",
"configparser",
"fs-err", "fs-err",
"indoc", "indoc",
"insta", "insta",

View file

@ -22,6 +22,7 @@ uv-cache = { path = "../uv-cache" }
uv-fs = { path = "../uv-fs" } uv-fs = { path = "../uv-fs" }
install-wheel-rs = { path = "../install-wheel-rs" } install-wheel-rs = { path = "../install-wheel-rs" }
configparser = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] }
once_cell = { workspace = true } once_cell = { workspace = true }
regex = { workspace = true } regex = { workspace = true }

View file

@ -3,6 +3,7 @@ use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use configparser::ini::Ini;
use fs_err as fs; use fs_err as fs;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -267,20 +268,41 @@ impl Interpreter {
self.prefix != self.base_prefix 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/> /// 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. // Per the spec, a virtual environment is never externally managed.
if self.is_virtualenv() { if self.is_virtualenv() {
return false; return None;
} }
// Per the spec, the existence of the file is the only requirement. let Ok(contents) =
self.sysconfig_paths fs::read_to_string(self.sysconfig_paths.stdlib.join("EXTERNALLY-MANAGED"))
.stdlib else {
.join("EXTERNALLY-MANAGED") return None;
.is_file() };
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. /// Returns the Python version.
@ -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()`. /// The installation paths returned by `sysconfig.get_paths()`.
/// ///
/// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths> /// See: <https://docs.python.org/3.12/library/sysconfig.html#installation-paths>

View file

@ -121,9 +121,7 @@ fn find_python(
} }
Err(Error::PyList(error)) => { Err(Error::PyList(error)) => {
if error.kind() == std::io::ErrorKind::NotFound { if error.kind() == std::io::ErrorKind::NotFound {
tracing::debug!( debug!("`py` is not installed. Falling back to searching Python on the path");
"`py` is not installed. Falling back to searching Python on the path"
);
// Continue searching for python installations on the path. // Continue searching for python installations on the path.
} }
} }
@ -156,7 +154,7 @@ fn find_python(
return Err(Error::Python2OrOlder); return Err(Error::Python2OrOlder);
} }
// Skip over Python 2 or older installation when querying for a recent python installation. // 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; continue;
} }
Err(error) => return Err(error), Err(error) => return Err(error),

View file

@ -118,12 +118,19 @@ pub(crate) async fn pip_install(
); );
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file. if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if venv.interpreter().externally_managed() { return if let Some(error) = externally_managed.into_error() {
return Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"The environment at {} is externally managed", "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() 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()?; let _lock = venv.lock()?;

View file

@ -86,12 +86,19 @@ pub(crate) async fn pip_sync(
); );
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file. if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if venv.interpreter().externally_managed() { return if let Some(error) = externally_managed.into_error() {
return Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"The environment at {} is externally managed", "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() 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()?; let _lock = venv.lock()?;

View file

@ -52,12 +52,19 @@ pub(crate) async fn pip_uninstall(
); );
// If the environment is externally managed, abort. // If the environment is externally managed, abort.
// TODO(charlie): Surface the error from the `EXTERNALLY-MANAGED` file. if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if venv.interpreter().externally_managed() { return if let Some(error) = externally_managed.into_error() {
return Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"The environment at {} is externally managed", "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() 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()?; let _lock = venv.lock()?;