Validate that discovered interpreters meet the Python preference

This commit is contained in:
Zanie Blue 2025-04-21 16:12:19 -05:00
parent cda72b297f
commit 94c125a1ee
5 changed files with 144 additions and 7 deletions

View file

@ -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 {

View file

@ -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,

View file

@ -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.
///

View file

@ -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;

View file

@ -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,
) {