Improve display of selected interpreter sources (#3748)

e.g. this error message is not great

```
❯ uv venv --python 3.12.2
  × No interpreter found for Python 3.12.2 in provided path, search path, managed toolchains, or parent interpreter
```
This commit is contained in:
Zanie Blue 2024-05-22 14:39:22 -04:00 committed by GitHub
parent 6962147831
commit 0e75eb8d78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 103 additions and 75 deletions

View file

@ -44,9 +44,15 @@ pub enum InterpreterRequest {
/// The sources to consider when finding a Python interpreter.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SourceSelector {
// Consider all interpreter sources.
#[default]
All,
Some(HashSet<InterpreterSource>),
// Only consider system interpreter sources
System,
// Only consider virtual environment sources
VirtualEnv,
// Only consider a custom set of sources
Custom(HashSet<InterpreterSource>),
}
/// A Python interpreter version request.
@ -579,11 +585,7 @@ pub fn find_interpreter(
/// See [`find_interpreter`] for more details on interpreter discovery.
pub fn find_default_interpreter(cache: &Cache) -> Result<InterpreterResult, Error> {
let request = InterpreterRequest::Version(VersionRequest::Default);
let sources = SourceSelector::from_sources([
InterpreterSource::SearchPath,
#[cfg(windows)]
InterpreterSource::PyLauncher,
]);
let sources = SourceSelector::System;
let result = find_interpreter(&request, SystemPython::Required, &sources, cache)?;
if let Ok(ref found) = result {
@ -612,7 +614,7 @@ pub fn find_best_interpreter(
debug!("Starting interpreter discovery for {}", request);
// Determine if we should be allowed to look outside of virtual environments.
let sources = SourceSelector::from_env(system);
let sources = SourceSelector::from_settings(system);
// First, check for an exact match (or the first available version if no Python versfion was provided)
debug!("Looking for exact match for request {request}");
@ -1079,19 +1081,33 @@ impl SourceSelector {
pub(crate) fn from_sources(iter: impl IntoIterator<Item = InterpreterSource>) -> Self {
let inner = HashSet::from_iter(iter);
assert!(!inner.is_empty(), "Source selectors cannot be empty");
Self::Some(inner)
Self::Custom(inner)
}
/// Return true if this selector includes the given [`InterpreterSource`].
fn contains(&self, source: InterpreterSource) -> bool {
match self {
Self::All => true,
Self::Some(sources) => sources.contains(&source),
Self::System => [
InterpreterSource::ProvidedPath,
InterpreterSource::SearchPath,
#[cfg(windows)]
InterpreterSource::PyLauncher,
InterpreterSource::ManagedToolchain,
InterpreterSource::ParentInterpreter,
]
.contains(&source),
Self::VirtualEnv => [
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::ActiveEnvironment,
]
.contains(&source),
Self::Custom(sources) => sources.contains(&source),
}
}
/// Return the default [`SourceSelector`] based on environment variables.
pub fn from_env(system: SystemPython) -> Self {
/// Return a [`SourceSelector`] based the settings.
pub fn from_settings(system: SystemPython) -> Self {
if env::var_os("UV_FORCE_MANAGED_PYTHON").is_some() {
debug!("Only considering managed toolchains due to `UV_FORCE_MANAGED_PYTHON`");
Self::from_sources([InterpreterSource::ManagedToolchain])
@ -1106,18 +1122,8 @@ impl SourceSelector {
} else {
match system {
SystemPython::Allowed | SystemPython::Explicit => Self::All,
SystemPython::Required => Self::from_sources([
InterpreterSource::ProvidedPath,
InterpreterSource::SearchPath,
#[cfg(windows)]
InterpreterSource::PyLauncher,
InterpreterSource::ManagedToolchain,
InterpreterSource::ParentInterpreter,
]),
SystemPython::Disallowed => Self::from_sources([
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::ActiveEnvironment,
]),
SystemPython::Required => Self::System,
SystemPython::Disallowed => Self::VirtualEnv,
}
}
}
@ -1232,7 +1238,21 @@ impl fmt::Display for SourceSelector {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::All => f.write_str("all sources"),
Self::Some(sources) => {
Self::VirtualEnv => f.write_str("virtual environments"),
Self::System => {
// TODO(zanieb): We intentionally omit managed toolchains for now since they are not public
if cfg!(windows) {
write!(
f,
"{} or {}",
InterpreterSource::SearchPath,
InterpreterSource::PyLauncher
)
} else {
write!(f, "{}", InterpreterSource::SearchPath)
}
}
Self::Custom(sources) => {
let sources: Vec<_> = sources
.iter()
.sorted()

View file

@ -51,10 +51,7 @@ impl PythonEnvironment {
/// Create a [`PythonEnvironment`] for an existing virtual environment.
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
let sources = SourceSelector::from_sources([
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::ActiveEnvironment,
]);
let sources = SourceSelector::VirtualEnv;
let request = InterpreterRequest::Version(VersionRequest::Default);
let found = find_interpreter(&request, SystemPython::Disallowed, &sources, cache)??;
@ -109,7 +106,7 @@ impl PythonEnvironment {
system: SystemPython,
cache: &Cache,
) -> Result<Self, Error> {
let sources = SourceSelector::from_env(system);
let sources = SourceSelector::from_settings(system);
let request = InterpreterRequest::parse(request);
let interpreter = find_interpreter(&request, system, &sources, cache)??.into_interpreter();
Ok(Self(Arc::new(PythonEnvironmentShared {

View file

@ -700,12 +700,7 @@ mod tests {
fn find_interpreter_version_minor() -> Result<()> {
let tempdir = TempDir::new()?;
let cache = Cache::temp()?;
let sources = SourceSelector::from_sources([
InterpreterSource::ProvidedPath,
InterpreterSource::ActiveEnvironment,
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::SearchPath,
]);
let sources = SourceSelector::All;
with_vars(
[
@ -757,12 +752,7 @@ mod tests {
fn find_interpreter_version_patch() -> Result<()> {
let tempdir = TempDir::new()?;
let cache = Cache::temp()?;
let sources = SourceSelector::from_sources([
InterpreterSource::ProvidedPath,
InterpreterSource::ActiveEnvironment,
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::SearchPath,
]);
let sources = SourceSelector::All;
with_vars(
[
@ -814,12 +804,7 @@ mod tests {
fn find_interpreter_version_minor_no_match() -> Result<()> {
let tempdir = TempDir::new()?;
let cache = Cache::temp()?;
let sources = SourceSelector::from_sources([
InterpreterSource::ProvidedPath,
InterpreterSource::ActiveEnvironment,
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::SearchPath,
]);
let sources = SourceSelector::All;
with_vars(
[
@ -861,12 +846,7 @@ mod tests {
fn find_interpreter_version_patch_no_match() -> Result<()> {
let tempdir = TempDir::new()?;
let cache = Cache::temp()?;
let sources = SourceSelector::from_sources([
InterpreterSource::ProvidedPath,
InterpreterSource::ActiveEnvironment,
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::SearchPath,
]);
let sources = SourceSelector::All;
with_vars(
[

View file

@ -170,7 +170,7 @@ pub(crate) async fn pip_compile(
};
let interpreter = if let Some(python) = python.as_ref() {
let request = InterpreterRequest::parse(python);
let sources = SourceSelector::from_env(system);
let sources = SourceSelector::from_settings(system);
find_interpreter(&request, system, &sources, &cache)??
} else {
let request = if let Some(version) = python_version.as_ref() {

View file

@ -121,7 +121,7 @@ async fn venv_impl(
let interpreter = if let Some(python) = python_request.as_ref() {
let system = uv_interpreter::SystemPython::Required;
let request = InterpreterRequest::parse(python);
let sources = SourceSelector::from_env(system);
let sources = SourceSelector::from_settings(system);
find_interpreter(&request, system, &sources, cache)
} else {
find_default_interpreter(cache)

View file

@ -218,20 +218,33 @@ fn create_venv_unknown_python_minor() {
let mut command = context.venv_command();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
.arg("--python")
.arg("3.15");
.arg("3.100")
// Unset this variable to force what the user would see
.env_remove("UV_TEST_PYTHON_PATH");
// Note the `py` launcher is not included in the search in Windows due to
// `UV_TEST_PYTHON_PATH` being set
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.15 in active virtual environment or search path
"###
);
----- stderr -----
× No interpreter found for Python 3.100 in search path or `py` launcher output
"###
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.100 in search path
"###
);
}
context.venv.assert(predicates::path::missing());
}
@ -240,18 +253,36 @@ fn create_venv_unknown_python_minor() {
fn create_venv_unknown_python_patch() {
let context = VenvTestContext::new(&["3.12"]);
uv_snapshot!(context.filters(), context.venv_command()
let mut command = context.venv_command();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
.arg("--python")
.arg("3.8.0"), @r###"
success: false
exit_code: 1
----- stdout -----
.arg("3.12.100")
// Unset this variable to force what the user would see
.env_remove("UV_TEST_PYTHON_PATH");
----- stderr -----
× No interpreter found for Python 3.8.0 in active virtual environment or search path
"###
);
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in search path or `py` launcher output
"###
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in search path
"###
);
}
context.venv.assert(predicates::path::missing());
}