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のパスを入れる
let g:python3_host_prog = $PYENV_PATH . '/versions/neovim3/bin/python'
-
ちなみに自分はanyenvとpyenvが端末によって入ったり入ってなかったりするので以下のように書いてます
if isdirectory(expand($PYENV_PATH)) let g:python3_host_prog = $PYENV_PATH . '/versions/neovim3/bin/python' endif if isdirectory(expand($ANYENV_PATH)) let g: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として登録
let g:ale_linters = {
\ 'python': ['flake8'],
\ }
" 各ツールをFixerとして登録
let g:ale_fixers = {
\ 'python': ['autopep8', 'black', 'isort'],
\ }
" 各ツールの実行オプションを変更してPythonパスを固定
let g:ale_python_flake8_executable = g:python3_host_prog
let g:ale_python_flake8_options = '-m flake8'
let g:ale_python_autopep8_executable = g:python3_host_prog
let g:ale_python_autopep8_options = '-m autopep8'
let g:ale_python_isort_executable = g:python3_host_prog
let g:ale_python_isort_options = '-m isort'
let g:ale_python_black_executable = g:python3_host_prog
let g:ale_python_black_options = '-m black'
" ついでにFixを実行するマッピングしとく
nmap <silent> <Leader>x <Plug>(ale_fix)
" ファイル保存時に自動的にFixするオプションもあるのでお好みで
let g: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のサンプルをはっつけるので試してみましょう。
import math, sys;
def example1():
####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)
def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key(''));
class Example3( object ):
def __init__ ( self, bar ):
#Comments should have a space after the hash.
if bar : bar+=1; bar=bar* bar ; return bar
else:
some_string = """
Indentation in multiline strings should not be touched.
Only actual code should be reindented.
"""
return (sys.path, some_string)
多分以下のように整形されるはずです。やったね。
import math
import sys
def example1():
# 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)
def example2():
return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key(""))
class Example3(object):
def __init__(self, bar):
# Comments should have a space after the hash.
if bar:
bar += 1
bar = bar * bar
return bar
else:
some_string = """
Indentation in multiline strings should not be touched.
Only actual code should be reindented.
"""
return (sys.path, some_string)