From 94c125a1eef52ee12aa59d64881986e62b8737eb Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 21 Apr 2025 16:12:19 -0500 Subject: [PATCH] Validate that discovered interpreters meet the Python preference --- crates/uv-python/src/discovery.rs | 102 ++++++++++++++++++++++++++ crates/uv-python/src/environment.rs | 3 +- crates/uv-python/src/interpreter.rs | 16 ++++ crates/uv-python/src/lib.rs | 5 +- crates/uv/src/commands/project/mod.rs | 25 ++++++- 5 files changed, 144 insertions(+), 7 deletions(-) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index ce09933cf..740b6a3f5 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -661,6 +661,9 @@ fn python_interpreters<'a>( false } }) + .filter_ok(move |(source, interpreter)| { + satisfies_python_preference(*source, interpreter, preference) + }) } /// Lazily convert Python executables into interpreters. @@ -788,6 +791,93 @@ fn source_satisfies_environment_preference( } } +/// Returns true if a Python interpreter matches the [`PythonPreference`]. +pub fn satisfies_python_preference( + source: PythonSource, + interpreter: &Interpreter, + preference: PythonPreference, +) -> bool { + // If the source is "explicit", we will not apply the Python preference, e.g., if the user has + // activated a virtual environment, we should always allow it. We may want to invalidate the + // environment in some cases, like in projects, but we can't distinguish between explicit + // requests for a different Python preference or a persistent preference in a configuration file + // which would result in overly aggressive invalidation. + let is_explicit = match source { + PythonSource::ProvidedPath + | PythonSource::ParentInterpreter + | PythonSource::ActiveEnvironment + | PythonSource::CondaPrefix => true, + PythonSource::Managed + | PythonSource::DiscoveredEnvironment + | PythonSource::SearchPath + | PythonSource::SearchPathFirst + | PythonSource::Registry + | PythonSource::MicrosoftStore + | PythonSource::BaseCondaPrefix => false, + }; + + match preference { + PythonPreference::OnlyManaged => { + // Perform a fast check using the source before querying the interpreter + if matches!(source, PythonSource::Managed) || interpreter.is_managed() { + true + } else { + if is_explicit { + debug!( + "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}", + interpreter.sys_executable().display() + ); + true + } else { + debug!( + "Ignoring Python interpreter at `{}`: only managed interpreters allowed", + interpreter.sys_executable().display() + ); + false + } + } + } + // If not "only" a kind, any interpreter is okay + PythonPreference::Managed | PythonPreference::System => true, + PythonPreference::OnlySystem => { + let is_system = match source { + // A managed interpreter is never a system interpreter + PythonSource::Managed => false, + // We can't be sure if this is a system interpreter without checking + PythonSource::ProvidedPath + | PythonSource::ParentInterpreter + | PythonSource::ActiveEnvironment + | PythonSource::CondaPrefix + | PythonSource::DiscoveredEnvironment + | PythonSource::SearchPath + | PythonSource::SearchPathFirst + | PythonSource::Registry + | PythonSource::BaseCondaPrefix => !interpreter.is_managed(), + // Managed interpreters should never be found in the store + PythonSource::MicrosoftStore => true, + }; + + if is_system { + true + } else { + if is_explicit { + debug!( + "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}", + interpreter.sys_executable().display() + ); + true + } else { + debug!( + "Ignoring Python interpreter at `{}`: only system interpreters allowed", + interpreter.sys_executable().display() + ); + false + } + } + } + } +} + /// Check if an encountered error is critical and should stop discovery. /// /// Returns false when an error could be due to a faulty Python installation and we should continue searching for a working one. @@ -2576,6 +2666,18 @@ impl PythonPreference { } } } + + /// Return the canonical name. + // TODO(zanieb): This should be a `Display` impl and we should have a different view for + // the sources + pub fn canonical_name(&self) -> &'static str { + match self { + Self::OnlyManaged => "only managed", + Self::Managed => "prefer managed", + Self::System => "prefer system", + Self::OnlySystem => "only system", + } + } } impl fmt::Display for PythonPreference { diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index dfc65359a..a7ffb31d0 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -150,8 +150,7 @@ impl PythonEnvironment { let installation = match find_python_installation( request, preference, - // Ignore managed installations when looking for environments - PythonPreference::OnlySystem, + PythonPreference::default(), cache, )? { Ok(installation) => installation, diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 4e4132b0d..5cffed5ea 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -26,6 +26,7 @@ use uv_platform_tags::{Tags, TagsError}; use uv_pypi_types::{ResolverMarkerEnvironment, Scheme}; use crate::implementation::LenientImplementationName; +use crate::managed::ManagedPythonInstallations; use crate::platform::{Arch, Libc, Os}; use crate::pointer_size::PointerSize; use crate::{ @@ -263,6 +264,21 @@ impl Interpreter { self.prefix.is_some() } + /// Returns `true` if this interpreter is managed by uv. + /// + /// Returns `false` if we cannot determine the path of the uv managed Python interpreters. + pub fn is_managed(&self) -> bool { + let Ok(installations) = ManagedPythonInstallations::from_settings(None) else { + return false; + }; + + installations + .find_all() + .into_iter() + .flatten() + .any(|install| install.path() == self.sys_base_prefix) + } + /// Returns `Some` if the environment is externally managed, optionally including an error /// message from the `EXTERNALLY-MANAGED` file. /// diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index ca723db6d..34b8c827d 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -5,8 +5,9 @@ use thiserror::Error; use uv_static::EnvVars; pub use crate::discovery::{ - find_python_installations, EnvironmentPreference, Error as DiscoveryError, PythonDownloads, - PythonNotFound, PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest, + find_python_installations, satisfies_python_preference, EnvironmentPreference, + Error as DiscoveryError, PythonDownloads, PythonNotFound, PythonPreference, PythonRequest, + PythonSource, PythonVariant, VersionRequest, }; pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment}; pub use crate::implementation::ImplementationName; diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index f522a132d..3aa368d78 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -29,9 +29,9 @@ use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; use uv_pypi_types::{ConflictPackage, ConflictSet, Conflicts}; use uv_python::{ - EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads, PythonEnvironment, - PythonInstallation, PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, - VersionFileDiscoveryOptions, VersionRequest, + satisfies_python_preference, EnvironmentPreference, Interpreter, InvalidEnvironmentKind, + PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, + PythonSource, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, VersionRequest, }; use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification}; @@ -770,6 +770,7 @@ impl ScriptInterpreter { fn environment_is_usable( environment: &PythonEnvironment, python_request: Option<&PythonRequest>, + python_preference: PythonPreference, requires_python: Option<&RequiresPython>, cache: &Cache, ) -> bool { @@ -800,6 +801,23 @@ fn environment_is_usable( } } + if satisfies_python_preference( + PythonSource::DiscoveredEnvironment, + environment.interpreter(), + python_preference, + ) { + trace!( + "The virtual environment's Python interpreter meets the Python preference: `{}`", + python_preference + ); + } else { + debug!( + "The virtual environment's Python interpreter does not meet the Python preference: `{}`", + python_preference + ); + return false; + } + true } @@ -843,6 +861,7 @@ impl ProjectInterpreter { if environment_is_usable( &venv, python_request.as_ref(), + python_preference, requires_python.as_ref(), cache, ) {