feat(sdk): create Python environments with uv and Poetry in Add Interpreter (#1888)

* feat(sdk): create Python environments with uv and Poetry in Add Interpreter

Add a VenvTool parameter to sdk_python_create_venv so the Add Python
Interpreter dialog can build a new environment with virtualenv, uv or
Poetry, matching what the environment discovery already finds. Poetry
keeps envs in its own cache, so the dialog disables the location field
for it and resolves the interpreter via 'poetry env info --path'.

Fixes l0ng-ai/openidea#1776

* fix(i18n): update text catalogs for the new env-tool strings

The Add Interpreter dialog gained uv/Poetry labels and per-tool notes,
and its spinner text changed from 'Creating virtual environment' to
'Creating environment'. Add the new zh entries and rename the stale
key in every catalog so the text-catalog test passes.
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-07 06:42:41 +08:00
1 джерело d018771a8d
коміт 88ecaf9ccd
14 files changed
+338 -38

No files matched your search

+92 -12
Переглянути файл
@@ -16,25 +16,105 @@ pub struct PythonPackage {
pub version: String,
}
/// Creates a virtualenv at `location` with `base -m venv` and returns its interpreter.
/// The tool that creates a new Python environment in "Add Python Interpreter".
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "lowercase")]
pub enum VenvTool {
/// `base -m venv <location>`.
Venv,
/// `uv venv <location> --python <base>` (uv must be on PATH).
Uv,
/// `poetry env use <base>` in the project, then `poetry env info --path`.
Poetry,
}
/// Creates a virtualenv at `location` with `tool` and returns its interpreter.
/// `base` is the interpreter uv / venv build on, or the one poetry's env uses.
pub fn create_venv(
tool: VenvTool,
base: &Path,
location: &Path,
cancel: &CancellationToken,
on_line: &dyn Fn(String),
) -> IdeResult<Sdk> {
if location.join("pyvenv.cfg").is_file() {
return Err(IdeError::new(
ide_core::error::ErrorKind::AlreadyExists,
format!("{} already contains a virtual environment", location.display()),
));
create_venv_with(tool, base, location, which, cancel, on_line)
}
/// [`create_venv`] with the tool lookup injectable (tests fake `uv` / `poetry`).
pub(crate) fn create_venv_with(
tool: VenvTool,
base: &Path,
location: &Path,
which: impl Fn(&str) -> Option<PathBuf>,
cancel: &CancellationToken,
on_line: &dyn Fn(String),
) -> IdeResult<Sdk> {
match tool {
VenvTool::Venv | VenvTool::Uv => {
if location.join("pyvenv.cfg").is_file() {
return Err(IdeError::new(
ide_core::error::ErrorKind::AlreadyExists,
format!("{} already contains a virtual environment", location.display()),
));
}
if let Some(parent) = location.parent() {
std::fs::create_dir_all(parent)?;
}
let (program, args) = match tool {
VenvTool::Uv => {
let uv = which("uv").ok_or_else(|| {
IdeError::unavailable("uv is not installed (install it from https://docs.astral.sh/uv)")
})?;
(
uv,
vec![
"venv".to_owned(),
location.to_string_lossy().into_owned(),
"--python".to_owned(),
base.to_string_lossy().into_owned(),
],
)
}
_ => (base.to_path_buf(), venv_args(location)),
};
stream(&program, &args, None, &[], cancel, on_line)?;
let python = location.join(bin_dir()).join(exe("python"));
crate::discover::python_sdk(&python, "venv", location.parent().and_then(Path::parent))
.ok_or_else(|| IdeError::internal(format!("{} was not created", python.display())))
}
VenvTool::Poetry => create_poetry_env(base, location, &which, cancel, on_line),
}
if let Some(parent) = location.parent() {
std::fs::create_dir_all(parent)?;
}
stream(base, &venv_args(location), None, &[], cancel, on_line)?;
let python = location.join(bin_dir()).join(exe("python"));
crate::discover::python_sdk(&python, "venv", location.parent().and_then(Path::parent))
}
/// `poetry env use <base>` in the project directory (the parent of `location`, which the
/// dialog fills with `<root>/.venv` only as a hint — poetry keeps its envs under its own
/// cache), then `poetry env info --path` for the interpreter it made or reused.
fn create_poetry_env(
base: &Path,
location: &Path,
which: &dyn Fn(&str) -> Option<PathBuf>,
cancel: &CancellationToken,
on_line: &dyn Fn(String),
) -> IdeResult<Sdk> {
let poetry = which("poetry")
.ok_or_else(|| IdeError::unavailable("Poetry is not installed (install it from https://python-poetry.org)"))?;
let project = location
.parent()
.filter(|p| p.is_dir())
.ok_or_else(|| IdeError::invalid("Poetry environments need an open project directory"))?;
stream(
&poetry,
&["env".to_owned(), "use".to_owned(), base.to_string_lossy().into_owned()],
Some(project),
&[],
cancel,
on_line,
)?;
let home = query_in(&poetry, &["env", "info", "--path"], Some(project), &[], Duration::from_secs(30))
.map(PathBuf::from)
.ok_or_else(|| IdeError::internal("poetry created no environment".to_owned()))?;
let python = home.join(bin_dir()).join(exe("python"));
crate::discover::python_sdk(&python, "venv", Some(project))
.ok_or_else(|| IdeError::internal(format!("{} was not created", python.display())))
}