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.
This commit is contained in:
arookieofc authored and GitHub committed 2026-11-07 06:42:41 +08:00
1 parent d018771a8d
commit 88ecaf9ccd
14 files changed
+338 -38

No files matched your search

+4 -2
View File
@@ -186,10 +186,11 @@ pub async fn sdk_locate_tools(
.map_err(join_err)
}
/// `base -m venv location`; output lines stream through `on_output`.
/// Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`.
pub async fn sdk_python_create_venv(
host: &Host,
window: &str,
tool: ide_sdk::python::VenvTool,
base: PathBuf,
location: PathBuf,
request: RequestId,
@@ -198,7 +199,7 @@ pub async fn sdk_python_create_venv(
host.require_trust(window)?;
let cancel = host.begin_project_request(window, request);
let result = tokio::task::spawn_blocking(move || {
ide_sdk::python::create_venv(&base, &location, &cancel, &|line| {
ide_sdk::python::create_venv(tool, &base, &location, &cancel, &|line| {
let _ = on_output.send(line);
})
})
@@ -275,6 +276,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
sdk_python_create_venv(
&c.host,
&c.window,
c.arg("tool")?,
c.arg("base")?,
c.arg("location")?,
c.arg("request")?,
+92 -12
View File
@@ -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())))
}
+44
View File
@@ -720,6 +720,50 @@ fn venv_creation_ignores_modules_in_the_working_directory() {
assert!(location.join("pyvenv.cfg").is_file());
}
#[test]
fn env_tools_are_reported_missing_before_they_run() {
// Without uv / Poetry on PATH the error names the install, not a spawn failure.
let dir = tempfile::tempdir().unwrap();
let location = dir.path().join(".venv");
let base = dir.path().join("python");
fn none(_: &str) -> Option<std::path::PathBuf> {
None
}
let uv = crate::python::create_venv_with(
crate::python::VenvTool::Uv,
&base,
&location,
none,
&Default::default(),
&|_| {},
)
.unwrap_err();
assert!(uv.message.contains("uv is not installed"), "{}", uv.message);
let poetry = crate::python::create_venv_with(
crate::python::VenvTool::Poetry,
&base,
&location,
none,
&Default::default(),
&|_| {},
)
.unwrap_err();
assert!(poetry.message.contains("Poetry is not installed"), "{}", poetry.message);
// An existing environment is refused before any tool runs.
std::fs::create_dir_all(&location).unwrap();
std::fs::write(location.join("pyvenv.cfg"), "home = /python").unwrap();
let again = crate::python::create_venv_with(
crate::python::VenvTool::Uv,
&base,
&location,
none,
&Default::default(),
&|_| {},
)
.unwrap_err();
assert!(again.message.contains("already contains a virtual environment"), "{}", again.message);
}
#[test]
fn neutral_dir_is_fresh_and_empty() {
let a = crate::python::neutral_dir().unwrap();
+3 -1
View File
@@ -93,12 +93,13 @@ pub async fn sdk_locate_tools(
ide_host::ops::sdk::sdk_locate_tools(&state.host, window.label(), names, near).await
}
/// `base -m venv location`; output lines stream through `on_output`.
/// Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`.
#[tauri::command]
#[specta::specta]
pub async fn sdk_python_create_venv(
window: Window,
state: State<'_, AppState>,
tool: ide_sdk::python::VenvTool,
base: PathBuf,
location: PathBuf,
request: RequestId,
@@ -107,6 +108,7 @@ pub async fn sdk_python_create_venv(
ide_host::ops::sdk::sdk_python_create_venv(
&state.host,
window.label(),
tool,
base,
location,
request,
+11 -2
View File
@@ -820,8 +820,8 @@ export const commands = {
sdkProjectLibraries: () => typedError<LibraryGroup[], IdeError>(__TAURI_INVOKE("sdk_project_libraries")),
/** Looks up `names` (bare command names): `node_modules/.bin` above `near`, then the search path. */
sdkLocateTools: (names: string[], near: string | null) => typedError<LocatedTools, IdeError>(__TAURI_INVOKE("sdk_locate_tools", { names, near })),
/** `base -m venv location`; output lines stream through `on_output`. */
sdkPythonCreateVenv: (base: string, location: string, request: RequestId, onOutput: Channel<string>) => typedError<Sdk, IdeError>(__TAURI_INVOKE("sdk_python_create_venv", { base, location, request, onOutput })),
/** Creates a Python environment with `tool` (`venv` / `uv` / `poetry`); output streams through `on_output`. */
sdkPythonCreateVenv: (tool: VenvTool, base: string, location: string, request: RequestId, onOutput: Channel<string>) => typedError<Sdk, IdeError>(__TAURI_INVOKE("sdk_python_create_venv", { tool, base, location, request, onOutput })),
sdkPythonPackages: (python: string, request: RequestId) => typedError<PythonPackage[], IdeError>(__TAURI_INVOKE("sdk_python_packages", { python, request })),
/** Installs (`uninstall = false`) or removes a package; output streams through `on_output`. */
sdkPythonPip: (python: string, requirement: string, uninstall: boolean, upgrade: boolean, request: RequestId, onOutput: Channel<string>) => typedError<null, IdeError>(__TAURI_INVOKE("sdk_python_pip", { python, requirement, uninstall, upgrade, request, onOutput })),
@@ -7778,6 +7778,15 @@ export type VcsSettings = {
issueNavigation?: IssueLink[],
};
/** The tool that creates a new Python environment in "Add Python Interpreter". */
export type 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";
/** What a view or custom editor shows. */
export type View = {
toolbar: ViewButton[],
@@ -0,0 +1,126 @@
import { act, fireEvent, render, screen, waitFor, cleanup } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Sdk } from "@/bindings";
const mocks = vi.hoisted(() => ({
detect: vi.fn(),
createVenv: vi.fn(),
choosePath: vi.fn(),
}));
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
return {
...real,
isTauri: () => true,
commands: {
...real.commands,
sdkDetect: mocks.detect,
sdkPythonCreateVenv: mocks.createVenv,
cancelRequest: vi.fn(),
},
};
});
vi.mock("@tauri-apps/plugin-dialog", () => ({ open: mocks.choosePath }));
const { AddInterpreterDialog } = await import("../languages/AddInterpreterDialog");
const { useSdks } = await import("../languages/api");
const SYSTEM: Sdk = {
path: "/usr/bin/python3",
name: "Python 3.12",
version: "3.12.0",
source: "system",
home: "/usr",
extra: null,
};
const VENV: Sdk = {
path: "/project/.venv/bin/python",
name: "Python 3.12 (.venv)",
version: "3.12.0",
source: "venv",
home: "/project/.venv",
extra: null,
};
const ok = <T,>(data: T) => ({ status: "ok" as const, data });
beforeEach(() => {
Object.assign(window, { __TAURI_INTERNALS__: { transformCallback: () => 1 } });
mocks.detect.mockResolvedValue(ok([VENV, SYSTEM]));
mocks.createVenv.mockResolvedValue(new Promise(() => {}));
useSdks.setState({ byKind: {}, loading: {}, errors: {} });
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
const open = (onAdded = vi.fn()) => {
render(<AddInterpreterDialog root="/project" onClose={() => {}} onAdded={onAdded} />);
return onAdded;
};
describe("Add Python Interpreter", () => {
it("lists the environment types PyCharm offers", async () => {
open();
await waitFor(() => expect(mocks.detect).toHaveBeenCalled());
for (const label of ["Virtualenv Environment", "uv Environment", "Poetry Environment"]) {
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
}
});
it("creates a virtualenv with the base interpreter by default", async () => {
let finish: (sdk: Sdk) => void = () => {};
mocks.createVenv.mockReturnValue(new Promise((r) => (finish = (s) => r(ok(s)))));
const onAdded = open();
await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument());
fireEvent.click(screen.getByRole("button", { name: "OK" }));
expect(mocks.createVenv).toHaveBeenCalledWith(
"venv",
"/usr/bin/python3",
"/project/.venv",
expect.anything(),
expect.anything(),
);
await act(async () => finish(VENV));
await waitFor(() => expect(onAdded).toHaveBeenCalledWith("/project/.venv/bin/python"));
});
it("passes uv as the tool and Poetry hides the location field", async () => {
let finish: (v: unknown) => void = () => {};
mocks.createVenv.mockImplementation(() => new Promise((r) => (finish = r)));
open();
await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument());
fireEvent.click(screen.getByRole("button", { name: "uv Environment" }));
fireEvent.click(screen.getByRole("button", { name: "OK" }));
expect(mocks.createVenv).toHaveBeenCalledWith(
"uv",
"/usr/bin/python3",
"/project/.venv",
expect.anything(),
expect.anything(),
);
await act(async () => finish(ok(VENV)));
fireEvent.click(screen.getByRole("button", { name: "Poetry Environment" }));
await waitFor(() => expect(screen.getByLabelText("Location:")).toBeDisabled());
fireEvent.click(screen.getByRole("button", { name: "OK" }));
expect(mocks.createVenv).toHaveBeenLastCalledWith(
"poetry",
"/usr/bin/python3",
"/project/.venv",
expect.anything(),
expect.anything(),
);
});
it("adds an existing interpreter without creating anything", async () => {
const onAdded = open();
await waitFor(() => expect(screen.getByRole("button", { name: "Base interpreter" })).toBeInTheDocument());
fireEvent.click(screen.getByRole("radio", { name: "Existing environment" }));
fireEvent.click(screen.getByRole("button", { name: "Interpreter" }));
fireEvent.click(await screen.findByRole("option", { name: /Python 3\.12 \(\.venv\)/ }));
fireEvent.click(screen.getByRole("button", { name: "OK" }));
expect(mocks.createVenv).not.toHaveBeenCalled();
expect(onAdded).toHaveBeenCalledWith("/project/.venv/bin/python");
});
});
@@ -1,17 +1,24 @@
/**
* PyCharm's "Add Python Interpreter" (local): generate a new virtualenv from a
* base interpreter (`python -m venv`) or select an existing interpreter.
* PyCharm's "Add Python Interpreter" (local): generate a new environment with a
* tool (virtualenv / uv / Poetry) or select an existing interpreter.
*/
import { isCancelled } from "@/platform/ipc";
import { useId, useMemo, useState } from "react";
import { Dialog, IconButton, Input, Select, Spinner, type SelectOption, T } from "@/ui";
import { choosePath, createVenv, rememberSdk } from "./api";
import { choosePath, createVenv, rememberSdk, type VenvTool } from "./api";
import { tildify, useDetected } from "./components";
import styles from "./Sdk.module.css";
import { tx } from "@/platform/i18n";
type Mode = "new" | "existing";
/** Environment types of the left column, in PyCharm's order. */
const KINDS: { tool: VenvTool; label: string; missing: string }[] = [
{ tool: "venv", label: "Virtualenv Environment", missing: "" },
{ tool: "uv", label: "uv Environment", missing: "uv is not installed" },
{ tool: "poetry", label: "Poetry Environment", missing: "Poetry is not installed" },
];
export function AddInterpreterDialog({
root,
onClose,
@@ -22,6 +29,7 @@ export function AddInterpreterDialog({
onAdded: (python: string) => void;
}) {
const { list, loading } = useDetected("python");
const [kind, setKind] = useState<VenvTool>("venv");
const [mode, setMode] = useState<Mode>("new");
const bases = useMemo(() => list.filter((s) => s.source !== "venv"), [list]);
const [base, setBase] = useState<string | null>(null);
@@ -54,7 +62,7 @@ export function AddInterpreterDialog({
}
if (!baseValue || !location.trim()) return;
setLog([]);
const job = createVenv(baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line]));
const job = createVenv(kind, baseValue, location.trim(), (line) => setLog((l) => [...l.slice(-200), line]));
setBusy({ cancel: job.cancel });
job.promise
.then((sdk) => {
@@ -66,6 +74,13 @@ export function AddInterpreterDialog({
};
const canOk = mode === "new" ? !!baseValue && !!location.trim() && !busy : !!existing;
const note = {
venv: tx("Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter."),
uv: tx("Runs “uv venv <location> --python <base>”; the new interpreter becomes the project interpreter."),
poetry: tx(
"Runs “poetry env use <base>” in the project; poetry keeps the environment under its own cache directory.",
),
}[kind];
return (
<Dialog
@@ -74,7 +89,7 @@ export function AddInterpreterDialog({
title="Add Python Interpreter"
width={680}
aria-label="Add Python Interpreter"
footerStart={busy && <Spinner size={14} label="Creating virtual environment" />}
footerStart={busy && <Spinner size={14} label="Creating environment" />}
buttons={[
{ label: "OK", role: "default", disabled: !canOk, onClick: ok },
{ label: "Cancel", role: "cancel" },
@@ -82,11 +97,21 @@ export function AddInterpreterDialog({
>
<div className={styles.dialogBody}>
<ul className={styles.kinds} aria-label={tx("Interpreter type")}>
<li>
<button type="button" className={`${styles.kind} ${styles.kindOn}`} aria-current="true">
<T>Virtualenv Environment</T>
</button>
</li>
{KINDS.map((k) => (
<li key={k.tool}>
<button
type="button"
className={`${styles.kind} ${kind === k.tool ? styles.kindOn : ""}`}
aria-current={kind === k.tool || undefined}
onClick={() => {
setKind(k.tool);
setError(null);
}}
>
{k.label}
</button>
</li>
))}
</ul>
<div className={styles.form}>
<div className={styles.radios} role="radiogroup" aria-label={tx("Environment")}>
@@ -110,11 +135,13 @@ export function AddInterpreterDialog({
className={styles.pathInput}
value={location}
onChange={(e) => setLocation(e.target.value)}
disabled={kind === "poetry"}
/>
<IconButton
icon="folder-open"
size="small"
title="Browse…"
disabled={kind === "poetry"}
onClick={async () => {
const p = await choosePath({ directory: true, title: "Virtualenv Location", defaultPath: root });
if (p) setLocation(p);
@@ -143,9 +170,7 @@ export function AddInterpreterDialog({
}}
/>
</div>
<div className={styles.note}>
{tx("Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter.")}
</div>
<div className={styles.note}>{note}</div>
</>
) : (
<>
+10 -2
View File
@@ -81,8 +81,16 @@ function streamed<T>(
return { promise: run(request, channel), cancel: () => void commands.cancelRequest(request) };
}
export function createVenv(base: string, location: string, onLine: (line: string) => void): Streamed<Sdk> {
return streamed((req, ch) => call(commands.sdkPythonCreateVenv(base, location, req, ch)), onLine);
/** Tool that creates a new Python environment (`VenvTool` in `ide-sdk`). */
export type VenvTool = "venv" | "uv" | "poetry";
export function createVenv(
tool: VenvTool,
base: string,
location: string,
onLine: (line: string) => void,
): Streamed<Sdk> {
return streamed((req, ch) => call(commands.sdkPythonCreateVenv(tool, base, location, req, ch)), onLine);
}
export function listPackages(python: string): Promise<PythonPackage[]> {
+1 -1
View File
@@ -601,7 +601,7 @@
"Create Terminal": "Crear terminal",
"Create Terminal in Container": "Crear terminal en el contenedor",
"Create UTF-8 files": "Crear archivos UTF-8",
"Creating virtual environment": "Creando entorno virtual",
"Creating environment": "Creando entorno virtual",
"Current": "Actual",
"Current Branch": "Rama actual",
"Current File": "Archivo actual",
+1 -1
View File
@@ -541,7 +541,7 @@
"Create Terminal": "Créer un terminal",
"Create Terminal in Container": "Créer un terminal dans le conteneur",
"Create UTF-8 files": "Créer des fichiers UTF-8",
"Creating virtual environment": "Création de l'environnement virtuel",
"Creating environment": "Création de l'environnement virtuel",
"Current": "Actuel",
"Current Branch": "Branche actuelle",
"Current File": "Fichier actuel",
+1 -1
View File
@@ -643,7 +643,7 @@
"Create Terminal": "터미널 생성",
"Create Terminal in Container": "컨테이너에서 터미널 생성",
"Create UTF-8 files": "UTF-8 파일 생성",
"Creating virtual environment": "가상 환경 생성 중",
"Creating environment": "가상 환경 생성 중",
"Current": "현재",
"Current Branch": "현재 브랜치",
"Current File": "현재 파일",
+1 -1
View File
@@ -593,7 +593,7 @@
"Create Terminal": "Utwórz terminal",
"Create Terminal in Container": "Utwórz terminal w kontenerze",
"Create UTF-8 files": "Twórz pliki w kodowaniu UTF-8",
"Creating virtual environment": "Tworzenie środowiska wirtualnego",
"Creating environment": "Tworzenie środowiska wirtualnego",
"Current": "Bieżący",
"Current Branch": "Bieżąca gałąź",
"Current File": "Bieżący plik",
+1 -1
View File
@@ -593,7 +593,7 @@
"Create Terminal": "Criar terminal",
"Create Terminal in Container": "Criar terminal no contêiner",
"Create UTF-8 files": "Criar arquivos UTF-8",
"Creating virtual environment": "Criando ambiente virtual",
"Creating environment": "Criando ambiente virtual",
"Current": "Atual",
"Current Branch": "Branch atual",
"Current File": "Arquivo atual",
+5 -1
View File
@@ -648,7 +648,7 @@
"Create Terminal": "创建终端",
"Create Terminal in Container": "在容器中创建终端",
"Create UTF-8 files": "创建 UTF-8 文件",
"Creating virtual environment": "正在创建虚拟环境",
"Creating environment": "正在创建环境",
"Current": "当前",
"Current Branch": "当前分支",
"Current File": "当前文件",
@@ -1599,6 +1599,7 @@
"Popup type": "弹出类型",
"Port:": "端口:",
"Postfix Completion": "后缀补全",
"Poetry Environment": "Poetry 环境",
"Postfix templates": "后缀模板",
"Prefer": "首选",
"Prefer moving caret line to minimize editor scrolling": "优先移动光标所在行,以尽量减少编辑器滚动",
@@ -2519,6 +2520,7 @@
"Virtual Space": "虚拟空间",
"Virtualenv Environment": "Virtualenv 环境",
"Virtualenv Location": "Virtualenv 位置",
"uv Environment": "uv 环境",
"Visual guides": "视觉参考线",
"WHERE filter": "WHERE 筛选器",
"Warn if CRLF line separators are about to be committed": "即将提交 CRLF 行分隔符时发出警告",
@@ -4680,6 +4682,8 @@
"{count} modified action": "已修改 {count} 个操作",
"{count} modified actions": "已修改 {count} 个操作",
"Runs “<base> -m venv <location>”; the new interpreter becomes the project interpreter.": "运行“<base> -m venv <location>”;新的解释器将成为项目解释器。",
"Runs “poetry env use <base>” in the project; poetry keeps the environment under its own cache directory.": "在项目中运行“poetry env use <base>”;Poetry 会将环境保存在其自己的缓存目录下。",
"Runs “uv venv <location> --python <base>”; the new interpreter becomes the project interpreter.": "运行“uv venv <location> --python <base>”;新的解释器将成为项目解释器。",
"{vendor} {version} is unpacked into a folder of its own in {location}, where JDK detection finds it.": "{vendor} {version} 将解压到 {location} 下它自己的文件夹中,JDK 检测会在那里找到它。",
"detected from {source}": "检测自 {source}",
"modified": "已修改",