Emacs:Python 调试,发送代码至任意 buffer

Emacs:Python 调试,发送代码至任意 buffer

我现在工作的一部分是做 OpenERP 开发,经常需要调试 Python 代码,这篇博客总结了一
些在 Emacs 中调试 Python 的技巧。理论上,这些技巧稍微修改一下也能用于其他动态语
言的调试。

1 在 PDB 中进入 Python 交互式解释器

用 PDB 调试的方法有好几种,我一般就是在需要做断点的地方插入一行(我实际用的是
IPDB —— PDB 的加强版):

import ipdb; ipdb.set_trace()

当然,由于经常用,我在网上找了个 Elisp 函数来避免手敲之苦:

1:  (defun python-add-breakpoint ()
2:    "Add a break point"
3:    (interactive)
4:    (newline-and-indent)
5:    (insert "import ipdb; ipdb.set_trace()")
6:    (highlight-lines-matching-regexp "^[ ]*import ipdb; ipdb.set_trace()"))
7:  
8:  (define-key python-mode-map (kbd "C-c C-b") 'python-add-breakpoint)

这个函数会插入一行上面提到的 Python 代码,并把其高亮显示。

然后我在 Emacs 中运行 shell,在 shell 中运行 Python 程序,当程序运行到断点的时
候,就会自动进入 PDB。

比如,我有 ~/test.py 这个文件,内容为:

 1:  #!/usr/bin/env python
 2:  # -*- encoding: utf-8 -*-
 3:  #
 4:  # Author: 任文山 (Ren Wenshan)
 5:  
 6:  
 7:  def f(a, b):
 8:      return "%d - %d" % (a, b)
 9:  
10:  if __name__ == "__main__":
11:      for i in xrange(9, 1, -1):
12:          if i == 6: import ipdb; ipdb.set_trace()
13:          print f(i, i - 2)

我在第12行设置了一个断点,按 M-x shell 打开一个 shell,在里面运行这个文件:

cd ~ && python test.py

然后就会在 i 等于6的时候进入 PDB:

 1:  wenshan@debian-vm-home:~$ cd ~ && python test.py
 2:  9 - 7
 3:  8 - 6
 4:  7 - 5
 5:  > /home/wenshan/test.py(13)<module>()
 6:       11     for i in xrange(9, 1, -1):
 7:       12         if i == 6: import ipdb; ipdb.set_trace()
 8:  ---> 13         print f(i, i - 2)
 9:  
10:  ipdb>

这个时候,如果想重新定义 f() 这个方法,可以直接在 PDB 里写,但是很不方便,我们
可以通过在 PDB 中执行以下代码进入 Python 的交互式解释器:

!import code; code.interact(local=vars())

同样,作为经常用到的一个功能,我写了一个 Elisp 函数来帮忙:

1:  (defun python-interactive ()
2:    "Enter the interactive Python environment"
3:    (interactive)
4:    (progn
5:      (insert "!import code; code.interact(local=vars())")
6:      (move-end-of-line 1)
7:      (comint-send-input)))
8:  
9:  (global-set-key (kbd "C-c i") 'python-interactive)

这样,在 PDB 中按 C-c i 就进入到交互式解释器了。

现在就可以在交互式解释器中写代码了。

但是,还是不够方便,因为 Python 对缩进要求得非常严格(我又爱又恨的特点),往往
写错一点儿就得重新来过。于是,我想直接在正常的 Python buffer 中写 Python 代码,
然后发送到这个交互式解释器执行。

2 isend 的安装与配置

Emacs 自带的 python.el 也有发送代码的功能,但不能自己指定目的 buffer。而
Python 社区维护的 python-mode 好像能发送代码到任意 buffer,但是我目前的设定都
是跟 python.el 绑在一起的,换起来很麻烦。

Google 一番之后,找到了 isend-mode.el,这是一个比较通用的代码发送插件(也可以
用于 shell、Perl、Ruby 等)。

从 Github 上把 isend-mode 克隆下来:

git clone git://github.com/ffevotte/isend-mode.el.git

然后打开里面的 isend-mode.el 这个文件,找到下面这两行:

1:  (defvar isend--command-buffer)
2:  (make-variable-buffer-local 'isend--command-buffer)

把第2行删掉,这样 `isend–command-buffer’ 就变成了一个全局变量,后面定义
advice 的时候会用到(见下一节)。

最后,添加以下代码到你的 Emacs 配置文件:

1:  ;; Python send code
2:  (add-to-list 'load-path "~/.emacs.d/dotEmacs/isend-mode.el")
3:  (require 'isend)
4:  (setq isend-skip-empty-lines nil)
5:  (setq isend-strip-empty-lines nil)
6:  (setq isend-delete-indentation t)
7:  (setq isend-end-with-empty-line t)

请把 “~/.emacs.d/dotEmacs/isend-mode.el” 替换成 isend-mode 在你机器上的路
径。

现在在 Emacs 中,按 M-x isend-associate 并输入目的 buffer,比如 *shell*
然后选中想要发送的代码段,按 C-Enter 就会把这段代码发送到 *shell* 中的
Python 解释器了。

3 定义 advice

还有一个问题是 isend 只会把 Python 代码发送到指定的 buffer,但并不会自动执行之。

自己写一个 advice (Emacs 中的 advice 可以在指定的函数执行 前/后 执行一些命
令),用 isend 发送代码到指定 buffer 之后,跳到那个 buffer,执行代码,再跳回来:

1:  (defadvice isend-send (after advice-run-code-sent activate compile)
2:    "Execute whatever sent to the (Python) buffer"
3:    (interactive)
4:    (let ((old-buf (buffer-name)))
5:      (progn
6:        (switch-to-buffer isend--command-buffer)
7:        (goto-char (point-max))
8:        (comint-send-input)
9:        (switch-to-buffer old-buf))))

这里用到了 `isend–command-buffer’ 这个变量。 因为上面这个 advice 是定义
在 init.el 里,这是我之前把 `isend–command-buffer’ 变成全局变量的原因。

4 测试

在第一节中代码执行到这里,在 i 等于 6 的时候,进入 PDB:

1:  9 - 7
2:  8 - 6
3:  7 - 5
4:  > /home/wenshan/test.py(13)<module>()
5:       11     for i in xrange(9, 1, -1):
6:       12         if i == 6: import ipdb; ipdb.set_trace()
7:  ---> 13         print f(i, i - 2)
8:  
9:  ipdb>

C-c i 进入 Python 交互式解释器:

1:       12         if i == 6: import ipdb; ipdb.set_trace()
2:  ---> 13         print f(i, i - 2)
3:  
4:  ipdb> !import code; code.interact(local=vars())
5:  Python 2.7.3 (default, Jan  2 2013, 16:53:07)
6:  [GCC 4.7.2] on linux2
7:  Type "help", "copyright", "credits" or "license" for more information.
8:  (InteractiveConsole)
9:  In :

然后,先把 isend 跟 shell 绑定: M-x isend-associate RET *shell* RET

现在,在任意 buffer 打开 isend-mode: M-x isend-mode , 并写点儿 Python 代码,
比如重新定义 f() 方法:

1:  def f(a, b):
2:      print "汝不知夫螳螂乎,怒其臂以当履带。 ——《庄子·查水表》"

选中新的 f(),按 C-RET ,把这段代码发送到 *shell* 中的 Python 解释器:

 1:  ipdb> !import code; code.interact(local=vars())
 2:  Python 2.7.3 (default, Jan  2 2013, 16:53:07)
 3:  [GCC 4.7.2] on linux2
 4:  Type "help", "copyright", "credits" or "license" for more information.
 5:  (InteractiveConsole)
 6:  In : def f(a, b):
 7:      print "汝不知夫螳螂乎,怒其臂以当履带。 ——《庄子·查水表》"
 8:  
 9:  
10:  ...: ...: In : In :

这个时候,如果在 *shell* 中运行 f(1, 2),你会发现原来的 f() 已经被覆盖了:

1:  In : f(1, 2)
2:  汝不知夫螳螂乎,怒其臂以当履带。 ——《庄子·查水表》

*shell* 中按 C-d 退出 Python 交互式解释器,退回到 PDB 中,然后输入 c
续执行程序,原来的 f() 依然是被覆盖的:

1:  In :
2:  ipdb> c
3:  汝不知夫螳螂乎,怒其臂以当履带。 ——《庄子·查水表》
4:  .
5:  .
6:  .

结束。

Happy Hacking!


Date: 2013-06-04

Author: 任文山 (Ren Wenshan)

Org version 7.9.3d with Emacs version 24

Validate XHTML 1.0

One thought on “Emacs:Python 调试,发送代码至任意 buffer

  1. Pingback: 链接推荐 —— 2013年6月 | 肉山博客 (Wenshan's Blog)