Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use uv_pep440::{
use uv_static::EnvVars;
use uv_warnings::warn_user_once;

use crate::downloads::PythonDownloadRequest;
use crate::downloads::{PlatformRequest, PythonDownloadRequest};
use crate::implementation::ImplementationName;
use crate::installation::PythonInstallation;
use crate::interpreter::Error as InterpreterError;
Expand Down Expand Up @@ -312,6 +312,7 @@ fn python_executables_from_virtual_environments<'a>()
fn python_executables_from_installed<'a>(
version: &'a VersionRequest,
implementation: Option<&'a ImplementationName>,
platform: PlatformRequest,
preference: PythonPreference,
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
let from_managed_installations = iter::once_with(move || {
Expand All @@ -323,16 +324,19 @@ fn python_executables_from_installed<'a>(
installed_installations.root().user_display()
);
let installations = installed_installations.find_matching_current_platform()?;
// Check that the Python version satisfies the request to avoid unnecessary interpreter queries later
// Check that the Python version and platform satisfy the request to avoid unnecessary interpreter queries later
Ok(installations
.into_iter()
.filter(move |installation| {
if version.matches_version(&installation.version()) {
true
} else {
debug!("Skipping incompatible managed installation `{installation}`");
false
if !version.matches_version(&installation.version()) {
debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
return false;
}
if !platform.matches(installation.key()) {
debug!("Skipping managed installation `{installation}`: does not satisfy `{platform}`");
return false;
}
true
})
.inspect(|installation| debug!("Found managed installation `{installation}`"))
.map(|installation| (PythonSource::Managed, installation.executable(false))))
Expand Down Expand Up @@ -415,15 +419,17 @@ fn python_executables_from_installed<'a>(

/// Lazily iterate over all discoverable Python executables.
///
/// Note that Python executables may be excluded by the given [`EnvironmentPreference`] and
/// [`PythonPreference`]. However, these filters are only applied for performance. We cannot
/// guarantee that the [`EnvironmentPreference`] is satisfied until we query the interpreter.
/// Note that Python executables may be excluded by the given [`EnvironmentPreference`],
/// [`PythonPreference`], and [`PlatformRequest`]. However, these filters are only applied for
/// performance. We cannot guarantee that the all requests or preferences are satisfied until we
/// query the interpreter.
Comment on lines +423 to +425
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like with this change it also impacts correctness?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, it prevents us from querying an interpreter that is likely to fail to run on the current platform, but I don't really intend for that to be a responsibility of this function. It's a nice side-effect that this pull request makes that unlikely, but it's more about performance.

///
/// See [`python_executables_from_installed`] and [`python_executables_from_virtual_environments`]
/// for more information on discovery.
fn python_executables<'a>(
version: &'a VersionRequest,
implementation: Option<&'a ImplementationName>,
platform: PlatformRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
Expand All @@ -445,7 +451,8 @@ fn python_executables<'a>(
.flatten();

let from_virtual_environments = python_executables_from_virtual_environments();
let from_installed = python_executables_from_installed(version, implementation, preference);
let from_installed =
python_executables_from_installed(version, implementation, platform, preference);

// Limit the search to the relevant environment preference; this avoids unnecessary work like
// traversal of the file system. Subsequent filtering should be done by the caller with
Expand Down Expand Up @@ -630,12 +637,17 @@ fn find_all_minor(

/// Lazily iterate over all discoverable Python interpreters.
///
/// Note interpreters may be excluded by the given [`EnvironmentPreference`] and [`PythonPreference`].
/// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`],
/// [`VersionRequest`], or [`PlatformRequest`].
///
/// The [`PlatformRequest`] is currently on applied to managed Python installations before querying
/// the interpreter. The caller is responsible for ensuring it is applied otherwise.
Copy link
Contributor

@Gankra Gankra Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "the caller is responsible" mean?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you call this function, you cannot assume that PlatformRequest is enforced

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this is common for pre-filtering operations in this module)

///
/// See [`python_executables`] for more information on discovery.
fn python_interpreters<'a>(
version: &'a VersionRequest,
implementation: Option<&'a ImplementationName>,
platform: PlatformRequest,
environments: EnvironmentPreference,
preference: PythonPreference,
cache: &'a Cache,
Expand All @@ -644,7 +656,7 @@ fn python_interpreters<'a>(
// Perform filtering on the discovered executables based on their source. This avoids
// unnecessary interpreter queries, which are generally expensive. We'll filter again
// with `interpreter_satisfies_environment_preference` after querying.
python_executables(version, implementation, environments, preference).filter_ok(
python_executables(version, implementation, platform, environments, preference).filter_ok(
move |(source, path)| {
source_satisfies_environment_preference(*source, path, environments)
},
Expand Down Expand Up @@ -971,14 +983,22 @@ pub fn find_python_installations<'a>(
}
PythonRequest::Any => Box::new({
debug!("Searching for any Python interpreter in {sources}");
python_interpreters(&VersionRequest::Any, None, environments, preference, cache)
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
python_interpreters(
&VersionRequest::Any,
None,
PlatformRequest::default(),
environments,
preference,
cache,
)
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
}),
PythonRequest::Default => Box::new({
debug!("Searching for default Python interpreter in {sources}");
python_interpreters(
&VersionRequest::Default,
None,
PlatformRequest::default(),
environments,
preference,
cache,
Expand All @@ -991,15 +1011,23 @@ pub fn find_python_installations<'a>(
}
Box::new({
debug!("Searching for {request} in {sources}");
python_interpreters(version, None, environments, preference, cache)
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
python_interpreters(
version,
None,
PlatformRequest::default(),
environments,
preference,
cache,
)
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
})
}
PythonRequest::Implementation(implementation) => Box::new({
debug!("Searching for a {request} interpreter in {sources}");
python_interpreters(
&VersionRequest::Default,
Some(implementation),
PlatformRequest::default(),
environments,
preference,
cache,
Expand All @@ -1020,6 +1048,7 @@ pub fn find_python_installations<'a>(
python_interpreters(
version,
Some(implementation),
PlatformRequest::default(),
environments,
preference,
cache,
Expand All @@ -1043,6 +1072,7 @@ pub fn find_python_installations<'a>(
python_interpreters(
request.version().unwrap_or(&VersionRequest::Default),
request.implementation(),
request.platform(),
environments,
preference,
cache,
Expand Down
57 changes: 57 additions & 0 deletions crates/uv-python/src/downloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,54 @@ pub enum ArchRequest {
Environment(Arch),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PlatformRequest {
pub(crate) os: Option<Os>,
pub(crate) arch: Option<ArchRequest>,
pub(crate) libc: Option<Libc>,
}

impl PlatformRequest {
/// Check if this platform request is satisfied by an installation key.
pub fn matches(&self, key: &PythonInstallationKey) -> bool {
if let Some(os) = self.os {
if key.os != os {
return false;
}
}

if let Some(arch) = self.arch {
if !arch.satisfied_by(key.arch) {
return false;
}
}

if let Some(libc) = self.libc {
if key.libc != libc {
return false;
}
}

true
}
}

impl Display for PlatformRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
if let Some(os) = &self.os {
parts.push(os.to_string());
}
if let Some(arch) = &self.arch {
parts.push(arch.to_string());
}
if let Some(libc) = &self.libc {
parts.push(libc.to_string());
}
write!(f, "{}", parts.join("-"))
}
}

impl Display for ArchRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand Down Expand Up @@ -412,6 +460,15 @@ impl PythonDownloadRequest {
}
true
}

/// Extract the platform components of this request.
pub fn platform(&self) -> PlatformRequest {
PlatformRequest {
os: self.os,
arch: self.arch,
libc: self.libc,
}
}
}

impl From<&ManagedPythonInstallation> for PythonDownloadRequest {
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use crate::discovery::{
PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
find_python_installations,
};
pub use crate::downloads::PlatformRequest;
pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
pub use crate::implementation::ImplementationName;
pub use crate::installation::{PythonInstallation, PythonInstallationKey};
Expand Down
Loading