mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-21 07:42:05 +00:00
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:

This commit is contained in:
parent
3116c371a7
commit
995fba8fec
7 changed files with 88 additions and 30 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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()?;
|
||||||
|
|
|
@ -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()?;
|
||||||
|
|
|
@ -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()?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue