Skip to content

Commit ce2d7d7

Browse files
committed
Validate that discovered interpreters meet the Python preference
1 parent e673e74 commit ce2d7d7

5 files changed

Lines changed: 191 additions & 33 deletions

File tree

crates/uv-python/src/discovery.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,9 @@ fn python_interpreters<'a>(
594594
)
595595
.filter(move |result| result_satisfies_environment_preference(result, environments))
596596
.filter(move |result| result_satisfies_version_request(result, version))
597+
// Ensure that interpreters, e.g., from the search path, meet the preference for managed
598+
// interpreters since the user can link them to arbitrary locations.
599+
.filter(move |result| result_satisfies_python_preference(result, preference))
597600
}
598601

599602
/// Lazily convert Python executables into interpreters.
@@ -696,6 +699,101 @@ fn result_satisfies_environment_preference(
696699
})
697700
}
698701

702+
/// Returns true if a Python interpreter matches the [`PythonPreference`].
703+
pub fn satisfies_python_preference(
704+
source: PythonSource,
705+
interpreter: &Interpreter,
706+
preference: PythonPreference,
707+
) -> bool {
708+
// If the source is "explicit", we will not apply the Python preference, e.g., if the user has
709+
// activated a virtual environment, we should always respect allow it. We may want to invalidate
710+
// the environment in some cases, like in projects, but we can't distinguish between explicit
711+
// requests for a different Python preference or a persistent preference in a configuration file
712+
// which would result in overly aggressive invalidation.
713+
let is_explicit = match source {
714+
PythonSource::ProvidedPath
715+
| PythonSource::ParentInterpreter
716+
| PythonSource::ActiveEnvironment
717+
| PythonSource::CondaPrefix => true,
718+
PythonSource::Managed
719+
| PythonSource::DiscoveredEnvironment
720+
| PythonSource::SearchPath
721+
| PythonSource::Registry
722+
| PythonSource::MicrosoftStore
723+
| PythonSource::BaseCondaPrefix => false,
724+
};
725+
726+
match preference {
727+
PythonPreference::OnlyManaged => {
728+
// Perform a fast check using the source before querying the interpreter
729+
if matches!(source, PythonSource::Managed) || interpreter.is_managed() {
730+
true
731+
} else {
732+
if is_explicit {
733+
debug!(
734+
"Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
735+
interpreter.sys_executable().display()
736+
);
737+
true
738+
} else {
739+
debug!(
740+
"Ignoring Python interpreter at `{}`: only managed interpreters allowed",
741+
interpreter.sys_executable().display()
742+
);
743+
false
744+
}
745+
}
746+
}
747+
// If not "only" a kind, any interpreter is okay
748+
PythonPreference::Managed | PythonPreference::System => true,
749+
PythonPreference::OnlySystem => {
750+
let is_system = match source {
751+
// A managed interpreter is never a system interpreter
752+
PythonSource::Managed => false,
753+
// We can't be sure if this is a system interpreter without checking
754+
PythonSource::ProvidedPath
755+
| PythonSource::ParentInterpreter
756+
| PythonSource::ActiveEnvironment
757+
| PythonSource::CondaPrefix
758+
| PythonSource::DiscoveredEnvironment
759+
| PythonSource::SearchPath
760+
| PythonSource::Registry
761+
| PythonSource::BaseCondaPrefix => !interpreter.is_managed(),
762+
// Managed interpreters should never be found in the store
763+
PythonSource::MicrosoftStore => true,
764+
};
765+
766+
if is_system {
767+
true
768+
} else {
769+
if is_explicit {
770+
debug!(
771+
"Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
772+
interpreter.sys_executable().display()
773+
);
774+
true
775+
} else {
776+
debug!(
777+
"Ignoring Python interpreter at `{}`: only system interpreters allowed",
778+
interpreter.sys_executable().display()
779+
);
780+
false
781+
}
782+
}
783+
}
784+
}
785+
}
786+
787+
/// Utility for applying [`satisfies_python_preference`] to a result type.
788+
fn result_satisfies_python_preference(
789+
result: &Result<(PythonSource, Interpreter), Error>,
790+
preference: PythonPreference,
791+
) -> bool {
792+
result.as_ref().ok().map_or(true, |(source, interpreter)| {
793+
satisfies_python_preference(*source, interpreter, preference)
794+
})
795+
}
796+
699797
/// Check if an encountered error is critical and should stop discovery.
700798
///
701799
/// Returns false when an error could be due to a faulty Python installation and we should continue searching for a working one.
@@ -2295,6 +2393,18 @@ impl PythonPreference {
22952393
}
22962394
}
22972395
}
2396+
2397+
/// Return the canonical name.
2398+
// TODO(zanieb): This should be a `Display` impl and we should have a different view for
2399+
// the sources
2400+
pub fn canonical_name(&self) -> &'static str {
2401+
match self {
2402+
Self::OnlyManaged => "only managed",
2403+
Self::Managed => "prefer managed",
2404+
Self::System => "prefer system",
2405+
Self::OnlySystem => "only system",
2406+
}
2407+
}
22982408
}
22992409

23002410
impl fmt::Display for PythonPreference {

crates/uv-python/src/environment.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,7 @@ impl PythonEnvironment {
145145
let installation = match find_python_installation(
146146
request,
147147
preference,
148-
// Ignore managed installations when looking for environments
149-
PythonPreference::OnlySystem,
148+
PythonPreference::default(),
150149
cache,
151150
)? {
152151
Ok(installation) => installation,

crates/uv-python/src/interpreter.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use uv_platform_tags::{Tags, TagsError};
2323
use uv_pypi_types::{ResolverMarkerEnvironment, Scheme};
2424

2525
use crate::implementation::LenientImplementationName;
26+
use crate::managed::ManagedPythonInstallations;
2627
use crate::platform::{Arch, Libc, Os};
2728
use crate::pointer_size::PointerSize;
2829
use crate::{
@@ -225,6 +226,21 @@ impl Interpreter {
225226
self.prefix.is_some()
226227
}
227228

229+
/// Returns `true` if this interpreter is managed by uv.
230+
///
231+
/// Returns `false` if we cannot determine the path of the uv managed Python interpreters.
232+
pub fn is_managed(&self) -> bool {
233+
let Ok(installations) = ManagedPythonInstallations::from_settings() else {
234+
return false;
235+
};
236+
237+
installations
238+
.find_all()
239+
.into_iter()
240+
.flatten()
241+
.any(|install| install.path() == self.sys_base_prefix)
242+
}
243+
228244
/// Returns `Some` if the environment is externally managed, optionally including an error
229245
/// message from the `EXTERNALLY-MANAGED` file.
230246
///

crates/uv-python/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use thiserror::Error;
55
use uv_static::EnvVars;
66

77
pub use crate::discovery::{
8-
find_python_installations, EnvironmentPreference, Error as DiscoveryError, PythonDownloads,
9-
PythonNotFound, PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
8+
find_python_installations, satisfies_python_preference, EnvironmentPreference,
9+
Error as DiscoveryError, PythonDownloads, PythonNotFound, PythonPreference, PythonRequest,
10+
PythonSource, PythonVariant, VersionRequest,
1011
};
1112
pub use crate::environment::{InvalidEnvironment, InvalidEnvironmentKind, PythonEnvironment};
1213
pub use crate::implementation::ImplementationName;

crates/uv/src/commands/project/mod.rs

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
33

44
use itertools::Itertools;
55
use owo_colors::OwoColorize;
6-
use tracing::debug;
6+
use tracing::{debug, trace};
77

88
use uv_cache::Cache;
99
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
@@ -24,9 +24,9 @@ use uv_pep440::{Version, VersionSpecifiers};
2424
use uv_pep508::MarkerTreeContents;
2525
use uv_pypi_types::Requirement;
2626
use uv_python::{
27-
EnvironmentPreference, Interpreter, InvalidEnvironmentKind, PythonDownloads, PythonEnvironment,
28-
PythonInstallation, PythonPreference, PythonRequest, PythonVariant, PythonVersionFile,
29-
VersionFileDiscoveryOptions, VersionRequest,
27+
satisfies_python_preference, EnvironmentPreference, Interpreter, InvalidEnvironmentKind,
28+
PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest,
29+
PythonSource, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions, VersionRequest,
3030
};
3131
use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements};
3232
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
@@ -522,6 +522,55 @@ impl ScriptPython {
522522
}
523523
}
524524

525+
fn environment_satisfies_requirements(
526+
environment: &PythonEnvironment,
527+
python_request: Option<&PythonRequest>,
528+
python_preference: PythonPreference,
529+
requires_python: Option<&RequiresPython>,
530+
cache: &Cache,
531+
) -> bool {
532+
if let Some(request) = python_request {
533+
if request.satisfied(environment.interpreter(), cache) {
534+
debug!("The virtual environment's Python version satisfies `{request}`");
535+
} else {
536+
debug!("The virtual environment's Python version does not satisfy `{request}`");
537+
return false;
538+
}
539+
};
540+
541+
if let Some(requires_python) = requires_python.as_ref() {
542+
if requires_python.contains(environment.interpreter().python_version()) {
543+
trace!(
544+
"The virtual environment's Python version meets the project's Python requirement: `{requires_python}`"
545+
);
546+
} else {
547+
debug!(
548+
"The virtual environment's Python version does not meet the project's Python requirement: `{requires_python}`"
549+
);
550+
return false;
551+
}
552+
}
553+
554+
if satisfies_python_preference(
555+
PythonSource::DiscoveredEnvironment,
556+
environment.interpreter(),
557+
python_preference,
558+
) {
559+
trace!(
560+
"The virtual environment's interpreter satisfies the `python-preference`: {}",
561+
python_preference.canonical_name()
562+
);
563+
} else {
564+
debug!(
565+
"The virtual environment's interpreter does not satisfy the `python-preference`: {}",
566+
python_preference.canonical_name()
567+
);
568+
return false;
569+
}
570+
571+
true
572+
}
573+
525574
impl ProjectInterpreter {
526575
/// Discover the interpreter to use in the current [`Workspace`].
527576
pub(crate) async fn discover(
@@ -549,31 +598,14 @@ impl ProjectInterpreter {
549598
let venv = workspace.venv();
550599
match PythonEnvironment::from_root(&venv, cache) {
551600
Ok(venv) => {
552-
if python_request.as_ref().map_or(true, |request| {
553-
if request.satisfied(venv.interpreter(), cache) {
554-
debug!(
555-
"The virtual environment's Python version satisfies `{}`",
556-
request.to_canonical_string()
557-
);
558-
true
559-
} else {
560-
debug!(
561-
"The virtual environment's Python version does not satisfy `{}`",
562-
request.to_canonical_string()
563-
);
564-
false
565-
}
566-
}) {
567-
if let Some(requires_python) = requires_python.as_ref() {
568-
if requires_python.contains(venv.interpreter().python_version()) {
569-
return Ok(Self::Environment(venv));
570-
}
571-
debug!(
572-
"The virtual environment's Python version does not meet the project's Python requirement: `{requires_python}`"
573-
);
574-
} else {
575-
return Ok(Self::Environment(venv));
576-
}
601+
if environment_satisfies_requirements(
602+
&venv,
603+
python_request.as_ref(),
604+
python_preference,
605+
requires_python.as_ref(),
606+
cache,
607+
) {
608+
return Ok(Self::Environment(venv));
577609
}
578610
}
579611
Err(uv_python::Error::MissingEnvironment(_)) => {}

0 commit comments

Comments
 (0)