mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-31 07:47:27 +00:00
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
7 changed files with 153 additions and 33 deletions
|
@ -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.
|
||||
///
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::vec;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -82,6 +83,7 @@ pub(crate) async fn venv(
|
|||
concurrency,
|
||||
native_tls,
|
||||
no_config,
|
||||
no_project,
|
||||
cache,
|
||||
printer,
|
||||
relocatable,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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()?
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
@ -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.
|
||||
let prompt = args.prompt.or_else(|| {
|
||||
if args.name == PathBuf::from(".venv") {
|
||||
if args.path.is_none() {
|
||||
Some(".".to_string())
|
||||
} else {
|
||||
None
|
||||
|
@ -688,7 +687,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
});
|
||||
|
||||
commands::venv(
|
||||
&args.name,
|
||||
args.path,
|
||||
args.settings.python.as_deref(),
|
||||
globals.python_preference,
|
||||
globals.python_downloads,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -1630,7 +1631,7 @@ impl VenvSettings {
|
|||
no_system,
|
||||
seed,
|
||||
allow_existing,
|
||||
name,
|
||||
path,
|
||||
prompt,
|
||||
system_site_packages,
|
||||
relocatable,
|
||||
|
@ -1639,6 +1640,7 @@ impl VenvSettings {
|
|||
keyring_provider,
|
||||
allow_insecure_host,
|
||||
exclude_newer,
|
||||
no_project,
|
||||
link_mode,
|
||||
compat_args: _,
|
||||
} = args;
|
||||
|
@ -1646,9 +1648,10 @@ impl VenvSettings {
|
|||
Self {
|
||||
seed,
|
||||
allow_existing,
|
||||
name,
|
||||
path,
|
||||
prompt,
|
||||
system_site_packages,
|
||||
no_project,
|
||||
relocatable,
|
||||
settings: PipSettings::combine(
|
||||
PipOptions {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
@ -74,14 +74,44 @@ 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
|
||||
|
@ -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
|
||||
.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(())
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
@ -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>
|
||||
|
||||
```
|
||||
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>
|
||||
|
||||
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue