feat(sdk): add Conda to the Add Python Interpreter env tools (#1889)

Extends the VenvTool set with a "Conda Environment" option: the dialog
runs 'conda create -p <location> -y', pinning 'python=<major.minor>' from
the chosen base interpreter when it reports a version. An existing
conda-meta directory is refused before conda runs, and a missing conda
binary reports an install hint instead of a spawn failure.

Refs l0ng-ai/openidea#1724
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-07 07:21:28 +08:00
1 джерело 88ecaf9ccd
коміт 66ecf4c967
7 files changed
+96 -14

No files matched your search

+39
Переглянути файл
@@ -26,6 +26,8 @@ pub enum VenvTool {
Uv,
/// `poetry env use <base>` in the project, then `poetry env info --path`.
Poetry,
/// `conda create -p <location> -y` (conda must be on PATH).
Conda,
}
/// Creates a virtualenv at `location` with `tool` and returns its interpreter.
@@ -83,6 +85,7 @@ pub(crate) fn create_venv_with(
.ok_or_else(|| IdeError::internal(format!("{} was not created", python.display())))
}
VenvTool::Poetry => create_poetry_env(base, location, &which, cancel, on_line),
VenvTool::Conda => create_conda_env(base, location, &which, cancel, on_line),
}
}
@@ -118,6 +121,42 @@ fn create_poetry_env(
.ok_or_else(|| IdeError::internal(format!("{} was not created", python.display())))
}
/// `conda create -p <location> -y` builds an environment at the requested prefix.
/// `base` pins the Python version (`python=<major.minor>`) when it reports one, so the
/// created env matches the interpreter the user picked rather than conda's default.
fn create_conda_env(
base: &Path,
location: &Path,
which: &dyn Fn(&str) -> Option<PathBuf>,
cancel: &CancellationToken,
on_line: &dyn Fn(String),
) -> IdeResult<Sdk> {
if location.join("conda-meta").is_dir() {
return Err(IdeError::new(
ide_core::error::ErrorKind::AlreadyExists,
format!("{} already contains a conda environment", location.display()),
));
}
let conda = which("conda")
.ok_or_else(|| IdeError::unavailable("conda is not installed (install it from https://docs.conda.io)"))?;
if let Some(parent) = location.parent() {
std::fs::create_dir_all(parent)?;
}
let mut args = vec!["create".to_owned(), "-p".to_owned(), location.to_string_lossy().into_owned(), "-y".to_owned()];
if let Some(version) = query_in(base, &["--version"], None, &[], Duration::from_secs(15))
.and_then(|v| v.strip_prefix("Python ").map(str::to_owned))
{
let short = version.split('.').take(2).collect::<Vec<_>>().join(".");
if !short.is_empty() {
args.push(format!("python={short}"));
}
}
stream(&conda, &args, None, &[], cancel, on_line)?;
let python = location.join(bin_dir()).join(exe("python"));
crate::discover::python_sdk(&python, "conda", location.parent().and_then(Path::parent))
.ok_or_else(|| IdeError::internal(format!("{} was not created", python.display())))
}
/// Arguments of `base -m venv location`. `-I` (isolated mode) keeps the working
/// directory off `sys.path`: a project's `logging.py` or `typing.py` there would
/// otherwise shadow the standard library modules venv imports. (`-P` does just
+32
Переглянути файл
@@ -749,6 +749,16 @@ fn env_tools_are_reported_missing_before_they_run() {
)
.unwrap_err();
assert!(poetry.message.contains("Poetry is not installed"), "{}", poetry.message);
let conda = crate::python::create_venv_with(
crate::python::VenvTool::Conda,
&base,
&location,
none,
&Default::default(),
&|_| {},
)
.unwrap_err();
assert!(conda.message.contains("conda is not installed"), "{}", conda.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();
@@ -764,6 +774,28 @@ fn env_tools_are_reported_missing_before_they_run() {
assert!(again.message.contains("already contains a virtual environment"), "{}", again.message);
}
#[test]
fn an_existing_conda_environment_is_not_recreated() {
// `conda-meta/` marks a conda prefix: refuse before `conda create` would fail obscurely.
let dir = tempfile::tempdir().unwrap();
let location = dir.path().join("envs").join("ds");
let base = dir.path().join("python");
std::fs::create_dir_all(location.join("conda-meta")).unwrap();
fn none(_: &str) -> Option<std::path::PathBuf> {
None
}
let err = crate::python::create_venv_with(
crate::python::VenvTool::Conda,
&base,
&location,
none,
&Default::default(),
&|_| {},
)
.unwrap_err();
assert!(err.message.contains("already contains a conda environment"), "{}", err.message);
}
#[test]
fn neutral_dir_is_fresh_and_empty() {
let a = crate::python::neutral_dir().unwrap();
+3 -1
Переглянути файл
@@ -7785,7 +7785,9 @@ export type VenvTool =
/** `uv venv <location> --python <base>` (uv must be on PATH). */
"uv" |
/** `poetry env use <base>` in the project, then `poetry env info --path`. */
"poetry";
"poetry" |
/** `conda create -p <location> -y` (conda must be on PATH). */
"conda";
/** What a view or custom editor shows. */
export type View = {
+17 -12
Переглянути файл
@@ -64,7 +64,7 @@ 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"]) {
for (const label of ["Virtualenv Environment", "Conda Environment", "uv Environment", "Poetry Environment"]) {
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
}
});
@@ -86,21 +86,26 @@ describe("Add Python Interpreter", () => {
await waitFor(() => expect(onAdded).toHaveBeenCalledWith("/project/.venv/bin/python"));
});
it("passes uv as the tool and Poetry hides the location field", async () => {
it("passes each 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)));
for (const [label, tool] of [
["uv Environment", "uv"],
["Conda Environment", "conda"],
] as const) {
fireEvent.click(screen.getByRole("button", { name: label }));
fireEvent.click(screen.getByRole("button", { name: "OK" }));
expect(mocks.createVenv).toHaveBeenLastCalledWith(
tool,
"/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" }));
+2
Переглянути файл
@@ -15,6 +15,7 @@ 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: "conda", label: "Conda Environment", missing: "conda is not installed" },
{ tool: "uv", label: "uv Environment", missing: "uv is not installed" },
{ tool: "poetry", label: "Poetry Environment", missing: "Poetry is not installed" },
];
@@ -76,6 +77,7 @@ 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."),
conda: tx("Runs “conda create -p <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.",
+1 -1
Переглянути файл
@@ -82,7 +82,7 @@ function streamed<T>(
}
/** Tool that creates a new Python environment (`VenvTool` in `ide-sdk`). */
export type VenvTool = "venv" | "uv" | "poetry";
export type VenvTool = "venv" | "uv" | "poetry" | "conda";
export function createVenv(
tool: VenvTool,
+2
Переглянути файл
@@ -575,6 +575,7 @@
"Conflicts:": "冲突:",
"Connect": "连接",
"Connected": "已连接",
"Conda Environment": "Conda 环境",
"Connecting to Docker…": "正在连接 Docker…",
"Connecting to GitHub…": "正在连接 GitHub…",
"Console": "控制台",
@@ -4682,6 +4683,7 @@
"{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 “conda create -p <location>”; the new interpreter becomes the project interpreter.": "运行“conda create -p <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 检测会在那里找到它。",