Rename and document venv discoveries (#2334)

Preparing for #2058, i found it hard to follow where which discovery
function gets called. I moved all the discovery functions to a
`find_python` module (some exposed through `PythonEnvironment`) and
documented which subcommand uses which python discovery strategy.

No functional changes.

![new uv-virtualenv docs
page](cd56df8a-754d-4640-9e7a-e1f9baf6441c)
This commit is contained in:
konsti 2024-03-10 14:44:50 +01:00 committed by GitHub
parent 73b30ba8ed
commit 262ca8b576
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 145 deletions

View file

@ -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::<Result<Vec<_>, _>>();
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<Interpreter, Error> {
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<Option<Interpreter>, 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<T: std::fmt::Debug>(err: Result<T, Error>) -> String {

View file

@ -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<Self, Error> {
let info = InterpreterInfo::query_cached(executable, cache)?;
pub fn query(
executable: impl AsRef<Path>,
platform: Platform,
cache: &Cache,
) -> Result<Self, Error> {
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<Self, Error> {
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<Option<Self>, 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 {

View file

@ -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;

View file

@ -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,

View file

@ -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(),