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.
This commit is contained in:
Charlie Marsh 2024-05-10 10:10:13 -04:00 committed by GitHub
parent 5f8c3b7e45
commit 835ebe60c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 83 additions and 21 deletions

View file

@ -18,12 +18,24 @@ pub struct PythonEnvironment {
} }
impl 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<Self, Error> { pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
let Some(venv) = detect_virtualenv()? else { let Some(venv) = detect_virtualenv()? else {
return Err(Error::VenvNotFound); 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<Self, Error> {
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 executable = virtualenv_python_executable(&venv);
let interpreter = Interpreter::query(&executable, cache)?; let interpreter = Interpreter::query(&executable, cache)?;

View file

@ -736,7 +736,7 @@ mod windows {
)); ));
assert_snapshot!( assert_snapshot!(
format_err(result), 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(), "/does/not/exists/python3.12".to_string(),
)); ));
assert_snapshot!( 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`");
} }
} }

View file

@ -14,6 +14,8 @@ use std::process::ExitStatus;
use thiserror::Error; use thiserror::Error;
use uv_fs::Simplified;
pub use crate::environment::PythonEnvironment; pub use crate::environment::PythonEnvironment;
pub use crate::find_python::{find_best_python, find_default_python, find_requested_python}; pub use crate::find_python::{find_best_python, find_default_python, find_requested_python};
pub use crate::interpreter::Interpreter; pub use crate::interpreter::Interpreter;
@ -35,13 +37,15 @@ mod virtualenv;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum 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), MissingPyVenvCfg(PathBuf),
#[error("No versions of Python could be found. Is Python installed?")] #[error("No versions of Python could be found. Is Python installed?")]
PythonNotFound, PythonNotFound,
#[error("Failed to locate a virtualenv or Conda environment (checked: `VIRTUAL_ENV`, `CONDA_PREFIX`, and `.venv`). Run `uv venv` to create a virtualenv.")] #[error("Failed to locate a virtualenv or Conda environment (checked: `VIRTUAL_ENV`, `CONDA_PREFIX`, and `.venv`). Run `uv venv` to create a virtualenv.")]
VenvNotFound, 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), RequestedPythonNotFound(String),
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),

View file

@ -120,7 +120,7 @@ mod tests {
)); ));
assert_snapshot!( assert_snapshot!(
format_err(result), 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`"
); );
} }
} }

View file

@ -7,7 +7,6 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy}; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy};
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_interpreter::PythonEnvironment;
use uv_requirements::{ExtrasSpecification, RequirementsSpecification}; use uv_requirements::{ExtrasSpecification, RequirementsSpecification};
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
use uv_types::{BuildIsolation, HashStrategy, InFlight}; 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."); 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. // Find the project requirements.
let Some(project) = Project::find(std::env::current_dir()?)? else { let Some(project) = Project::find(std::env::current_dir()?)? else {
return Err(anyhow::anyhow!( 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 // TODO(zanieb): Support client configuration
let client_builder = BaseClientBuilder::default(); let client_builder = BaseClientBuilder::default();

View file

@ -13,8 +13,9 @@ use uv_cache::Cache;
use uv_client::RegistryClient; use uv_client::RegistryClient;
use uv_configuration::{Constraints, NoBinary, Overrides, Reinstall}; use uv_configuration::{Constraints, NoBinary, Overrides, Reinstall};
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_fs::Simplified;
use uv_installer::{Downloader, Plan, Planner, SitePackages}; use uv_installer::{Downloader, Plan, Planner, SitePackages};
use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
use uv_requirements::{ use uv_requirements::{
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSpecification, ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSpecification,
SourceTreeResolver, SourceTreeResolver,
@ -25,6 +26,7 @@ use uv_resolver::{
}; };
use uv_types::{EmptyInstalledPackages, HashStrategy, InFlight}; use uv_types::{EmptyInstalledPackages, HashStrategy, InFlight};
use crate::commands::project::discovery::Project;
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
use crate::printer::Printer; use crate::printer::Printer;
@ -45,6 +47,12 @@ pub(crate) enum Error {
#[error(transparent)] #[error(transparent)]
Platform(#[from] platform_tags::PlatformError), Platform(#[from] platform_tags::PlatformError),
#[error(transparent)]
Interpreter(#[from] uv_interpreter::Error),
#[error(transparent)]
Virtualenv(#[from] uv_virtualenv::Error),
#[error(transparent)] #[error(transparent)]
Hash(#[from] uv_types::HashStrategyError), Hash(#[from] uv_types::HashStrategyError),
@ -61,6 +69,47 @@ pub(crate) enum Error {
Anyhow(#[from] anyhow::Error), Anyhow(#[from] anyhow::Error),
} }
/// Initialize a virtual environment for the current project.
pub(crate) fn init(
project: &Project,
cache: &Cache,
printer: Printer,
) -> Result<PythonEnvironment, Error> {
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`. /// Resolve a set of requirements, similar to running `pip compile`.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) async fn resolve( pub(crate) async fn resolve(

View file

@ -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. // Install the project requirements.
Some(update_environment(venv, &project.requirements(), preview, cache, printer).await?) Some(update_environment(venv, &project.requirements(), preview, cache, printer).await?)

View file

@ -7,7 +7,6 @@ use uv_client::RegistryClientBuilder;
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy}; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy};
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_installer::SitePackages; use uv_installer::SitePackages;
use uv_interpreter::PythonEnvironment;
use uv_resolver::{FlatIndex, InMemoryIndex, Lock}; use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_types::{BuildIsolation, HashStrategy, InFlight};
use uv_warnings::warn_user; 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."); 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. // Find the project requirements.
let Some(project) = Project::find(std::env::current_dir()?)? else { let Some(project) = Project::find(std::env::current_dir()?)? else {
return Err(anyhow::anyhow!( 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. // Read the lockfile.
let resolution = { let resolution = {
let encoded = fs_err::tokio::read_to_string(project.root().join("uv.lock")).await?; let encoded = fs_err::tokio::read_to_string(project.root().join("uv.lock")).await?;

View file

@ -108,8 +108,7 @@ fn missing_venv() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: failed to canonicalize path `[VENV]/` error: Virtualenv does not exist at: `[VENV]/`
Caused by: No such file or directory (os error 2)
"###); "###);
assert!(predicates::path::missing().eval(&context.venv)); assert!(predicates::path::missing().eval(&context.venv));