Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve project handling in uv venv #6835

Merged
merged 2 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ pub enum Commands {
/// By default, creates a virtual environment named `.venv` in the working
/// directory. An alternative path may be provided positionally.
///
/// If in a project, the default environment name can be changed with
/// the `UV_PROJECT_ENVIRONMENT` environment variable; this only applies
/// when run from the project root directory.
///
/// If a virtual environment exists at the target path, it will be removed
/// and a new, empty virtual environment will be created.
///
Expand Down Expand Up @@ -1961,6 +1965,14 @@ pub struct VenvArgs {
#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,

/// Avoid discovering a project or workspace.
///
/// By default, uv searches for projects in the current directory or any parent directory to
/// determine the default path of the virtual environment and check for Python version
/// constraints, if any.
#[arg(long, alias = "no-workspace")]
pub no_project: bool,

/// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment.
///
/// Note `setuptools` and `wheel` are not included in Python 3.12+ environments.
Expand All @@ -1980,8 +1992,11 @@ pub struct VenvArgs {
pub allow_existing: bool,

/// The path to the virtual environment to create.
#[arg(default_value = ".venv")]
pub name: PathBuf,
///
/// Default to `.venv` in the working directory.
///
/// Relative paths are resolved relative to the working directory.
pub path: Option<PathBuf>,

/// Provide an alternative prompt prefix for the virtual environment.
///
Expand Down
50 changes: 35 additions & 15 deletions crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fmt::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::vec;

Expand Down Expand Up @@ -41,7 +41,7 @@ use crate::printer::Printer;
/// Create a virtual environment.
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
pub(crate) async fn venv(
path: &Path,
path: Option<PathBuf>,
python_request: Option<&str>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
Expand All @@ -59,6 +59,7 @@ pub(crate) async fn venv(
concurrency: Concurrency,
native_tls: bool,
no_config: bool,
no_project: bool,
cache: &Cache,
printer: Printer,
relocatable: bool,
Expand All @@ -82,6 +83,7 @@ pub(crate) async fn venv(
concurrency,
native_tls,
no_config,
no_project,
cache,
printer,
relocatable,
Expand Down Expand Up @@ -118,7 +120,7 @@ enum VenvError {
/// Create a virtual environment.
#[allow(clippy::fn_params_excessive_bools)]
async fn venv_impl(
path: &Path,
path: Option<PathBuf>,
python_request: Option<&str>,
link_mode: LinkMode,
index_locations: &IndexLocations,
Expand All @@ -136,10 +138,39 @@ async fn venv_impl(
concurrency: Concurrency,
native_tls: bool,
no_config: bool,
no_project: bool,
cache: &Cache,
printer: Printer,
relocatable: bool,
) -> miette::Result<ExitStatus> {
let project = if no_project {
None
} else {
match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await {
Ok(project) => Some(project),
Err(WorkspaceError::MissingProject(_)) => None,
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(WorkspaceError::NonWorkspace(_)) => None,
Err(err) => {
warn_user_once!("{err}");
None
}
}
};

// Determine the default path; either the virtual environment for the project or `.venv`
let path = path.unwrap_or(
project
.as_ref()
.and_then(|project| {
// Only use the project environment path if we're invoked from the root
// This isn't strictly necessary and we may want to change it later, but this
// avoids a breaking change when adding project environment support to `uv venv`.
(project.workspace().install_path() == &*CWD).then(|| project.workspace().venv())
})
.unwrap_or(PathBuf::from(".venv")),
);

let client_builder = BaseClientBuilder::default()
.connectivity(connectivity)
.native_tls(native_tls);
Expand All @@ -159,17 +190,6 @@ async fn venv_impl(

// (3) `Requires-Python` in `pyproject.toml`
if interpreter_request.is_none() {
let project = match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await {
Ok(project) => Some(project),
Err(WorkspaceError::MissingProject(_)) => None,
Err(WorkspaceError::MissingPyprojectToml) => None,
Err(WorkspaceError::NonWorkspace(_)) => None,
Err(err) => {
warn_user_once!("{err}");
None
}
};

if let Some(project) = project {
interpreter_request = find_requires_python(project.workspace())
.into_diagnostic()?
Expand Down Expand Up @@ -229,7 +249,7 @@ async fn venv_impl(

// Create the virtual environment.
let venv = uv_virtualenv::create_venv(
path,
&path,
interpreter,
prompt,
system_site_packages,
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::ffi::OsString;
use std::fmt::Write;
use std::io::stdout;
use std::path::PathBuf;
use std::process::ExitCode;

use anstream::eprintln;
Expand Down Expand Up @@ -680,15 +679,15 @@ async fn run(cli: Cli) -> Result<ExitStatus> {

// Since we use ".venv" as the default name, we use "." as the default prompt.
let prompt = args.prompt.or_else(|| {
if args.name == PathBuf::from(".venv") {
if args.path.is_none() {
Some(".".to_string())
} else {
None
}
});

commands::venv(
&args.name,
args.path,
args.settings.python.as_deref(),
globals.python_preference,
globals.python_downloads,
Expand All @@ -706,6 +705,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
globals.concurrency,
globals.native_tls,
cli.no_config,
args.no_project,
&cache,
printer,
args.relocatable,
Expand Down
9 changes: 6 additions & 3 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1614,10 +1614,11 @@ impl PipCheckSettings {
pub(crate) struct VenvSettings {
pub(crate) seed: bool,
pub(crate) allow_existing: bool,
pub(crate) name: PathBuf,
pub(crate) path: Option<PathBuf>,
pub(crate) prompt: Option<String>,
pub(crate) system_site_packages: bool,
pub(crate) relocatable: bool,
pub(crate) no_project: bool,
pub(crate) settings: PipSettings,
}

Expand All @@ -1630,7 +1631,7 @@ impl VenvSettings {
no_system,
seed,
allow_existing,
name,
path,
prompt,
system_site_packages,
relocatable,
Expand All @@ -1639,16 +1640,18 @@ impl VenvSettings {
keyring_provider,
allow_insecure_host,
exclude_newer,
no_project,
link_mode,
compat_args: _,
} = args;

Self {
seed,
allow_existing,
name,
path,
prompt,
system_site_packages,
no_project,
relocatable,
settings: PipSettings::combine(
PipOptions {
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ impl TestContext {

// Make virtual environment activation cross-platform and shell-agnostic
filters.push((
r"Activate with: (?:.*)\\Scripts\\activate".to_string(),
"Activate with: source .venv/bin/activate".to_string(),
r"Activate with: (.*)\\Scripts\\activate".to_string(),
"Activate with: source $1/bin/activate".to_string(),
));
filters.push((
r"Activate with: source .venv/bin/activate(?:\.\w+)".to_string(),
Expand Down
84 changes: 78 additions & 6 deletions crates/uv/tests/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ fn create_venv() {
}

#[test]
fn create_venv_uv_project_environment() -> Result<()> {
fn create_venv_project_environment() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);

// `uv venv` ignores UV_PROJECT_ENVIRONMENT
// `uv venv` ignores `UV_PROJECT_ENVIRONMENT` when it's not a project
uv_snapshot!(context.filters(), context.venv().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###"
success: true
exit_code: 0
Expand All @@ -74,25 +74,97 @@ fn create_venv_uv_project_environment() -> Result<()> {
.child("foo")
.assert(predicates::path::missing());

context.temp_dir.child("pyproject.toml").touch()?;
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;

// Even if there's a `pyproject.toml`
// But, if we're in a project we'll respect it
uv_snapshot!(context.filters(), context.venv().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: foo
Activate with: source foo/bin/activate
"###
);

context
.temp_dir
.child("foo")
.assert(predicates::path::is_dir());

// Unless we're in a child directory
let child = context.temp_dir.child("child");
child.create_dir_all()?;

uv_snapshot!(context.filters(), context.venv().env("UV_PROJECT_ENVIRONMENT", "foo").current_dir(child.path()), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);

// In which case, we'll use the default name of `.venv`
child.child("foo").assert(predicates::path::missing());
child.child(".venv").assert(predicates::path::is_dir());

// Or, if a name is provided
uv_snapshot!(context.filters(), context.venv().arg("bar"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: bar
Activate with: source bar/bin/activate
"###
);

context
.temp_dir
.child("foo")
.assert(predicates::path::missing());
.child("bar")
.assert(predicates::path::is_dir());

// Or, of they opt-out with `--no-workspace` or `--no-project`
uv_snapshot!(context.filters(), context.venv().arg("--no-workspace"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);

uv_snapshot!(context.filters(), context.venv().arg("--no-project"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);

Ok(())
}
Expand Down
14 changes: 12 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -5956,19 +5956,25 @@ Create a virtual environment.

By default, creates a virtual environment named `.venv` in the working directory. An alternative path may be provided positionally.

If in a project, the default environment name can be changed with the `UV_PROJECT_ENVIRONMENT` environment variable; this only applies when run from the project root directory.

If a virtual environment exists at the target path, it will be removed and a new, empty virtual environment will be created.

When using uv, the virtual environment does not need to be activated. uv will find a virtual environment (named `.venv`) in the working directory or any parent directories.

<h3 class="cli-reference">Usage</h3>

```
uv venv [OPTIONS] [NAME]
uv venv [OPTIONS] [PATH]
```

<h3 class="cli-reference">Arguments</h3>

<dl class="cli-reference"><dt><code>NAME</code></dt><dd><p>The path to the virtual environment to create</p>
<dl class="cli-reference"><dt><code>PATH</code></dt><dd><p>The path to the virtual environment to create.</p>

<p>Default to <code>.venv</code> in the working directory.</p>

<p>Relative paths are resolved relative to the working directory.</p>

</dd></dl>

Expand Down Expand Up @@ -6105,6 +6111,10 @@ uv venv [OPTIONS] [NAME]

<p>For example, spinners or progress bars.</p>

</dd><dt><code>--no-project</code></dt><dd><p>Avoid discovering a project or workspace.</p>

<p>By default, uv searches for projects in the current directory or any parent directory to determine the default path of the virtual environment and check for Python version constraints, if any.</p>

</dd><dt><code>--no-python-downloads</code></dt><dd><p>Disable automatic downloads of Python.</p>

</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>
Expand Down
Loading