Skip to content

Commit

Permalink
Improve display of selected interpreter sources (#3748)
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
zanieb authored May 22, 2024
1 parent 6962147 commit 0e75eb8
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 78 deletions.
68 changes: 44 additions & 24 deletions crates/uv-interpreter/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}");
Expand Down Expand Up @@ -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])
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 2 additions & 5 deletions crates/uv-interpreter/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)??;

Expand Down Expand Up @@ -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 {
Expand Down
28 changes: 4 additions & 24 deletions crates/uv-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -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(
[
Expand Down Expand Up @@ -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(
[
Expand Down Expand Up @@ -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(
[
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
77 changes: 54 additions & 23 deletions crates/uv/tests/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

// 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 -----
----- stderr -----
× No interpreter found for Python 3.15 in active virtual environment or search path
"###
);
.arg("3.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
exit_code: 1
----- stdout -----
----- 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());
}
Expand All @@ -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 -----
----- stderr -----
× No interpreter found for Python 3.8.0 in active virtual environment or search path
"###
);
.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
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());
}
Expand Down

0 comments on commit 0e75eb8

Please sign in to comment.