From 835ebe60c682d330fe5d5a11d8a69fffa97027b6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 10 May 2024 10:10:13 -0400 Subject: [PATCH] Create virtualenv if it doesn't exist in project API (#3499) ## Summary This doesn't yet respect `--python` or the `requires-python` in the project itself. Closes #3449. --- crates/uv-interpreter/src/environment.rs | 16 +++++++- crates/uv-interpreter/src/find_python.rs | 4 +- crates/uv-interpreter/src/lib.rs | 8 +++- crates/uv-interpreter/src/py_launcher.rs | 2 +- crates/uv/src/commands/project/lock.rs | 7 ++-- crates/uv/src/commands/project/mod.rs | 51 +++++++++++++++++++++++- crates/uv/src/commands/project/run.rs | 2 +- crates/uv/src/commands/project/sync.rs | 11 +++-- crates/uv/tests/pip_sync.rs | 3 +- 9 files changed, 83 insertions(+), 21 deletions(-) diff --git a/crates/uv-interpreter/src/environment.rs b/crates/uv-interpreter/src/environment.rs index 33b785ce3..2811755e5 100644 --- a/crates/uv-interpreter/src/environment.rs +++ b/crates/uv-interpreter/src/environment.rs @@ -18,12 +18,24 @@ pub struct PythonEnvironment { } impl PythonEnvironment { - /// Create a [`PythonEnvironment`] for an existing virtual environment. + /// Create a [`PythonEnvironment`] for an existing virtual environment, detected from the + /// environment variables and filesystem. pub fn from_virtualenv(cache: &Cache) -> Result { let Some(venv) = detect_virtualenv()? else { return Err(Error::VenvNotFound); }; - let venv = fs_err::canonicalize(venv)?; + Self::from_root(&venv, cache) + } + + /// Create a [`PythonEnvironment`] from the virtual environment at the given root. + pub fn from_root(root: &Path, cache: &Cache) -> Result { + let venv = match fs_err::canonicalize(root) { + Ok(venv) => venv, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::VenvDoesNotExist(root.to_path_buf())); + } + Err(err) => return Err(err.into()), + }; let executable = virtualenv_python_executable(&venv); let interpreter = Interpreter::query(&executable, cache)?; diff --git a/crates/uv-interpreter/src/find_python.rs b/crates/uv-interpreter/src/find_python.rs index 5378e30ab..6e953075e 100644 --- a/crates/uv-interpreter/src/find_python.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -736,7 +736,7 @@ mod windows { )); assert_snapshot!( format_err(result), - @"Failed to locate Python interpreter at `C:\\does\\not\\exists\\python3.12`" + @"Failed to locate Python interpreter at: `C:\\does\\not\\exists\\python3.12`" ); } } @@ -793,6 +793,6 @@ mod tests { "/does/not/exists/python3.12".to_string(), )); assert_snapshot!( - format_err(result), @"Failed to locate Python interpreter at `/does/not/exists/python3.12`"); + format_err(result), @"Failed to locate Python interpreter at: `/does/not/exists/python3.12`"); } } diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index 7b02197ad..f0a359e00 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -14,6 +14,8 @@ use std::process::ExitStatus; use thiserror::Error; +use uv_fs::Simplified; + pub use crate::environment::PythonEnvironment; pub use crate::find_python::{find_best_python, find_default_python, find_requested_python}; pub use crate::interpreter::Interpreter; @@ -35,13 +37,15 @@ mod virtualenv; #[derive(Debug, Error)] pub enum Error { - #[error("Expected `{0}` to be a virtualenv, but `pyvenv.cfg` is missing")] + #[error("Expected `{}` to be a virtualenv, but `pyvenv.cfg` is missing", _0.user_display())] MissingPyVenvCfg(PathBuf), #[error("No versions of Python could be found. Is Python installed?")] PythonNotFound, #[error("Failed to locate a virtualenv or Conda environment (checked: `VIRTUAL_ENV`, `CONDA_PREFIX`, and `.venv`). Run `uv venv` to create a virtualenv.")] VenvNotFound, - #[error("Failed to locate Python interpreter at `{0}`")] + #[error("Virtualenv does not exist at: `{}`", _0.user_display())] + VenvDoesNotExist(PathBuf), + #[error("Failed to locate Python interpreter at: `{0}`")] RequestedPythonNotFound(String), #[error(transparent)] Io(#[from] io::Error), diff --git a/crates/uv-interpreter/src/py_launcher.rs b/crates/uv-interpreter/src/py_launcher.rs index 24a59282f..d29e26963 100644 --- a/crates/uv-interpreter/src/py_launcher.rs +++ b/crates/uv-interpreter/src/py_launcher.rs @@ -120,7 +120,7 @@ mod tests { )); assert_snapshot!( format_err(result), - @"Failed to locate Python interpreter at `C:\\does\\not\\exists\\python3.12`" + @"Failed to locate Python interpreter at: `C:\\does\\not\\exists\\python3.12`" ); } } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index cd0b61ee3..0450b5304 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -7,7 +7,6 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy}; use uv_dispatch::BuildDispatch; -use uv_interpreter::PythonEnvironment; use uv_requirements::{ExtrasSpecification, RequirementsSpecification}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; @@ -29,9 +28,6 @@ pub(crate) async fn lock( warn_user!("`uv lock` is experimental and may change without warning."); } - // TODO(charlie): If the environment doesn't exist, create it. - let venv = PythonEnvironment::from_virtualenv(cache)?; - // Find the project requirements. let Some(project) = Project::find(std::env::current_dir()?)? else { return Err(anyhow::anyhow!( @@ -39,6 +35,9 @@ pub(crate) async fn lock( )); }; + // Discover or create the virtual environment. + let venv = project::init(&project, cache, printer)?; + // TODO(zanieb): Support client configuration let client_builder = BaseClientBuilder::default(); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 6936a98c5..4cd1616d0 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -13,8 +13,9 @@ use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{Constraints, NoBinary, Overrides, Reinstall}; use uv_dispatch::BuildDispatch; +use uv_fs::Simplified; use uv_installer::{Downloader, Plan, Planner, SitePackages}; -use uv_interpreter::{Interpreter, PythonEnvironment}; +use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment}; use uv_requirements::{ ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSpecification, SourceTreeResolver, @@ -25,6 +26,7 @@ use uv_resolver::{ }; use uv_types::{EmptyInstalledPackages, HashStrategy, InFlight}; +use crate::commands::project::discovery::Project; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind}; use crate::printer::Printer; @@ -45,6 +47,12 @@ pub(crate) enum Error { #[error(transparent)] Platform(#[from] platform_tags::PlatformError), + #[error(transparent)] + Interpreter(#[from] uv_interpreter::Error), + + #[error(transparent)] + Virtualenv(#[from] uv_virtualenv::Error), + #[error(transparent)] Hash(#[from] uv_types::HashStrategyError), @@ -61,6 +69,47 @@ pub(crate) enum Error { Anyhow(#[from] anyhow::Error), } +/// Initialize a virtual environment for the current project. +pub(crate) fn init( + project: &Project, + cache: &Cache, + printer: Printer, +) -> Result { + let venv = project.root().join(".venv"); + + // Discover or create the virtual environment. + // TODO(charlie): If the environment isn't compatible with `--python`, recreate it. + match PythonEnvironment::from_root(&venv, cache) { + Ok(venv) => Ok(venv), + Err(uv_interpreter::Error::VenvDoesNotExist(_)) => { + // TODO(charlie): Respect `--python`; if unset, respect `Requires-Python`. + let interpreter = find_default_python(cache)?; + + writeln!( + printer.stderr(), + "Using Python {} interpreter at: {}", + interpreter.python_version(), + interpreter.sys_executable().user_display().cyan() + )?; + + writeln!( + printer.stderr(), + "Creating virtualenv at: {}", + venv.user_display().cyan() + )?; + + Ok(uv_virtualenv::create_venv( + &venv, + interpreter, + uv_virtualenv::Prompt::None, + false, + false, + )?) + } + Err(e) => Err(e.into()), + } +} + /// Resolve a set of requirements, similar to running `pip compile`. #[allow(clippy::too_many_arguments)] pub(crate) async fn resolve( diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index c2039b3b9..77d566af5 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -68,7 +68,7 @@ pub(crate) async fn run( )); }; - let venv = PythonEnvironment::from_virtualenv(cache)?; + let venv = project::init(&project, cache, printer)?; // Install the project requirements. Some(update_environment(venv, &project.requirements(), preview, cache, printer).await?) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 142250ea7..92fe51be2 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -7,7 +7,6 @@ use uv_client::RegistryClientBuilder; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; -use uv_interpreter::PythonEnvironment; use uv_resolver::{FlatIndex, InMemoryIndex, Lock}; use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -27,11 +26,6 @@ pub(crate) async fn sync( warn_user!("`uv sync` is experimental and may change without warning."); } - // TODO(charlie): If the environment doesn't exist, create it. - let venv = PythonEnvironment::from_virtualenv(cache)?; - let markers = venv.interpreter().markers(); - let tags = venv.interpreter().tags()?; - // Find the project requirements. let Some(project) = Project::find(std::env::current_dir()?)? else { return Err(anyhow::anyhow!( @@ -39,6 +33,11 @@ pub(crate) async fn sync( )); }; + // Discover or create the virtual environment. + let venv = project::init(&project, cache, printer)?; + let markers = venv.interpreter().markers(); + let tags = venv.interpreter().tags()?; + // Read the lockfile. let resolution = { let encoded = fs_err::tokio::read_to_string(project.root().join("uv.lock")).await?; diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 0a0ec440e..522c1e77b 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -108,8 +108,7 @@ fn missing_venv() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: failed to canonicalize path `[VENV]/` - Caused by: No such file or directory (os error 2) + error: Virtualenv does not exist at: `[VENV]/` "###); assert!(predicates::path::missing().eval(&context.venv));