diff --git a/crates/ide-sdk/src/python.rs b/crates/ide-sdk/src/python.rs
index 35c33beee..ed12cf32b 100644
--- a/crates/ide-sdk/src/python.rs
+++ b/crates/ide-sdk/src/python.rs
@@ -26,6 +26,8 @@ pub enum VenvTool {
Uv,
/// `poetry env use ` in the project, then `poetry env info --path`.
Poetry,
+ /// `conda create -p -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 -y` builds an environment at the requested prefix.
+/// `base` pins the Python version (`python=`) 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,
+ cancel: &CancellationToken,
+ on_line: &dyn Fn(String),
+) -> IdeResult {
+ 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::>().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
diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs
index ceb43473b..1442b61fc 100644
--- a/crates/ide-sdk/src/tests.rs
+++ b/crates/ide-sdk/src/tests.rs
@@ -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 {
+ 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();
diff --git a/src/bindings.ts b/src/bindings.ts
index 0ba380462..07b6dabb0 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -7785,7 +7785,9 @@ export type VenvTool =
/** `uv venv --python ` (uv must be on PATH). */
"uv" |
/** `poetry env use ` in the project, then `poetry env info --path`. */
-"poetry";
+"poetry" |
+/** `conda create -p -y` (conda must be on PATH). */
+"conda";
/** What a view or custom editor shows. */
export type View = {
diff --git a/src/features/settings/__tests__/addInterpreter.test.tsx b/src/features/settings/__tests__/addInterpreter.test.tsx
index b7198e382..07832ab4b 100644
--- a/src/features/settings/__tests__/addInterpreter.test.tsx
+++ b/src/features/settings/__tests__/addInterpreter.test.tsx
@@ -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" }));
diff --git a/src/features/settings/languages/AddInterpreterDialog.tsx b/src/features/settings/languages/AddInterpreterDialog.tsx
index 3d50a2a00..ea85a3119 100644
--- a/src/features/settings/languages/AddInterpreterDialog.tsx
+++ b/src/features/settings/languages/AddInterpreterDialog.tsx
@@ -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 “ -m venv ”; the new interpreter becomes the project interpreter."),
+ conda: tx("Runs “conda create -p ”; the new interpreter becomes the project interpreter."),
uv: tx("Runs “uv venv --python ”; the new interpreter becomes the project interpreter."),
poetry: tx(
"Runs “poetry env use ” in the project; poetry keeps the environment under its own cache directory.",
diff --git a/src/features/settings/languages/api.ts b/src/features/settings/languages/api.ts
index 00ed41afb..77f179c0c 100644
--- a/src/features/settings/languages/api.ts
+++ b/src/features/settings/languages/api.ts
@@ -82,7 +82,7 @@ function streamed(
}
/** 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,
diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json
index d4b498ebf..afef76eb7 100644
--- a/src/platform/locales/text/zh.json
+++ b/src/platform/locales/text/zh.json
@@ -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 “ -m venv ”; the new interpreter becomes the project interpreter.": "运行“ -m venv ”;新的解释器将成为项目解释器。",
+ "Runs “conda create -p ”; the new interpreter becomes the project interpreter.": "运行“conda create -p ”;新的解释器将成为项目解释器。",
"Runs “poetry env use ” in the project; poetry keeps the environment under its own cache directory.": "在项目中运行“poetry env use ”;Poetry 会将环境保存在其自己的缓存目录下。",
"Runs “uv venv --python ”; the new interpreter becomes the project interpreter.": "运行“uv venv --python ”;新的解释器将成为项目解释器。",
"{vendor} {version} is unpacked into a folder of its own in {location}, where JDK detection finds it.": "{vendor} {version} 将解压到 {location} 下它自己的文件夹中,JDK 检测会在那里找到它。",