LoginSignup
2
3

More than 3 years have passed since last update.

py-isortの実装を探る

Last updated at Posted at 2020-02-15
1 / 46

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のグループ化の順序に関する規則性

  1. 標準ライブラリのインポート
  2. 関連するサードパーティのインポート
  3. ローカルアプリケーション/ライブラリ固有のインポート

isortとは

  • Pythonのサードパーティパッケージ (標準ではない)
  • `import …文` もしくは `from … import …文` を並び替えてくれる

isortの実行例

https://camo.githubusercontent.com/841a7830ea6e8d363a53214917144b259685cc15/68747470733a2f2f7261772e6769746875622e636f6d2f74696d6f74687963726f736c65792f69736f72742f646576656c6f702f6578616d706c652e676966
(https://github.com/timothycrosley/isort から)


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` を返す。


(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の詳しい動きが学べた
  • 呼びだす外部プロセスを変更すれば他の編集コマンドも実装できそう
  • エディタのプラグインを読むのもたまにはいいかも
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3