diff --git a/crates/uv-interpreter/src/discovery.rs b/crates/uv-interpreter/src/discovery.rs index c761750c2..05c9b16f8 100644 --- a/crates/uv-interpreter/src/discovery.rs +++ b/crates/uv-interpreter/src/discovery.rs @@ -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), + // Only consider system interpreter sources + System, + // Only consider virtual environment sources + VirtualEnv, + // Only consider a custom set of sources + Custom(HashSet), } /// 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 { 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) -> 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() diff --git a/crates/uv-interpreter/src/environment.rs b/crates/uv-interpreter/src/environment.rs index 9484e4ead..65abab115 100644 --- a/crates/uv-interpreter/src/environment.rs +++ b/crates/uv-interpreter/src/environment.rs @@ -51,10 +51,7 @@ impl PythonEnvironment { /// Create a [`PythonEnvironment`] for an existing virtual environment. pub fn from_virtualenv(cache: &Cache) -> Result { - 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 { - 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 { diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index ee6501546..51397ddd1 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -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( [ diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 76e55d7dc..6768c25b8 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -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() { diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index affcc9d6f..5486e9de9 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -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) diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 7d4ea665b..579a99230 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -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()); }