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

# 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のインストールはもちろん必要です。[公式](https://github.com/w0rp/ale)は丁寧に書いてあるのでわかりやすいです。

### pyenv

ALEから呼び出しするflake8などのコマンドはpyenvにてインストールしたPythonのものを使用します。
これはNeoVimから呼び出すPythonのパス解決を固定するためです。

- pyenv 1.2.7-1-g71902168 

pyenvにてインストールしたPythonのパスを固定する手段としては、
[deoplete-jediのwiki](https://github.com/zchee/deoplete-jedi/wiki/Setting-up-Python-for-Neovim)が非常に参考になるため参照してみてください。

## 準備

まず準備としてNeovimが参照するPythonのパスが固定されている必要があります。
なぜならばNeoVim起動時にどのPythonを参照するかによって都度都度必要なツールをインストールするのは面倒だからです。

### pyenvでNeoVim参照用のPython仮想環境を作成する

開発者であればPythonはvirtualenvやpipenv,pyenvなどにより複数のPythonバージョンを持っていることが多いと思います。
Pythonのパスが固定されていなければ、ALEから呼び出しするflake8等のPythonツール群は、
実行環境によって様々なPythonパスを参照してしまい、参照したパスにツールがインストールされていない場合、
パス解決ができずALEから実行できない恐れがあるからです。

globalのpyenv使ってるときは問題ないのに、virtualenvをロードした後にALEでflake8実行できなかったことないですか？
あったら原因はパス解決のせいだと思います。

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

- [pyenv](https://github.com/pyenv/pyenv)
- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)

1. NeoVim参照用のPythonを作る

    ```sh
    # pyenvに最新のpython3を入れる
    pyenv install 3.6.5
    # pyenv-virtualenvでneovim用の仮想環境として定義
    pyenv virtualenv 3.6.5 neovim3
    ```
2. pyenvのパスは環境変数に入れとく

    ```sh
    export PYENV_PATH=$HOME/.pyenv
    ```
3. init.vimにPythonのパスを入れる

    ```vim
    let g:python3_host_prog = $PYENV_PATH . '/versions/neovim3/bin/python'
    ```
    - ちなみに自分は[anyenv](https://github.com/riywo/anyenv)とpyenvが端末によって入ったり入ってなかったりするので以下のように書いてます

        ```vim
        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](http://flake8.pycqa.org)                                   | いわずもがなPythonのLinter             |
| [flake8-import-order](https://github.com/PyCQA/flake8-import-order) | flake8でimport順序をチェックする拡張   |
| [autopep8](https://github.com/hhatto/autopep8)                      | pep8の規約に沿って整形するフォーマッタ |
| [black](https://github.com/ambv/black)                              | 改行を整形するフォーマッタ             |
| [isort](https://github.com/timothycrosley/isort)                    | import順序を整形するフォーマッタ       |

下記手順で、pyenvに必要ツールぶち込みます

```sh
# 上記で作った環境をロード
pyenv shell neovim3
# 仮想環境に必要ツールをインストール
pip install -U flake8 flake8-import-order autopep8 black isort
# アンロード
pyenv shell --unset
```

## ALEの設定

なかなか面倒ですがやっとALEの設定ができます。
大きく分けてやりたいことは以下の3つです。

1. flake8をLinterとして登録
1. 各ツールをFixerとして登録
1. 各ツールの実行オプションを変更してPythonパスを固定

```vim
" 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](https://github.com/google/yapf)ってフォーマッタがまだあって、
これが賢いらしいので使ってみたかったのですが、なんかALE側がちゃんと整備できてなさそう。
`g:ale_python_*_executable`以外のyapf用のオプション何故かないんですよね。
そのうちPR送りたい。

## 実行する

autopep8のサンプルをはっつけるので試してみましょう。

```python
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)
```

多分以下のように整形されるはずです。やったね。

```python
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)
```