mirror of https://github.com/astral-sh/uv
Add support for Python version templating
This commit is contained in:
parent
9cec7486c8
commit
c297af5922
|
|
@ -6074,6 +6074,7 @@ dependencies = [
|
|||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-pypi-types",
|
||||
"uv-python",
|
||||
"uv-redacted",
|
||||
"uv-static",
|
||||
"uv-warnings",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ uv-options-metadata = { workspace = true }
|
|||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-redacted = { workspace = true }
|
||||
uv-static = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
pub use workspace::{
|
||||
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, RequiresPythonSources, VirtualProject,
|
||||
Workspace, WorkspaceCache, WorkspaceError, WorkspaceMember,
|
||||
CustomProjectEnvironmentPath, DiscoveryOptions, MemberDiscovery, ProjectWorkspace,
|
||||
RequiresPythonSources, VirtualProject, Workspace, WorkspaceCache, WorkspaceError,
|
||||
WorkspaceMember,
|
||||
};
|
||||
|
||||
pub mod dependency_groups;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName};
|
|||
use uv_pep440::VersionSpecifiers;
|
||||
use uv_pep508::{MarkerTree, VerbatimUrl};
|
||||
use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl};
|
||||
use uv_python::Interpreter;
|
||||
use uv_static::EnvVars;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
|
|
@ -632,17 +634,19 @@ 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>, preview: Preview) -> PathBuf {
|
||||
pub fn venv(
|
||||
&self,
|
||||
active: Option<bool>,
|
||||
python: Option<&Interpreter>,
|
||||
preview: Preview,
|
||||
) -> PathBuf {
|
||||
/// Resolve the `UV_PROJECT_ENVIRONMENT` value, if any.
|
||||
fn from_project_environment_variable(
|
||||
workspace: &Workspace,
|
||||
python: Option<&Interpreter>,
|
||||
preview: Preview,
|
||||
) -> Option<PathBuf> {
|
||||
let value = std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)?;
|
||||
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let value = CustomProjectEnvironmentPath::from_env()?.into_os_string();
|
||||
|
||||
let templated = value
|
||||
.to_string_lossy()
|
||||
|
|
@ -658,6 +662,24 @@ impl Workspace {
|
|||
.as_ref()
|
||||
.map(|project| project.name.to_string())
|
||||
.unwrap_or("none".to_string()),
|
||||
)
|
||||
.replace(
|
||||
"{python_version_minor}",
|
||||
&python
|
||||
.map(|python| python.python_minor_version().to_string())
|
||||
.unwrap_or("none".to_string()),
|
||||
)
|
||||
.replace(
|
||||
"{python_implementation}",
|
||||
&python
|
||||
.map(|python| python.implementation_name().to_lowercase().to_string())
|
||||
.unwrap_or("none".to_string()),
|
||||
)
|
||||
.replace(
|
||||
"{python_version_full}",
|
||||
&python
|
||||
.map(|python| python.python_full_version().to_string())
|
||||
.unwrap_or("none".to_string()),
|
||||
);
|
||||
|
||||
let path = if templated != value.to_string_lossy() {
|
||||
|
|
@ -699,7 +721,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
// Determine the default value
|
||||
let project_env = from_project_environment_variable(self, preview)
|
||||
let project_env = from_project_environment_variable(self, python, preview)
|
||||
.unwrap_or_else(|| self.install_path.join(".venv"));
|
||||
|
||||
// Warn if it conflicts with `VIRTUAL_ENV`
|
||||
|
|
@ -1087,6 +1109,37 @@ impl WorkspaceMember {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CustomProjectEnvironmentPath(std::ffi::OsString);
|
||||
|
||||
impl CustomProjectEnvironmentPath {
|
||||
/// Returns the value of the `UV_PROJECT_ENVIRONMENT` environment variable, if set.
|
||||
pub fn from_env() -> Option<Self> {
|
||||
std::env::var_os(EnvVars::UV_PROJECT_ENVIRONMENT)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(Self)
|
||||
}
|
||||
|
||||
pub fn into_os_string(self) -> std::ffi::OsString {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Whether this path contains any Python interpreter template variables.
|
||||
pub fn contains_python_template(&self) -> bool {
|
||||
let as_str = self.0.to_string_lossy();
|
||||
as_str.contains("{python_version_minor}")
|
||||
|| as_str.contains("{python_version_full}")
|
||||
|| as_str.contains("{python_implementation}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for CustomProjectEnvironmentPath {
|
||||
type Target = std::ffi::OsStr;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.as_os_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// The current project and the workspace it is part of, with all of the workspace members.
|
||||
///
|
||||
/// # Structure
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ use uv_virtualenv::remove_virtualenv;
|
|||
use uv_warnings::{warn_user, warn_user_once};
|
||||
use uv_workspace::dependency_groups::DependencyGroupError;
|
||||
use uv_workspace::pyproject::PyProjectToml;
|
||||
use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache};
|
||||
use uv_workspace::{
|
||||
CustomProjectEnvironmentPath, RequiresPythonSources, Workspace, WorkspaceCache,
|
||||
};
|
||||
|
||||
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
|
||||
use crate::commands::pip::operations::{Changelog, Modifications};
|
||||
|
|
@ -903,8 +905,50 @@ impl ProjectInterpreter {
|
|||
)
|
||||
.await?;
|
||||
|
||||
let client_builder = BaseClientBuilder::default()
|
||||
.retries_from_env()?
|
||||
.connectivity(network_settings.connectivity)
|
||||
.native_tls(network_settings.native_tls)
|
||||
.allow_insecure_host(network_settings.allow_insecure_host.clone());
|
||||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// Locate the Python interpreter to use in the environment.
|
||||
let find_python = || {
|
||||
PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::OnlySystem,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&reporter),
|
||||
install_mirrors.python_install_mirror.as_deref(),
|
||||
install_mirrors.pypy_install_mirror.as_deref(),
|
||||
install_mirrors.python_downloads_json_url.as_deref(),
|
||||
preview,
|
||||
)
|
||||
};
|
||||
|
||||
// If we need an interpreter to determine the environment path, discover it _first_.
|
||||
// Otherwise, we defer this case because we may be be able to skip searching for an
|
||||
// interpreter.
|
||||
let python = if CustomProjectEnvironmentPath::from_env()
|
||||
.as_ref()
|
||||
.is_some_and(CustomProjectEnvironmentPath::contains_python_template)
|
||||
{
|
||||
// Locate the Python interpreter to use in the environment.
|
||||
Some(find_python().await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read from the virtual environment first.
|
||||
let root = workspace.venv(active, preview);
|
||||
let root = workspace.venv(
|
||||
active,
|
||||
python.as_ref().map(PythonInstallation::interpreter),
|
||||
preview,
|
||||
);
|
||||
match PythonEnvironment::from_root(&root, cache) {
|
||||
Ok(venv) => {
|
||||
match environment_is_usable(
|
||||
|
|
@ -972,29 +1016,12 @@ impl ProjectInterpreter {
|
|||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
let client_builder = BaseClientBuilder::default()
|
||||
.retries_from_env()?
|
||||
.connectivity(network_settings.connectivity)
|
||||
.native_tls(network_settings.native_tls)
|
||||
.allow_insecure_host(network_settings.allow_insecure_host.clone());
|
||||
|
||||
let reporter = PythonDownloadReporter::single(printer);
|
||||
|
||||
// Locate the Python interpreter to use in the environment.
|
||||
let python = PythonInstallation::find_or_download(
|
||||
python_request.as_ref(),
|
||||
EnvironmentPreference::OnlySystem,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
&client_builder,
|
||||
cache,
|
||||
Some(&reporter),
|
||||
install_mirrors.python_install_mirror.as_deref(),
|
||||
install_mirrors.pypy_install_mirror.as_deref(),
|
||||
install_mirrors.python_downloads_json_url.as_deref(),
|
||||
preview,
|
||||
)
|
||||
.await?;
|
||||
let python = if let Some(python) = python {
|
||||
python
|
||||
} else {
|
||||
find_python().await?
|
||||
};
|
||||
|
||||
let managed = python.source().is_managed();
|
||||
let implementation = python.implementation();
|
||||
|
|
@ -1307,7 +1334,7 @@ impl ProjectEnvironment {
|
|||
|
||||
// Otherwise, create a virtual environment with the discovered interpreter.
|
||||
ProjectInterpreter::Interpreter(interpreter) => {
|
||||
let root = workspace.venv(active, preview);
|
||||
let root = workspace.venv(active, Some(&interpreter), preview);
|
||||
|
||||
// Avoid removing things that are not virtual environments
|
||||
let replace = match (root.try_exists(), root.join("pyvenv.cfg").try_exists()) {
|
||||
|
|
|
|||
|
|
@ -110,20 +110,6 @@ pub(crate) async fn venv(
|
|||
}
|
||||
};
|
||||
|
||||
// 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() == project_dir)
|
||||
.then(|| project.workspace().venv(Some(false), preview))
|
||||
})
|
||||
.unwrap_or(PathBuf::from(".venv")),
|
||||
);
|
||||
|
||||
// TODO(zanieb): We don't use [`BaseClientBuilder::retries_from_env`] here because it's a pain
|
||||
// to map into a miette diagnostic. We should just remove miette diagnostics here, we're not
|
||||
// using them elsewhere.
|
||||
|
|
@ -174,6 +160,23 @@ pub(crate) async fn venv(
|
|||
python.into_interpreter()
|
||||
};
|
||||
|
||||
// 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() == project_dir).then(|| {
|
||||
project
|
||||
.workspace()
|
||||
.venv(Some(false), Some(&interpreter), preview)
|
||||
})
|
||||
})
|
||||
.unwrap_or(PathBuf::from(".venv")),
|
||||
);
|
||||
|
||||
index_locations.cache_index_credentials();
|
||||
|
||||
// Check if the discovered Python version is incompatible with the current workspace
|
||||
|
|
|
|||
|
|
@ -7320,7 +7320,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | TEMPLATE_PROJECT_ENVIRONMENT,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
@ -7524,7 +7524,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | TEMPLATE_PROJECT_ENVIRONMENT,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
|
|||
Loading…
Reference in New Issue