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

View file

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

View file

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

View file

@ -170,7 +170,7 @@ pub(crate) async fn pip_compile(
}; };
let interpreter = if let Some(python) = python.as_ref() { let interpreter = if let Some(python) = python.as_ref() {
let request = InterpreterRequest::parse(python); let request = InterpreterRequest::parse(python);
let sources = SourceSelector::from_env(system); let sources = SourceSelector::from_settings(system);
find_interpreter(&request, system, &sources, &cache)?? find_interpreter(&request, system, &sources, &cache)??
} else { } else {
let request = if let Some(version) = python_version.as_ref() { 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 interpreter = if let Some(python) = python_request.as_ref() {
let system = uv_interpreter::SystemPython::Required; let system = uv_interpreter::SystemPython::Required;
let request = InterpreterRequest::parse(python); let request = InterpreterRequest::parse(python);
let sources = SourceSelector::from_env(system); let sources = SourceSelector::from_settings(system);
find_interpreter(&request, system, &sources, cache) find_interpreter(&request, system, &sources, cache)
} else { } else {
find_default_interpreter(cache) find_default_interpreter(cache)

View file

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