Refactor uv-toolchain types (#4121)

Extends #4120 
Part of #2607 

There should be no behavior changes here. Restructures the discovery API
to be focused on a toolchain first perspective in preparation for
exposing a `find_or_fetch` method for toolchains in
https://github.com/astral-sh/uv/pull/4138.
This commit is contained in:
Zanie Blue 2024-06-07 15:20:28 -04:00 committed by GitHub
parent 325982c418
commit 53035d65a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 777 additions and 746 deletions

View file

@ -15,7 +15,7 @@ fn resolve_warm_jupyter(c: &mut Criterion<WallTime>) {
.unwrap();
let cache = &Cache::from_path("../../.cache").init().unwrap();
let venv = PythonEnvironment::from_virtualenv(cache).unwrap();
let venv = PythonEnvironment::from_root("../../.venv", cache).unwrap();
let client = &RegistryClientBuilder::new(cache.clone()).build();
let manifest = &Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("jupyter").unwrap(),
@ -44,7 +44,7 @@ fn resolve_warm_airflow(c: &mut Criterion<WallTime>) {
.unwrap();
let cache = &Cache::from_path("../../.cache").init().unwrap();
let venv = PythonEnvironment::from_virtualenv(cache).unwrap();
let venv = PythonEnvironment::from_root("../../.venv", cache).unwrap();
let client = &RegistryClientBuilder::new(cache.clone()).build();
let manifest = &Manifest::simple(vec![
Requirement::from(pep508_rs::Requirement::from_str("apache-airflow[all]").unwrap()),

View file

@ -16,7 +16,7 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_git::GitResolver;
use uv_resolver::{FlatIndex, InMemoryIndex};
use uv_toolchain::PythonEnvironment;
use uv_toolchain::Toolchain;
use uv_types::{BuildContext, BuildIsolation, InFlight};
#[derive(Parser)]
@ -65,12 +65,12 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
let index = InMemoryIndex::default();
let index_urls = IndexLocations::default();
let setup_py = SetupPyStrategy::default();
let venv = PythonEnvironment::from_virtualenv(&cache)?;
let toolchain = Toolchain::find_virtualenv(&cache)?;
let build_dispatch = BuildDispatch::new(
&client,
&cache,
venv.interpreter(),
toolchain.interpreter(),
&index_urls,
&flat_index,
&index,

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use clap::Parser;
use tracing::info;
use uv_cache::{Cache, CacheArgs};
use uv_toolchain::PythonEnvironment;
use uv_toolchain::Toolchain;
#[derive(Parser)]
pub(crate) struct CompileArgs {
@ -20,8 +20,8 @@ pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> {
let interpreter = if let Some(python) = args.python {
python
} else {
let venv = PythonEnvironment::from_virtualenv(&cache)?;
venv.python_executable().to_path_buf()
let interpreter = Toolchain::find_virtualenv(&cache)?.into_interpreter();
interpreter.sys_executable().to_path_buf()
};
let files = uv_installer::compile_tree(

View file

@ -11,6 +11,7 @@ use crate::implementation::{ImplementationName, LenientImplementationName};
use crate::interpreter::Error as InterpreterError;
use crate::managed::InstalledToolchains;
use crate::py_launcher::py_list_paths;
use crate::toolchain::Toolchain;
use crate::virtualenv::{
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
virtualenv_python_executable,
@ -24,12 +25,12 @@ use std::num::ParseIntError;
use std::{env, io};
use std::{path::Path, path::PathBuf, str::FromStr};
/// A request to find a Python interpreter.
/// A request to find a Python toolchain.
///
/// See [`InterpreterRequest::from_str`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum InterpreterRequest {
/// Use any discovered Python interpreter
pub enum ToolchainRequest {
/// Use any discovered Python toolchain
#[default]
Any,
/// A Python version without an implementation name e.g. `3.10`
@ -46,20 +47,20 @@ pub enum InterpreterRequest {
ImplementationVersion(ImplementationName, VersionRequest),
}
/// The sources to consider when finding a Python interpreter.
/// The sources to consider when finding a Python toolchain.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceSelector {
// Consider all interpreter sources.
pub enum ToolchainSources {
// Consider all toolchain sources.
All(PreviewMode),
// Only consider system interpreter sources
// Only consider system toolchain sources
System(PreviewMode),
// Only consider virtual environment sources
VirtualEnv,
// Only consider a custom set of sources
Custom(HashSet<InterpreterSource>),
Custom(HashSet<ToolchainSource>),
}
/// A Python interpreter version request.
/// A Python toolchain version request.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum VersionRequest {
#[default]
@ -72,7 +73,7 @@ pub enum VersionRequest {
/// The policy for discovery of "system" Python interpreters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SystemPython {
/// Only allow a system Python if passed directly i.e. via [`InterpreterSource::ProvidedPath`] or [`InterpreterSource::ParentInterpreter`]
/// Only allow a system Python if passed directly i.e. via [`ToolchainSource::ProvidedPath`] or [`ToolchainSource::ParentInterpreter`]
#[default]
Explicit,
/// Do not allow a system Python
@ -83,24 +84,24 @@ pub enum SystemPython {
Required,
}
/// The result of an interpreter search.
/// The result of an toolchain search.
///
/// Returned by [`find_interpreter`].
type InterpreterResult = Result<DiscoveredInterpreter, InterpreterNotFound>;
/// Returned by [`find_toolchain`].
type ToolchainResult = Result<Toolchain, ToolchainNotFound>;
/// The result of failed interpreter discovery.
/// The result of failed toolchain discovery.
///
/// See [`InterpreterResult`].
#[derive(Clone, Debug, Error)]
pub enum InterpreterNotFound {
pub enum ToolchainNotFound {
/// No Python installations were found.
NoPythonInstallation(SourceSelector, Option<VersionRequest>),
NoPythonInstallation(ToolchainSources, Option<VersionRequest>),
/// No Python installations with the requested version were found.
NoMatchingVersion(SourceSelector, VersionRequest),
NoMatchingVersion(ToolchainSources, VersionRequest),
/// No Python installations with the requested implementation name were found.
NoMatchingImplementation(SourceSelector, ImplementationName),
NoMatchingImplementation(ToolchainSources, ImplementationName),
/// No Python installations with the requested implementation name and version were found.
NoMatchingImplementationVersion(SourceSelector, ImplementationName, VersionRequest),
NoMatchingImplementationVersion(ToolchainSources, ImplementationName, VersionRequest),
/// The requested file path does not exist.
FileNotFound(PathBuf),
/// The requested directory path does not exist.
@ -113,19 +114,10 @@ pub enum InterpreterNotFound {
FileNotExecutable(PathBuf),
}
/// The result of successful interpreter discovery.
///
/// See [`InterpreterResult`].
#[derive(Clone, Debug)]
pub struct DiscoveredInterpreter {
pub(crate) source: InterpreterSource,
pub(crate) interpreter: Interpreter,
}
/// The source of a discovered Python interpreter.
/// The source of a discovered Python toolchain.
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
pub enum InterpreterSource {
/// The interpreter path was provided directly
pub enum ToolchainSource {
/// The toolchain path was provided directly
ProvidedPath,
/// An environment was active e.g. via `VIRTUAL_ENV`
ActiveEnvironment,
@ -137,9 +129,9 @@ pub enum InterpreterSource {
SearchPath,
/// An executable was found via the `py` launcher
PyLauncher,
/// The interpreter was found in the uv toolchain directory
ManagedToolchain,
/// The interpreter invoked uv i.e. via `python -m uv ...`
/// The toolchain was found in the uv toolchain directory
Managed,
/// The toolchain was found via the invoking interpreter i.e. via `python -m uv ...`
ParentInterpreter,
// TODO(zanieb): Add support for fetching the interpreter from a remote source
}
@ -166,7 +158,7 @@ pub enum Error {
PyLauncher(#[from] crate::py_launcher::Error),
#[error("Interpreter discovery for `{0}` requires `{1}` but it is not selected; the following are selected: {2}")]
SourceNotSelected(InterpreterRequest, InterpreterSource, SourceSelector),
SourceNotSelected(ToolchainRequest, ToolchainSource, ToolchainSources),
}
/// Lazily iterate over all discoverable Python executables.
@ -191,43 +183,43 @@ pub enum Error {
fn python_executables<'a>(
version: Option<&'a VersionRequest>,
implementation: Option<&'a ImplementationName>,
sources: &SourceSelector,
) -> impl Iterator<Item = Result<(InterpreterSource, PathBuf), Error>> + 'a {
sources: &ToolchainSources,
) -> impl Iterator<Item = Result<(ToolchainSource, PathBuf), Error>> + 'a {
// Note we are careful to ensure the iterator chain is lazy to avoid unnecessary work
// (1) The parent interpreter
sources.contains(InterpreterSource::ParentInterpreter).then(||
sources.contains(ToolchainSource::ParentInterpreter).then(||
std::env::var_os("UV_INTERNAL__PARENT_INTERPRETER")
.into_iter()
.map(|path| Ok((InterpreterSource::ParentInterpreter, PathBuf::from(path))))
.map(|path| Ok((ToolchainSource::ParentInterpreter, PathBuf::from(path))))
).into_iter().flatten()
// (2) An active virtual environment
.chain(
sources.contains(InterpreterSource::ActiveEnvironment).then(||
sources.contains(ToolchainSource::ActiveEnvironment).then(||
virtualenv_from_env()
.into_iter()
.map(virtualenv_python_executable)
.map(|path| Ok((InterpreterSource::ActiveEnvironment, path)))
.map(|path| Ok((ToolchainSource::ActiveEnvironment, path)))
).into_iter().flatten()
)
// (3) An active conda environment
.chain(
sources.contains(InterpreterSource::CondaPrefix).then(||
sources.contains(ToolchainSource::CondaPrefix).then(||
conda_prefix_from_env()
.into_iter()
.map(virtualenv_python_executable)
.map(|path| Ok((InterpreterSource::CondaPrefix, path)))
.map(|path| Ok((ToolchainSource::CondaPrefix, path)))
).into_iter().flatten()
)
// (4) A discovered environment
.chain(
sources.contains(InterpreterSource::DiscoveredEnvironment).then(||
sources.contains(ToolchainSource::DiscoveredEnvironment).then(||
std::iter::once(
virtualenv_from_working_dir()
.map(|path|
path
.map(virtualenv_python_executable)
.map(|path| (InterpreterSource::DiscoveredEnvironment, path))
.map(|path| (ToolchainSource::DiscoveredEnvironment, path))
.into_iter()
)
.map_err(Error::from)
@ -236,7 +228,7 @@ fn python_executables<'a>(
)
// (5) Managed toolchains
.chain(
sources.contains(InterpreterSource::ManagedToolchain).then(move ||
sources.contains(ToolchainSource::Managed).then(move ||
std::iter::once(
InstalledToolchains::from_settings().map_err(Error::from).and_then(|installed_toolchains| {
debug!("Searching for managed toolchains at `{}`", installed_toolchains.root().user_display());
@ -249,7 +241,7 @@ fn python_executables<'a>(
)
)
.inspect(|toolchain| debug!("Found managed toolchain `{toolchain}`"))
.map(|toolchain| (InterpreterSource::ManagedToolchain, toolchain.executable()))
.map(|toolchain| (ToolchainSource::Managed, toolchain.executable()))
)
})
).flatten_ok()
@ -257,15 +249,15 @@ fn python_executables<'a>(
)
// (6) The search path
.chain(
sources.contains(InterpreterSource::SearchPath).then(move ||
sources.contains(ToolchainSource::SearchPath).then(move ||
python_executables_from_search_path(version, implementation)
.map(|path| Ok((InterpreterSource::SearchPath, path))),
.map(|path| Ok((ToolchainSource::SearchPath, path))),
).into_iter().flatten()
)
// (7) The `py` launcher (windows only)
// TODO(konstin): Implement <https://peps.python.org/pep-0514/> to read python installations from the registry instead.
.chain(
(sources.contains(InterpreterSource::PyLauncher) && cfg!(windows)).then(||
(sources.contains(ToolchainSource::PyLauncher) && cfg!(windows)).then(||
std::iter::once(
py_list_paths()
.map(|entries|
@ -275,7 +267,7 @@ fn python_executables<'a>(
version.has_patch() || version.matches_major_minor(entry.major, entry.minor)
)
)
.map(|entry| (InterpreterSource::PyLauncher, entry.executable_path))
.map(|entry| (ToolchainSource::PyLauncher, entry.executable_path))
)
.map_err(Error::from)
).flatten_ok()
@ -354,14 +346,14 @@ fn python_executables_from_search_path<'a>(
/// Lazily iterate over all discoverable Python interpreters.
///
///See [`python_executables`] for more information on discovery.
/// See [`python_executables`] for more information on discovery.
fn python_interpreters<'a>(
version: Option<&'a VersionRequest>,
implementation: Option<&'a ImplementationName>,
system: SystemPython,
sources: &SourceSelector,
sources: &ToolchainSources,
cache: &'a Cache,
) -> impl Iterator<Item = Result<(InterpreterSource, Interpreter), Error>> + 'a {
) -> impl Iterator<Item = Result<(ToolchainSource, Interpreter), Error>> + 'a {
python_executables(version, implementation, sources)
.map(|result| match result {
Ok((source, path)) => Interpreter::query(&path, cache)
@ -383,13 +375,13 @@ fn python_interpreters<'a>(
Ok((source, interpreter)) => match (
system,
// Conda environments are not conformant virtual environments but we should not treat them as system interpreters
interpreter.is_virtualenv() || matches!(source, InterpreterSource::CondaPrefix),
interpreter.is_virtualenv() || matches!(source, ToolchainSource::CondaPrefix),
) {
(SystemPython::Allowed, _) => true,
(SystemPython::Explicit, false) => {
if matches!(
source,
InterpreterSource::ProvidedPath | InterpreterSource::ParentInterpreter
ToolchainSource::ProvidedPath | ToolchainSource::ParentInterpreter
) {
debug!(
"Allowing system Python interpreter at `{}`",
@ -429,11 +421,11 @@ fn python_interpreters<'a>(
/// Check if an encountered error should stop discovery.
///
/// Returns false when an error could be due to a faulty interpreter and we should continue searching for a working one.
/// Returns false when an error could be due to a faulty toolchain and we should continue searching for a working one.
fn should_stop_discovery(err: &Error) -> bool {
match err {
// When querying the interpreter fails, we will only raise errors that demonstrate that something is broken
// If the interpreter returned a bad response, we'll continue searching for one that works
// When querying the toolchain interpreter fails, we will only raise errors that demonstrate that something is broken
// If the toolchain interpreter returned a bad response, we'll continue searching for one that works
Error::Query(err) => match err {
InterpreterError::Encode(_)
| InterpreterError::Io(_)
@ -449,81 +441,81 @@ fn should_stop_discovery(err: &Error) -> bool {
}
}
/// Find an interpreter that satisfies the given request.
/// Find a toolchain that satisfies the given request.
///
/// If an error is encountered while locating or inspecting a candidate interpreter,
/// If an error is encountered while locating or inspecting a candidate toolchain,
/// the error will raised instead of attempting further candidates.
pub fn find_interpreter(
request: &InterpreterRequest,
pub fn find_toolchain(
request: &ToolchainRequest,
system: SystemPython,
sources: &SourceSelector,
sources: &ToolchainSources,
cache: &Cache,
) -> Result<InterpreterResult, Error> {
) -> Result<ToolchainResult, Error> {
let result = match request {
InterpreterRequest::File(path) => {
ToolchainRequest::File(path) => {
debug!("Checking for Python interpreter at {request}");
if !sources.contains(InterpreterSource::ProvidedPath) {
if !sources.contains(ToolchainSource::ProvidedPath) {
return Err(Error::SourceNotSelected(
request.clone(),
InterpreterSource::ProvidedPath,
ToolchainSource::ProvidedPath,
sources.clone(),
));
}
if !path.try_exists()? {
return Ok(InterpreterResult::Err(InterpreterNotFound::FileNotFound(
return Ok(ToolchainResult::Err(ToolchainNotFound::FileNotFound(
path.clone(),
)));
}
DiscoveredInterpreter {
source: InterpreterSource::ProvidedPath,
Toolchain {
source: ToolchainSource::ProvidedPath,
interpreter: Interpreter::query(path, cache)?,
}
}
InterpreterRequest::Directory(path) => {
ToolchainRequest::Directory(path) => {
debug!("Checking for Python interpreter in {request}");
if !sources.contains(InterpreterSource::ProvidedPath) {
if !sources.contains(ToolchainSource::ProvidedPath) {
return Err(Error::SourceNotSelected(
request.clone(),
InterpreterSource::ProvidedPath,
ToolchainSource::ProvidedPath,
sources.clone(),
));
}
if !path.try_exists()? {
return Ok(InterpreterResult::Err(InterpreterNotFound::FileNotFound(
return Ok(ToolchainResult::Err(ToolchainNotFound::FileNotFound(
path.clone(),
)));
}
let executable = virtualenv_python_executable(path);
if !executable.try_exists()? {
return Ok(InterpreterResult::Err(
InterpreterNotFound::ExecutableNotFoundInDirectory(path.clone(), executable),
return Ok(ToolchainResult::Err(
ToolchainNotFound::ExecutableNotFoundInDirectory(path.clone(), executable),
));
}
DiscoveredInterpreter {
source: InterpreterSource::ProvidedPath,
Toolchain {
source: ToolchainSource::ProvidedPath,
interpreter: Interpreter::query(executable, cache)?,
}
}
InterpreterRequest::ExecutableName(name) => {
ToolchainRequest::ExecutableName(name) => {
debug!("Searching for Python interpreter with {request}");
if !sources.contains(InterpreterSource::SearchPath) {
if !sources.contains(ToolchainSource::SearchPath) {
return Err(Error::SourceNotSelected(
request.clone(),
InterpreterSource::SearchPath,
ToolchainSource::SearchPath,
sources.clone(),
));
}
let Some(executable) = which(name).ok() else {
return Ok(InterpreterResult::Err(
InterpreterNotFound::ExecutableNotFoundInSearchPath(name.clone()),
return Ok(ToolchainResult::Err(
ToolchainNotFound::ExecutableNotFoundInSearchPath(name.clone()),
));
};
DiscoveredInterpreter {
source: InterpreterSource::SearchPath,
Toolchain {
source: ToolchainSource::SearchPath,
interpreter: Interpreter::query(executable, cache)?,
}
}
InterpreterRequest::Implementation(implementation) => {
ToolchainRequest::Implementation(implementation) => {
debug!("Searching for a {request} interpreter in {sources}");
let Some((source, interpreter)) =
python_interpreters(None, Some(implementation), system, sources, cache)
@ -538,16 +530,16 @@ pub fn find_interpreter(
})
.transpose()?
else {
return Ok(InterpreterResult::Err(
InterpreterNotFound::NoMatchingImplementation(sources.clone(), *implementation),
return Ok(ToolchainResult::Err(
ToolchainNotFound::NoMatchingImplementation(sources.clone(), *implementation),
));
};
DiscoveredInterpreter {
Toolchain {
source,
interpreter,
}
}
InterpreterRequest::ImplementationVersion(implementation, version) => {
ToolchainRequest::ImplementationVersion(implementation, version) => {
debug!("Searching for {request} in {sources}");
let Some((source, interpreter)) =
python_interpreters(Some(version), Some(implementation), system, sources, cache)
@ -565,20 +557,20 @@ pub fn find_interpreter(
else {
// TODO(zanieb): Peek if there are any interpreters with the requested implementation
// to improve the error message e.g. using `NoMatchingImplementation` instead
return Ok(InterpreterResult::Err(
InterpreterNotFound::NoMatchingImplementationVersion(
return Ok(ToolchainResult::Err(
ToolchainNotFound::NoMatchingImplementationVersion(
sources.clone(),
*implementation,
*version,
),
));
};
DiscoveredInterpreter {
Toolchain {
source,
interpreter,
}
}
InterpreterRequest::Any => {
ToolchainRequest::Any => {
debug!("Searching for Python interpreter in {sources}");
let Some((source, interpreter)) =
python_interpreters(None, None, system, sources, cache)
@ -591,16 +583,16 @@ pub fn find_interpreter(
})
.transpose()?
else {
return Ok(InterpreterResult::Err(
InterpreterNotFound::NoPythonInstallation(sources.clone(), None),
return Ok(ToolchainResult::Err(
ToolchainNotFound::NoPythonInstallation(sources.clone(), None),
));
};
DiscoveredInterpreter {
Toolchain {
source,
interpreter,
}
}
InterpreterRequest::Version(version) => {
ToolchainRequest::Version(version) => {
debug!("Searching for {request} in {sources}");
let Some((source, interpreter)) =
python_interpreters(Some(version), None, system, sources, cache)
@ -614,106 +606,106 @@ pub fn find_interpreter(
.transpose()?
else {
let err = if matches!(version, VersionRequest::Any) {
InterpreterNotFound::NoPythonInstallation(sources.clone(), Some(*version))
ToolchainNotFound::NoPythonInstallation(sources.clone(), Some(*version))
} else {
InterpreterNotFound::NoMatchingVersion(sources.clone(), *version)
ToolchainNotFound::NoMatchingVersion(sources.clone(), *version)
};
return Ok(InterpreterResult::Err(err));
return Ok(ToolchainResult::Err(err));
};
DiscoveredInterpreter {
Toolchain {
source,
interpreter,
}
}
};
Ok(InterpreterResult::Ok(result))
Ok(ToolchainResult::Ok(result))
}
/// Find the default Python interpreter on the system.
/// Find the default Python toolchain on the system.
///
/// Virtual environments are not included in discovery.
///
/// See [`find_interpreter`] for more details on interpreter discovery.
pub fn find_default_interpreter(
/// See [`find_toolchain`] for more details on toolchain discovery.
pub fn find_default_toolchain(
preview: PreviewMode,
cache: &Cache,
) -> Result<InterpreterResult, Error> {
let request = InterpreterRequest::default();
let sources = SourceSelector::System(preview);
) -> Result<ToolchainResult, Error> {
let request = ToolchainRequest::default();
let sources = ToolchainSources::System(preview);
let result = find_interpreter(&request, SystemPython::Required, &sources, cache)?;
if let Ok(ref found) = result {
warn_on_unsupported_python(found.interpreter());
let result = find_toolchain(&request, SystemPython::Required, &sources, cache)?;
if let Ok(ref toolchain) = result {
warn_on_unsupported_python(toolchain.interpreter());
}
Ok(result)
}
/// Find the best-matching Python interpreter.
/// Find the best-matching Python toolchain.
///
/// If no Python version is provided, we will use the first available interpreter.
/// If no Python version is provided, we will use the first available toolchain.
///
/// 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 [`find_interpreter`] for more details on interpreter discovery.
/// See [`find_toolchain`] for more details on toolchain discovery.
#[instrument(skip_all, fields(request))]
pub fn find_best_interpreter(
request: &InterpreterRequest,
pub fn find_best_toolchain(
request: &ToolchainRequest,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<InterpreterResult, Error> {
debug!("Starting interpreter discovery for {}", request);
) -> Result<ToolchainResult, Error> {
debug!("Starting toolchain discovery for {}", request);
// Determine if we should be allowed to look outside of virtual environments.
let sources = SourceSelector::from_settings(system, preview);
let sources = ToolchainSources::from_settings(system, preview);
// First, check for an exact match (or the first available version if no Python versfion was provided)
debug!("Looking for exact match for request {request}");
let result = find_interpreter(request, system, &sources, cache)?;
if let Ok(ref found) = result {
warn_on_unsupported_python(found.interpreter());
let result = find_toolchain(request, system, &sources, cache)?;
if let Ok(ref toolchain) = result {
warn_on_unsupported_python(toolchain.interpreter());
return Ok(result);
}
// If that fails, and a specific patch version was requested try again allowing a
// different patch version
if let Some(request) = match request {
InterpreterRequest::Version(version) => {
ToolchainRequest::Version(version) => {
if version.has_patch() {
Some(InterpreterRequest::Version((*version).without_patch()))
Some(ToolchainRequest::Version((*version).without_patch()))
} else {
None
}
}
InterpreterRequest::ImplementationVersion(implementation, version) => Some(
InterpreterRequest::ImplementationVersion(*implementation, (*version).without_patch()),
ToolchainRequest::ImplementationVersion(implementation, version) => Some(
ToolchainRequest::ImplementationVersion(*implementation, (*version).without_patch()),
),
_ => None,
} {
debug!("Looking for relaxed patch version {request}");
let result = find_interpreter(&request, system, &sources, cache)?;
if let Ok(ref found) = result {
warn_on_unsupported_python(found.interpreter());
let result = find_toolchain(&request, system, &sources, cache)?;
if let Ok(ref toolchain) = result {
warn_on_unsupported_python(toolchain.interpreter());
return Ok(result);
}
}
// If a Python version was requested but cannot be fulfilled, just take any version
debug!("Looking for Python interpreter with any version");
let request = InterpreterRequest::Any;
Ok(find_interpreter(
debug!("Looking for Python toolchain with any version");
let request = ToolchainRequest::Any;
Ok(find_toolchain(
// TODO(zanieb): Add a dedicated `Default` variant to `InterpreterRequest`
&request, system, &sources, cache,
)?
.map_err(|err| {
// Use a more general error in this case since we looked for multiple versions
if matches!(err, InterpreterNotFound::NoMatchingVersion(..)) {
InterpreterNotFound::NoPythonInstallation(sources.clone(), None)
if matches!(err, ToolchainNotFound::NoMatchingVersion(..)) {
ToolchainNotFound::NoPythonInstallation(sources.clone(), None)
} else {
err
}
@ -865,7 +857,7 @@ fn is_windows_store_shim(_path: &Path) -> bool {
false
}
impl InterpreterRequest {
impl ToolchainRequest {
/// Create a request from a string.
///
/// This cannot fail, which means weird inputs will be parsed as [`InterpreterRequest::File`] or [`InterpreterRequest::ExecutableName`].
@ -1132,38 +1124,38 @@ impl fmt::Display for VersionRequest {
}
}
impl SourceSelector {
impl ToolchainSources {
/// Create a new [`SourceSelector::Some`] from an iterator.
pub(crate) fn from_sources(iter: impl IntoIterator<Item = InterpreterSource>) -> Self {
pub(crate) fn from_sources(iter: impl IntoIterator<Item = ToolchainSource>) -> Self {
let inner = HashSet::from_iter(iter);
assert!(!inner.is_empty(), "Source selectors cannot be empty");
Self::Custom(inner)
}
/// Return true if this selector includes the given [`InterpreterSource`].
fn contains(&self, source: InterpreterSource) -> bool {
/// Return true if this selector includes the given [`ToolchainSource`].
fn contains(&self, source: ToolchainSource) -> bool {
match self {
Self::All(preview) => {
// Always return `true` except for `ManagedToolchain` which requires preview mode
source != InterpreterSource::ManagedToolchain || preview.is_enabled()
source != ToolchainSource::Managed || preview.is_enabled()
}
Self::System(preview) => {
[
InterpreterSource::ProvidedPath,
InterpreterSource::SearchPath,
ToolchainSource::ProvidedPath,
ToolchainSource::SearchPath,
#[cfg(windows)]
InterpreterSource::PyLauncher,
InterpreterSource::ParentInterpreter,
ToolchainSource::PyLauncher,
ToolchainSource::ParentInterpreter,
]
.contains(&source)
// Allow `ManagedToolchain` in preview
|| (source == InterpreterSource::ManagedToolchain
|| (source == ToolchainSource::Managed
&& preview.is_enabled())
}
Self::VirtualEnv => [
InterpreterSource::DiscoveredEnvironment,
InterpreterSource::ActiveEnvironment,
InterpreterSource::CondaPrefix,
ToolchainSource::DiscoveredEnvironment,
ToolchainSource::ActiveEnvironment,
ToolchainSource::CondaPrefix,
]
.contains(&source),
Self::Custom(sources) => sources.contains(&source),
@ -1174,15 +1166,15 @@ impl SourceSelector {
pub fn from_settings(system: SystemPython, preview: PreviewMode) -> Self {
if env::var_os("UV_FORCE_MANAGED_PYTHON").is_some() {
debug!("Only considering managed toolchains due to `UV_FORCE_MANAGED_PYTHON`");
Self::from_sources([InterpreterSource::ManagedToolchain])
Self::from_sources([ToolchainSource::Managed])
} else if env::var_os("UV_TEST_PYTHON_PATH").is_some() {
debug!(
"Only considering search path, provided path, and active environments due to `UV_TEST_PYTHON_PATH`"
);
Self::from_sources([
InterpreterSource::ActiveEnvironment,
InterpreterSource::SearchPath,
InterpreterSource::ProvidedPath,
ToolchainSource::ActiveEnvironment,
ToolchainSource::SearchPath,
ToolchainSource::ProvidedPath,
])
} else {
match system {
@ -1206,7 +1198,7 @@ impl SystemPython {
}
}
impl fmt::Display for InterpreterRequest {
impl fmt::Display for ToolchainRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Any => write!(f, "any Python"),
@ -1224,7 +1216,7 @@ impl fmt::Display for InterpreterRequest {
}
}
impl fmt::Display for InterpreterSource {
impl fmt::Display for ToolchainSource {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::ProvidedPath => f.write_str("provided path"),
@ -1233,13 +1225,13 @@ impl fmt::Display for InterpreterSource {
Self::DiscoveredEnvironment => f.write_str("virtual environment"),
Self::SearchPath => f.write_str("search path"),
Self::PyLauncher => f.write_str("`py` launcher output"),
Self::ManagedToolchain => f.write_str("managed toolchains"),
Self::Managed => f.write_str("managed toolchains"),
Self::ParentInterpreter => f.write_str("parent interpreter"),
}
}
}
impl fmt::Display for InterpreterNotFound {
impl fmt::Display for ToolchainNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::NoPythonInstallation(sources, None | Some(VersionRequest::Any)) => {
@ -1295,7 +1287,7 @@ impl fmt::Display for InterpreterNotFound {
}
}
impl fmt::Display for SourceSelector {
impl fmt::Display for ToolchainSources {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::All(_) => f.write_str("all sources"),
@ -1306,27 +1298,27 @@ impl fmt::Display for SourceSelector {
write!(
f,
"{} or {}",
InterpreterSource::SearchPath,
InterpreterSource::PyLauncher
ToolchainSource::SearchPath,
ToolchainSource::PyLauncher
)
} else {
write!(
f,
"{}, {}, or {}",
InterpreterSource::SearchPath,
InterpreterSource::PyLauncher,
InterpreterSource::ManagedToolchain
ToolchainSource::SearchPath,
ToolchainSource::PyLauncher,
ToolchainSource::Managed
)
}
} else {
if preview.is_disabled() {
write!(f, "{}", InterpreterSource::SearchPath)
write!(f, "{}", ToolchainSource::SearchPath)
} else {
write!(
f,
"{} or {}",
InterpreterSource::SearchPath,
InterpreterSource::ManagedToolchain
ToolchainSource::SearchPath,
ToolchainSource::Managed
)
}
}
@ -1335,7 +1327,7 @@ impl fmt::Display for SourceSelector {
let sources: Vec<_> = sources
.iter()
.sorted()
.map(InterpreterSource::to_string)
.map(ToolchainSource::to_string)
.collect();
match sources[..] {
[] => unreachable!("Source selectors must contain at least one source"),
@ -1348,21 +1340,6 @@ impl fmt::Display for SourceSelector {
}
}
impl DiscoveredInterpreter {
#[allow(dead_code)]
pub fn source(&self) -> &InterpreterSource {
&self.source
}
pub fn interpreter(&self) -> &Interpreter {
&self.interpreter
}
pub fn into_interpreter(self) -> Interpreter {
self.interpreter
}
}
#[cfg(test)]
mod tests {
@ -1373,74 +1350,74 @@ mod tests {
use assert_fs::{prelude::*, TempDir};
use crate::{
discovery::{InterpreterRequest, VersionRequest},
discovery::{ToolchainRequest, VersionRequest},
implementation::ImplementationName,
};
#[test]
fn interpreter_request_from_str() {
assert_eq!(
InterpreterRequest::parse("3.12"),
InterpreterRequest::Version(VersionRequest::from_str("3.12").unwrap())
ToolchainRequest::parse("3.12"),
ToolchainRequest::Version(VersionRequest::from_str("3.12").unwrap())
);
assert_eq!(
InterpreterRequest::parse("foo"),
InterpreterRequest::ExecutableName("foo".to_string())
ToolchainRequest::parse("foo"),
ToolchainRequest::ExecutableName("foo".to_string())
);
assert_eq!(
InterpreterRequest::parse("cpython"),
InterpreterRequest::Implementation(ImplementationName::CPython)
ToolchainRequest::parse("cpython"),
ToolchainRequest::Implementation(ImplementationName::CPython)
);
assert_eq!(
InterpreterRequest::parse("cpython3.12.2"),
InterpreterRequest::ImplementationVersion(
ToolchainRequest::parse("cpython3.12.2"),
ToolchainRequest::ImplementationVersion(
ImplementationName::CPython,
VersionRequest::from_str("3.12.2").unwrap()
)
);
assert_eq!(
InterpreterRequest::parse("pypy"),
InterpreterRequest::Implementation(ImplementationName::PyPy)
ToolchainRequest::parse("pypy"),
ToolchainRequest::Implementation(ImplementationName::PyPy)
);
assert_eq!(
InterpreterRequest::parse("pypy3.10"),
InterpreterRequest::ImplementationVersion(
ToolchainRequest::parse("pypy3.10"),
ToolchainRequest::ImplementationVersion(
ImplementationName::PyPy,
VersionRequest::from_str("3.10").unwrap()
)
);
assert_eq!(
InterpreterRequest::parse("pypy@3.10"),
InterpreterRequest::ImplementationVersion(
ToolchainRequest::parse("pypy@3.10"),
ToolchainRequest::ImplementationVersion(
ImplementationName::PyPy,
VersionRequest::from_str("3.10").unwrap()
)
);
assert_eq!(
InterpreterRequest::parse("pypy310"),
InterpreterRequest::ExecutableName("pypy310".to_string())
ToolchainRequest::parse("pypy310"),
ToolchainRequest::ExecutableName("pypy310".to_string())
);
let tempdir = TempDir::new().unwrap();
assert_eq!(
InterpreterRequest::parse(tempdir.path().to_str().unwrap()),
InterpreterRequest::Directory(tempdir.path().to_path_buf()),
ToolchainRequest::parse(tempdir.path().to_str().unwrap()),
ToolchainRequest::Directory(tempdir.path().to_path_buf()),
"An existing directory is treated as a directory"
);
assert_eq!(
InterpreterRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
InterpreterRequest::File(tempdir.child("foo").path().to_path_buf()),
ToolchainRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
ToolchainRequest::File(tempdir.child("foo").path().to_path_buf()),
"A path that does not exist is treated as a file"
);
tempdir.child("bar").touch().unwrap();
assert_eq!(
InterpreterRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
InterpreterRequest::File(tempdir.child("bar").path().to_path_buf()),
ToolchainRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
ToolchainRequest::File(tempdir.child("bar").path().to_path_buf()),
"An existing file is treated as a file"
);
assert_eq!(
InterpreterRequest::parse("./foo"),
InterpreterRequest::File(PathBuf::from_str("./foo").unwrap()),
ToolchainRequest::parse("./foo"),
ToolchainRequest::File(PathBuf::from_str("./foo").unwrap()),
"A string with a file system separator is treated as a file"
);
}

View file

@ -24,31 +24,31 @@ pub enum Error {
PlatformError(#[from] PlatformError),
#[error(transparent)]
ImplementationError(#[from] ImplementationError),
#[error("invalid python version: {0}")]
#[error("Invalid python version: {0}")]
InvalidPythonVersion(String),
#[error("download failed")]
#[error("Download failed")]
NetworkError(#[from] BetterReqwestError),
#[error("download failed")]
#[error("Download failed")]
NetworkMiddlewareError(#[source] anyhow::Error),
#[error(transparent)]
ExtractError(#[from] uv_extract::Error),
#[error("invalid download url")]
#[error("Invalid download url")]
InvalidUrl(#[from] url::ParseError),
#[error("failed to create download directory")]
#[error("Failed to create download directory")]
DownloadDirError(#[source] io::Error),
#[error("failed to copy to: {0}", to.user_display())]
#[error("Failed to copy to: {0}", to.user_display())]
CopyError {
to: PathBuf,
#[source]
err: io::Error,
},
#[error("failed to read toolchain directory: {0}", dir.user_display())]
#[error("Failed to read toolchain directory: {0}", dir.user_display())]
ReadError {
dir: PathBuf,
#[source]
err: io::Error,
},
#[error("failed to parse toolchain directory name: {0}")]
#[error("Failed to parse toolchain directory name: {0}")]
NameError(String),
}

View file

@ -4,15 +4,11 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::{LockedFile, Simplified};
use crate::discovery::{InterpreterRequest, SourceSelector, SystemPython};
use crate::toolchain::Toolchain;
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
use crate::{
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Prefix,
Target,
};
use crate::{Error, Interpreter, Prefix, Target};
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
#[derive(Debug, Clone)]
@ -25,83 +21,13 @@ struct PythonEnvironmentShared {
}
impl PythonEnvironment {
/// Find a [`PythonEnvironment`].
///
/// This is the standard interface for discovering a Python environment for use with uv.
pub fn find(
python: Option<&str>,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<Self, Error> {
// Detect the current Python interpreter.
if let Some(python) = python {
Self::from_requested_python(python, system, preview, cache)
} else if system.is_preferred() {
Self::from_default_python(preview, cache)
} else {
// First check for a parent intepreter
// We gate this check to avoid an extra log message when it is not set
if std::env::var_os("UV_INTERNAL__PARENT_INTERPRETER").is_some() {
match Self::from_parent_interpreter(system, cache) {
Ok(env) => return Ok(env),
Err(Error::NotFound(_)) => {}
Err(err) => return Err(err),
}
}
// Then a virtual environment
match Self::from_virtualenv(cache) {
Ok(venv) => Ok(venv),
Err(Error::NotFound(_)) if system.is_allowed() => {
Self::from_default_python(preview, cache)
}
Err(err) => Err(err),
}
}
}
/// Create a [`PythonEnvironment`] for an existing virtual environment.
///
/// Allows Conda environments (via `CONDA_PREFIX`) though they are not technically virtual environments.
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
let sources = SourceSelector::VirtualEnv;
let request = InterpreterRequest::Any;
let found = find_interpreter(&request, SystemPython::Disallowed, &sources, cache)??;
debug_assert!(
found.interpreter().is_virtualenv()
|| matches!(found.source(), InterpreterSource::CondaPrefix),
"Not a virtualenv (source: {}, prefix: {})",
found.source(),
found.interpreter().sys_base_prefix().display()
);
Ok(Self(Arc::new(PythonEnvironmentShared {
root: found.interpreter().sys_prefix().to_path_buf(),
interpreter: found.into_interpreter(),
})))
}
/// Create a [`PythonEnvironment`] for the parent interpreter i.e. the executable in `python -m uv ...`
pub fn from_parent_interpreter(system: SystemPython, cache: &Cache) -> Result<Self, Error> {
let sources = SourceSelector::from_sources([InterpreterSource::ParentInterpreter]);
let request = InterpreterRequest::Any;
let found = find_interpreter(&request, system, &sources, cache)??;
Ok(Self(Arc::new(PythonEnvironmentShared {
root: found.interpreter().sys_prefix().to_path_buf(),
interpreter: found.into_interpreter(),
})))
}
/// Create a [`PythonEnvironment`] from the virtual environment at the given root.
pub fn from_root(root: &Path, cache: &Cache) -> Result<Self, Error> {
let venv = match fs_err::canonicalize(root) {
pub fn from_root(root: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
let venv = match fs_err::canonicalize(root.as_ref()) {
Ok(venv) => venv,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(
crate::InterpreterNotFound::DirectoryNotFound(root.to_path_buf()),
crate::ToolchainNotFound::DirectoryNotFound(root.as_ref().to_path_buf()),
));
}
Err(err) => return Err(Error::Discovery(err.into())),
@ -115,29 +41,9 @@ impl PythonEnvironment {
})))
}
/// Create a [`PythonEnvironment`] for a Python interpreter specifier (e.g., a path or a binary name).
pub fn from_requested_python(
request: &str,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<Self, Error> {
let sources = SourceSelector::from_settings(system, preview);
let request = InterpreterRequest::parse(request);
let interpreter = find_interpreter(&request, system, &sources, cache)??.into_interpreter();
Ok(Self(Arc::new(PythonEnvironmentShared {
root: interpreter.sys_prefix().to_path_buf(),
interpreter,
})))
}
/// Create a [`PythonEnvironment`] for the default Python interpreter.
pub fn from_default_python(preview: PreviewMode, cache: &Cache) -> Result<Self, Error> {
let interpreter = find_default_interpreter(preview, cache)??.into_interpreter();
Ok(Self(Arc::new(PythonEnvironmentShared {
root: interpreter.sys_prefix().to_path_buf(),
interpreter,
})))
/// Create a [`PythonEnvironment`] from an existing [`Toolchain`].
pub fn from_toolchain(toolchain: Toolchain) -> Self {
Self::from_interpreter(toolchain.into_interpreter())
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`].

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ pub use crate::downloads::Error;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
/// A collection of installed Python toolchains.
/// A collection of uv-managed Python toolchains installed on the current system.
#[derive(Debug, Clone)]
pub struct InstalledToolchains {
/// The path to the top-level directory of the installed toolchains.
@ -72,7 +72,7 @@ impl InstalledToolchains {
/// ordering across platforms. This also results in newer Python versions coming first,
/// but should not be relied on — instead the toolchains should be sorted later by
/// the parsed Python version.
fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
fn find_all(&self) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
let dirs = match fs_err::read_dir(&self.root) {
Ok(toolchain_dirs) => {
// Collect sorted directory paths; `read_dir` is not stable across platforms
@ -101,14 +101,14 @@ impl InstalledToolchains {
};
Ok(dirs
.into_iter()
.map(|path| Toolchain::new(path).unwrap())
.map(|path| InstalledToolchain::new(path).unwrap())
.rev())
}
/// Iterate over toolchains that support the current platform.
pub fn find_matching_current_platform(
&self,
) -> Result<impl DoubleEndedIterator<Item = Toolchain>, Error> {
) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain>, Error> {
let platform_key = platform_key_from_env()?;
let iter = InstalledToolchains::from_settings()?
@ -133,7 +133,7 @@ impl InstalledToolchains {
pub fn find_version<'a>(
&self,
version: &'a PythonVersion,
) -> Result<impl DoubleEndedIterator<Item = Toolchain> + 'a, Error> {
) -> Result<impl DoubleEndedIterator<Item = InstalledToolchain> + 'a, Error> {
Ok(self
.find_matching_current_platform()?
.filter(move |toolchain| {
@ -150,28 +150,28 @@ impl InstalledToolchains {
}
}
/// An installed Python toolchain.
/// A uv-managed Python toolchain installed on the current system..
#[derive(Debug, Clone)]
pub struct Toolchain {
pub struct InstalledToolchain {
/// The path to the top-level directory of the installed toolchain.
path: PathBuf,
python_version: PythonVersion,
}
impl Toolchain {
impl InstalledToolchain {
pub fn new(path: PathBuf) -> Result<Self, Error> {
let python_version = PythonVersion::from_str(
path.file_name()
.ok_or(Error::NameError("No directory name".to_string()))?
.ok_or(Error::NameError("name is empty".to_string()))?
.to_str()
.ok_or(Error::NameError("Name not a valid string".to_string()))?
.ok_or(Error::NameError("not a valid string".to_string()))?
.split('-')
.nth(1)
.ok_or(Error::NameError(
"Not enough `-`-separated values".to_string(),
"not enough `-`-separated values".to_string(),
))?,
)
.map_err(|err| Error::NameError(format!("Name has invalid Python version: {err}")))?;
.map_err(|err| Error::NameError(format!("invalid Python version: {err}")))?;
Ok(Self {
path,
@ -202,7 +202,7 @@ fn platform_key_from_env() -> Result<String, Error> {
Ok(format!("{os}-{arch}-{libc}").to_lowercase())
}
impl fmt::Display for Toolchain {
impl fmt::Display for InstalledToolchain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,

View file

@ -0,0 +1,136 @@
use uv_configuration::PreviewMode;
use uv_cache::Cache;
use crate::discovery::{SystemPython, ToolchainRequest, ToolchainSources};
use crate::{
find_best_toolchain, find_default_toolchain, find_toolchain, Error, Interpreter,
ToolchainSource,
};
/// A Python interpreter and accompanying tools.
#[derive(Clone, Debug)]
pub struct Toolchain {
// Public in the crate for test assertions
pub(crate) source: ToolchainSource,
pub(crate) interpreter: Interpreter,
}
impl Toolchain {
/// Find an installed [`Toolchain`].
///
/// This is the standard interface for discovering a Python toolchain for use with uv.
///
/// See [`uv-toolchain::discovery`] for implementation details.
pub fn find(
python: Option<&str>,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<Self, Error> {
if let Some(python) = python {
Self::find_requested(python, system, preview, cache)
} else if system.is_preferred() {
Self::find_default(preview, cache)
} else {
// First check for a parent intepreter
// We gate this check to avoid an extra log message when it is not set
if std::env::var_os("UV_INTERNAL__PARENT_INTERPRETER").is_some() {
match Self::find_parent_interpreter(system, cache) {
Ok(env) => return Ok(env),
Err(Error::NotFound(_)) => {}
Err(err) => return Err(err),
}
}
// Then a virtual environment
match Self::find_virtualenv(cache) {
Ok(venv) => Ok(venv),
Err(Error::NotFound(_)) if system.is_allowed() => {
Self::find_default(preview, cache)
}
Err(err) => Err(err),
}
}
}
/// Find an installed [`Toolchain`] that satisfies a request.
pub fn find_requested(
request: &str,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<Self, Error> {
let sources = ToolchainSources::from_settings(system, preview);
let request = ToolchainRequest::parse(request);
let toolchain = find_toolchain(&request, system, &sources, cache)??;
Ok(toolchain)
}
/// Find an installed [`Toolchain`] that satisfies a requested version, if the request cannot
/// be satisfied, fallback to the best available toolchain.
pub fn find_best(
request: &ToolchainRequest,
system: SystemPython,
preview: PreviewMode,
cache: &Cache,
) -> Result<Self, Error> {
Ok(find_best_toolchain(request, system, preview, cache)??)
}
/// Find an installed [`Toolchain`] in an existing virtual environment.
///
/// Allows Conda environments (via `CONDA_PREFIX`) though they are not technically virtual environments.
pub fn find_virtualenv(cache: &Cache) -> Result<Self, Error> {
let sources = ToolchainSources::VirtualEnv;
let request = ToolchainRequest::Any;
let toolchain = find_toolchain(&request, SystemPython::Disallowed, &sources, cache)??;
debug_assert!(
toolchain.interpreter().is_virtualenv()
|| matches!(toolchain.source(), ToolchainSource::CondaPrefix),
"Not a virtualenv (source: {}, prefix: {})",
toolchain.source(),
toolchain.interpreter().sys_base_prefix().display()
);
Ok(toolchain)
}
/// Find the [`Toolchain`] belonging to the parent interpreter i.e. from `python -m uv ...`
///
/// If not spawned by `python -m uv`, the toolchain will not be found.
pub fn find_parent_interpreter(system: SystemPython, cache: &Cache) -> Result<Self, Error> {
let sources = ToolchainSources::from_sources([ToolchainSource::ParentInterpreter]);
let request = ToolchainRequest::Any;
let toolchain = find_toolchain(&request, system, &sources, cache)??;
Ok(toolchain)
}
/// Find the default installed [`Toolchain`].
pub fn find_default(preview: PreviewMode, cache: &Cache) -> Result<Self, Error> {
let toolchain = find_default_toolchain(preview, cache)??;
Ok(toolchain)
}
/// Create a [`Toolchain`] from an existing [`Interpreter`].
pub fn from_interpreter(interpreter: Interpreter) -> Self {
Self {
source: ToolchainSource::ProvidedPath,
interpreter,
}
}
pub fn source(&self) -> &ToolchainSource {
&self.source
}
pub fn interpreter(&self) -> &Interpreter {
&self.interpreter
}
pub fn into_interpreter(self) -> Interpreter {
self.interpreter
}
}

View file

@ -17,7 +17,7 @@ pub enum Error {
#[error("Failed to determine Python interpreter to use")]
Discovery(#[from] uv_toolchain::DiscoveryError),
#[error("Failed to determine Python interpreter to use")]
InterpreterNotFound(#[from] uv_toolchain::InterpreterNotFound),
InterpreterNotFound(#[from] uv_toolchain::ToolchainNotFound),
#[error(transparent)]
Platform(#[from] PlatformError),
#[error("Could not find a suitable Python executable for the virtual environment based on the interpreter: {0}")]

View file

@ -10,7 +10,7 @@ use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_installer::{SitePackages, SitePackagesDiagnostic};
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain};
use crate::commands::{elapsed, ExitStatus};
use crate::printer::Printer;
@ -31,16 +31,17 @@ pub(crate) fn pip_check(
} else {
SystemPython::Allowed
};
let venv = PythonEnvironment::find(python, system, preview, cache)?;
let environment =
PythonEnvironment::from_toolchain(Toolchain::find(python, system, preview, cache)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Build the installed index.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
let packages: Vec<&InstalledDist> = site_packages.iter().collect();
let s = if packages.len() == 1 { "" } else { "s" };

View file

@ -42,8 +42,7 @@ use uv_resolver::{
Resolver,
};
use uv_toolchain::{
find_best_interpreter, InterpreterRequest, PythonEnvironment, PythonVersion, SystemPython,
VersionRequest,
PythonEnvironment, PythonVersion, SystemPython, Toolchain, ToolchainRequest, VersionRequest,
};
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
use uv_warnings::warn_user;
@ -162,17 +161,19 @@ pub(crate) async fn pip_compile(
SystemPython::Allowed
};
let interpreter = if let Some(python) = python.as_ref() {
PythonEnvironment::from_requested_python(python, system, preview, &cache)?
.into_interpreter()
Toolchain::find_requested(python, system, preview, &cache)
} else {
// TODO(zanieb): The split here hints at a problem with the abstraction; we should be able to use
// `Toolchain::find(...)` here.
let request = if let Some(version) = python_version.as_ref() {
// TODO(zanieb): We should consolidate `VersionRequest` and `PythonVersion`
InterpreterRequest::Version(VersionRequest::from(version))
ToolchainRequest::Version(VersionRequest::from(version))
} else {
InterpreterRequest::default()
ToolchainRequest::default()
};
find_best_interpreter(&request, system, preview, &cache)??.into_interpreter()
};
Toolchain::find_best(&request, system, preview, &cache)
}?
.into_interpreter();
debug!(
"Using Python {} interpreter at {} for builds",
@ -297,10 +298,10 @@ pub(crate) async fn pip_compile(
let in_flight = InFlight::default();
// Determine whether to enable build isolation.
let venv;
let environment;
let build_isolation = if no_build_isolation {
venv = PythonEnvironment::from_interpreter(interpreter.clone());
BuildIsolation::Shared(&venv)
environment = PythonEnvironment::from_interpreter(interpreter.clone());
BuildIsolation::Shared(&environment)
} else {
BuildIsolation::Isolated
};

View file

@ -10,7 +10,7 @@ use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_installer::SitePackages;
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain};
use crate::commands::ExitStatus;
use crate::printer::Printer;
@ -31,16 +31,17 @@ pub(crate) fn pip_freeze(
} else {
SystemPython::Allowed
};
let venv = PythonEnvironment::find(python, system, preview, cache)?;
let environment =
PythonEnvironment::from_toolchain(Toolchain::find(python, system, preview, cache)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Build the installed index.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
for dist in site_packages
.iter()
.filter(|dist| !(exclude_editable && dist.is_editable()))

View file

@ -27,7 +27,7 @@ use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder, PreReleaseMode,
ResolutionMode,
};
use uv_toolchain::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_toolchain::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target, Toolchain};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip::operations;
@ -122,57 +122,62 @@ pub(crate) async fn pip_install(
} else {
SystemPython::Explicit
};
let venv = PythonEnvironment::find(python.as_deref(), system, preview, &cache)?;
let environment = PythonEnvironment::from_toolchain(Toolchain::find(
python.as_deref(),
system,
preview,
&cache,
)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
target.init()?;
venv.with_target(target)
environment.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
environment.with_prefix(prefix)
} else {
venv
environment
};
// If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if let Some(externally_managed) = environment.interpreter().is_externally_managed() {
if break_system_packages {
debug!("Ignoring externally managed environment due to `--break-system-packages`");
} else {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().user_display().cyan(),
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv environment`.",
environment.root().user_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().user_display().cyan()
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv environment`.",
environment.root().user_display().cyan()
))
};
}
}
let _lock = venv.lock()?;
let _lock = environment.lock()?;
// Determine the set of installed packages.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
// Check if the current environment satisfies the requirements.
// Ideally, the resolver would be fast enough to let us remove this check. But right now, for large environments,
@ -215,7 +220,7 @@ pub(crate) async fn pip_install(
}
}
let interpreter = venv.interpreter();
let interpreter = environment.interpreter();
// Determine the tags, markers, and interpreter to use for resolution.
let tags = match (python_platform, python_version.as_ref()) {
@ -302,7 +307,7 @@ pub(crate) async fn pip_install(
// Determine whether to enable build isolation.
let build_isolation = if no_build_isolation {
BuildIsolation::Shared(&venv)
BuildIsolation::Shared(&environment)
} else {
BuildIsolation::Isolated
};
@ -431,7 +436,7 @@ pub(crate) async fn pip_install(
concurrency,
&install_dispatch,
&cache,
&venv,
&environment,
dry_run,
printer,
preview,
@ -443,7 +448,7 @@ pub(crate) async fn pip_install(
// Notify the user of any environment diagnostics.
if strict && !dry_run {
operations::diagnose_environment(&resolution, &venv, printer)?;
operations::diagnose_environment(&resolution, &environment, printer)?;
}
Ok(ExitStatus::Success)

View file

@ -14,6 +14,7 @@ use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_toolchain::Toolchain;
use uv_toolchain::{PythonEnvironment, SystemPython};
use crate::commands::ExitStatus;
@ -40,16 +41,17 @@ pub(crate) fn pip_list(
} else {
SystemPython::Allowed
};
let venv = PythonEnvironment::find(python, system, preview, cache)?;
let environment =
PythonEnvironment::from_toolchain(Toolchain::find(python, system, preview, cache)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Build the installed index.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
// Filter if `--editable` is specified; always sort by name.
let results = site_packages

View file

@ -12,7 +12,7 @@ use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain};
use crate::commands::ExitStatus;
use crate::printer::Printer;
@ -46,19 +46,20 @@ pub(crate) fn pip_show(
} else {
SystemPython::Allowed
};
let venv = PythonEnvironment::find(python, system, preview, cache)?;
let environment =
PythonEnvironment::from_toolchain(Toolchain::find(python, system, preview, cache)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Build the installed index.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
// Determine the markers to use for resolution.
let markers = venv.interpreter().markers();
let markers = environment.interpreter().markers();
// Sort and deduplicate the packages, which are keyed by name.
packages.sort_unstable();

View file

@ -26,7 +26,7 @@ use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder, PreReleaseMode,
ResolutionMode,
};
use uv_toolchain::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_toolchain::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target, Toolchain};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip::operations;
@ -117,56 +117,61 @@ pub(crate) async fn pip_sync(
} else {
SystemPython::Explicit
};
let venv = PythonEnvironment::find(python.as_deref(), system, preview, &cache)?;
let environment = PythonEnvironment::from_toolchain(Toolchain::find(
python.as_deref(),
system,
preview,
&cache,
)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan()
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan()
);
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
target.init()?;
venv.with_target(target)
environment.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
environment.with_prefix(prefix)
} else {
venv
environment
};
// If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if let Some(externally_managed) = environment.interpreter().is_externally_managed() {
if break_system_packages {
debug!("Ignoring externally managed environment due to `--break-system-packages`");
} else {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().user_display().cyan(),
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv environment`.",
environment.root().user_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().user_display().cyan()
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv environment`.",
environment.root().user_display().cyan()
))
};
}
}
let _lock = venv.lock()?;
let _lock = environment.lock()?;
let interpreter = venv.interpreter();
let interpreter = environment.interpreter();
// Determine the current environment markers.
let tags = match (python_platform, python_version.as_ref()) {
@ -245,7 +250,7 @@ pub(crate) async fn pip_sync(
// Determine whether to enable build isolation.
let build_isolation = if no_build_isolation {
BuildIsolation::Shared(&venv)
BuildIsolation::Shared(&environment)
} else {
BuildIsolation::Isolated
};
@ -289,7 +294,7 @@ pub(crate) async fn pip_sync(
.with_options(OptionsBuilder::new().exclude_newer(exclude_newer).build());
// Determine the set of installed packages.
let site_packages = SitePackages::from_executable(&venv)?;
let site_packages = SitePackages::from_executable(&environment)?;
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
@ -383,7 +388,7 @@ pub(crate) async fn pip_sync(
concurrency,
&install_dispatch,
&cache,
&venv,
&environment,
dry_run,
printer,
preview,
@ -395,7 +400,7 @@ pub(crate) async fn pip_sync(
// Notify the user of any environment diagnostics.
if strict && !dry_run {
operations::diagnose_environment(&resolution, &venv, printer)?;
operations::diagnose_environment(&resolution, &environment, printer)?;
}
Ok(ExitStatus::Success)

View file

@ -14,6 +14,7 @@ use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{KeyringProviderType, PreviewMode};
use uv_fs::Simplified;
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_toolchain::Toolchain;
use uv_toolchain::{Prefix, PythonEnvironment, SystemPython, Target};
use crate::commands::{elapsed, ExitStatus};
@ -50,57 +51,62 @@ pub(crate) async fn pip_uninstall(
} else {
SystemPython::Explicit
};
let venv = PythonEnvironment::find(python.as_deref(), system, preview, &cache)?;
let environment = PythonEnvironment::from_toolchain(Toolchain::find(
python.as_deref(),
system,
preview,
&cache,
)?);
debug!(
"Using Python {} environment at {}",
venv.interpreter().python_version(),
venv.python_executable().user_display().cyan(),
environment.interpreter().python_version(),
environment.python_executable().user_display().cyan(),
);
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
target.init()?;
venv.with_target(target)
environment.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
environment.with_prefix(prefix)
} else {
venv
environment
};
// If the environment is externally managed, abort.
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
if let Some(externally_managed) = environment.interpreter().is_externally_managed() {
if break_system_packages {
debug!("Ignoring externally managed environment due to `--break-system-packages`");
} else {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
venv.root().user_display().cyan(),
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv environment`.",
environment.root().user_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
venv.root().user_display().cyan()
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv environment`.",
environment.root().user_display().cyan()
))
};
}
}
let _lock = venv.lock()?;
let _lock = environment.lock()?;
// Index the current `site-packages` directory.
let site_packages = uv_installer::SitePackages::from_executable(&venv)?;
let site_packages = uv_installer::SitePackages::from_executable(&environment)?;
// Partition the requirements into named and unnamed requirements.
let (named, unnamed): (Vec<Requirement>, Vec<UnnamedRequirement<VerbatimParsedUrl>>) = spec

View file

@ -21,7 +21,7 @@ use uv_git::GitResolver;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, Options, RequiresPython};
use uv_toolchain::{find_default_interpreter, PythonEnvironment};
use uv_toolchain::{find_default_toolchain, PythonEnvironment};
use uv_types::{BuildIsolation, HashStrategy, InFlight};
use crate::commands::pip;
@ -82,7 +82,7 @@ pub(crate) fn init_environment(
Ok(venv) => Ok(venv),
Err(uv_toolchain::Error::NotFound(_)) => {
// TODO(charlie): Respect `--python`; if unset, respect `Requires-Python`.
let interpreter = find_default_interpreter(preview, cache)
let interpreter = find_default_toolchain(preview, cache)
.map_err(uv_toolchain::Error::from)?
.map_err(uv_toolchain::Error::from)?
.into_interpreter();

View file

@ -15,7 +15,7 @@ use uv_distribution::{ProjectWorkspace, Workspace};
use uv_normalize::PackageName;
use uv_requirements::RequirementsSource;
use uv_resolver::ExcludeNewer;
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain};
use uv_warnings::warn_user;
use crate::commands::{project, ExitStatus};
@ -109,10 +109,10 @@ pub(crate) async fn run(
let interpreter = if let Some(project_env) = &project_env {
project_env.interpreter().clone()
} else if let Some(python) = python.as_ref() {
PythonEnvironment::from_requested_python(python, SystemPython::Allowed, preview, cache)?
Toolchain::find_requested(python, SystemPython::Allowed, preview, cache)?
.into_interpreter()
} else {
PythonEnvironment::from_default_python(preview, cache)?.into_interpreter()
Toolchain::find_default(preview, cache)?.into_interpreter()
};
// TODO(charlie): If the environment satisfies the requirements, skip creation.

View file

@ -12,7 +12,7 @@ use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::PreviewMode;
use uv_requirements::RequirementsSource;
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain};
use uv_warnings::warn_user;
use crate::commands::project::update_environment;
@ -53,10 +53,9 @@ pub(crate) async fn run(
// Discover an interpreter.
let interpreter = if let Some(python) = python.as_ref() {
PythonEnvironment::from_requested_python(python, SystemPython::Allowed, preview, cache)?
.into_interpreter()
Toolchain::find_requested(python, SystemPython::Allowed, preview, cache)?.into_interpreter()
} else {
PythonEnvironment::from_default_python(preview, cache)?.into_interpreter()
Toolchain::find_default(preview, cache)?.into_interpreter()
};
// Create a virtual environment

View file

@ -21,7 +21,7 @@ use uv_dispatch::BuildDispatch;
use uv_fs::Simplified;
use uv_git::GitResolver;
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder};
use uv_toolchain::{PythonEnvironment, SystemPython};
use uv_toolchain::{SystemPython, Toolchain};
use uv_types::{BuildContext, BuildIsolation, HashStrategy, InFlight};
use crate::commands::{pip, ExitStatus};
@ -120,10 +120,9 @@ async fn venv_impl(
printer: Printer,
) -> miette::Result<ExitStatus> {
// Locate the Python interpreter to use in the environment
let interpreter =
PythonEnvironment::find(python_request, SystemPython::Required, preview, cache)
.into_diagnostic()?
.into_interpreter();
let interpreter = Toolchain::find(python_request, SystemPython::Required, preview, cache)
.into_diagnostic()?
.into_interpreter();
// Add all authenticated sources to the cache.
for url in index_locations.urls() {

View file

@ -19,7 +19,7 @@ use uv_cache::Cache;
use uv_fs::Simplified;
use uv_toolchain::managed::InstalledToolchains;
use uv_toolchain::{
find_interpreter, InterpreterRequest, PythonVersion, SourceSelector, VersionRequest,
find_toolchain, PythonVersion, ToolchainRequest, ToolchainSources, VersionRequest,
};
// Exclude any packages uploaded after this date.
@ -416,7 +416,7 @@ pub fn create_venv<Parent: assert_fs::prelude::PathChild + AsRef<std::path::Path
.expect("Tests are run on a supported platform")
.next()
.as_ref()
.map(uv_toolchain::managed::Toolchain::executable)
.map(uv_toolchain::managed::InstalledToolchain::executable)
})
// We'll search for the request Python on the PATH if not found in the toolchain versions
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
@ -476,12 +476,12 @@ pub fn python_path_with_versions(
.unwrap_or_default();
if inner.is_empty() {
// Fallback to a system lookup if we failed to find one in the toolchain directory
let request = InterpreterRequest::Version(
let request = ToolchainRequest::Version(
VersionRequest::from_str(python_version)
.expect("The test version request must be valid"),
);
let sources = SourceSelector::All(PreviewMode::Enabled);
if let Ok(found) = find_interpreter(
let sources = ToolchainSources::All(PreviewMode::Enabled);
if let Ok(found) = find_toolchain(
&request,
// Without required, we could pick the current venv here and the test fails
// because the venv subcommand requires a system interpreter.