mcpField
P
python-debugpy

python-debugpy

github.com/nousresearch开发258.1k400 次安装今天更新
代码仓库

关于 python-debugpy

面向 Python 开发者的调试技能,涵盖本地 pdb 流程,以及使用 debugpy 远程调试或连接运行中的进程,并说明测试、异常和子进程的排查方法。

主要功能

  1. 01使用 breakpoint() 和 pdb 本地调试
  2. 02无需修改代码即可启动 pdb
  3. 03用 debugpy 连接运行中进程
  4. 04检查异常现场与子进程

适用场景

  1. 01追查失败测试中的局部变量
  2. 02无需重启即可排查长时间运行的进程
  3. 03检查异常发生时的状态
  4. 04调试 Python 子进程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nousresearch/hermes-agent --skill python-debugpy

安装后在 Claude Code 中输入/python-debugpy确认已加载。

详细介绍

Python 调试器(pdb + debugpy)

概述

根据情况选择以下三种工具:

工具使用场景
breakpoint() + pdb本地交互调试,最简单。在源代码中添加 breakpoint(),正常运行,即可在该行进入 REPL。
python -m pdb在 pdb 下启动现有脚本,无需修改源代码。适合快速探查。
debugpy远程/无头调试/“附加到已运行的进程”。使用 DAP 通信,可通过终端编写脚本,适用于长时间运行的进程(网关、守护进程、PTY 子进程)。

从 breakpoint() 开始。 这是最省事的有效方法。

何时使用

  • 测试失败,但回溯信息无法说明某个值为何不正确
  • 需要单步执行函数,并观察集合的变化
  • 长时间运行的进程(hermes gateway、tui_gateway)行为异常,且无法重启
  • 事后分析:类似生产环境的代码中触发了异常,你想检查崩溃位置的局部变量
  • 子进程/子进程(Python _SlashWorker、PTY 桥接工作进程)才是真正出错的位置

不适用于: print()/logging.debug 能在一分钟内解决的问题,或 pytest -vv --tb=long --showlocals 已能揭示的问题。

pdb 快速参考

在任意 pdb 提示符((Pdb))中:

命令操作
h / h cmd帮助
n执行下一行(单步跳过)
s单步进入
r从当前函数返回
c继续
unt N继续执行,直到第 N 行
j N跳转到第 N 行(仅限同一函数)
l / ll列出当前行附近的源代码/完整函数
w查看位置(堆栈跟踪)
u / d在堆栈中向上/向下移动
a打印当前函数的参数
p expr / pp expr打印/美化打印表达式
display expr每次暂停时自动打印表达式
b file:line设置断点
b func在函数入口处设置断点
b file:line, cond设置条件断点
cl N清除断点 N
tbreak file:line设置一次性断点
!stmt执行任意 Python 语句(包括赋值)
interact在当前作用域进入完整的 Python REPL(按 Ctrl+D 退出)
q退出

interact 命令功能最强大——你可以导入任意内容、检查复杂对象,甚至调用会修改状态的方法。默认情况下局部变量为只读;要修改变量,请在 (Pdb) 提示符中使用 !x = 42。

配方 1:本地断点

最简单的方法。编辑文件:

def compute(x, y):
    result = some_helper(x)
    breakpoint()           # <-- drops into pdb here
    return result + y

正常运行代码。程序会停在 breakpoint() 所在行,你可以完整访问局部变量。

提交前别忘了移除 breakpoint()。 使用 git diff 或提交前 grep 检查:

rg -n 'breakpoint\(\)' --type py

配方 2:在 pdb 下启动脚本(无需修改源代码)

python -m pdb path/to/script.py arg1 arg2
# Lands at first line of script
(Pdb) b path/to/script.py:42
(Pdb) c

配方 3:调试 pytest 测试

使用 terminal 和规范的运行器进行非交互式诊断:

# Show locals in tracebacks without pdb:
scripts/run_tests.sh tests/path/to/test_file.py --showlocals --tb=long

scripts/run_tests.sh 会在独立的子进程中运行每个文件,因此 --pdb 或 --trace 无法在其中提供交互式提示符。若只需要交互式调试器,请使用 配方 5 中准备的独立开发/测试解释器(绝不要使用生产环境的生成版本):

.venv/bin/python -m pytest tests/foo_test.py::test_bar --pdb

这会绕过 hermetic-env 保证——用于调试没有问题,但推送前请在包装器下重新运行并确认。

配方 4:对任意异常进行事后分析

import pdb, sys
try:
    run_the_thing()
except Exception:
    pdb.post_mortem(sys.exc_info()[2])

也可以包装整个脚本:

python -m pdb -c continue script.py
# When it crashes, pdb catches it and you're in the frame of the exception

或者在 REPL/jupyter 中设置全局钩子:

import sys
def excepthook(etype, value, tb):
    import pdb; pdb.post_mortem(tb)
sys.excepthook = excepthook

配方 5:使用 debugpy 进行远程调试(附加到运行中的进程)

适用于长时间运行的进程:Hermes gateway、tui_gateway、守护进程,或已经出现异常且无法正常重启的进程。

设置

对于 Hermes,请使用单独的开发检出和数据主目录,而不是正在运行的生产环境。按照 PM 开发工作流 操作并激活该检出 — PowerShell:. .\activate.ps1。声明的 dev 额外依赖包含 debugpy,但 PM 激活不会同步它(all 不包含它)。 通过 terminal,使用准备好的检出中的 Python 构建一个全新的、由调用方拥有的调试/测试环境:

source ./activate
python -m pm.build_env --source . --out .venv --group dev --group test
.venv/bin/python -c "import debugpy; print(debugpy.__file__)"

输出路径必须尚不存在。停止其中运行的进程,并且仅在重建前有意删除这个一次性环境。为调试目标保留相同的隔离 HERMES_HOME。.venv/bin/python 是这个明确构建的调试环境,并非猜测出来的应用虚拟环境;以下模式都通过它运行。不要将 debugpy 添加到正在运行的生产环境;只有使用已准备好的调试目标,或在开发环境中安排重启,才能在那里复现问题。

模式 A:修改源码 — 进程启动时等待调试器

在入口点靠上位置(或要调试的函数内部)添加:

import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy listening on 5678, waiting for client...", flush=True)
debugpy.wait_for_client()
debugpy.breakpoint()       # optional: pause immediately once attached

启动进程;它会在 wait_for_client() 处阻塞。

模式 B:不修改源码 — 使用 -m debugpy 启动
.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --wait-for-client your_script.py arg1

模块入口的等效方式:

.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m your.module
模式 C:附加到已运行的进程

需要 PID,并且目标环境中必须预先安装 debugpy:

.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --pid <pid>
# debugpy injects itself into the process. Then attach a client as below.

某些内核/安全配置会阻止基于 ptrace 的注入(/proc/sys/kernel/yama/ptrace_scope)。可通过以下方式修复:

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
从终端连接客户端

最简单的终端侧 DAP 客户端是 VS Code CLI 或一个小型脚本。在 Hermes 内部,有两个实用选项:

选项 1:debugpy 自带的 CLI REPL — 这不是官方功能,而是一个小型 DAP 客户端脚本:

# ~/.hermes/cache/scratch/dap_client.py
import socket, json, itertools, time, sys

HOST, PORT = "127.0.0.1", 5678
s = socket.create_connection((HOST, PORT))
seq = itertools.count(1)

def send(msg):
    msg["seq"] = next(seq)
    body = json.dumps(msg).encode()
    s.sendall(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)

def recv():
    header = b""
    while b"\r\n\r\n" not in header:
        header += s.recv(1)
    length = int(header.decode().split("Content-Length:")[1].split("\r\n")[0].strip())
    body = b""
    while len(body) < length:
        body += s.recv(length - len(body))
    return json.loads(body)

send({"type": "request", "command": "initialize", "arguments": {"adapterID": "python"}})
print(recv())
send({"type": "request", "command": "attach", "arguments": {}})
print(recv())
send({"type": "request", "command": "setBreakpoints",
      "arguments": {"source": {"path": sys.argv[1]},
                    "breakpoints": [{"line": int(sys.argv[2])}]}})
print(recv())
send({"type": "request", "command": "configurationDone"})
# ... loop reading events and sending continue/stepIn/etc.

这适合一次性自动化任务,但作为交互式用户体验会很难用。

选项 2:从 VS Code / Cursor / Zed 附加 — 如果用户已经打开了其中一个,可以添加 launch.json:

{
  "name": "Attach to Hermes",
  "type": "debugpy",
  "request": "attach",
  "connect": { "host": "127.0.0.1", "port": 5678 },
  "justMyCode": false,
  "pathMappings": [
    { "localRoot": "${workspaceFolder}", "remoteRoot": "<hermes-agent-repo>" }
  ]
}

选项 3:放弃 DAP,改用 remote-pdb — 对终端 Agent 来说,这通常才是你真正想要的方式:

对于独立维护的 Python 项目,请在该项目的开发依赖中声明 remote-pdb,并通过项目的包管理器准备其调试环境。这不是 Hermes SDK 安装方法。对于 Hermes,优先使用已声明的 debugpy 依赖;以下 remote-pdb 示例要求另行声明并全新构建调试环境,绝不能在所选应用版本中直接通过 pip 安装。

在代码中:

from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444)   # blocks until connection

然后在终端中:

nc 127.0.0.1 4444
# You get a (Pdb) prompt exactly as if debugging locally.

当 debugpy 的 DAP 协议功能过于繁杂时,remote-pdb 是最简洁、最适合 Agent 的选择。只有在确实需要 IDE 集成时才使用 debugpy。

调试 Hermes 专属进程

测试

参见 Recipe 3。包装器会捕获子进程输出,因此如需交互式 pdb,请直接运行 pytest。

run_agent.py / CLI — 单次运行

在准备好的调试检出中,于可疑代码行附近添加 breakpoint(), 然后运行 python hermes。暂停时,控制权会返回到终端。

tui_gateway 子进程(由 hermes --tui 启动)

该网关作为 Node TUI 的子进程运行。选项:

A. 修改网关源码:

# tui_gateway/server.py near the top of serve()
import debugpy
debugpy.listen(("127.0.0.1", 5678))
debugpy.wait_for_client()

从准备好的调试检出中启动 python hermes --tui。TUI 会看起来像是卡住了(其后端正在等待)。附加客户端;执行会在你输入 continue 后恢复。请先检查子进程的解释器和导入情况,再假设它继承了调试环境。

B. 在特定处理器中使用 remote-pdb:

from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444)   # in the RPC handler you want to trap

在 TUI 中触发匹配的斜杠命令,然后在另一个终端中运行 nc 127.0.0.1 4444。

_SlashWorker 子进程

使用相同模式 — 在 worker 的 exec 路径中通过 set_trace() 使用 remote-pdb。该 worker 会在多条斜杠命令间持续运行,因此第一次触发时会阻塞,直到你连接;之后的斜杠命令会正常执行,除非你重新启用断点。

网关(gateway/run.py)

长期运行。可在处理器处使用 remote-pdb,或者如果你反正要重启网关,也可以使用带 --wait-for-client 的 debugpy。

常见陷阱

  1. 在并行运行器或捕获输出的运行器中使用 pdb,会悄无声息地失效。 你看不到提示符,测试只会一直挂起(pytest-xdist 和 scripts/run_tests.sh 捕获每个文件的子进程输出时都如此)。要进行交互式调试,请直接对单个文件运行 pytest。

  2. 在 CI / 非 TTY 环境中使用 breakpoint() 会让进程挂起。 在本地使用是安全的;切勿提交。添加一个 pre-commit grep 作为安全网。

  3. PYTHONBREAKPOINT=0 会禁用所有 breakpoint() 调用。如果断点没有触发,请检查环境变量:

    echo $PYTHONBREAKPOINT
    
  4. 只有同时调用 wait_for_client() 时,debugpy.listen 才会阻塞。 如果不调用,程序会继续执行,可能会在客户端连接之前触发第一个断点。

  5. 在强化安全的内核上,附加到 PID 会失败。 ptrace_scope=1(Ubuntu 默认值)只允许同一用户对其子进程执行 ptrace。解决方法:echo 0 > /proc/sys/kernel/yama/ptrace_scope(需要 root),或者从一开始就在 debugpy 下启动。

  6. 线程。 pdb 只能调试当前线程。对于多线程代码,请使用 debugpy(支持线程的 DAP),或为每个线程设置 threading.settrace()。

  7. asyncio。 pdb 可用于协程,但在 pdb 中使用 await 需要 Python 3.13+,或者在较旧版本中从 interact 模式使用 await。对于 3.11/3.12,可使用 asyncio.run_coroutine_threadsafe 技巧,或通过 asyncio.ensure_future 使用基于 !stmt 的 await。

  8. scripts/run_tests.sh 会剥离凭据并设置 HOME=<tmpdir>。 如果你的 bug 依赖用户配置或真实 API 密钥,就无法在这个包装脚本下复现。先用原始 pytest 调试并复现,然后再通过包装脚本重新确认。

  9. 分叉 / 多进程。 pdb 不会跟随分叉。每个子进程都需要自己的 breakpoint() 或 set_trace()。对于 Hermes 子 Agent,请一次调试一个进程。

验证清单

  • 在独立构建的调试环境中确认:.venv/bin/python -c "import debugpy; print(debugpy.__version__); print(debugpy.__file__)"
  • 对于远程调试,确认端口确实正在监听:ss -tlnp | grep 5678
  • 确认第一个断点确实触发(如果没有,可能是设置了 PYTHONBREAKPOINT=0、你正在使用并行 / 输出捕获运行器,或者程序在附加之前就已执行完毕)
  • where / w 显示预期的调用栈
  • 调试后清理:已提交的代码中没有遗留的 breakpoint() / set_trace()
    rg -n 'breakpoint\(\)|set_trace\(|debugpy\.listen' --type py
    

一次性方案

“为什么这个 dict 缺少一个键?”

# add above the KeyError site
breakpoint()
# then in pdb:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w                # how did we get here

“这个测试单独运行时通过,但在测试套件中失败。”

scripts/run_tests.sh tests/the_test.py   # confirm it fails under the isolated runner first
# For interactive debugging, or if it only fails WITH other tests, use the
# independent development/test interpreter prepared in Recipe 5:
.venv/bin/python -m pytest tests/ -x --pdb
# Now it pdb-traps at the exact failing test after state accumulated.

“我的异步处理器死锁了。”

# Add at handler entry
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)

触发处理器。运行 nc 127.0.0.1 4444,然后运行 w 查看挂起的栈帧,运行 !import asyncio; asyncio.all_tasks() 查看还有哪些任务待处理。

“对 Ink 子进程 / 子进程中的崩溃进行事后分析。”

PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py
# On crash, pdb lands at the frame of the exception with full locals

评论

支持 Markdown