Python
xonsh
XonshDay 3

Xonshを快適にするptk(Python Prompt Toolkit)

ptk(Python Prompt Toolkit)とは

Xonshよりはよっぽど有名なライブラリなので説明はいらないかもしれないですが、簡単に言うとpure pythonのreadlineみたいなものです。
readlineに対して、multilineに対応していたり高度な補完や編集を提供していたり、初めて使うと結構驚く便利なライブラリです。
早速余談をすると、確か2年くらい前に自分がXonshを試したときは真っ白の画面に不安定なフォント、激重レスポンスみたいな状態で、コンセプトには感動するけど実用はちょっとなあという感じでした。
日本語版arch wikiを見ると未だにこんなふうに書かれているくらいです。(2017/11/18時点)

xonsh.png

それが最近触ってみたらptkのおかげでデフォルトでfish並の見た目と補完を備えていて、大層たまげたという個人的な体験があったりします。
それくらいptkは今のXonshにとって重要なライブラリだということです。

ptkが何かは全然ちゃんと説明していませんが、まあそんな感じのpython製readlineです。

アウトライン

今回は以下のような感じで説明していきます。

  • 快適なptkの設定とお試し
  • ptkの編集機能
  • Xonshキーバインディングの設定方法
  • キーバインディングを活用した機能の実例
  • ptkの使われている便利なものたち

快適なptkの設定

触る前に快適にしないとストレスが溜まるので、ある程度は最初にやってしまいましょう。知っている人は飛ばして下さい。
突然ですが、Xonshではないソフトウェアをまずは入れて頂きます。

$ sudo pip install ptpython

最初からxonshrcを書き始めるよりはまずは親切なUIのあるツールでどんな設定があるのか試してみましょう。
$ ptpythonすると一見普通のpythonインタラクティブシェルが始まりますが、F2を押して下さい。
右に設定が出てくるので、勘で心地よい感じに設定してみてください。カラーテーマなどはあとで同じ選択肢から選べるので、好きなのをここで試すといいです。
ここで関数を書いたりループを書いたり、pythonシェルがいつものエディタだと思って触ってみて下さい。
行の上を移動したり、検索したり、行頭行末にジャンブしたりコロンからの補完をしてみたり。
vi, emacsあたりを使っている人ならかなり快適なんじゃないでしょうか。
しかも、この補完はjediだったりして結構賢いんです。
口でptkを説明するよりptpythonを触るのが一番ptkがなんなのかよくわかると思います。

では、設定していきましょう。以下は自分の設定です。

.xonshrc
$VI_MODE = False
$XONSH_COLOR_STYLE = 'native'

終了です。この辺はご自身の宗教に合わせて記述して下さい。

ptkの編集機能

さて、さっき編集能力について長々と言いましたが、こんなものではありません。
ヘビーに改造したエディタやさらにはその中でシェルを起動して使っている方も多いかと思いますので、ptkでは満足できないかもしれません。
では、以下のように設定してみて下さい。

.xonshrc
import os

os.environ['EDITOR'] = '/usr/bin/emacsclient'
os.environ['VISUAL'] = '/usr/bin/emacsclient'

突然os.environを使ったXonshらしくないコードが入りましたが、現状のptkの実装(Bufferの実装)であるとこうした方がちゃんと動きます。
ちなみに

$ source ~/.xonshrc

もちゃんとできるので、設定をいじる時は活用して下さい。

では、Xonshのプロンプトにいって、Control-x, Control-eとツーストロークで押してみて下さい。
お気に入りのエディタが起きたんじゃないでしょうか。好きなだけ編集したら、セーブして閉じて下さい。
多分その結果がXonshに入力されています。素晴らしいですね。tmpfileに.pyついてないからハイライトがーと思った人はあとで解決する方法を書くので安心してください。

Xonshキーバインディングの設定方法

キーバインディングのテンプレートを先に示します。

.xonshrc
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection,
                                    EmacsInsertMode, ViInsertMode)


@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    handler = bindings.registry.add_binding
    insert_mode = EmacsInsertMode()
    # insert_mode = ViInsertMode()

    @handler(Keys.ControlX, Keys.ControlE, filter=insert_mode)
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

上から説明すると、Keysの中にはptkで使えるキーを表すものが入っています。
filtersというのは、トリガーが入力された時にキーバインドに設定された機能が起動する際の条件を表すものたちです。複数ライン編集状態の時、領域選択中、InsertModeかどうかなどですね。

events.on_ptk_createというデコレータは実はXonsh的には極めて重要ですが、細かく説明すると一つ記事が書けるので、呪文だと思って下さい。興味がある方はCore Eventsというのを調べるといいです。

handlerはトリガーを管理するためのものです。これが作るデコレータで起動する機能を定義する関数を修飾するように使用します。

次に肝心の機能をキーに配置する例ですが、さりげなく先ほど説明したエディタ編集モードを改善したものを書いておきました。
この関数が受けるeventという引数に色々と入っていますが、一番よく触るのはevent.current_bufferだと思います。
これが現在自分がいる編集領域にあたるprompt_toolkit.Bufferオブジェクトです。
なぜXonshがデフォルトで拡張子をつけてくれないのかちょっと理解に苦しみますが、Buffer.tempfile_suffixをいじればtempfileに拡張子を付与できるので、突然エディタで開かれても正しいモードで表示できるようになります。

キーバインディングと機能の実例

これでよくあるキーバインドを作る分には説明は十分かと思います。
次は、実例としてよくある機能を実装してみたいと思います。

xontribにはfzf-widgetsがありますが、あえて練習としてControl-rを押したらfzfを使って履歴を検索できる機能を作ってみます。

まずはxonshの履歴を全部引きずりだして、必要な部分だけにして重複削除して返す関数を書きます。

import json
from collections import OrderedDict
from operator import itemgetter


def get_history(session_history=None, return_list=False):
    hist_dir = __xonsh_env__['XONSH_DATA_DIR']

    files = [ os.path.join(hist_dir,f) for f in os.listdir(hist_dir)
              if f.startswith('xonsh-') and f.endswith('.json') ]
    file_hist = [ json.load(open(f))['data']['cmds'] for f in files ]
    cmds = [ ( c['inp'].replace('\n', ''), c['ts'][0] )
                 for cmds in file_hist for c in cmds if c]
    cmds.sort(key=itemgetter(1))
    cmds = [ c[0] for c in cmds[::-1] ]
    if session_history:
        cmds.extend(session_history)
    # dedupe
    zip_with_dummy = list(zip(cmds, [0] * len(cmds)))[::-1]
    cmds = list(OrderedDict(zip_with_dummy).keys())[::-1]

    if return_list:
        return cmds
    else:    
        return '\n'.join(cmds)

どこかから拾ってきたものを自分がいじったものです。xonsh.history.json(.JsonHistory)というモジュールも提供されていますが、とろすぎてキーバインドに仕掛けるにはつらいので無理矢理やります。
さて、ついでに説明すると、xonshの履歴はjsonで保存されており実行時間など色々な情報を含んだリッチなものです。
というのも、履歴はちゃんと残さないと後であれが必要となったときにはすでに遅いという思想のもとリッチな履歴を提供しているようです。historyもやろうと思えばぎりぎり一記事書けるくらいには機能が豊富です。(replayとか)

これがあればあとは簡単です。しかけます。

.xonshrc
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection,
                                    EmacsInsertMode, ViInsertMode)


@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    handler = bindings.registry.add_binding
    insert_mode = EmacsInsertMode()
    # insert_mode = ViInsertMode()

    @handler(Keys.ControlX, Keys.ControlE, filter=insert_mode)
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

    @handler(Keys.ControlR, filter=insert_mode)
    def select_history(event):
        sess_history = $(history).split('\n')
        hist = get_history(sess_history)
        selected = $(echo @(hist) | fzf)
        event.current_buffer.insert_text(selected.strip())

subprocessとpythonが入り乱れてかなりXonshしている感じがします。

おまけ: ptkの使われている便利なものたち

いろいろあるようですが、おすすめは以下のものです。

  • ptpdb: pdbが便利になります。pudbみたいなバカっぽい画面から解放されつつ同等の機能です
  • ipython: 新しいやつからptkに乗り換えたようです
  • Xonsh