mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 02:48:17 +00:00
Require opt-in to use alternative Python implementations (#7650)
Closes #7118 This only really affects managed interpreters, as we exclude alternative Python implementations from the search path during the `VersionRequest::executable_names` part of discovery.
This commit is contained in:
parent
da328379c1
commit
a53ddaa24a
6 changed files with 100 additions and 14 deletions
|
@ -934,21 +934,44 @@ pub(crate) fn find_python_installation(
|
|||
return result;
|
||||
};
|
||||
|
||||
// If it's a pre-release, and pre-releases aren't allowed skip it but store it for later
|
||||
// Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
|
||||
// pre-release version or an alternative implementation, using it requires opt-in.
|
||||
|
||||
// If the interpreter has a default executable name, e.g. `python`, and was found on the
|
||||
// search path, we consider this opt-in to use it.
|
||||
let has_default_executable_name = installation.interpreter.has_default_executable_name()
|
||||
&& installation.source == PythonSource::SearchPath;
|
||||
|
||||
// If it's a pre-release and pre-releases aren't allowed, skip it — but store it for later
|
||||
// since we'll use a pre-release if no other versions are available.
|
||||
if installation.python_version().pre().is_some()
|
||||
&& !request.allows_prereleases()
|
||||
&& !installation.source.allows_prereleases()
|
||||
&& !has_default_executable_name
|
||||
{
|
||||
debug!("Skipping pre-release {}", installation.key());
|
||||
first_prerelease = Some(installation.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's an alternative implementation and alternative implementations aren't allowed,
|
||||
// skip it. Note we avoid querying these interpreters at all if they're on the search path
|
||||
// and are not requested, but other sources such as the managed installations will include
|
||||
// them.
|
||||
if installation.is_alternative_implementation()
|
||||
&& !request.allows_alternative_implementations()
|
||||
&& !installation.source.allows_alternative_implementations()
|
||||
&& !has_default_executable_name
|
||||
{
|
||||
debug!("Skipping alternative implementation {}", installation.key());
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we didn't skip it, this is the installation to use
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we only found pre-releases, they're implicitly allowed and we should return the first one
|
||||
// If we only found pre-releases, they're implicitly allowed and we should return the first one.
|
||||
if let Some(installation) = first_prerelease {
|
||||
return Ok(Ok(installation));
|
||||
}
|
||||
|
@ -1205,10 +1228,7 @@ impl PythonRequest {
|
|||
for implementation in
|
||||
ImplementationName::long_names().chain(ImplementationName::short_names())
|
||||
{
|
||||
if let Some(remainder) = value
|
||||
.to_ascii_lowercase()
|
||||
.strip_prefix(Into::<&str>::into(implementation))
|
||||
{
|
||||
if let Some(remainder) = value.to_ascii_lowercase().strip_prefix(implementation) {
|
||||
// e.g. `pypy`
|
||||
if remainder.is_empty() {
|
||||
return Self::Implementation(
|
||||
|
@ -1369,6 +1389,7 @@ impl PythonRequest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether this request opts-in to a pre-release Python version.
|
||||
pub(crate) fn allows_prereleases(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
|
@ -1381,6 +1402,19 @@ impl PythonRequest {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
|
||||
pub(crate) fn allows_alternative_implementations(&self) -> bool {
|
||||
match self {
|
||||
Self::Default => false,
|
||||
Self::Any => true,
|
||||
Self::Version(_) => false,
|
||||
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
|
||||
Self::Implementation(_) => true,
|
||||
Self::ImplementationVersion(_, _) => true,
|
||||
Self::Key(request) => request.allows_alternative_implementations(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_explicit_system(&self) -> bool {
|
||||
matches!(self, Self::File(_) | Self::Directory(_))
|
||||
}
|
||||
|
@ -1410,7 +1444,7 @@ impl PythonSource {
|
|||
matches!(self, Self::Managed)
|
||||
}
|
||||
|
||||
/// Whether a pre-release Python installation from the source should be used without opt-in.
|
||||
/// Whether a pre-release Python installation from this source can be used without opt-in.
|
||||
pub(crate) fn allows_prereleases(self) -> bool {
|
||||
match self {
|
||||
Self::Managed | Self::Registry | Self::MicrosoftStore => false,
|
||||
|
@ -1422,6 +1456,18 @@ impl PythonSource {
|
|||
| Self::DiscoveredEnvironment => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an alternative Python implementation from this source can be used without opt-in.
|
||||
pub(crate) fn allows_alternative_implementations(self) -> bool {
|
||||
match self {
|
||||
Self::Managed | Self::Registry | Self::SearchPath | Self::MicrosoftStore => false,
|
||||
Self::CondaPrefix
|
||||
| Self::ProvidedPath
|
||||
| Self::ParentInterpreter
|
||||
| Self::ActiveEnvironment
|
||||
| Self::DiscoveredEnvironment => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PythonPreference {
|
||||
|
|
|
@ -281,6 +281,7 @@ impl PythonDownloadRequest {
|
|||
self.satisfied_by_key(download.key())
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to pre-release Python versions.
|
||||
pub fn allows_prereleases(&self) -> bool {
|
||||
self.prereleases.unwrap_or_else(|| {
|
||||
self.version
|
||||
|
@ -289,6 +290,11 @@ impl PythonDownloadRequest {
|
|||
})
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to alternative Python implementations.
|
||||
pub fn allows_alternative_implementations(&self) -> bool {
|
||||
self.implementation.is_some()
|
||||
}
|
||||
|
||||
pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
|
||||
if let Some(version) = self.version() {
|
||||
if !version.matches_interpreter(interpreter) {
|
||||
|
|
|
@ -52,8 +52,8 @@ impl LenientImplementationName {
|
|||
}
|
||||
|
||||
impl From<&ImplementationName> for &'static str {
|
||||
fn from(v: &ImplementationName) -> &'static str {
|
||||
match v {
|
||||
fn from(value: &ImplementationName) -> &'static str {
|
||||
match value {
|
||||
ImplementationName::CPython => "cpython",
|
||||
ImplementationName::PyPy => "pypy",
|
||||
ImplementationName::GraalPy => "graalpy",
|
||||
|
@ -61,9 +61,15 @@ impl From<&ImplementationName> for &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ImplementationName> for &'static str {
|
||||
fn from(value: ImplementationName) -> &'static str {
|
||||
(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a LenientImplementationName> for &'a str {
|
||||
fn from(v: &'a LenientImplementationName) -> &'a str {
|
||||
match v {
|
||||
fn from(value: &'a LenientImplementationName) -> &'a str {
|
||||
match value {
|
||||
LenientImplementationName::Known(implementation) => implementation.into(),
|
||||
LenientImplementationName::Unknown(name) => name,
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ use crate::implementation::LenientImplementationName;
|
|||
use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
|
||||
use crate::platform::{Arch, Libc, Os};
|
||||
use crate::{
|
||||
downloads, Error, Interpreter, PythonDownloads, PythonPreference, PythonSource, PythonVersion,
|
||||
downloads, Error, ImplementationName, Interpreter, PythonDownloads, PythonPreference,
|
||||
PythonSource, PythonVersion,
|
||||
};
|
||||
|
||||
/// A Python interpreter and accompanying tools.
|
||||
|
@ -176,6 +177,16 @@ impl PythonInstallation {
|
|||
LenientImplementationName::from(self.interpreter.implementation_name())
|
||||
}
|
||||
|
||||
/// Whether this is a CPython installation.
|
||||
///
|
||||
/// Returns false if it is an alternative implementation, e.g., PyPy.
|
||||
pub(crate) fn is_alternative_implementation(&self) -> bool {
|
||||
!matches!(
|
||||
self.implementation(),
|
||||
LenientImplementationName::Known(ImplementationName::CPython)
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
|
||||
pub fn arch(&self) -> Arch {
|
||||
self.interpreter.arch()
|
||||
|
|
|
@ -25,7 +25,9 @@ use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
|||
use crate::implementation::LenientImplementationName;
|
||||
use crate::platform::{Arch, Libc, Os};
|
||||
use crate::pointer_size::PointerSize;
|
||||
use crate::{Prefix, PythonInstallationKey, PythonVersion, Target, VirtualEnvironment};
|
||||
use crate::{
|
||||
Prefix, PythonInstallationKey, PythonVersion, Target, VersionRequest, VirtualEnvironment,
|
||||
};
|
||||
|
||||
/// A Python executable and its associated platform markers.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -494,6 +496,21 @@ impl Interpreter {
|
|||
(version.major(), version.minor()) == self.python_tuple()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this Python interpreter is from a default Python executable name, like
|
||||
/// `python`, `python3`, or `python.exe`.
|
||||
pub(crate) fn has_default_executable_name(&self) -> bool {
|
||||
let Some(file_name) = self.sys_executable().file_name() else {
|
||||
return false;
|
||||
};
|
||||
let Some(name) = file_name.to_str() else {
|
||||
return false;
|
||||
};
|
||||
VersionRequest::Default
|
||||
.executable_names(None)
|
||||
.into_iter()
|
||||
.any(|default_name| name == default_name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `EXTERNALLY-MANAGED` file in a Python installation.
|
||||
|
|
|
@ -1793,7 +1793,7 @@ mod tests {
|
|||
})?;
|
||||
assert!(
|
||||
matches!(result, Err(PythonNotFound { .. })),
|
||||
"We should not the pypy interpreter if not named `python` or requested; got {result:?}"
|
||||
"We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
|
||||
);
|
||||
|
||||
// But we should find it
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue