дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(run): Windows system python, pytest templates in gutter configs, pixi env detection (#1886)
* fix(run): use system python per OS and pytest templates, detect pixi envs Gutter/context pytest configs on Windows used `python3`, which is not a command there; fall back to `python` on Windows. Gutter-created pytest configs now also inherit "Edit configuration templates... > pytest" values via a template cache populated with the run configurations. Pixi environments (`.pixi/envs/<env>`) are detected as the project's Python: as run-configuration env (`PyEnv::Pixi`, tools via `pixi run` or the env's own executables), as the project interpreter for scripts/tests and the debugger, and as selectable SDKs, so the interpreter picked in the IDE feeds completion. Fixes l0ng-ai/openidea#1885 * fix(run): drop the pytest template's own target in gutter configs testConfigFor always appends the gutter file, so a target stored on the template would run twice (template target plus the file). The gutter file is the target. Also corrects pixi_env's doc comment to the implemented name order.
Цей коміт міститься в:
1 джерело
0eca4366d0
коміт
d018771a8d
8 files changed
+270
-12
No files matched your search
@@ -29,6 +29,7 @@ import { importConfigs, importLabel } from "./importConfigs";
|
||||
import { targetName } from "./remote";
|
||||
import { saveEditedConfigs, selectConfig, useRun } from "./store";
|
||||
import { blankTemplate, COMPOUND, missingParams, newConfigOfType, templateId, typeFor } from "./types";
|
||||
import { cacheRunTemplates } from "./runTemplates";
|
||||
import styles from "./Run.module.css";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
@@ -142,7 +143,11 @@ function EditConfigsDialog() {
|
||||
if (!isTauri()) return;
|
||||
let live = true;
|
||||
call(commands.runTemplates())
|
||||
.then((t) => live && setTemplates(t))
|
||||
.then((t) => {
|
||||
if (!live) return;
|
||||
setTemplates(t);
|
||||
cacheRunTemplates(t);
|
||||
})
|
||||
.catch(() => live && setTemplates([]));
|
||||
return () => {
|
||||
live = false;
|
||||
@@ -305,7 +310,10 @@ function EditConfigsDialog() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (dirty) await saveEditedConfigs(draft, removedDetectedIds);
|
||||
if (templatesDirty && templates) await call(commands.runSaveTemplates(templates));
|
||||
if (templatesDirty && templates) {
|
||||
await call(commands.runSaveTemplates(templates));
|
||||
cacheRunTemplates(templates);
|
||||
}
|
||||
setRemovedDetectedIds([]);
|
||||
setDirty(false);
|
||||
setTemplatesDirty(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import {
|
||||
commandLine,
|
||||
duplicateConfig,
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
validateConfig,
|
||||
} from "./configs";
|
||||
import { contextConfig, fileRole } from "./context";
|
||||
import { cacheRunTemplates } from "./runTemplates";
|
||||
import { registerBuiltinTypes } from "./types";
|
||||
import { envFromRows } from "./EnvDialog";
|
||||
import { configRemoved, defaultSelection, rerunConfig, useRun, type RunSession } from "./store";
|
||||
|
||||
@@ -380,6 +383,85 @@ describe("context configuration", () => {
|
||||
expect(contextConfig("tools/gen.py", plain)).toMatchObject({ command: "python3", args: ["tools/gen.py"] });
|
||||
expect(contextConfig("tools/gen.py", [])).toMatchObject({ command: "python3", args: ["tools/gen.py"] });
|
||||
});
|
||||
|
||||
describe("system python per OS (#1885)", () => {
|
||||
const os = usePlatform.getState().os;
|
||||
afterEach(() => {
|
||||
usePlatform.setState({ os });
|
||||
cacheRunTemplates([]);
|
||||
});
|
||||
|
||||
it("falls back to `python` on Windows, `python3` elsewhere", () => {
|
||||
usePlatform.setState({ os: "windows" });
|
||||
// `python3` is not a command on Windows.
|
||||
expect(contextConfig("tools/gen.py", [])).toMatchObject({ command: "python", args: ["tools/gen.py"] });
|
||||
expect(contextConfig("worker/test_worker.py", [])).toMatchObject({
|
||||
command: "python",
|
||||
args: ["-m", "pytest", "worker/test_worker.py"],
|
||||
});
|
||||
usePlatform.setState({ os: "linux" });
|
||||
expect(contextConfig("tools/gen.py", [])).toMatchObject({ command: "python3", args: ["tools/gen.py"] });
|
||||
expect(contextConfig("worker/test_worker.py", [])).toMatchObject({
|
||||
command: "python3",
|
||||
args: ["-m", "pytest", "worker/test_worker.py"],
|
||||
});
|
||||
});
|
||||
|
||||
it("seeds gutter pytest configs from the pytest template", () => {
|
||||
const dispose = registerBuiltinTypes();
|
||||
try {
|
||||
cacheRunTemplates([
|
||||
cfg({ id: "template:pytest", kind: "pytest", params: { command: "pixi", runnerArgs: "run pytest" } }),
|
||||
]);
|
||||
expect(contextConfig("worker/test_worker.py", [])).toMatchObject({
|
||||
command: "pixi",
|
||||
args: ["run", "pytest", "worker/test_worker.py"],
|
||||
isTest: true,
|
||||
});
|
||||
// Template options come along; the file stays the target.
|
||||
cacheRunTemplates([
|
||||
cfg({
|
||||
id: "template:pytest",
|
||||
kind: "pytest",
|
||||
params: { command: "uv", runnerArgs: "run pytest", options: "-x" },
|
||||
}),
|
||||
]);
|
||||
expect(contextConfig("worker/test_worker.py", [])!.args).toEqual([
|
||||
"run",
|
||||
"pytest",
|
||||
"-x",
|
||||
"worker/test_worker.py",
|
||||
]);
|
||||
// A target stored on the template itself is dropped: the gutter file is
|
||||
// the target, or the run would execute both.
|
||||
cacheRunTemplates([
|
||||
cfg({
|
||||
id: "template:pytest",
|
||||
kind: "pytest",
|
||||
params: { command: "pixi", runnerArgs: "run pytest", target: "tests", options: "-x" },
|
||||
}),
|
||||
]);
|
||||
expect(contextConfig("worker/test_worker.py", [])!.args).toEqual([
|
||||
"run",
|
||||
"pytest",
|
||||
"-x",
|
||||
"worker/test_worker.py",
|
||||
]);
|
||||
// No template: the detected interpreter / system python as before.
|
||||
cacheRunTemplates([]);
|
||||
const venv = [
|
||||
cfg({ id: "python:module:app", kind: "python", command: ".venv/bin/python", args: ["-m", "app"] }),
|
||||
];
|
||||
expect(contextConfig("worker/test_worker.py", venv)).toMatchObject({
|
||||
command: ".venv/bin/python",
|
||||
args: ["-m", "pytest", "worker/test_worker.py"],
|
||||
});
|
||||
} finally {
|
||||
dispose();
|
||||
cacheRunTemplates([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("rerunning a session after its configuration changed (#1497)", () => {
|
||||
|
||||
@@ -9,6 +9,8 @@ import { phpunitArgs } from "./testing/phpunit";
|
||||
import { goTestParts } from "./goArguments";
|
||||
import { goBuildType } from "./goTypes";
|
||||
import { isWindows } from "./jvmTypes";
|
||||
import { typeFor } from "./types";
|
||||
import { runTemplate } from "./runTemplates";
|
||||
|
||||
export type FileRole = "test" | "main" | "script" | null;
|
||||
|
||||
@@ -135,9 +137,16 @@ export function projectPython(configs: RunConfig[], relPath?: string): { command
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `python` on Windows (where `python3` is not a command), `python3` elsewhere. */
|
||||
function systemPython(): string {
|
||||
return isWindows() ? "python" : "python3";
|
||||
}
|
||||
|
||||
/**
|
||||
* PyCharm runs pytest for a `test_*.py` file even when the project declares no
|
||||
* test setup: `python -m pytest`, with the project's interpreter when detected.
|
||||
* The "Edit configuration templates…" › pytest values win over both the
|
||||
* interpreter and the runner arguments, like a template a new config inherits.
|
||||
*/
|
||||
function implicitPytest(relPath: string, configs: RunConfig[]): RunConfig | null {
|
||||
if (!relPath.endsWith(".py")) return null;
|
||||
@@ -146,11 +155,11 @@ function implicitPytest(relPath: string, configs: RunConfig[]): RunConfig | null
|
||||
relPath,
|
||||
);
|
||||
const py = projectPython(owners);
|
||||
return {
|
||||
const base: RunConfig = {
|
||||
id: "pytest:implicit",
|
||||
name: "pytest",
|
||||
kind: "pytest",
|
||||
command: py?.command ?? "python3",
|
||||
command: py?.command ?? systemPython(),
|
||||
args: [...(py?.prefix ?? []), "-m", "pytest"],
|
||||
cwd: owners[0]?.cwd ?? null,
|
||||
module: owners[0]?.module,
|
||||
@@ -159,6 +168,25 @@ function implicitPytest(relPath: string, configs: RunConfig[]): RunConfig | null
|
||||
isTest: true,
|
||||
debugAdapter: "debugpy",
|
||||
};
|
||||
return applyPytestTemplate(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Edit configuration templates…" › pytest applied to a gutter/context pytest
|
||||
* configuration: its Command and Runner arguments replace the detected
|
||||
* interpreter (`pixi` + `run pytest`, a venv's `pytest`, …) while the file the
|
||||
* context adds stays the target — a target stored on the template itself is
|
||||
* dropped, or the run would execute both it and the file. Without a template
|
||||
* the config is unchanged.
|
||||
*/
|
||||
function applyPytestTemplate(config: RunConfig): RunConfig {
|
||||
const type = typeFor("pytest");
|
||||
const template = runTemplate("pytest");
|
||||
const params = type && template ? { ...type.defaults, ...template.params, target: "" } : null;
|
||||
if (!type || !params) return config;
|
||||
const line = type.commandLine(params);
|
||||
if (!line) return config;
|
||||
return { ...config, command: line.command, args: line.args };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -347,7 +375,7 @@ function scriptConfigFor(
|
||||
};
|
||||
if (relPath.endsWith(".py")) {
|
||||
// Reuse the project's interpreter (venv / uv / poetry) when detected.
|
||||
const py = projectPython(configs) ?? { command: "python3", prefix: [] };
|
||||
const py = projectPython(configs) ?? { command: systemPython(), prefix: [] };
|
||||
const module = pythonModuleRun(relPath, content, configs);
|
||||
if (module)
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* A synchronous cache of the "Edit configuration templates…" values, so the
|
||||
* run-context producers (gutter icons, ⌃⇧R) — which build configurations
|
||||
* synchronously — can apply them. `loadConfigs` refreshes it from disk; the
|
||||
* Edit Configurations dialog writes through it on save.
|
||||
*/
|
||||
import { call, commands, isTauri, type RunConfig } from "@/platform/ipc";
|
||||
|
||||
const cache = new Map<string, RunConfig>();
|
||||
|
||||
/** The stored template of `kind` ("Edit configuration templates…" › kind), if any. */
|
||||
export function runTemplate(kind: string): RunConfig | undefined {
|
||||
return cache.get(kind);
|
||||
}
|
||||
|
||||
/** Caches the stored templates (called with `runTemplates()`'s result). */
|
||||
export function cacheRunTemplates(templates: readonly RunConfig[]): void {
|
||||
cache.clear();
|
||||
for (const t of templates) cache.set(t.kind, t);
|
||||
}
|
||||
|
||||
/** Refreshes the cache from disk; a no-op off the desktop. */
|
||||
export async function loadRunTemplates(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
cacheRunTemplates(await call(commands.runTemplates()));
|
||||
} catch {
|
||||
/* keep the previous cache */
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import { compoundChildren } from "./configs";
|
||||
import { remoteLabel, startWithRemedies } from "./remote";
|
||||
import { TerminalOutput } from "./terminalOutput";
|
||||
import { typeFor } from "./types";
|
||||
import { loadRunTemplates } from "./runTemplates";
|
||||
import { recordRun } from "./services";
|
||||
import { startedPort } from "./serverPort";
|
||||
import { tomcatBrowserUrl } from "./tomcat";
|
||||
@@ -197,7 +198,7 @@ export async function loadConfigs(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const saved = await call(commands.runConfigs());
|
||||
const [saved] = await Promise.all([call(commands.runConfigs()), loadRunTemplates()]);
|
||||
if (useProject.getState().project?.root !== root) return;
|
||||
// Temporary configs survive reloads (and restarts) until saved or evicted; a saved one wins.
|
||||
// They are kept without what settings derived: today's toolchains apply to them, as to the others.
|
||||
|
||||
Reference in new issue
Заблокувати користувача