从程序中运行Python调试会话,而非从控制台

5 投票
1 回答
1865 浏览
提问于 2025-04-17 04:00

我正在写一个简单的Python集成开发环境(IDE),想要添加一些基本的调试功能。我不需要像winpdb那样复杂的所有功能。 我想知道怎么通过文件名启动一个Python程序,并在某一行设置一个断点,这样程序就会运行到那一行然后停下来。 需要注意的是,我不想通过命令行来做这件事,也不想修改源代码(比如插入set_trace之类的)。而且我也不想让程序在第一行就停下来,这样我就得从那里开始调试。我试过用pdb和bdb的各种明显的方法,但我觉得我可能漏掉了什么。

1 个回答

7

根据我所知道的,唯一可行的方法就是在你的开发环境(IDE)中将Python作为一个子进程运行。这样可以避免当前Python解释器的“污染”,使得程序的运行方式更接近于你独立启动它时的情况。如果你在这方面遇到问题,可以检查一下子进程的环境设置。通过这种方式,你可以使用

p = subprocess.Popen(args=[sys.executable, '-m', 'pdb', 'scriptname.py', 'arg1'],
                     stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)

以“调试模式”运行脚本。这会在调试器提示符下启动Python。你需要运行一些调试命令来设置断点,可以这样做:

o,e = p.communicate('break scriptname.py:lineno')

如果一切正常,o应该是Python解释器在设置断点后正常的输出,而e应该是空的。我建议你多尝试一下,并在代码中添加一些检查,以确保断点设置正确。

之后,你可以用以下命令开始运行程序:

p.communicate('continue')

此时,你可能想把输入、输出和错误流连接到你在IDE中嵌入的控制台。你可能需要用事件循环来实现,大致像这样:

while p.returncode is None:
    o,e = p.communicate(console.read())
    console.write(o)
    console.write(e)

你可以把这段代码视为伪代码,因为根据你的控制台具体是怎么工作的,可能需要一些调整才能正确运行。

如果这看起来有点复杂,你可以利用Python的pdbbdb模块的功能来简化这个过程(我猜“Python调试器”和“基本调试器”分别对应这两个模块)。关于如何做到这一点,最好的参考就是pdb模块的源代码。基本上,这两个模块的职责分工是:bdb处理“底层”的调试功能,比如设置断点、停止和重新开始执行;而pdb则是一个包装器,负责用户交互,也就是读取命令和显示输出。

对于你集成在IDE中的调试器,调整pdb模块的行为有两个方面是我能想到的:

  1. 在初始化时自动设置断点,而不需要你手动发送文本命令来做到这一点
  2. 从IDE的控制台接收输入并发送输出

这两个改动通过子类化pdb.Pdb应该很容易实现。你可以创建一个子类,其初始化方法接受一个断点列表作为额外参数:

class MyPDB(pdb.Pdb):
    def __init__(self, breakpoints, completekey='tab',
                 stdin=None, stdout=None, skip=None):
        pdb.Pdb.__init__(self, completekey, stdin, stdout, skip)
        self._breakpoints = breakpoints

实际上设置断点的合适位置是在调试器读取其.pdbrc文件之后,这个过程发生在pdb.Pdb.setup方法中。要进行实际的设置,可以使用从bdb.Bdb继承的set_break方法:

    def setInitialBreakpoints(self):
        _breakpoints = self._breakpoints
        self._breakpoints = None  # to avoid setting breaks twice
        for bp in _breakpoints:
            self.set_break(filename=bp.filename, line=bp.line,
                           temporary=bp.temporary, conditional=bp.conditional,
                           funcname=bp.funcname)

    def setup(self, f, t):
        pdb.Pdb.setup(self, f, t)
        self.setInitialBreakpoints()

这段代码可以处理每个作为命名元组传入的断点。你也可以尝试直接构造bdb.Breakpoint实例,但我不确定这样是否能正常工作,因为bdb.Bdb会维护自己的断点信息。

接下来,你需要为你的模块创建一个新的main方法,使其以pdb的方式运行。在某种程度上,你可以复制pdbmain方法(当然还有if __name__ == '__main__'语句),但你需要增加一些方法来传递额外断点的信息。我建议从你的IDE将断点写入一个临时文件,并将该文件的名称作为第二个参数传递:

tmpfilename = ...
# write breakpoint info
p = subprocess.Popen(args=[sys.executable, '-m', 'mypdb', tmpfilename, ...], ...)
# delete the temporary file

然后在mypdb.main()中,你可以添加类似这样的代码:

def main():
    # code excerpted from pdb.main()
    ...
    del sys.argv[0]

    # add this
    bpfilename = sys.argv[0]
    with open(bpfilename) as f:
        # read breakpoint info
        breakpoints = ...
    del sys.argv[0]
    # back to excerpt from pdb.main()

    sys.path[0] = os.path.dirname(mainpyfile)

    pdb = Pdb(breakpoints) # modified

现在你可以像使用pdb一样使用你的新调试模块,只不过在进程开始之前不需要显式发送break命令。这有个好处,就是如果你的控制台允许,你可以直接将Python子进程的标准输入和输出连接到你的控制台。

撰写回答