дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
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
Цей коміт міститься в:
1 джерело
88ecaf9ccd
коміт
66ecf4c967
7 files changed
+96
-14
No files matched your search
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 检测会在那里找到它。",
|
||||
|
||||
Reference in new issue
Заблокувати користувача