fix(setup): align Python setup probe with eval

This commit is contained in:
Paolo Frazzetto
2026-08-02 14:55:52 +02:00
parent 06343fef42
commit 24ecd0269c
3 changed files with 99 additions and 21 deletions
+5
View File
@@ -2,6 +2,11 @@
## [Unreleased]
### Fixed
- Fixed `omp setup python` to validate the same configured or discovered interpreter used by the Python eval runtime.
## [17.2.4] - 2026-08-01
### Added
+13 -21
View File
@@ -4,10 +4,10 @@
* Handles `omp setup` for onboarding and `omp setup <component>` for optional dependencies.
*/
import * as path from "node:path";
import { $which, APP_NAME, getProjectDir, getPythonEnvDir } from "@oh-my-pi/pi-utils";
import { $ } from "bun";
import { APP_NAME, getProjectDir, getPythonEnvDir } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { Settings, settings } from "../config/settings";
import { checkPythonKernelAvailability } from "../eval/py/kernel";
import { theme } from "../modes/theme/theme";
import { downloadSttModel, isSttModelCached } from "../stt/downloader";
import { isSttModelKey, STT_MODEL_OPTIONS } from "../stt/models";
@@ -82,25 +82,14 @@ function managedPythonPath(): string {
/**
* Check Python environment and kernel dependencies.
*/
async function checkPythonSetup(): Promise<PythonCheckResult> {
const result: PythonCheckResult = {
available: false,
async function checkPythonSetup(cwd: string, interpreter?: string): Promise<PythonCheckResult> {
const availability = await checkPythonKernelAvailability(cwd, interpreter);
return {
available: availability.ok,
pythonPath: availability.pythonPath,
usingManagedEnv: availability.pythonPath === managedPythonPath(),
managedEnvPath: MANAGED_PYTHON_ENV,
};
const systemPythonPath = $which("python") ?? $which("python3");
const managedPath = managedPythonPath();
const hasManagedEnv = await Bun.file(managedPath).exists();
const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined);
if (!pythonPath) {
return result;
}
const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow();
result.pythonPath = pythonPath;
result.available = probe.exitCode === 0;
result.usingManagedEnv = pythonPath === managedPath;
return result;
}
/**
@@ -126,7 +115,10 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
}
async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
const check = await checkPythonSetup();
const cwd = getProjectDir();
const projectSettings = await Settings.init({ cwd });
const interpreter = projectSettings.get("python.interpreter")?.trim() || undefined;
const check = await checkPythonSetup(cwd, interpreter);
if (flags.json) {
console.log(JSON.stringify(check, null, 2));
@@ -136,7 +128,7 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
if (!check.pythonPath) {
console.error(chalk.red(`${theme.status.error} Python not found`));
console.error(chalk.dim("Install Python 3.8+ and ensure it's in your PATH"));
console.error(chalk.dim("Install Python 3.8+ or set python.interpreter to its executable path"));
process.exit(1);
}
@@ -0,0 +1,81 @@
import { afterEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { TempDir } from "@oh-my-pi/pi-utils";
const cliEntry = path.join(import.meta.dir, "..", "src", "cli.ts");
interface CliProcessResult {
exitCode: number;
output: string;
error: string;
}
async function runSetupPython(cwd: string): Promise<CliProcessResult> {
const env: NodeJS.ProcessEnv = {
...process.env,
NO_COLOR: "1",
PI_CODING_AGENT_DIR: path.join(cwd, "agent"),
};
delete env.VIRTUAL_ENV;
delete env.CONDA_DEFAULT_ENV;
delete env.CONDA_PREFIX;
const proc = Bun.spawn([process.execPath, cliEntry, "setup", "python", "--json"], {
cwd,
stdout: "pipe",
stderr: "pipe",
env,
});
const output = new Response(proc.stdout).text();
const error = new Response(proc.stderr).text();
const [exitCode, stdout, stderr] = await Promise.all([proc.exited, output, error]);
return { exitCode, output: stdout, error: stderr };
}
describe("omp setup python", () => {
let projectDir: TempDir | undefined;
afterEach(async () => {
await projectDir?.remove();
projectDir = undefined;
});
it.skipIf(process.platform === "win32")(
"probes the project-configured interpreter instead of the PATH interpreter",
async () => {
projectDir = TempDir.createSync("@omp-setup-python-");
const cwd = projectDir.path();
const interpreter = path.join(cwd, "configured-python");
await Bun.write(interpreter, "#!/bin/sh\nexit 0\n");
await fs.chmod(interpreter, 0o755);
await Bun.write(path.join(cwd, ".omp", "config.yml"), `python:\n interpreter: ${interpreter}\n`);
const result = await runSetupPython(cwd);
expect(result.error).toBe("");
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.output)).toMatchObject({
available: true,
pythonPath: interpreter,
usingManagedEnv: false,
});
},
);
it.skipIf(process.platform === "win32")("prefers the project venv over the PATH interpreter", async () => {
projectDir = TempDir.createSync("@omp-setup-python-");
const cwd = projectDir.path();
const interpreter = path.join(cwd, ".venv", "bin", "python");
await Bun.write(interpreter, "#!/bin/sh\nexit 0\n");
await fs.chmod(interpreter, 0o755);
const result = await runSetupPython(cwd);
expect(result.error).toBe("");
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.output)).toMatchObject({
available: true,
pythonPath: interpreter,
usingManagedEnv: false,
});
});
});