Allow templating the `UV_PROJECT_ENVIRONMENT` path

This commit is contained in:
Zanie Blue 2025-07-28 07:46:13 -05:00
parent 0a51489ec4
commit 9cec7486c8
4 changed files with 40 additions and 8 deletions

View File

@ -14,6 +14,7 @@ bitflags::bitflags! {
const JSON_OUTPUT = 1 << 2;
const PYLOCK = 1 << 3;
const ADD_BOUNDS = 1 << 4;
const TEMPLATE_PROJECT_ENVIRONMENT = 1 << 5;
}
}

View File

@ -8,7 +8,8 @@ use glob::{GlobError, PatternError, glob};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, trace, warn};
use uv_configuration::DependencyGroupsWithDefaults;
use uv_cache_key::cache_digest;
use uv_configuration::{DependencyGroupsWithDefaults, Preview, PreviewFeatures};
use uv_distribution_types::{Index, Requirement, RequirementSource};
use uv_fs::{CWD, Simplified};
use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName};
@ -631,16 +632,46 @@ impl Workspace {
/// If `active` is `true`, the `VIRTUAL_ENV` variable will be preferred. If it is `false`, any
/// warnings about mismatch between the active environment and the project environment will be
/// silenced.
pub fn venv(&self, active: Option<bool>) -> PathBuf {
pub fn venv(&self, active: Option<bool>, preview: Preview) -> PathBuf {
/// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any.
fn from_project_environment_variable(workspace: &Workspace) -> Option<PathBuf> {
fn from_project_environment_variable(
workspace: &Workspace,
preview: Preview,
) -> Option<PathBuf> {
let value = std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)?;
if value.is_empty() {
return None;
}
let path = PathBuf::from(value);
let templated = value
.to_string_lossy()
.replace(
"{project_path_hash}",
&cache_digest(&workspace.install_path),
)
.replace(
"{project_name}",
&workspace
.pyproject_toml
.project
.as_ref()
.map(|project| project.name.to_string())
.unwrap_or("none".to_string()),
);
let path = if templated != value.to_string_lossy() {
if !preview.is_enabled(PreviewFeatures::TEMPLATE_PROJECT_ENVIRONMENT) {
warn_user_once!(
"Templating the `UV_PROJECT_ENVIRONMENT` setting is in preview and may change without warning; use `--preview-features {}` to disable this warning",
PreviewFeatures::TEMPLATE_PROJECT_ENVIRONMENT
);
}
PathBuf::from(templated)
} else {
PathBuf::from(value)
};
if path.is_absolute() {
return Some(path);
}
@ -668,7 +699,7 @@ impl Workspace {
}
// Determine the default value
let project_env = from_project_environment_variable(self)
let project_env = from_project_environment_variable(self, preview)
.unwrap_or_else(|| self.install_path.join(".venv"));
// Warn if it conflicts with `VIRTUAL_ENV`

View File

@ -904,7 +904,7 @@ impl ProjectInterpreter {
.await?;
// Read from the virtual environment first.
let root = workspace.venv(active);
let root = workspace.venv(active, preview);
match PythonEnvironment::from_root(&root, cache) {
Ok(venv) => {
match environment_is_usable(
@ -1307,7 +1307,7 @@ impl ProjectEnvironment {
// Otherwise, create a virtual environment with the discovered interpreter.
ProjectInterpreter::Interpreter(interpreter) => {
let root = workspace.venv(active);
let root = workspace.venv(active, preview);
// Avoid removing things that are not virtual environments
let replace = match (root.try_exists(), root.join("pyvenv.cfg").try_exists()) {

View File

@ -119,7 +119,7 @@ pub(crate) async fn venv(
// 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() == project_dir)
.then(|| project.workspace().venv(Some(false)))
.then(|| project.workspace().venv(Some(false), preview))
})
.unwrap_or(PathBuf::from(".venv")),
);