Skip to content

Commit

Permalink
Create virtualenv if it doesn't exist in project API
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed May 10, 2024
1 parent 5f8c3b7 commit e06d76e
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 21 deletions.
16 changes: 14 additions & 2 deletions crates/uv-interpreter/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Error> {
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<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 interpreter = Interpreter::query(&executable, cache)?;

Expand Down
4 changes: 2 additions & 2 deletions crates/uv-interpreter/src/find_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`"
);
}
}
Expand Down Expand Up @@ -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`");
}
}
8 changes: 6 additions & 2 deletions crates/uv-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-interpreter/src/py_launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`"
);
}
}
7 changes: 3 additions & 4 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -29,16 +28,16 @@ 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!(
"Unable to find `pyproject.toml` for project."
));
};

// Discover or create the virtual environment.
let venv = project::init(&project, cache, printer)?;

// TODO(zanieb): Support client configuration
let client_builder = BaseClientBuilder::default();

Expand Down
51 changes: 50 additions & 1 deletion crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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),

Expand All @@ -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<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`.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn resolve(
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
Expand Down
11 changes: 5 additions & 6 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,18 +26,18 @@ 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!(
"Unable to find `pyproject.toml` for project."
));
};

// 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?;
Expand Down
3 changes: 1 addition & 2 deletions crates/uv/tests/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down

0 comments on commit e06d76e

Please sign in to comment.