tl;dr;
PythonのLinterであるflake8はpep8に従っていろいろと教えてくれますが、
マニュアルで行うとなかなかに面倒な修正案を提示します。
近頃Golangの自動フォーマッタに慣れてしまった自分としては、
もう勝手にやってくれよ、とちょいちょい思ってました。
非同期にLinterを実行するVimプラグインとして有名なALEには、
エラー箇所を自動で修正するALEFix
コマンドがあり、前々からやってみようかなと思っていたので、
この機会にALEでPythonコードのLintと自動整形をやらせてみたいと思います。
前提
NeoVim
私は素のVimではなくNeoVim使いなのでこっちを使った方法になります。
素のVimとはPythonのパス解決の仕方が異なるのでご注意ください。
- NVIM v0.3.1
あとALEを使うのでALEのインストールはもちろん必要です。公式は丁寧に書いてあるのでわかりやすいです。
pyenv
ALEから呼び出しするflake8などのコマンドはpyenvにてインストールしたPythonのものを使用します。
これはNeoVimから呼び出すPythonのパス解決を固定するためです。
- pyenv 1.2.7-1-g71902168
pyenvにてインストールしたPythonのパスを固定する手段としては、
deoplete-jediのwikiが非常に参考になるため参照してみてください。
準備
まず準備としてNeovimが参照するPythonのパスが固定されている必要があります。
なぜならばNeoVim起動時にどのPythonを参照するかによって都度都度必要なツールをインストールするのは面倒だからです。
pyenvでNeoVim参照用のPython仮想環境を作成する
開発者であればPythonはvirtualenvやpipenv,pyenvなどにより複数のPythonバージョンを持っていることが多いと思います。
Pythonのパスが固定されていなければ、ALEから呼び出しするflake8等のPythonツール群は、
実行環境によって様々なPythonパスを参照してしまい、参照したパスにツールがインストールされていない場合、
パス解決ができずALEから実行できない恐れがあるからです。
globalのpyenv使ってるときは問題ないのに、virtualenvをロードした後にALEでflake8実行できなかったことないですか?
あったら原因はパス解決のせいだと思います。
手順は以下のツールが既にインストール済みである前提で話を進めます
NeoVim参照用のPythonを作る
# pyenvに最新のpython3を入れる pyenv install 3.6.5 # pyenv-virtualenvでneovim用の仮想環境として定義 pyenv virtualenv 3.6.5 neovim3
pyenvのパスは環境変数に入れとく
export PYENV_PATH=$HOME/.pyenv
init.vimにPythonのパスを入れる
letg:python3_host_prog= $PYENV_PATH .'/versions/neovim3/bin/python'
ちなみに自分はanyenvとpyenvが端末によって入ったり入ってなかったりするので以下のように書いてます
if isdirectory(expand($PYENV_PATH))letg:python3_host_prog= $PYENV_PATH .'/versions/neovim3/bin/python'endifif isdirectory(expand($ANYENV_PATH))letg:python3_host_prog= $ANYENV_PATH .'/envs/pyenv/versions/neovim3/bin/python'endif
ツールのインストール
なお今回使用するツールは以下の通りです。
ツール名 | 用途 |
---|---|
flake8 | いわずもがなPythonのLinter |
flake8-import-order | flake8でimport順序をチェックする拡張 |
autopep8 | pep8の規約に沿って整形するフォーマッタ |
black | 改行を整形するフォーマッタ |
isort | import順序を整形するフォーマッタ |
下記手順で、pyenvに必要ツールぶち込みます
# 上記で作った環境をロード
pyenv shell neovim3
# 仮想環境に必要ツールをインストール
pip install-U flake8 flake8-import-order autopep8 black isort
# アンロード
pyenv shell --unset
ALEの設定
なかなか面倒ですがやっとALEの設定ができます。
大きく分けてやりたいことは以下の3つです。
- flake8をLinterとして登録
- 各ツールをFixerとして登録
- 各ツールの実行オプションを変更してPythonパスを固定
" flake8をLinterとして登録letg:ale_linters={ \'python':['flake8'], \}" 各ツールをFixerとして登録letg:ale_fixers={ \'python':['autopep8','black','isort'], \}" 各ツールの実行オプションを変更してPythonパスを固定letg:ale_python_flake8_executable=g:python3_host_progletg:ale_python_flake8_options='-m flake8'letg:ale_python_autopep8_executable=g:python3_host_progletg:ale_python_autopep8_options='-m autopep8'letg:ale_python_isort_executable=g:python3_host_progletg:ale_python_isort_options='-m isort'letg:ale_python_black_executable=g:python3_host_progletg:ale_python_black_options='-m black'" ついでにFixを実行するマッピングしとく
nmap <silent><Leader>x<Plug>(ale_fix)" ファイル保存時に自動的にFixするオプションもあるのでお好みでletg:ale_fix_on_save=1
補足
わかりづらいので解説しときますと各ツールの実行オプションを変更してPythonパスを固定
のところは
オプション | 説明 |
---|---|
g:ale_python_*_executable | Fixer実行時のPythonのパスを指定 |
g:ale_python_*_options | g:ale_python_*_executable のパスで実行した場合のPythonのオプションを指定 |
となっているご様子です。英語弱者なので解釈違ったら申し訳ございません。
特にg:ale_python_*_options
は自信ない...
ちなみにPythonの-m
オプションは-m
で指定したPythonモジュールを実行するフラグですね。
あと、別にg:python3_host_prog
を使わなくてもpyenvで作ったpyenv-virtualenv環境を、g:ale_python_*_executable
に入れてあげてもよかったのですが、
こっちのほうが後でpyenvの環境つくり直したときとかに便利かなと思ってあえてそう書いてます。
素のVim使いの方々はg:ale_python_*_executable
に直接書いてもいいかも。
実はyapfってフォーマッタがまだあって、
これが賢いらしいので使ってみたかったのですが、なんかALE側がちゃんと整備できてなさそう。g:ale_python_*_executable
以外のyapf用のオプション何故かないんですよね。
そのうちPR送りたい。
実行する
autopep8のサンプルをはっつけるので試してみましょう。
importmath,sys;defexample1():####This is a long comment. This should be wrapped to fit within 72 characters.some_tuple=(1,2,3,'a');some_variable={'long':'Long code lines should be wrapped within 79 characters.','other':[math.pi,100,200,300,9876543210,'This is a long string that goes on'],'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1,20,300,40000,500000000,60000000000000000]}}return(some_tuple,some_variable)defexample2():return{'has_key() is deprecated':True}.has_key({'f':2}.has_key(''));classExample3(object):def__init__(self,bar):#Comments should have a space after the hash.ifbar:bar+=1;bar=bar*bar;returnbarelse:some_string="""
Indentation in multiline strings should not be touched.
Only actual code should be reindented.
"""return(sys.path,some_string)
多分以下のように整形されるはずです。やったね。
importmathimportsysdefexample1():# This is a long comment. This should be wrapped to fit within 72 characters.some_tuple=(1,2,3,"a")some_variable={"long":"Long code lines should be wrapped within 79 characters.","other":[math.pi,100,200,300,9876543210,"This is a long string that goes on",],"more":{"inner":"This whole logical line should be wrapped.",some_tuple:[1,20,300,40000,500000000,60000000000000000],},}return(some_tuple,some_variable)defexample2():return{"has_key() is deprecated":True}.has_key({"f":2}.has_key(""))classExample3(object):def__init__(self,bar):# Comments should have a space after the hash.ifbar:bar+=1bar=bar*barreturnbarelse:some_string="""
Indentation in multiline strings should not be touched.
Only actual code should be reindented.
"""return(sys.path,some_string)