Consolidate PythonPreference enforcement (#18567)

This commit is contained in:
Zanie Blue
2026-03-19 10:01:03 -05:00
committed by GitHub
parent bd2e0c9b09
commit d7da792648
5 changed files with 105 additions and 110 deletions
+85 -99
View File
@@ -784,9 +784,7 @@ fn python_installations<'a>(
false
}
})
.filter_ok(move |installation| {
satisfies_python_preference(installation.source, &installation.interpreter, preference)
});
.filter_ok(move |installation| preference.allows_installation(installation));
if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
Either::Left(installations.map_ok(|mut installation| {
@@ -930,95 +928,6 @@ 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 => {
if is_system_interpreter(source, interpreter) {
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
}
}
}
}
}
pub(crate) fn is_system_interpreter(source: PythonSource, interpreter: &Interpreter) -> bool {
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,
}
}
/// 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.
@@ -1133,7 +1042,7 @@ pub fn find_python_installations<'a>(
match request {
PythonRequest::File(path) => Box::new(iter::once({
if preference.allows(PythonSource::ProvidedPath) {
if preference.allows_source(PythonSource::ProvidedPath) {
debug!("Checking for Python interpreter at {request}");
match python_installation_from_executable(path, cache) {
Ok(installation) => Ok(Ok(installation)),
@@ -1159,7 +1068,7 @@ pub fn find_python_installations<'a>(
}
})),
PythonRequest::Directory(path) => Box::new(iter::once({
if preference.allows(PythonSource::ProvidedPath) {
if preference.allows_source(PythonSource::ProvidedPath) {
debug!("Checking for Python interpreter in {request}");
match python_installation_from_directory(path, cache) {
Ok(installation) => Ok(Ok(installation)),
@@ -1185,7 +1094,7 @@ pub fn find_python_installations<'a>(
}
})),
PythonRequest::ExecutableName(name) => {
if preference.allows(PythonSource::SearchPath) {
if preference.allows_source(PythonSource::SearchPath) {
debug!("Searching for Python interpreter with {request}");
Box::new(
python_installations_with_executable_name(name, cache)
@@ -1400,9 +1309,7 @@ pub(crate) fn find_python_installation(
// If it's a managed Python installation, and system interpreters are preferred, skip it
// for now.
if matches!(preference, PythonPreference::System)
&& !is_system_interpreter(installation.source, installation.interpreter())
{
if matches!(preference, PythonPreference::System) && installation.is_managed() {
debug!(
"Skipping managed installation {}: system installation preferred",
installation.key()
@@ -2393,7 +2300,7 @@ impl PythonSource {
}
impl PythonPreference {
fn allows(self, source: PythonSource) -> bool {
fn allows_source(self, source: PythonSource) -> bool {
// If not dealing with a system interpreter source, we don't care about the preference
if !matches!(
source,
@@ -2421,6 +2328,85 @@ impl PythonPreference {
}
}
/// Returns `true` if the given interpreter is allowed by this preference.
///
/// Unlike [`PythonPreference::allows_source`], which checks the [`PythonSource`], this checks
/// whether the interpreter's base prefix is in a managed location.
pub fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
match self {
Self::OnlyManaged => interpreter.is_managed(),
Self::OnlySystem => !interpreter.is_managed(),
Self::Managed | Self::System => true,
}
}
/// Returns `true` if the given installation is allowed by this preference.
///
/// Explicit sources (e.g., provided paths, active environments) are always allowed, even if
/// they conflict with the preference.
pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
let source = installation.source;
let interpreter = &installation.interpreter;
// 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 self {
Self::OnlyManaged => {
if self.allows_interpreter(interpreter) {
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
Self::Managed | Self::System => true,
Self::OnlySystem => {
if self.allows_interpreter(interpreter) {
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
}
}
}
}
/// Returns a new preference when the `--system` flag is used.
///
/// This will convert [`PythonPreference::Managed`] to [`PythonPreference::System`] when system
+15
View File
@@ -38,6 +38,14 @@ pub struct PythonInstallation {
}
impl PythonInstallation {
/// Create a new [`PythonInstallation`] from a source and interpreter.
pub fn new(source: PythonSource, interpreter: Interpreter) -> Self {
Self {
source,
interpreter,
}
}
/// Find an installed [`PythonInstallation`].
///
/// This is the standard interface for discovering a Python installation for creating
@@ -350,6 +358,13 @@ impl PythonInstallation {
LenientImplementationName::from(self.interpreter.implementation_name())
}
/// Returns `true` if this is a managed (uv-installed) Python installation.
///
/// Uses the source as a fast path, then falls back to checking the interpreter's base prefix.
pub fn is_managed(&self) -> bool {
self.source.is_managed() || self.interpreter.is_managed()
}
/// Whether this is a CPython installation.
///
/// Returns false if it is an alternative implementation, e.g., PyPy.
+1 -1
View File
@@ -8,7 +8,7 @@ use uv_static::EnvVars;
pub use crate::discovery::{
EnvironmentPreference, Error as DiscoveryError, PythonDownloads, PythonNotFound,
PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
find_python_installations, satisfies_python_preference,
find_python_installations,
};
pub use crate::downloads::PlatformRequest;
pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};