これで決まり!最強自動コード整形ツール3選!

いやいや何が決まり!だよ、全然決まらねーよっていうタイトルすみません。
キャッチーなタイトルをつける検証をしています。

きっかけ

自動コード整形ツール。弊社では全く導入されていない。
何故か。
知らないからだ。
vimプラグインと手作業での修正という現状だ。
これはどげんかせんといかん。

代表的なautopep8、yapf、blackについて、社内で布教したいので調べてみた。

着目すべきは、

  • コード整形の精度(ある程度のflake8準拠)
  • 設定のしやすさ(+ あんま設定いじらなくて済むか)
  • 勢い(githubスター数)

各ツールのテストには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)

autopep8

まずは一番の古株。autopep8からご紹介します。 github.com

特徴

  • あくまでPEP8準拠
  • 直観的にわかりやすいオプション(--aggressive
  • 古株。歴史がある。
  • 作者が日本人

インストール

pip install --upgrade autopep8

テスト(--aggressiveなし)

デフォルトの設定でテストしてみる。

$ autopep8 --in-place test.py

結果は以下。

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)

見るからに1行の文字数が多い。
これをflake8にかけると、

flake8 test.py
test.py:6:80: E501 line too long (81 > 79 characters)
test.py:8:80: E501 line too long (87 > 79 characters)
test.py:9:80: E501 line too long (105 > 79 characters)
test.py:10:80: E501 line too long (100 > 79 characters)
test.py:11:80: E501 line too long (145 > 79 characters)
test.py:15:57: W601 .has_key() is deprecated, use 'in'

やっぱり。。
デフォルトの実行だと文字列の長さは無視するみたいです。

テスト(--aggressive --aggressiveあり)

autopep8の特徴として、--aggressiveを付けることで、コード整形のレベルを指定できます。 READMEでは、--aggressiveを2つ付けているため、このモードでも実行してみました。

$ autopep8 --in-place --aggressive --aggressive <filename>

結果は以下。

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 (
    '' in {'f': 2}) in {'has_key() is deprecated': True}


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)

当たり前だけどさっきより綺麗になりました。
ただ、やや冗長?改行が多すぎて無駄にコードが長くなってしまっている?感じがしますね。
これをflake8にかけると、

$ flake8 test.py
$

何も出ない!
全てのフォーマットチェックをクリアしているようです。
さすがPEP8準拠クオリティですね。

変更点を表示

-d, --diffを付けると、+-を使用して変更点をわかりやすく表示してくれます。

$ autopep8 -d test.py

変更内容で上書き

デフォルトでは、ファイルを指定しただけでは実行時に上書きされません。
-i, --in-placeを使用することで、変更内容で上書きすることができます。

autopep test.py -i

yapf

次にご紹介するのはyapfというツール github.com

ロゴからもわかる通りgoogle社が作成しており、go言語のgofmtのアイデアを参考にしています。

ブラウザで利用できるテスト環境が提供されているので、まずはここで試してみるのもよいでしょう。因みにyapfというのは、Yet Another Python Formatterの略だそうです。
制約が強いフォーマッターしか存在しない現状に対して、よりカスタマイズ可能なフォーマッターを提案しています。

特徴

  • 高いカスタマイズ性(knobsで設定できるものすべて)
  • グローバル設定可能
  • 再帰的な実行が可能
  • githubスター数高い
  • あのgoogle社が開発

設定

割と自由なカスタマイズが可能です。 詳細はGithubのknobsを見てください。

設定ファイルは以下のようにkey = value形式で書きます。

[yapf]
based_on_style = pep8
spaces_before_comment = 4
split_before_logical_operator = true

設定は以下の順序で優先して読まれます。

  1. コマンドライン引数
  2. 同じディレクトリ内の.style.yapf[style]セクション
  3. 同じディレクトリ内のsetup.cfg[yapf]セクション
  4. ~/.config/yapf/style[style]セクション

設定

割と自由なカスタマイズが可能です。 詳細はGithubのknobsを見てください。

設定ファイルは以下のようにkey = value形式で書きます。

[yapf]
based_on_style = pep8
spaces_before_comment = 4
split_before_logical_operator = true

設定は以下の順序で優先して読まれます。

  1. コマンドライン引数
  2. 同じディレクトリ内の.style.yapf[style]セクション
  3. 同じディレクトリ内のsetup.cfg[yapf]セクション
  4. ~/.config/yapf/style[style]セクション

テスト

yapfでは、デフォルトでbased_on_style = pep8が設定されています。どの程度PEP8準拠なのか調べてみましょう

$ yapf test.py

実行結果は以下になります。

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)

これをflake8にかけた結果以下のようになりました。

$ flake8 test.py
test.py:1:12: E401 multiple imports on one line
test.py:5:5: E265 block comment should start with '# '
test.py:5:80: E501 line too long (83 > 79 characters)
test.py:23:45: W601 .has_key() is deprecated, use 'in'

E401:multiple importsについては意図的に設定されていないらしい(参考
E265:ブロックコメントを'#'始まりで書くことなんてないのであまり気にしなくてOK
E501:設定でline lengthを指定してあげればOK
W601:deprecatedな表現を使わない。これは最近のformatterでも設定が難しいかも

yapfはあえてPEP8基準でなくても設定できるようにしていることもあり、flake8で引っかかる。
基本的に追加の記述が必要な変更は加えないような方針となっているらしい。

変更点を表示

-d, --diffを付けると、+-を使用して変更点をわかりやすく表示してくれます。

$ yapf test.py -d

なお、変更点がないときに-dを付けて実行すると、0が返り値となります。

変更内容で上書き

デフォルトでは、ファイルを指定しただけでは実行時に上書きされません。
-i, --in-placeを使用することで、変更内容で上書きすることができます。

yapf test.py -i

再帰的に実行

-rを使用することで、再帰的な実行が可能です。

yapf test -r

black

black.now.sh

最後にblack。

“Any color you like.”

とか言っている妥協を許さないコードフォーマッターです。
かっこいい。惚れた。

blackはyapf同様、Playgroundというブラウザ上で試すことができるテスト環境が提供されています。

特徴

  • Emacs、PyCharm、Vim、VSCode、SublimeText 3、Jupyter Notebook、Atom、Nuclideといったほぼすべてのテキストエディタ用のプラグインが存在する。
  • カスタマイズ性が低い(プロジェクト間の統一性を保つため、敢えて低くされている)。
  • グローバル設定ファイルが使えない。
  • 表示される絵文字がかわいい。
  • 新しい(2018年3月first commit)

条件

blackは、Python 3.6.0 以上に対応しています。
Python 3.6.0 以上特有の記法に対応している代わりにPython 3.6.0 以上が必要になるのです。

インストール

pipからインストールすることができます。

pip install black

設定

Pro-tip: If you're asking yourself "Do I need to configure anything?" the answer is "No". Black is all about sensible defaults.

github中の上記文章にもある通り、blackでは基本的に設定ファイルで設定することは推奨していません。
設定したい場合、プロジェクトごとにpyproject.tomlで設定することになります。
このファイルはビルド時に必要な依存パッケージを設定できるファイルとしてPEP 518で制定されたもので、Poetlyflitを使用する場合、setup.pysetup.cfgの代替品として扱うこともできます。
具体的な設定方法としては、以下のように[tool.black]セクションに設定したい項目をkey=value形式で記載していきます。 ここでkeyとなる値はコマンドライン引数になります。

[tool.black]
line-length = 88
py36 = true
include = '\.pyi?$'
exclude = '''
/(
    \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist

  # The following are specific to Black, you probably don't want those.
  | blib2to3
  | tests/data
)/
'''

テスト

デフォルトの設定でテストしてみる。

$ black test.py

結果は以下のようになった。

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(""))


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)

これをflake8にかけると、

$ flake8 test.py
test.py:1:12: E401 multiple imports on one line
test.py:5:5: E265 block comment should start with '# '
test.py:5:80: E501 line too long (83 > 79 characters)
test.py:26:45: W601 .has_key() is deprecated, use 'in'

blackの場合も完全にflake8準拠というわけではなく、あくまでformatterとして適切な変更のみを加える仕様となっているようです。

変更点を表示

他ツール同様に、-d, --diffを付けると+-を使用して変更点をわかりやすく表示してくれます。

$ black --diff test.py

変更済みか否かを表示

--checkオプションを使用すると、blackにより変更済みか否かをstatusで返してくれます。

$ black --check {対象のファイル}

未変更の場合(返り値は1)

$ black --check test.py
would reformat test.py
All done! 💥 💔 💥
1 file would be reformatted.

変更済みの場合(返り値は0)

black --check test.py
All done! ✨ 🍰 ✨
1 file would be left unchanged.

Githubスター数の比較

2018/12/23現在で

autopep8 yapf black
2547 8379 6819

となっています。 yapf > black > autopep8 の順ですね。

結論

調べているうちにいろいろ目移りしちゃう。

それぞれ指向の違うフォーマッターであることがわかりました。
選び方をざっくりというなれば、

PEP8準拠でないと嫌!安定したツールがいい! → autopep8
自分好みにカスタマイズして使いたい!google好き! → yapf
制約が強いべきだ!エディタでプラグイン使いたい! → black

って感じですかね。

最初の評価軸からぶれますが、自分はVimでプラグイン使える点が気に入ったので、最後に紹介したBlackをしばらく使ってみようと思います。
一方で、blackとflake8は組み合わせるとそれぞれの想定している行当たりの文字数が違うため、そこのすり合わせだけしてあげる必要があります。
pre-commit × black × flake8の以下の記事とかスゴイよさそうです。 ljvmiranda921.github.io

時間できたら使ってみよう。