py-isortの実装を探る
(use-package py-isort :ensure t)
Pythonのimportの規則
- Imports are always put at the top of the file, just after any module
comments and docstrings, and before module globals and constants.
Imports should be grouped in the following order:
1. Standard library imports.
2. Related third party imports.
3. Local application/library specific imports.
You should put a blank line between each group of imports.
(https://github.com/python/peps/blob/f5ce32302fd799022ce8f349b46b1e7d0314286f/pep-0008.txt#L355-L357 から引用)
importのグループ化の順序に関する規則性
- 標準ライブラリのインポート
- 関連するサードパーティのインポート
- ローカルアプリケーション/ライブラリ固有のインポート
isortとは
- Pythonのサードパーティパッケージ (標準ではない)
- `import …文` もしくは `from … import …文` を並び替えてくれる
isortの実行例
https://camo.githubusercontent.com/841a7830ea6e8d363a53214917144b259685cc15/68747470733a2f2f7261772e6769746875622e636f6d2f74696d6f74687963726f736c65792f69736f72742f646576656c6f702f6578616d706c652e676966
(https://github.com/timothycrosley/isort から)
isortのインストールと主な使い方
- インストール
https://github.com/timothycrosley/isort#installing-isort - 使い方
https://github.com/timothycrosley/isort#using-isort
py-isort
py-isortとは
- isortをEmacsから使うためのEmacsのサードパーティパッケージ
- https://github.com/paetzke/py-isort.el
- 180行しかない
- `py-isort-region` や `py-isort-buffer` が提供されている
py-isort-regionとは
- regionを指定してisortを実行する関数
- region内のimport文がいい感じにsortされる
ここからだいたいEmacs Lispでの実装の話
py-isort-region
(defun py-isort-region ()
"Uses the \"isort\" tool to reformat the current region."
(interactive)
(py-isort--call t))
- `py-isort–call` を呼び出し
py-isort–call
(defun py-isort--call (only-on-region)
(py-isort-bf--apply-executable-to-buffer "isort"
'py-isort--call-executable
only-on-region
"py"))
- `only-on-region` には `t` が渡されてくる.
- `py-isort-bf–apply-executable-to-buffer` を呼び出し
- `py-isort–call-executable` のシンボルを渡している
py-isort–call-executable
(defun py-isort--call-executable (errbuf file)
(let ((default-directory (py-isort--find-settings-path)))
(zerop (apply 'call-process "isort" nil errbuf nil
(append `(" " , file, " ",
(concat "--settings-path=" default-directory))
py-isort-options)))))
- `call-process` で外部プログラム(今回の場合はisort)を同期的に呼び出し
- `file` には一時ファイルのパスが渡される
- 設定ファイルの場所をカレントディレクトリに設定
isort 一時ファイルへのパス --settings-path=設定ファイルへのパス [その他の設定]
つまり実際に実行されるコマンドは次の通りになる。
(defcustom py-isort-options nil
"Options used for isort."
:group 'py-isort
:type '(repeat (string :tag "option")))
その他の設定は `py-isort-options` にあらかじめ値を設定することで動作をカスタマイズできる。
py-isort–find-settings-path
(defun py-isort--find-settings-path ()
(expand-file-name
(or (locate-dominating-file buffer-file-name ".isort.cfg")
(file-name-directory buffer-file-name))))
- isortの設定ファイルのパスを返す
- `expand-file-name` はfilename を絶対パス名に変換するEmacsの標準関数
(expand-file-name "foo")
=> "/xcssun/users/rms/lewis/foo"
例えばこんな感じ。
(http://flex.phys.tohoku.ac.jp/texi/eljman/eljman_158.html から引用)
- `locate-dominating-file`
ディレクトリツリーを親方向に辿って `name` で指定されたファイルやディレクトリを探し出す関数 - `(locate-dominating-file buffer-file-name ".isort.cfg")`
親方向に `.isort.cfg` という名称のisortの設定ファイルを探している - `file-name-directory`
ファイルが入っているディレクトリまでのパスを返す. - `buffer-file-name`
現在のバッファのファイルパスが入っている
py-isort-bf–apply-executable-to-buffer
py-isortではこの関数が中心的な役割を果たしている。
(defun py-isort-bf--apply-executable-to-buffer (executable-name
executable-call
only-on-region
file-extension)
"Formats the current buffer according to the executable"
(when (not (executable-find executable-name))
(error (format "%s command not found." executable-name)))
`executable-find` でisortコマンドを探している。なければerrorして終了。
(let ((tmpfile (make-temp-file executable-name nil (concat "." file-extension)))
一時ファイルを作成している。file-extentionには `"py"` が渡されてくるので拡張子は `.py` となる。
(patchbuf (get-buffer-create (format "*%s patch*" executable-name)))
(errbuf (get-buffer-create (format "*%s Errors*" executable-name)))
処理に必要なバッファを作成する。
変数名 | 用途 |
---|---|
patchbuf | diffの出力を受けるためのバッファ |
errbuf | isortコマンドのエラー出力を受けるためのバッファ |
(coding-system-for-read buffer-file-coding-system)
(coding-system-for-write buffer-file-coding-system))
対象ファイルのコーディングシステムを取得して変数に保持する。
(with-current-buffer errbuf
(setq buffer-read-only nil)
(erase-buffer))
(with-current-buffer patchbuf
(erase-buffer))
エラーバッファおよびパッチバッファを初期化する。
(if (and only-on-region (use-region-p))
(write-region (region-beginning) (region-end) tmpfile)
(write-region nil nil tmpfile))
`(use-region-p)` はリージョン選択時にtになる。
実行関数 | リージョン | 対象となる範囲 | 意味合い |
---|---|---|---|
py-isort-region | 指定あり | リージョン | 選択したリージョンに対してisortを実行 |
py-isort-region | 指定なし | バッファ | リージョンが選択されていないのでバッファ全体に対してisortを実行 |
py-isort-buffer | 指定あり | バッファ | 選択したリージョンを無視しバッファ全体に対してisortを実行 |
py-isort-buffer | 指定なし | バッファ | バッファに対してisortを実行 |
(if (funcall executable-call errbuf tmpfile)
`executable-call` は `py-isort–call-executable` のシンボル。
(if (zerop (call-process-region (point-min) (point-max) "diff" nil
patchbuf nil "-n" "-" tmpfile))
- diffを実行
- 対象のバッファ全体をdiffコマンドの標準入力に渡す
- 対象のバッファ全体とtmpfileを `diff -n` で差分をとってpatchbufに出力している。
-n --rcs Output an RCS format diff.
`-n` はRCS形式のフォーマットである。
(`diff –help`の一部を抜粋)
d1 1
a1 1
from __future__ import absolute_import
d4 5
d11 7
a17 3
from my_lib import Object, Object2, Object3
from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9,
lib10, lib11, lib12, lib13, lib14, lib15)
patchbufには次のようなデータが出力される。
RCSとはRevision Control Systemのこと。RCSの解説はここではしない。
RCSについてはGNUのRCSのページから情報を辿れる。
(progn
(kill-buffer errbuf)
(message (format "Buffer is already %sed" executable-name)))
diffコマンドが0を返す場合は差分がないためこの時点でerrbufを削除して、メッセージを表示し終了する。
(if only-on-region
(py-isort-bf--replace-region tmpfile)
(py-isort-bf--apply-rcs-patch patchbuf))
-
only-on-regionがnon nil
リージョンの範囲内のみを書き換える必要があるので
差分ではなくtmpfileの内容で置き換える(2.7.6)。 -
only-on-regionがnil
バッファ全体に対して適応するため
先ほどdiffコマンドで生成した内容でpatchを当てる(2.7.7)。
(kill-buffer errbuf)
(message (format "Applied %s" executable-name)))
(error (format "Could not apply %s. Check *%s Errors* for details"
executable-name executable-name)))
(kill-buffer patchbuf)
(delete-file tmpfile)))
後始末をする。
- patchbufを削除
- 一時ファイルを削除。
py-isort-bf–replace-region
(defun py-isort-bf--replace-region (filename)
(delete-region (region-beginning) (region-end))
(insert-file-contents filename))
- 単純にregionの範囲内を全て消して、filenameで指定したファイルの中身をinsertしている。
py-isort-bf–apply-rcs-patch
(defun py-isort-bf--apply-rcs-patch (patch-buffer)
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer."
(let ((target-buffer (current-buffer))
(line-offset 0))
(save-excursion
(with-current-buffer patch-buffer
(goto-char (point-min))
(while (not (eobp))
- RCS形式のdiffを自力でパッチする
- `(eobp)` はポイントがバッファの最後にある場合 `t` を返す。
http://flex.phys.tohoku.ac.jp/texi/eljman/eljman_196.html
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
(error "invalid rcs patch or internal error in py-isort-bf--apply-rcs-patch"))
`looking-at` はカレントバッファ中のポイントの後に(すぐ)続くテキストが正規表現 regexp にマッチするか否かを調べる。
http://flex.phys.tohoku.ac.jp/texi/eljman/eljman_220.html
(forward-line)
(let ((action (match-string 1))
(from (string-to-number (match-string 2)))
(len (string-to-number (match-string 3))))
`match-string` は最後の検索でマッチした文字列を返す。
(cond
((equal action "a")
(let ((start (point)))
(forward-line len)
(let ((text (buffer-substring start (point))))
(with-current-buffer target-buffer
(setq line-offset (- line-offset len))
(goto-char (point-min))
(forward-line (- from len line-offset))
(insert text)))))
行頭がaだった時の挙動。行の追加を表現している。
((equal action "d")
(with-current-buffer target-buffer
(goto-char (point-min))
(forward-line (- from line-offset 1))
(setq line-offset (+ line-offset len))
(kill-whole-line len)))
行頭がdだった時の挙動。行の削除を表現している。
(t
(error "invalid rcs patch or internal error in py-isort-bf-apply--rcs-patch")))))))))
それ以外の文字列を受け取ったらエラーとして処理する。
py-isort-regionはこんな実装
- 一時ファイルを作成しisortをcall-processで呼び出して整形
- バッファ全体に関しては差分をRCS形式で取得
- 差分を当てるのは外部プロセスではなくて自前で編集関数を実装している
雑感
- py-isortの詳しい動きが学べた
- 呼びだす外部プロセスを変更すれば他の編集コマンドも実装できそう
- エディタのプラグインを読むのもたまにはいいかも