Skip to content

Commit 04f50a9

Browse files
committed
Require opt-in to use alternative Python implementations
1 parent 9a6f455 commit 04f50a9

File tree

6 files changed

+100
-14
lines changed

6 files changed

+100
-14
lines changed

crates/uv-python/src/discovery.rs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -934,21 +934,44 @@ pub(crate) fn find_python_installation(
934934
return result;
935935
};
936936

937-
// If it's a pre-release, and pre-releases aren't allowed skip it but store it for later
937+
// Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
938+
// pre-release version or an alternative implementation, using it requires opt-in.
939+
940+
// If the interpreter has a default executable name, e.g. `python`, and was found on the
941+
// search path, we consider this opt-in to use it.
942+
let has_default_executable_name = installation.interpreter.has_default_executable_name()
943+
&& installation.source == PythonSource::SearchPath;
944+
945+
// If it's a pre-release and pre-releases aren't allowed, skip it — but store it for later
946+
// since we'll use a pre-release if no other versions are available.
938947
if installation.python_version().pre().is_some()
939948
&& !request.allows_prereleases()
940949
&& !installation.source.allows_prereleases()
950+
&& !has_default_executable_name
941951
{
942952
debug!("Skipping pre-release {}", installation.key());
943953
first_prerelease = Some(installation.clone());
944954
continue;
945955
}
946956

957+
// If it's an alternative implementation and alternative implementations aren't allowed,
958+
// skip it. Note we avoid querying these interpreters at all if they're on the search path
959+
// and are not requested, but other sources such as the managed installations will include
960+
// them.
961+
if installation.is_alternative_implementation()
962+
&& !request.allows_alternative_implementations()
963+
&& !installation.source.allows_alternative_implementations()
964+
&& !has_default_executable_name
965+
{
966+
debug!("Skipping alternative implementation {}", installation.key());
967+
continue;
968+
}
969+
947970
// If we didn't skip it, this is the installation to use
948971
return result;
949972
}
950973

951-
// If we only found pre-releases, they're implicitly allowed and we should return the first one
974+
// If we only found pre-releases, they're implicitly allowed and we should return the first one.
952975
if let Some(installation) = first_prerelease {
953976
return Ok(Ok(installation));
954977
}
@@ -1205,10 +1228,7 @@ impl PythonRequest {
12051228
for implementation in
12061229
ImplementationName::long_names().chain(ImplementationName::short_names())
12071230
{
1208-
if let Some(remainder) = value
1209-
.to_ascii_lowercase()
1210-
.strip_prefix(Into::<&str>::into(implementation))
1211-
{
1231+
if let Some(remainder) = value.to_ascii_lowercase().strip_prefix(implementation) {
12121232
// e.g. `pypy`
12131233
if remainder.is_empty() {
12141234
return Self::Implementation(
@@ -1369,6 +1389,7 @@ impl PythonRequest {
13691389
}
13701390
}
13711391

1392+
/// Whether this request opts-in to a pre-release Python version.
13721393
pub(crate) fn allows_prereleases(&self) -> bool {
13731394
match self {
13741395
Self::Default => false,
@@ -1381,6 +1402,19 @@ impl PythonRequest {
13811402
}
13821403
}
13831404

1405+
/// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
1406+
pub(crate) fn allows_alternative_implementations(&self) -> bool {
1407+
match self {
1408+
Self::Default => false,
1409+
Self::Any => true,
1410+
Self::Version(_) => false,
1411+
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
1412+
Self::Implementation(_) => true,
1413+
Self::ImplementationVersion(_, _) => true,
1414+
Self::Key(request) => request.allows_alternative_implementations(),
1415+
}
1416+
}
1417+
13841418
pub(crate) fn is_explicit_system(&self) -> bool {
13851419
matches!(self, Self::File(_) | Self::Directory(_))
13861420
}
@@ -1410,7 +1444,7 @@ impl PythonSource {
14101444
matches!(self, Self::Managed)
14111445
}
14121446

1413-
/// Whether a pre-release Python installation from the source should be used without opt-in.
1447+
/// Whether a pre-release Python installation from this source can be used without opt-in.
14141448
pub(crate) fn allows_prereleases(self) -> bool {
14151449
match self {
14161450
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
@@ -1422,6 +1456,18 @@ impl PythonSource {
14221456
| Self::DiscoveredEnvironment => true,
14231457
}
14241458
}
1459+
1460+
/// Whether an alternative Python implementation from this source can be used without opt-in.
1461+
pub(crate) fn allows_alternative_implementations(self) -> bool {
1462+
match self {
1463+
Self::Managed | Self::Registry | Self::SearchPath | Self::MicrosoftStore => false,
1464+
Self::CondaPrefix
1465+
| Self::ProvidedPath
1466+
| Self::ParentInterpreter
1467+
| Self::ActiveEnvironment
1468+
| Self::DiscoveredEnvironment => true,
1469+
}
1470+
}
14251471
}
14261472

14271473
impl PythonPreference {

crates/uv-python/src/downloads.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ impl PythonDownloadRequest {
281281
self.satisfied_by_key(download.key())
282282
}
283283

284+
/// Whether this download request opts-in to pre-release Python versions.
284285
pub fn allows_prereleases(&self) -> bool {
285286
self.prereleases.unwrap_or_else(|| {
286287
self.version
@@ -289,6 +290,11 @@ impl PythonDownloadRequest {
289290
})
290291
}
291292

293+
/// Whether this download request opts-in to alternative Python implementations.
294+
pub fn allows_alternative_implementations(&self) -> bool {
295+
self.implementation.is_some()
296+
}
297+
292298
pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
293299
if let Some(version) = self.version() {
294300
if !version.matches_interpreter(interpreter) {

crates/uv-python/src/implementation.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,24 @@ impl LenientImplementationName {
5252
}
5353

5454
impl From<&ImplementationName> for &'static str {
55-
fn from(v: &ImplementationName) -> &'static str {
56-
match v {
55+
fn from(value: &ImplementationName) -> &'static str {
56+
match value {
5757
ImplementationName::CPython => "cpython",
5858
ImplementationName::PyPy => "pypy",
5959
ImplementationName::GraalPy => "graalpy",
6060
}
6161
}
6262
}
6363

64+
impl From<ImplementationName> for &'static str {
65+
fn from(value: ImplementationName) -> &'static str {
66+
(&value).into()
67+
}
68+
}
69+
6470
impl<'a> From<&'a LenientImplementationName> for &'a str {
65-
fn from(v: &'a LenientImplementationName) -> &'a str {
66-
match v {
71+
fn from(value: &'a LenientImplementationName) -> &'a str {
72+
match value {
6773
LenientImplementationName::Known(implementation) => implementation.into(),
6874
LenientImplementationName::Unknown(name) => name,
6975
}

crates/uv-python/src/installation.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use crate::implementation::LenientImplementationName;
1616
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
1717
use crate::platform::{Arch, Libc, Os};
1818
use crate::{
19-
downloads, Error, Interpreter, PythonDownloads, PythonPreference, PythonSource, PythonVersion,
19+
downloads, Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference,
20+
PythonSource, PythonVersion,
2021
};
2122

2223
/// A Python interpreter and accompanying tools.
@@ -176,6 +177,16 @@ impl PythonInstallation {
176177
LenientImplementationName::from(self.interpreter.implementation_name())
177178
}
178179

180+
/// Whether this is a CPython installation.
181+
///
182+
/// Returns false if it is an alternative implementation, e.g., PyPy.
183+
pub(crate) fn is_alternative_implementation(&self) -> bool {
184+
!matches!(
185+
self.implementation(),
186+
LenientImplementationName::Known(ImplementationName::CPython)
187+
)
188+
}
189+
179190
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
180191
pub fn arch(&self) -> Arch {
181192
self.interpreter.arch()

crates/uv-python/src/interpreter.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ use uv_fs::{write_atomic_sync, PythonExt, Simplified};
2525
use crate::implementation::LenientImplementationName;
2626
use crate::platform::{Arch, Libc, Os};
2727
use crate::pointer_size::PointerSize;
28-
use crate::{Prefix, PythonInstallationKey, PythonVersion, Target, VirtualEnvironment};
28+
use crate::{
29+
Prefix, PythonInstallationKey, PythonVersion, Target, VersionRequest, VirtualEnvironment,
30+
};
2931

3032
/// A Python executable and its associated platform markers.
3133
#[derive(Debug, Clone)]
@@ -494,6 +496,21 @@ impl Interpreter {
494496
(version.major(), version.minor()) == self.python_tuple()
495497
}
496498
}
499+
500+
/// Whether or not this Python interpreter is from a default Python executable name, like
501+
/// `python`, `python3`, or `python.exe`.
502+
pub(crate) fn has_default_executable_name(&self) -> bool {
503+
let Some(file_name) = self.sys_executable().file_name() else {
504+
return false;
505+
};
506+
let Some(name) = file_name.to_str() else {
507+
return false;
508+
};
509+
VersionRequest::Default
510+
.executable_names(None)
511+
.into_iter()
512+
.any(|default_name| name == default_name.to_string())
513+
}
497514
}
498515

499516
/// The `EXTERNALLY-MANAGED` file in a Python installation.

crates/uv-python/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1793,7 +1793,7 @@ mod tests {
17931793
})?;
17941794
assert!(
17951795
matches!(result, Err(PythonNotFound { .. })),
1796-
"We should not the pypy interpreter if not named `python` or requested; got {result:?}"
1796+
"We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
17971797
);
17981798

17991799
// But we should find it

0 commit comments

Comments
 (0)