Simulate interactive python session

なんかデジャブってる気がしてならないんだけれど、検索しても自分は書いてないようであるし…。

このネタは基本的には以下2種類の人たちのためのもの:

  1. Sphinx などの pygments ベースなドキュメントに、python 対話セッションでの例をたくさん書きたい
  2. docstring として doctest 用の例を作ることが多い

「リアルなコンソール」が履歴を簡単に取れるなら別にそんなに大変じゃないとも言えるけれど、対話セッションで「試行錯誤する」と間違いもガンガン増えちゃっていくのでね、「何度も繰り返して、いい結果だけ残す」という作業には向かない。要するにドキュメント作成という目標がある場合は「失敗したら先頭からやり直し」たいことが多いのだ、てこと。

これなー、ぜってー大昔にやったはずなのな、個人的には。でもめでたく紛失してて、ここんとこのドキュメント作成でも「うー、面倒だなぁ」思うておったの。答えはこれ:

interact.py
 1 # original from https://stackoverflow.com/questions/23809327/simulate-interactive-python-session
 2 from __future__ import print_function
 3 import code
 4 import fileinput
 5 import sys
 6 
 7 
 8 def show(input):
 9     lines = iter(input)
10 
11     def readline(prompt):
12         try:
13             command = next(lines).rstrip('\n')
14         except StopIteration:
15             raise EOFError()
16         print(prompt, command, sep='')
17         return command
18 
19     # 1. if you don't pass banner, you will see extra "(InteractiveConsole)".
20     # 2. banner will be written to sys.stderr.
21     #    note that even if you're using MSDOS, you can merge stderr to stdout.
22     #    (ref. https://stackoverflow.com/questions/1420965/redirect-windows-cmd-stdout-and-stderr-to-a-single-file)
23     cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
24     banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt)
25 
26     code.interact(banner=banner, readfunc=readline)
27 
28 
29 if __name__=="__main__":
30     import argparse
31     parser = argparse.ArgumentParser()
32     parser.add_argument("-m", help="merge stderr to stdout", action="store_true")
33     args = parser.parse_args()
34     if args.m:
35         sys.stderr = sys.stdout  # (*)
36 
37     show(fileinput.input(args._get_args()))

((*) 部分の stderr = stdout はね、リダイレクトする際に順序を維持出来ないから必要なの。行儀悪くてやなんだけれど。)

てわけで:

 1 me@host: ~$ echo "2 + 3" | python interact.py -m | sed 's@^@    @'
 2     Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
 3     Type "help", "copyright", "credits" or "license" for more information.
 4     >>> 2 + 3
 5     5
 6 
 7 me@host: ~$ echo "a = b" | python interact.py -m | sed 's@^@    @'
 8     Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
 9     Type "help", "copyright", "credits" or "license" for more information.
10     >>> a = b
11     Traceback (most recent call last):
12       File "<console>", line 1, in <module>
13     NameError: name 'b' is not defined
14 
15 me@host: ~$ cat zzzzz
16 a = list(range(5))
17 a
18 b = list(range(5, -1, -1))
19 b
20 list(zip(a, b))
21 
22 me@host: ~$ python interact.py -m < zzzzz | sed 's@^@    @'
23     Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
24     Type "help", "copyright", "credits" or "license" for more information.
25     >>> a = list(range(5))
26     >>> a
27     [0, 1, 2, 3, 4]
28     >>> b = list(range(5, -1, -1))
29     >>> b
30     [5, 4, 3, 2, 1, 0]
31     >>> list(zip(a, b))
32     [(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]
33     >>>
34 
35 me@host: ~$ echo -e '.. code-block:: pycon\n' ; (python interact.py -m < zzzzz | sed 's@^@    @')
36 .. code-block:: pycon
37 
38     Python 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)] on win32
39     Type "help", "copyright", "credits" or "license" for more information.
40     >>> a = list(range(5))
41     >>> a
42     [0, 1, 2, 3, 4]
43     >>> b = list(range(5, -1, -1))
44     >>> b
45     [5, 4, 3, 2, 1, 0]
46     >>> list(zip(a, b))
47     [(0, 5), (1, 4), (2, 3), (3, 2), (4, 1)]
48     >>>

2.7.9 を例にしてるけどさ、「同じもので python 3.x ならこうだ」てのもすぐに出来るでしょ、このやり方なら。

上の例で sed で結果を加工しとるでしょ。色んな加工が必要なのよね。例えば相手が WordPress に貼り付けることだったりすると、大なり記号・小なり記号を変換しないといけなかったりとかね。こういった加工があるのにさらに「間違いを繰り返しながら頑張った長い対話結果を、コマンドウィンドウを目一杯スクロールしつつコピー」(してから不要な箇所を手作業で取り除く)なんてのはさ、効率悪いったらありゃしない、ってわけだ。

2017/07/01 追記: 続編