mirror of https://github.com/astral-sh/uv
Stop looking for `-p` in PATH
See #2386 for the motivation. We keep only two behaviours for `-p`, we have an absolute or relative path (containing a system path separator) or `-p <implementation><version>`. Open question: Should `python3.10` or `3.10` force cpython 3.10 or is any python implementation acceptable? My assumption right now is that i assume that a user specifying nothing wouldn't like it if we picked pypy, rather we should force cpython and make pypy explicit. I will add tests tomorrow, please already give the semantics a look.
This commit is contained in:
parent
90a60bc4f2
commit
de3a9161c1
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{cmp, num::NonZeroU32};
|
use std::{cmp, num::NonZeroU32};
|
||||||
|
|
@ -252,8 +253,9 @@ impl TryFrom<usize> for TagPriority {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Implementation {
|
pub enum Implementation {
|
||||||
|
#[default]
|
||||||
CPython,
|
CPython,
|
||||||
PyPy,
|
PyPy,
|
||||||
Pyston,
|
Pyston,
|
||||||
|
|
@ -322,6 +324,16 @@ impl FromStr for Implementation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Implementation {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Implementation::CPython => f.write_str("cpython"),
|
||||||
|
Implementation::PyPy => f.write_str("pypy"),
|
||||||
|
Implementation::Pyston => f.write_str("pyston"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the compatible tags for the current [`Platform`] (e.g., `manylinux_2_17`,
|
/// Returns the compatible tags for the current [`Platform`] (e.g., `manylinux_2_17`,
|
||||||
/// `macosx_11_0_arm64`, or `win_amd64`).
|
/// `macosx_11_0_arm64`, or `win_amd64`).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::{OsStr, OsString};
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use platform_host::Platform;
|
use platform_host::Platform;
|
||||||
|
use platform_tags::Implementation;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_fs::normalize_path;
|
use uv_fs::{normalize_path, Simplified};
|
||||||
|
|
||||||
use crate::python_environment::{detect_python_executable, detect_virtual_env};
|
use crate::python_environment::{detect_python_executable, detect_virtual_env};
|
||||||
use crate::{Error, Interpreter, PythonVersion};
|
use crate::{Error, Interpreter, PythonVersion};
|
||||||
|
|
@ -31,33 +34,55 @@ pub fn find_requested_python(
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
) -> Result<Option<Interpreter>, Error> {
|
) -> Result<Option<Interpreter>, Error> {
|
||||||
debug!("Starting interpreter discovery for Python @ `{request}`");
|
debug!("Starting interpreter discovery for Python @ `{request}`");
|
||||||
let versions = request
|
|
||||||
.splitn(3, '.')
|
if request.contains(std::path::MAIN_SEPARATOR) {
|
||||||
.map(str::parse::<u8>)
|
|
||||||
.collect::<Result<Vec<_>, _>>();
|
|
||||||
if let Ok(versions) = versions {
|
|
||||||
// `-p 3.10` or `-p 3.10.1`
|
|
||||||
let selector = match versions.as_slice() {
|
|
||||||
[requested_major] => PythonVersionSelector::Major(*requested_major),
|
|
||||||
[major, minor] => PythonVersionSelector::MajorMinor(*major, *minor),
|
|
||||||
[major, minor, requested_patch] => {
|
|
||||||
PythonVersionSelector::MajorMinorPatch(*major, *minor, *requested_patch)
|
|
||||||
}
|
|
||||||
// SAFETY: Guaranteed by the Ok(versions) guard
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
find_python(selector, platform, cache)
|
|
||||||
} else if !request.contains(std::path::MAIN_SEPARATOR) {
|
|
||||||
// `-p python3.10`; Generally not used on windows because all Python are `python.exe`.
|
|
||||||
let Some(executable) = find_executable(request)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
Interpreter::query(executable, platform.clone(), cache).map(Some)
|
|
||||||
} else {
|
|
||||||
// `-p /home/ferris/.local/bin/python3.10`
|
// `-p /home/ferris/.local/bin/python3.10`
|
||||||
let executable = normalize_path(request);
|
let executable = normalize_path(request);
|
||||||
|
|
||||||
Interpreter::query(executable, platform.clone(), cache).map(Some)
|
Interpreter::query(executable, platform.clone(), cache).map(Some)
|
||||||
|
} else {
|
||||||
|
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
Regex::new(r"^(?<implementation>[A-Za-z]+)?(?<version>(\d+)?(\.\d+)?(\.\d+)?)?$")
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(captures) = RE.captures(request) {
|
||||||
|
let implementation = if let Some(implementation) = captures.name("implementation") {
|
||||||
|
// We assume that a user specifying python wants cpython and not pypy.
|
||||||
|
if implementation.as_str() == "python" {
|
||||||
|
Implementation::default()
|
||||||
|
} else {
|
||||||
|
Implementation::from_str(&implementation.as_str().to_ascii_lowercase())
|
||||||
|
.map_err(Error::UnrecognizedImplementation)?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Implementation::default()
|
||||||
|
};
|
||||||
|
let versions = if let Some(version) = captures.name("version") {
|
||||||
|
version
|
||||||
|
.as_str()
|
||||||
|
.splitn(3, '.')
|
||||||
|
.map(str::parse::<u8>)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| Error::UnrecognizedPython(request.to_string()))?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
// `-p 3.10` or `-p 3.10.1`
|
||||||
|
let selector = match versions.as_slice() {
|
||||||
|
[] => PythonVersionSelector::Default,
|
||||||
|
[requested_major] => PythonVersionSelector::Major(*requested_major),
|
||||||
|
[major, minor] => PythonVersionSelector::MajorMinor(*major, *minor),
|
||||||
|
[major, minor, requested_patch] => {
|
||||||
|
PythonVersionSelector::MajorMinorPatch(*major, *minor, *requested_patch)
|
||||||
|
}
|
||||||
|
// SAFETY: Guaranteed by the Ok(versions) guard
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
find_python(selector, implementation, platform, cache)
|
||||||
|
} else {
|
||||||
|
Err(Error::UnrecognizedPython(request.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,7 +107,12 @@ pub(crate) fn try_find_default_python(
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
) -> Result<Option<Interpreter>, Error> {
|
) -> Result<Option<Interpreter>, Error> {
|
||||||
find_python(PythonVersionSelector::Default, platform, cache)
|
find_python(
|
||||||
|
PythonVersionSelector::Default,
|
||||||
|
Implementation::default(),
|
||||||
|
platform,
|
||||||
|
cache,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a Python version matching `selector`.
|
/// Find a Python version matching `selector`.
|
||||||
|
|
@ -100,6 +130,7 @@ pub(crate) fn try_find_default_python(
|
||||||
/// (Windows): Filter out the Windows store shim (Enabled in Settings/Apps/Advanced app settings/App execution aliases).
|
/// (Windows): Filter out the Windows store shim (Enabled in Settings/Apps/Advanced app settings/App execution aliases).
|
||||||
fn find_python(
|
fn find_python(
|
||||||
selector: PythonVersionSelector,
|
selector: PythonVersionSelector,
|
||||||
|
requested_implementation: Implementation,
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
) -> Result<Option<Interpreter>, Error> {
|
) -> Result<Option<Interpreter>, Error> {
|
||||||
|
|
@ -139,6 +170,28 @@ fn find_python(
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(konsti): Move this into the `Interpreter` type.
|
||||||
|
let Ok(actual_implementation) =
|
||||||
|
Implementation::from_str(&interpreter.implementation_name().to_lowercase())
|
||||||
|
else {
|
||||||
|
debug!(
|
||||||
|
"Skipping interpreter with unsupported implementation `{}` at `{}`",
|
||||||
|
interpreter.implementation_name(),
|
||||||
|
path.simplified_display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if requested_implementation != actual_implementation {
|
||||||
|
debug!(
|
||||||
|
"Skipping interpreter with implementation `{}` different from the requested `{}` at `{}`",
|
||||||
|
actual_implementation,
|
||||||
|
interpreter.implementation_name(),
|
||||||
|
path.simplified_display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let installation = PythonInstallation::Interpreter(interpreter);
|
let installation = PythonInstallation::Interpreter(interpreter);
|
||||||
|
|
||||||
if let Some(interpreter) = installation.select(selector, platform, cache)? {
|
if let Some(interpreter) = installation.select(selector, platform, cache)? {
|
||||||
|
|
@ -197,76 +250,6 @@ fn find_python(
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the Python interpreter in `PATH` matching the given name (e.g., `python3`, respecting
|
|
||||||
/// `UV_PYTHON_PATH`.
|
|
||||||
///
|
|
||||||
/// Returns `Ok(None)` if not found.
|
|
||||||
fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
|
|
||||||
requested: R,
|
|
||||||
) -> Result<Option<PathBuf>, Error> {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let UV_TEST_PYTHON_PATH = env::var_os("UV_TEST_PYTHON_PATH");
|
|
||||||
|
|
||||||
let use_override = UV_TEST_PYTHON_PATH.is_some();
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let PATH = UV_TEST_PYTHON_PATH
|
|
||||||
.or(env::var_os("PATH"))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// We use `which` here instead of joining the paths ourselves because `which` checks for us if the python
|
|
||||||
// binary is executable and exists. It also has some extra logic that handles inconsistent casing on Windows
|
|
||||||
// and expands `~`.
|
|
||||||
for path in env::split_paths(&PATH) {
|
|
||||||
let paths = match which::which_in_global(requested, Some(&path)) {
|
|
||||||
Ok(paths) => paths,
|
|
||||||
Err(which::Error::CannotFindBinaryPath) => continue,
|
|
||||||
Err(err) => return Err(Error::WhichError(requested.into(), err)),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(clippy::never_loop)]
|
|
||||||
for path in paths {
|
|
||||||
#[cfg(windows)]
|
|
||||||
if windows::is_windows_store_shim(&path) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Some(path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg!(windows) && !use_override {
|
|
||||||
// Use `py` to find the python installation on the system.
|
|
||||||
match windows::py_list_paths() {
|
|
||||||
Ok(paths) => {
|
|
||||||
for entry in paths {
|
|
||||||
// Ex) `--python python3.12.exe`
|
|
||||||
if entry.executable_path.file_name() == Some(requested.as_ref()) {
|
|
||||||
return Ok(Some(entry.executable_path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ex) `--python python3.12`
|
|
||||||
if entry
|
|
||||||
.executable_path
|
|
||||||
.file_stem()
|
|
||||||
.is_some_and(|stem| stem == requested.as_ref())
|
|
||||||
{
|
|
||||||
return Ok(Some(entry.executable_path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(Error::PyList(error)) => {
|
|
||||||
if error.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
debug!("`py` is not installed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct PyListPath {
|
struct PyListPath {
|
||||||
major: u8,
|
major: u8,
|
||||||
|
|
@ -767,7 +750,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_such_python_version() {
|
fn no_such_python_version() {
|
||||||
let request = "3.1000";
|
let request = "3.100";
|
||||||
let result = find_requested_python(
|
let result = find_requested_python(
|
||||||
request,
|
request,
|
||||||
&Platform::current().unwrap(),
|
&Platform::current().unwrap(),
|
||||||
|
|
@ -777,13 +760,13 @@ mod tests {
|
||||||
.ok_or(Error::NoSuchPython(request.to_string()));
|
.ok_or(Error::NoSuchPython(request.to_string()));
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
format_err(result),
|
format_err(result),
|
||||||
@"No Python 3.1000 In `PATH`. Is Python 3.1000 installed?"
|
@"No Python 3.100 In `PATH`. Is Python 3.100 installed?"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_such_python_binary() {
|
fn no_such_python_binary() {
|
||||||
let request = "python3.1000";
|
let request = "python3.100";
|
||||||
let result = find_requested_python(
|
let result = find_requested_python(
|
||||||
request,
|
request,
|
||||||
&Platform::current().unwrap(),
|
&Platform::current().unwrap(),
|
||||||
|
|
@ -793,7 +776,7 @@ mod tests {
|
||||||
.ok_or(Error::NoSuchPython(request.to_string()));
|
.ok_or(Error::NoSuchPython(request.to_string()));
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
format_err(result),
|
format_err(result),
|
||||||
@"No Python python3.1000 In `PATH`. Is Python python3.1000 installed?"
|
@"No Python python3.100 In `PATH`. Is Python python3.100 installed?"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitStatus;
|
use std::process::ExitStatus;
|
||||||
|
|
||||||
|
use platform_tags::TagsError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use crate::cfg::PyVenvConfiguration;
|
pub use crate::cfg::PyVenvConfiguration;
|
||||||
|
|
@ -79,4 +80,12 @@ pub enum Error {
|
||||||
Cfg(#[from] cfg::Error),
|
Cfg(#[from] cfg::Error),
|
||||||
#[error("Error finding `{}` in PATH", _0.to_string_lossy())]
|
#[error("Error finding `{}` in PATH", _0.to_string_lossy())]
|
||||||
WhichError(OsString, #[source] which::Error),
|
WhichError(OsString, #[source] which::Error),
|
||||||
|
#[error(
|
||||||
|
"Unrecognized python specifier: `{0}`. You can use a version (e.g. `3.12`), \
|
||||||
|
an implementation and a version (e.g. `pypy3.10`) or \
|
||||||
|
the path to a python interpreter (e.g. `/usr/bin/python`)"
|
||||||
|
)]
|
||||||
|
UnrecognizedPython(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
UnrecognizedImplementation(TagsError),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue