mirror of https://github.com/astral-sh/uv
Improve project handling in `uv venv` (#6835)
- Respect `UV_PROJECT_ENVIRONMENT` when in project root - Add `--no-project` and `--no-workspace` to opt-out of above and `requires-python` detection - Rename `[NAME]` to `[PATH]` in CLI
This commit is contained in:
parent
1e89d3e44f
commit
d87256bebe
|
|
@ -322,6 +322,10 @@ pub enum Commands {
|
||||||
/// By default, creates a virtual environment named `.venv` in the working
|
/// By default, creates a virtual environment named `.venv` in the working
|
||||||
/// directory. An alternative path may be provided positionally.
|
/// 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
|
/// If a virtual environment exists at the target path, it will be removed
|
||||||
/// and a new, empty virtual environment will be created.
|
/// and a new, empty virtual environment will be created.
|
||||||
///
|
///
|
||||||
|
|
@ -1961,6 +1965,14 @@ pub struct VenvArgs {
|
||||||
#[arg(long, overrides_with("system"), hide = true)]
|
#[arg(long, overrides_with("system"), hide = true)]
|
||||||
pub no_system: bool,
|
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.
|
/// 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.
|
/// Note `setuptools` and `wheel` are not included in Python 3.12+ environments.
|
||||||
|
|
@ -1980,8 +1992,11 @@ pub struct VenvArgs {
|
||||||
pub allow_existing: bool,
|
pub allow_existing: bool,
|
||||||
|
|
||||||
/// The path to the virtual environment to create.
|
/// 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.
|
/// Provide an alternative prompt prefix for the virtual environment.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ use crate::printer::Printer;
|
||||||
/// Create a virtual environment.
|
/// Create a virtual environment.
|
||||||
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
|
#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
|
||||||
pub(crate) async fn venv(
|
pub(crate) async fn venv(
|
||||||
path: &Path,
|
path: Option<PathBuf>,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
|
|
@ -59,6 +59,7 @@ pub(crate) async fn venv(
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
no_config: bool,
|
no_config: bool,
|
||||||
|
no_project: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
relocatable: bool,
|
relocatable: bool,
|
||||||
|
|
@ -82,6 +83,7 @@ pub(crate) async fn venv(
|
||||||
concurrency,
|
concurrency,
|
||||||
native_tls,
|
native_tls,
|
||||||
no_config,
|
no_config,
|
||||||
|
no_project,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
relocatable,
|
relocatable,
|
||||||
|
|
@ -118,7 +120,7 @@ enum VenvError {
|
||||||
/// Create a virtual environment.
|
/// Create a virtual environment.
|
||||||
#[allow(clippy::fn_params_excessive_bools)]
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
async fn venv_impl(
|
async fn venv_impl(
|
||||||
path: &Path,
|
path: Option<PathBuf>,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
|
@ -136,10 +138,39 @@ async fn venv_impl(
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
no_config: bool,
|
no_config: bool,
|
||||||
|
no_project: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
relocatable: bool,
|
relocatable: bool,
|
||||||
) -> miette::Result<ExitStatus> {
|
) -> 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()
|
let client_builder = BaseClientBuilder::default()
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.native_tls(native_tls);
|
.native_tls(native_tls);
|
||||||
|
|
@ -159,17 +190,6 @@ async fn venv_impl(
|
||||||
|
|
||||||
// (3) `Requires-Python` in `pyproject.toml`
|
// (3) `Requires-Python` in `pyproject.toml`
|
||||||
if interpreter_request.is_none() {
|
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 {
|
if let Some(project) = project {
|
||||||
interpreter_request = find_requires_python(project.workspace())
|
interpreter_request = find_requires_python(project.workspace())
|
||||||
.into_diagnostic()?
|
.into_diagnostic()?
|
||||||
|
|
@ -229,7 +249,7 @@ async fn venv_impl(
|
||||||
|
|
||||||
// Create the virtual environment.
|
// Create the virtual environment.
|
||||||
let venv = uv_virtualenv::create_venv(
|
let venv = uv_virtualenv::create_venv(
|
||||||
path,
|
&path,
|
||||||
interpreter,
|
interpreter,
|
||||||
prompt,
|
prompt,
|
||||||
system_site_packages,
|
system_site_packages,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use anstream::eprintln;
|
use anstream::eprintln;
|
||||||
|
|
@ -680,7 +679,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
|
|
||||||
// Since we use ".venv" as the default name, we use "." as the default prompt.
|
// Since we use ".venv" as the default name, we use "." as the default prompt.
|
||||||
let prompt = args.prompt.or_else(|| {
|
let prompt = args.prompt.or_else(|| {
|
||||||
if args.name == PathBuf::from(".venv") {
|
if args.path.is_none() {
|
||||||
Some(".".to_string())
|
Some(".".to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -688,7 +687,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
});
|
});
|
||||||
|
|
||||||
commands::venv(
|
commands::venv(
|
||||||
&args.name,
|
args.path,
|
||||||
args.settings.python.as_deref(),
|
args.settings.python.as_deref(),
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
|
|
@ -706,6 +705,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
cli.no_config,
|
cli.no_config,
|
||||||
|
args.no_project,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
args.relocatable,
|
args.relocatable,
|
||||||
|
|
|
||||||
|
|
@ -1614,10 +1614,11 @@ impl PipCheckSettings {
|
||||||
pub(crate) struct VenvSettings {
|
pub(crate) struct VenvSettings {
|
||||||
pub(crate) seed: bool,
|
pub(crate) seed: bool,
|
||||||
pub(crate) allow_existing: bool,
|
pub(crate) allow_existing: bool,
|
||||||
pub(crate) name: PathBuf,
|
pub(crate) path: Option<PathBuf>,
|
||||||
pub(crate) prompt: Option<String>,
|
pub(crate) prompt: Option<String>,
|
||||||
pub(crate) system_site_packages: bool,
|
pub(crate) system_site_packages: bool,
|
||||||
pub(crate) relocatable: bool,
|
pub(crate) relocatable: bool,
|
||||||
|
pub(crate) no_project: bool,
|
||||||
pub(crate) settings: PipSettings,
|
pub(crate) settings: PipSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1630,7 +1631,7 @@ impl VenvSettings {
|
||||||
no_system,
|
no_system,
|
||||||
seed,
|
seed,
|
||||||
allow_existing,
|
allow_existing,
|
||||||
name,
|
path,
|
||||||
prompt,
|
prompt,
|
||||||
system_site_packages,
|
system_site_packages,
|
||||||
relocatable,
|
relocatable,
|
||||||
|
|
@ -1639,6 +1640,7 @@ impl VenvSettings {
|
||||||
keyring_provider,
|
keyring_provider,
|
||||||
allow_insecure_host,
|
allow_insecure_host,
|
||||||
exclude_newer,
|
exclude_newer,
|
||||||
|
no_project,
|
||||||
link_mode,
|
link_mode,
|
||||||
compat_args: _,
|
compat_args: _,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
@ -1646,9 +1648,10 @@ impl VenvSettings {
|
||||||
Self {
|
Self {
|
||||||
seed,
|
seed,
|
||||||
allow_existing,
|
allow_existing,
|
||||||
name,
|
path,
|
||||||
prompt,
|
prompt,
|
||||||
system_site_packages,
|
system_site_packages,
|
||||||
|
no_project,
|
||||||
relocatable,
|
relocatable,
|
||||||
settings: PipSettings::combine(
|
settings: PipSettings::combine(
|
||||||
PipOptions {
|
PipOptions {
|
||||||
|
|
|
||||||
|
|
@ -335,8 +335,8 @@ impl TestContext {
|
||||||
|
|
||||||
// Make virtual environment activation cross-platform and shell-agnostic
|
// Make virtual environment activation cross-platform and shell-agnostic
|
||||||
filters.push((
|
filters.push((
|
||||||
r"Activate with: (?:.*)\\Scripts\\activate".to_string(),
|
r"Activate with: (.*)\\Scripts\\activate".to_string(),
|
||||||
"Activate with: source .venv/bin/activate".to_string(),
|
"Activate with: source $1/bin/activate".to_string(),
|
||||||
));
|
));
|
||||||
filters.push((
|
filters.push((
|
||||||
r"Activate with: source .venv/bin/activate(?:\.\w+)".to_string(),
|
r"Activate with: source .venv/bin/activate(?:\.\w+)".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ fn create_venv() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_venv_uv_project_environment() -> Result<()> {
|
fn create_venv_project_environment() -> Result<()> {
|
||||||
let context = TestContext::new_with_versions(&["3.12"]);
|
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###"
|
uv_snapshot!(context.filters(), context.venv().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
|
|
@ -74,14 +74,44 @@ fn create_venv_uv_project_environment() -> Result<()> {
|
||||||
.child("foo")
|
.child("foo")
|
||||||
.assert(predicates::path::missing());
|
.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###"
|
uv_snapshot!(context.filters(), context.venv().env("UV_PROJECT_ENVIRONMENT", "foo"), @r###"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- 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 -----
|
----- stderr -----
|
||||||
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
Creating virtualenv at: .venv
|
Creating virtualenv at: .venv
|
||||||
|
|
@ -89,10 +119,52 @@ fn create_venv_uv_project_environment() -> Result<()> {
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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
|
context
|
||||||
.temp_dir
|
.temp_dir
|
||||||
.child("foo")
|
.child("bar")
|
||||||
.assert(predicates::path::missing());
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5956,6 +5956,8 @@ Create a virtual environment.
|
||||||
|
|
||||||
By default, creates a virtual environment named `.venv` in the working directory. An alternative path may be provided positionally.
|
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.
|
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.
|
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.
|
||||||
|
|
@ -5963,12 +5965,16 @@ When using uv, the virtual environment does not need to be activated. uv will fi
|
||||||
<h3 class="cli-reference">Usage</h3>
|
<h3 class="cli-reference">Usage</h3>
|
||||||
|
|
||||||
```
|
```
|
||||||
uv venv [OPTIONS] [NAME]
|
uv venv [OPTIONS] [PATH]
|
||||||
```
|
```
|
||||||
|
|
||||||
<h3 class="cli-reference">Arguments</h3>
|
<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>
|
</dd></dl>
|
||||||
|
|
||||||
|
|
@ -6105,6 +6111,10 @@ uv venv [OPTIONS] [NAME]
|
||||||
|
|
||||||
<p>For example, spinners or progress bars.</p>
|
<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>--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>
|
</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue