Edited at

ALE(on NeoVim)でPythonコードを楽に整形する

More than 1 year has passed since last update.


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実行できなかったことないですか?

あったら原因はパス解決のせいだと思います。

手順は以下のツールが既にインストール済みである前提で話を進めます



  1. NeoVim参照用のPythonを作る

    # pyenvに最新のpython3を入れる
    
    pyenv install 3.6.5
    # pyenv-virtualenvでneovim用の仮想環境として定義
    pyenv virtualenv 3.6.5 neovim3



  2. pyenvのパスは環境変数に入れとく

    export PYENV_PATH=$HOME/.pyenv
    



  3. 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つです。


  1. flake8をLinterとして登録

  2. 各ツールをFixerとして登録

  3. 各ツールの実行オプションを変更して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)