Fallback to interpreter discovery in uv run (#4549)

## Summary

This PR modifies `uv run` to fallback to discovering an interpreter
(e.g., a local `.venv`) if the command is run outside of a workspace.

`uv run --isolated` continues to completely skip workspace _and_
interpreter discovering, only installing whatever's provided with
`--with`.

The next step here is adding some ergonomic controls for enabling this
behavior even if your project is technically in a workspace (i.e., you
have a `pyproject.toml` but aren't using the Project APIs and don't want
locking etc.). I could imagine a setting in `pyproject.toml` that's also
exposed on the command-line. Something like: `managed = false` or
`project = false`.

See: https://github.com/astral-sh/uv/issues/3836.
This commit is contained in:
Charlie Marsh 2024-06-26 12:25:18 -04:00 committed by GitHub
parent c9657b0015
commit 0fe5eacdba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,7 +10,7 @@ use uv_cache::Cache;
use uv_cli::ExternalCommand; use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity}; use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode}; use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
use uv_distribution::{ProjectWorkspace, Workspace}; use uv_distribution::{ProjectWorkspace, Workspace, WorkspaceError};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_requirements::RequirementsSource; use uv_requirements::RequirementsSource;
use uv_toolchain::{ use uv_toolchain::{
@ -46,68 +46,112 @@ pub(crate) async fn run(
warn_user!("`uv run` is experimental and may change without warning."); warn_user!("`uv run` is experimental and may change without warning.");
} }
// Discover and sync the project. // Discover and sync the base environment.
let project_env = if isolated { let base_env = if isolated {
// package is `None`, isolated and package are marked as conflicting in clap. // package is `None`, isolated and package are marked as conflicting in clap.
None None
} else { } else {
debug!("Syncing project environment.");
let project = if let Some(package) = package { let project = if let Some(package) = package {
// We need a workspace, but we don't need to have a current package, we can be e.g. in // We need a workspace, but we don't need to have a current package, we can be e.g. in
// the root of a virtual workspace and then switch into the selected package. // the root of a virtual workspace and then switch into the selected package.
Workspace::discover(&std::env::current_dir()?, None) Some(
.await? Workspace::discover(&std::env::current_dir()?, None)
.with_current_project(package.clone()) .await?
.with_context(|| format!("Package `{package}` not found in workspace"))? .with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else { } else {
ProjectWorkspace::discover(&std::env::current_dir()?, None).await? match ProjectWorkspace::discover(&std::env::current_dir()?, None).await {
Ok(project) => Some(project),
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(err) => return Err(err.into()),
}
}; };
let venv = project::init_environment(
project.workspace(),
python.as_deref().map(ToolchainRequest::parse),
toolchain_preference,
connectivity,
native_tls,
cache,
printer,
)
.await?;
// Lock and sync the environment. let venv = if let Some(project) = project {
let lock = project::lock::do_lock( debug!(
project.workspace(), "Discovered project `{}` at: {}",
venv.interpreter(), project.project_name(),
settings.as_ref().into(), project.workspace().root().display()
preview, );
connectivity,
concurrency, let venv = project::init_environment(
native_tls, project.workspace(),
cache, python.as_deref().map(ToolchainRequest::parse),
printer, toolchain_preference,
) connectivity,
.await?; native_tls,
project::sync::do_sync( cache,
project.project_name(), printer,
project.workspace().root(), )
&venv, .await?;
&lock,
extras, // Lock and sync the environment.
dev, let lock = project::lock::do_lock(
Modifications::Sufficient, project.workspace(),
settings.as_ref().into(), venv.interpreter(),
preview, settings.as_ref().into(),
connectivity, preview,
concurrency, connectivity,
native_tls, concurrency,
cache, native_tls,
printer, cache,
) printer,
.await?; )
.await?;
project::sync::do_sync(
project.project_name(),
project.workspace().root(),
&venv,
&lock,
extras,
dev,
Modifications::Sufficient,
settings.as_ref().into(),
preview,
connectivity,
concurrency,
native_tls,
cache,
printer,
)
.await?;
venv
} else {
debug!("No project found; searching for Python interpreter");
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
let toolchain = Toolchain::find_or_fetch(
python.as_deref().map(ToolchainRequest::parse),
// No opt-in is required for system environments, since we are not mutating it.
EnvironmentPreference::Any,
toolchain_preference,
client_builder,
cache,
)
.await?;
// Creating a `PythonEnvironment` from a `find_or_fetch` is generally discouraged, since
// we may end up modifying a managed toolchain. However, the environment here is
// read-only.
PythonEnvironment::from_toolchain(toolchain)
};
Some(venv) Some(venv)
}; };
if let Some(base_env) = &base_env {
debug!(
"Using Python {} interpreter at: {}",
base_env.interpreter().python_version(),
base_env.interpreter().sys_executable().display()
);
}
// If necessary, create an environment for the ephemeral requirements. // If necessary, create an environment for the ephemeral requirements.
let temp_dir; let temp_dir;
let ephemeral_env = if requirements.is_empty() { let ephemeral_env = if requirements.is_empty() {
@ -115,14 +159,14 @@ pub(crate) async fn run(
} else { } else {
debug!("Syncing ephemeral environment."); debug!("Syncing ephemeral environment.");
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
// Discover an interpreter. // Discover an interpreter.
let interpreter = if let Some(project_env) = &project_env { let interpreter = if let Some(base_env) = &base_env {
project_env.interpreter().clone() base_env.interpreter().clone()
} else { } else {
let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls);
// Note we force preview on during `uv run` for now since the entire interface is in preview // Note we force preview on during `uv run` for now since the entire interface is in preview
Toolchain::find_or_fetch( Toolchain::find_or_fetch(
python.as_deref().map(ToolchainRequest::parse), python.as_deref().map(ToolchainRequest::parse),
@ -194,7 +238,7 @@ pub(crate) async fn run(
.map(PythonEnvironment::scripts) .map(PythonEnvironment::scripts)
.into_iter() .into_iter()
.chain( .chain(
project_env base_env
.as_ref() .as_ref()
.map(PythonEnvironment::scripts) .map(PythonEnvironment::scripts)
.into_iter(), .into_iter(),
@ -217,7 +261,7 @@ pub(crate) async fn run(
.into_iter() .into_iter()
.flatten() .flatten()
.chain( .chain(
project_env base_env
.as_ref() .as_ref()
.map(PythonEnvironment::site_packages) .map(PythonEnvironment::site_packages)
.into_iter() .into_iter()