Skip to content

Commit

Permalink
Respect UV_PROJECT_ENVIRONMENT in uv venv; add --no-project to …
Browse files Browse the repository at this point in the history
…`uv venv`
  • Loading branch information
zanieb committed Aug 29, 2024
1 parent 55cc043 commit dbf65cd
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 33 deletions.
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 @@ -1610,10 +1610,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 @@ -1626,7 +1627,7 @@ impl VenvSettings {
no_system,
seed,
allow_existing,
name,
path,
prompt,
system_site_packages,
relocatable,
Expand All @@ -1635,16 +1636,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
2 changes: 1 addition & 1 deletion crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ impl TestContext {
.into_iter()
.map(|pattern| (pattern, format!("[{name}]/")))
{
self.filters.insert(0, pattern)
self.filters.insert(0, pattern);
}
self
}
Expand Down
1 change: 0 additions & 1 deletion crates/uv/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,6 @@ fn sync_custom_environment_path() -> Result<()> {

// And, it can be outside the project
let tempdir = tempdir_in(TestContext::test_bucket_dir())?;
dbg!(tempdir.path());
context = context.with_filtered_path(tempdir.path(), "OTHER_TEMPDIR");
uv_snapshot!(context.filters(), context.sync().env("UV_PROJECT_ENVIRONMENT", tempdir.path().join(".venv")), @r###"
success: true
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 @@ -5596,19 +5596,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 @@ -5733,6 +5739,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

0 comments on commit dbf65cd

Please sign in to comment.