diff --git a/crates/uv-interpreter/src/python_query.rs b/crates/uv-interpreter/src/find_python.rs similarity index 83% rename from crates/uv-interpreter/src/python_query.rs rename to crates/uv-interpreter/src/find_python.rs index 032229168..078e6acfe 100644 --- a/crates/uv-interpreter/src/python_query.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -1,5 +1,3 @@ -//! Find a user requested python version/interpreter. - use std::borrow::Cow; use std::env; use std::ffi::{OsStr, OsString}; @@ -11,9 +9,10 @@ use platform_host::Platform; use uv_cache::Cache; use uv_fs::normalize_path; -use crate::{Error, Interpreter}; +use crate::python_environment::{detect_python_executable, detect_virtual_env}; +use crate::{Error, Interpreter, PythonVersion}; -/// Find a python version/interpreter of a specific version. +/// Find a Python of a specific version, a binary with a name or a path to a binary. /// /// Supported formats: /// * `-p 3.10` searches for an installed Python 3.10 (`py --list-paths` on Windows, `python3.10` on @@ -38,36 +37,27 @@ pub fn find_requested_python( .collect::, _>>(); if let Ok(versions) = versions { // `-p 3.10` or `-p 3.10.1` - match versions.as_slice() { - [requested_major] => find_python( - PythonVersionSelector::Major(*requested_major), - platform, - cache, - ), - [major, minor] => find_python( - PythonVersionSelector::MajorMinor(*major, *minor), - platform, - cache, - ), - [major, minor, requested_patch] => find_python( - PythonVersionSelector::MajorMinorPatch(*major, *minor, *requested_patch), - platform, - cache, - ), + 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) + Interpreter::query(executable, platform.clone(), cache).map(Some) } else { // `-p /home/ferris/.local/bin/python3.10` let executable = normalize_path(request); - Interpreter::query(&executable, platform.clone(), cache).map(Some) + Interpreter::query(executable, platform.clone(), cache).map(Some) } } @@ -95,7 +85,8 @@ pub(crate) fn try_find_default_python( find_python(PythonVersionSelector::Default, platform, cache) } -/// Finds a python version matching `selector`. +/// Find a Python version matching `selector`. +/// /// It searches for an existing installation in the following order: /// * Search for the python binary in `PATH` (or `UV_TEST_PYTHON_PATH` if set). Visits each path and for each path resolves the /// files in the following order: @@ -106,7 +97,7 @@ pub(crate) fn try_find_default_python( /// * (windows): For each of the above, test for the existence of `python.bat` shim (pyenv-windows) last. /// * (windows): Discover installations using `py --list-paths` (PEP514). Continue if `py` is not installed. /// -/// (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( selector: PythonVersionSelector, platform: &Platform, @@ -350,7 +341,7 @@ impl PythonInstallation { match self { Self::PyListPath(PyListPath { executable_path, .. - }) => Interpreter::query(&executable_path, platform.clone(), cache), + }) => Interpreter::query(executable_path, platform.clone(), cache), Self::Interpreter(interpreter) => Ok(interpreter), } } @@ -411,6 +402,113 @@ impl PythonVersionSelector { } } +/// Find a matching Python or any fallback Python. +/// +/// If no Python version is provided, we will use the first available interpreter. +/// +/// If a Python version is provided, we will first try to find an exact match. If +/// that cannot be found and a patch version was requested, we will look for a match +/// without comparing the patch version number. If that cannot be found, we fall back to +/// the first available version. +/// +/// See [`Self::find_version`] for details on the precedence of Python lookup locations. +#[instrument(skip_all, fields(?python_version))] +pub fn find_best_python( + python_version: Option<&PythonVersion>, + platform: &Platform, + cache: &Cache, +) -> Result { + if let Some(python_version) = python_version { + debug!( + "Starting interpreter discovery for Python {}", + python_version + ); + } else { + debug!("Starting interpreter discovery for active Python"); + } + + // First, check for an exact match (or the first available version if no Python version was provided) + if let Some(interpreter) = find_version(python_version, platform, cache)? { + return Ok(interpreter); + } + + if let Some(python_version) = python_version { + // If that fails, and a specific patch version was requested try again allowing a + // different patch version + if python_version.patch().is_some() { + if let Some(interpreter) = + find_version(Some(&python_version.without_patch()), platform, cache)? + { + return Ok(interpreter); + } + } + } + + // If a Python version was requested but cannot be fulfilled, just take any version + if let Some(interpreter) = find_version(None, platform, cache)? { + return Ok(interpreter); + } + + Err(Error::PythonNotFound) +} + +/// Find a Python interpreter. +/// +/// We check, in order, the following locations: +/// +/// - `UV_DEFAULT_PYTHON`, which is set to the python interpreter when using `python -m uv`. +/// - `VIRTUAL_ENV` and `CONDA_PREFIX` +/// - A `.venv` folder +/// - If a python version is given: Search `PATH` and `py --list-paths`, see `find_python` +/// - `python3` (unix) or `python.exe` (windows) +/// +/// If `UV_TEST_PYTHON_PATH` is set, we will not check for Python versions in the +/// global PATH, instead we will search using the provided path. Virtual environments +/// will still be respected. +/// +/// If a version is provided and an interpreter cannot be found with the given version, +/// we will return [`None`]. +fn find_version( + python_version: Option<&PythonVersion>, + platform: &Platform, + cache: &Cache, +) -> Result, Error> { + let version_matches = |interpreter: &Interpreter| -> bool { + if let Some(python_version) = python_version { + // If a patch version was provided, check for an exact match + python_version.is_satisfied_by(interpreter) + } else { + // The version always matches if one was not provided + true + } + }; + + // Check if the venv Python matches. + if let Some(venv) = detect_virtual_env()? { + let executable = detect_python_executable(venv); + let interpreter = Interpreter::query(executable, platform.clone(), cache)?; + + if version_matches(&interpreter) { + return Ok(Some(interpreter)); + } + }; + + // Look for the requested version with by search for `python{major}.{minor}` in `PATH` on + // Unix and `py --list-paths` on Windows. + let interpreter = if let Some(python_version) = python_version { + find_requested_python(&python_version.string, platform, cache)? + } else { + try_find_default_python(platform, cache)? + }; + + if let Some(interpreter) = interpreter { + debug_assert!(version_matches(&interpreter)); + Ok(Some(interpreter)) + } else { + Ok(None) + } +} + mod windows { use std::path::PathBuf; use std::process::Command; @@ -419,7 +517,7 @@ mod windows { use regex::Regex; use tracing::info_span; - use crate::python_query::PyListPath; + use crate::find_python::PyListPath; use crate::Error; /// ```text @@ -658,7 +756,7 @@ mod tests { use platform_host::Platform; use uv_cache::Cache; - use crate::python_query::find_requested_python; + use crate::find_python::find_requested_python; use crate::Error; fn format_err(err: Result) -> String { diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index 03f4d75ee..310a7d367 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -6,7 +6,7 @@ use configparser::ini::Ini; use fs_err as fs; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; -use tracing::{debug, instrument, warn}; +use tracing::{debug, warn}; use cache_key::digest; use install_wheel_rs::Layout; @@ -18,9 +18,8 @@ use pypi_types::Scheme; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; use uv_fs::write_atomic_sync; -use crate::python_environment::{detect_python_executable, detect_virtual_env}; -use crate::python_query::try_find_default_python; -use crate::{find_requested_python, Error, PythonVersion, Virtualenv}; +use crate::Error; +use crate::Virtualenv; /// A Python executable and its associated platform markers. #[derive(Debug, Clone)] @@ -40,8 +39,12 @@ pub struct Interpreter { impl Interpreter { /// Detect the interpreter info for the given Python executable. - pub fn query(executable: &Path, platform: Platform, cache: &Cache) -> Result { - let info = InterpreterInfo::query_cached(executable, cache)?; + pub fn query( + executable: impl AsRef, + platform: Platform, + cache: &Cache, + ) -> Result { + let info = InterpreterInfo::query_cached(executable.as_ref(), cache)?; debug_assert!( info.sys_executable.is_absolute(), @@ -104,112 +107,6 @@ impl Interpreter { } } - /// Find the best available Python interpreter to use. - /// - /// If no Python version is provided, we will use the first available interpreter. - /// - /// If a Python version is provided, we will first try to find an exact match. If - /// that cannot be found and a patch version was requested, we will look for a match - /// without comparing the patch version number. If that cannot be found, we fall back to - /// the first available version. - /// - /// See [`Self::find_version`] for details on the precedence of Python lookup locations. - #[instrument(skip_all, fields(?python_version))] - pub fn find_best( - python_version: Option<&PythonVersion>, - platform: &Platform, - cache: &Cache, - ) -> Result { - if let Some(python_version) = python_version { - debug!( - "Starting interpreter discovery for Python {}", - python_version - ); - } else { - debug!("Starting interpreter discovery for active Python"); - } - - // First, check for an exact match (or the first available version if no Python version was provided) - if let Some(interpreter) = Self::find_version(python_version, platform, cache)? { - return Ok(interpreter); - } - - if let Some(python_version) = python_version { - // If that fails, and a specific patch version was requested try again allowing a - // different patch version - if python_version.patch().is_some() { - if let Some(interpreter) = - Self::find_version(Some(&python_version.without_patch()), platform, cache)? - { - return Ok(interpreter); - } - } - - // If a Python version was requested but cannot be fulfilled, just take any version - if let Some(interpreter) = Self::find_version(None, platform, cache)? { - return Ok(interpreter); - } - } - - Err(Error::PythonNotFound) - } - - /// Find a Python interpreter. - /// - /// We check, in order, the following locations: - /// - /// - `VIRTUAL_ENV` and `CONDA_PREFIX` - /// - A `.venv` folder - /// - If a python version is given: `pythonx.y` - /// - `python3` (unix) or `python.exe` (windows) - /// - /// If `UV_TEST_PYTHON_PATH` is set, we will not check for Python versions in the - /// global PATH, instead we will search using the provided path. Virtual environments - /// will still be respected. - /// - /// If a version is provided and an interpreter cannot be found with the given version, - /// we will return [`None`]. - pub(crate) fn find_version( - python_version: Option<&PythonVersion>, - platform: &Platform, - cache: &Cache, - ) -> Result, Error> { - let version_matches = |interpreter: &Self| -> bool { - if let Some(python_version) = python_version { - // If a patch version was provided, check for an exact match - python_version.is_satisfied_by(interpreter) - } else { - // The version always matches if one was not provided - true - } - }; - - // Check if the venv Python matches. - if let Some(venv) = detect_virtual_env()? { - let executable = detect_python_executable(venv); - let interpreter = Self::query(&executable, platform.clone(), cache)?; - - if version_matches(&interpreter) { - return Ok(Some(interpreter)); - } - }; - - // Look for the requested version with by search for `python{major}.{minor}` in `PATH` on - // Unix and `py --list-paths` on Windows. - let interpreter = if let Some(python_version) = python_version { - find_requested_python(&python_version.string, platform, cache)? - } else { - try_find_default_python(platform, cache)? - }; - - if let Some(interpreter) = interpreter { - debug_assert!(version_matches(&interpreter)); - Ok(Some(interpreter)) - } else { - Ok(None) - } - } - /// Returns the path to the Python virtual environment. #[inline] pub fn platform(&self) -> &Platform { diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index fcd82f118..5298d065b 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -1,3 +1,12 @@ +//! Find matching Python interpreter and query information about python interpreter. +//! +//! * The `venv` subcommand uses [`find_requested_python`] if `-p`/`--python` is used and +//! `find_default_python` otherwise. +//! * The `compile` subcommand uses [`find_best_python`]. +//! * The `sync`, `install`, `uninstall`, `freeze`, `list` and `show` subcommands use +//! [`find_default_python`] when `--python` is used, [`find_default_python`] when `--system` is used +//! and the current venv by default. + use std::ffi::OsString; use std::io; use std::path::PathBuf; @@ -6,16 +15,16 @@ use std::process::ExitStatus; use thiserror::Error; pub use crate::cfg::PyVenvConfiguration; +pub use crate::find_python::{find_best_python, find_default_python, find_requested_python}; pub use crate::interpreter::Interpreter; pub use crate::python_environment::PythonEnvironment; -pub use crate::python_query::{find_default_python, find_requested_python}; pub use crate::python_version::PythonVersion; pub use crate::virtualenv::Virtualenv; mod cfg; +mod find_python; mod interpreter; mod python_environment; -mod python_query; mod python_version; mod virtualenv; diff --git a/crates/uv-interpreter/src/python_environment.rs b/crates/uv-interpreter/src/python_environment.rs index 4d19bc38c..a30261bc2 100644 --- a/crates/uv-interpreter/src/python_environment.rs +++ b/crates/uv-interpreter/src/python_environment.rs @@ -10,7 +10,7 @@ use uv_fs::{LockedFile, Simplified}; use crate::cfg::PyVenvConfiguration; use crate::{find_default_python, find_requested_python, Error, Interpreter}; -/// A Python environment, consisting of a Python [`Interpreter`] and a root directory. +/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths. #[derive(Debug, Clone)] pub struct PythonEnvironment { root: PathBuf, diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 4d64fc942..339f25a06 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -24,7 +24,7 @@ use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder} use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::{Downloader, NoBinary}; -use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion}; +use uv_interpreter::{find_best_python, PythonEnvironment, PythonVersion}; use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, @@ -132,7 +132,7 @@ pub(crate) async fn pip_compile( // Find an interpreter to use for building distributions let platform = Platform::current()?; - let interpreter = Interpreter::find_best(python_version.as_ref(), &platform, &cache)?; + let interpreter = find_best_python(python_version.as_ref(), &platform, &cache)?; debug!( "Using Python {} interpreter at {} for builds", interpreter.python_version(),