Compare commits

...
Author SHA1 Message Date
l0ng-ai 15abca599a fix(debugger): debug uv / poetry run configurations in the project's environment
A Python run configuration such as `uv run python main.py` or `poetry run pytest` was debugged as `python3 -m uv run python main.py`: debugpy treated the manager as a console script and ran it as a module, so the session failed or ran outside the project's environment.

Configurations that run through uv, poetry, pdm, pipenv, hatch or rye now pass the manager to debugpy as the debuggee's interpreter (`["uv", "run", "python"]`) and keep the script, `-m` module or tool (`uv run pytest` → module `pytest`) as the target. The adapter and its launcher run on a plain interpreter (`debugLauncherPython`), which the backend now prefers when choosing the adapter's command.
2026-11-05 08:17:55 +08:00
3 changed files with 54 additions and 17 deletions

No files matched your search

+5 -4
View File
@@ -589,11 +589,12 @@ impl DebugManager {
if let Some(command) = custom_command(&spec.command) {
spec.command = command;
}
// debugpy: run the adapter with the debuggee's interpreter (venvs carry their own debugpy).
// debugpy: run the adapter with the debuggee's interpreter (venvs carry their own debugpy),
// or with the launcher's when the debuggee runs through a manager (`uv run python`).
if config.adapter == "debugpy" {
let python = match arguments.get("python") {
Some(Value::String(p)) => Some(p.clone()),
Some(Value::Array(a)) => a.first().and_then(Value::as_str).map(str::to_owned),
let python = match (arguments.get("debugLauncherPython"), arguments.get("python")) {
(Some(Value::String(p)), _) | (_, Some(Value::String(p))) => Some(p.clone()),
(_, Some(Value::Array(a))) => a.first().and_then(Value::as_str).map(str::to_owned),
_ => None,
};
if let Some(python) = python {
@@ -79,6 +79,27 @@ describe("run config → launch config", () => {
expect(extra(globalPytest).python).toBe("python3");
});
it("debugpy: uv / poetry run the debuggee in the project's environment", () => {
const debug = (command: string, args: string[]) =>
launchConfigFor(rc({ kind: "python", command, args, debugAdapter: "debugpy" }), "/p")!;
const script = debug("uv", ["run", "python", "main.py", "-v"]);
expect(script.program).toBe("/p/main.py");
expect(script.args).toEqual(["-v"]);
expect(extra(script)).toMatchObject({ python: ["uv", "run", "python"], debugLauncherPython: "python3" });
expect(extra(script).module).toBeUndefined();
const mod = debug("poetry", ["run", "python3", "-m", "app"]);
expect(extra(mod)).toMatchObject({ module: "app", python: ["poetry", "run", "python3"] });
const tool = debug("uv", ["run", "pytest", "-x"]);
expect(extra(tool)).toMatchObject({ module: "pytest", python: ["uv", "run", "python"] });
expect(tool.args).toEqual(["-x"]);
const django = debug("pdm", ["run", "python", "manage.py", "runserver"]);
expect(extra(django)).toMatchObject({ django: true, python: ["pdm", "run", "python"] });
});
it("js-debug: npm scripts run through the package manager, node scripts directly", () => {
const npm = launchConfigFor(
rc({ kind: "npm", command: "pnpm", args: ["run", "dev"], cwd: "web", debugAdapter: "js-debug" }),
+28 -13
View File
@@ -58,15 +58,20 @@ function cargoLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig
};
}
const PYTHON_NAME = /^python[\d.]*(\.exe)?$/;
function pythonFor(rc: RunConfig, cwd: string): string {
const cmd = rc.command;
const name = cmd.split("/").pop() ?? cmd;
if (/^python[\d.]*(\.exe)?$/.test(name)) return resolveCommand(cmd, cwd);
if (PYTHON_NAME.test(name)) return resolveCommand(cmd, cwd);
// A venv tool such as `.venv/bin/pytest`: use the interpreter next to it.
if (cmd.includes("/")) return resolveCommand(cmd.slice(0, cmd.lastIndexOf("/") + 1) + "python", cwd);
return "python3";
}
/** Project managers whose `run` executes in the project's environment (`uv run python …`). */
const PYTHON_RUNNERS = new Set(["uv", "poetry", "pdm", "pipenv", "hatch", "rye"]);
function debugpyLaunch(rc: RunConfig, cwd: string): LaunchConfig {
const out = base(rc, "debugpy", cwd);
const dbg = workflowSettings().debugger;
@@ -78,19 +83,29 @@ function debugpyLaunch(rc: RunConfig, cwd: string): LaunchConfig {
redirectOutput: true,
};
if (dbg.python.gevent) extra.gevent = true;
const name = rc.command.split("/").pop() ?? rc.command;
const isPython = /^python[\d.]*(\.exe)?$/.test(name);
if (!isPython) {
const name = (rc.command.split("/").pop() ?? rc.command).replace(/\.exe$/, "");
let args = rc.args;
let tool: string | null = PYTHON_NAME.test(name) ? null : name;
if (PYTHON_RUNNERS.has(name) && args[0] === "run" && args[1]) {
// The debuggee runs through the manager (`uv run python <debugpy> …`) to get the project's
// environment; the adapter and its launcher need a plain interpreter.
const python = PYTHON_NAME.test(args[1]) ? args[1] : "python";
extra.python = [resolveCommand(rc.command, cwd), "run", python];
extra.debugLauncherPython = "python3";
tool = PYTHON_NAME.test(args[1]) ? null : args[1];
args = args.slice(2);
}
if (tool) {
// A console script (`pytest`, `uvicorn`, …): run it as a module.
extra.module = name.replace(/\.exe$/, "");
out.args = [...rc.args];
} else if (rc.args[0] === "-m" && rc.args[1]) {
extra.module = rc.args[1];
out.args = rc.args.slice(2);
} else if (rc.args[0]) {
out.program = join(cwd, rc.args[0]);
out.args = rc.args.slice(1);
if (rc.args[0].endsWith("manage.py")) extra.django = true;
extra.module = tool;
out.args = [...args];
} else if (args[0] === "-m" && args[1]) {
extra.module = args[1];
out.args = args.slice(2);
} else if (args[0]) {
out.program = join(cwd, args[0]);
out.args = args.slice(1);
if (args[0].endsWith("manage.py")) extra.django = true;
}
out.extraJson = JSON.stringify(extra);
return out;