From 51988985c5b2d87d79950d00a113b8c1068c9b10 Mon Sep 17 00:00:00 2001 From: Andrea Basile Date: Mon, 2 Nov 2026 16:37:47 +0100 Subject: [PATCH] fix(run): run a Python script with the project interpreter, not the runserver command line (#897) Root cause: projectPython built the interpreter prefix from the first Python configuration by dropping only the .py arguments, so a Django `manage.py runserver` config left `runserver` in the prefix. The prefix now ends where the program starts (`-m` or the script), so program arguments such as `runserver` and `test` never reach it; `uv run python` is kept. Covers both plain scripts and pytest files derived from the same prefix. Co-authored-by: andreabasile <7715018+andreabasile@users.noreply.github.com> --- src/features/run/configs.test.ts | 22 ++++++++++++++++++++++ src/features/run/context.ts | 7 +++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/features/run/configs.test.ts b/src/features/run/configs.test.ts index ce70a443f..162e0adf8 100644 --- a/src/features/run/configs.test.ts +++ b/src/features/run/configs.test.ts @@ -271,4 +271,26 @@ describe("context configuration", () => { const uv = [cfg({ id: "pytest", kind: "pytest", command: "uv", args: ["run", "pytest"], isTest: true })]; expect(contextConfig("tools/gen.py", uv)).toMatchObject({ command: "uv", args: ["run", "python", "tools/gen.py"] }); }); + + it("takes the interpreter from a Django runserver config without its arguments", () => { + const runserver = (command: string, args: string[]) => + cfg({ id: "django:runserver", kind: "python", command, args: [...args, "manage.py", "runserver"] }); + expect(contextConfig("tools/seed.py", [runserver(".venv/bin/python", [])])).toMatchObject({ + command: ".venv/bin/python", + args: ["tools/seed.py"], + }); + expect(contextConfig("tools/seed.py", [runserver("uv", ["run", "python"])])).toMatchObject({ + command: "uv", + args: ["run", "python", "tools/seed.py"], + }); + expect(contextConfig("worker/test_worker.py", [runserver(".venv/bin/python", [])])).toMatchObject({ + args: ["-m", "pytest", "worker/test_worker.py"], + }); + }); + + it("keeps the interpreter of a plain python config", () => { + const plain = [cfg({ id: "python:module:app", kind: "python", command: "python3", args: ["-m", "app"] })]; + 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"] }); + }); }); diff --git a/src/features/run/context.ts b/src/features/run/context.ts index 768e3c017..c71bf6e6d 100644 --- a/src/features/run/context.ts +++ b/src/features/run/context.ts @@ -91,8 +91,11 @@ function derived(base: RunConfig, relPath: string, patch: Partial): R export function projectPython(configs: RunConfig[]): { command: string; prefix: string[] } | null { const py = configs.find((c) => c.kind === "python"); if (py) { - const m = py.args.indexOf("-m"); - return { command: py.command, prefix: m >= 0 ? py.args.slice(0, m) : py.args.filter((a) => !a.endsWith(".py")) }; + // The interpreter part ends where the program starts: `-m`, or the script (`manage.py`). What follows + // (`runserver`, `test`…) belongs to that program, never to the interpreter. + const end = py.args.findIndex((a) => a === "-m" || a.endsWith(".py")); + const python = py.args.findIndex((a) => /^python[\d.]*$/.test(a)); + return { command: py.command, prefix: py.args.slice(0, end >= 0 ? end : python + 1) }; } const pytest = configs.find((c) => c.kind === "pytest"); if (!pytest) return null;