qtatsuの週報

初心者ですわぁ

【Python】コミット差分のみblackで整形する 【darker】

前書き

コードの整形はフォーマッタに任せたいものです。

理想的には、全員が同じスタイルでコードを整形できるようにpre-commitなどを利用してコミット時にフォーマッタを自動実行します。

しかしプロジェクトの途中参加など、導入が難しいケースもあると思います。

今回の自分は、プロジェクトにフォーマッタが導入がされておらず

  • せめて自分のコミット分だけはblackで整形したい。
  • 共通ライブラリを更新時、ファイル単位ではなく行単位で整形したい。

という状況でした。 根本解決を諦め、次善の方法はないかと調べたところ darkerという、black(とisort)のwrapperライブラリが良さそうでした。

(なお、Darker作者はGitHubのREADMEに、本家のblackにも行単位のフォーマット機能は将来導入されそうだと言及しています.)

darkerについては日本語の情報が少ないようだったので、試してみた内容をまとめておこうと思います。

参考リンク

環境

バージョン
MacOS Big Sur 11.6
Python3 3.10.2
darker 1.3.2
black 21.12b0
Pygments 2.11.2

darkerのインストール

インストール

仮想環境を作ってpip installします。

$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install darker

注意: blackのバージョンを下げる必要がある.(2022-02-05) 修正されています.

(2022-03-04)追記: 現在は修正されています。この項目は不要ですが、記録として残しておきます。

2022-02-05 現在、このままだとdarkerを利用できません。

darkerはblackのwrapperなので、darkerをインストールすると最新のblackが一緒に落とされます。しかし、最新のblack(ついにβが取れた22.1.0)はdef find_project_root関数の返り値の型がPathからtupleに変わってしまい、darerは未対応です。

こちらのプルリクで、既にdarkerの作者(akaiholaさん)が修正中のようです。 追記: -> すでに修正されています。

とりあえずは、blackのバージョンをβ版まで落とせば良いです。

(余談ですが、以下のようにして実行するとinstall可能バージョンを見ることができて便利です.(多分正当な方法ではないですが...))

(env)$ pip install black==
............................(省略)...........................
 20.8b1, 21.4b0, 21.4b1, 21.4b2, 21.5b0, 21.5b1, 21.5b2, 21.6b0, 21.7b0, 21.8b0, 21.9b0, 21.10b0, 21.11b0, 21.11b1, 21.12b0, 22.1.0)
ERROR: No matching distribution found for black==

最新の一個前は21.12b0ですので、こちらにダウングレードしておきます。

(env)$ pip install black==21.12b0

Pygmentsで色をつける

もうひと手間加えて、出力結果の見た目をきれいにしておきます。

Pygments を同じ環境にインストールしてあると、darkerは出力結果をカラーにしてくれます。

(env)$ pip install Pygments==2.11.2

特に設定は必要ありません。

darkerの出力結果は、元々このような見た目ですが、

f:id:Qtatsu:20220206002203p:plain

このように変わります。

f:id:Qtatsu:20220206002220p:plain

使い方

新規差分を整形

まず、darkerはgit diffを利用するので、ディレクトリをgit 管理下におく必要があります。

適当なディレクトリを作成し、最初のコミットまで済ませておきます。(内容はなんでもOKです.)

$ git init
$ touch README.md
$ git add README.md
$ git commit -m "first"

これでHEADができたので、darkerを利用できます。以下のようなpythonファイル(darker_test.py)を作成します。

(とにかく横に長くしたかっただけなので、適当です)

def format_name_and_age_to_profile(name: str | None, age: int | None, address: str | None): return f"{name} -- {age} -- {address}"

一旦、ここでコミットします. darkerが「コミット差分」に効くことを検証したいからです。

$ git add darker_test.py
$ git commit -m "一つ目の関数"
$ git log --oneline
7e25910 (HEAD -> master) 一つ目の関数
b6bee90 first

では、darker_test.pyにもう一つ記述を加えて以下のようにします。

def format_name_and_age_to_profile(name: str | None, age: int | None, address: str | None): return f"{name} -- {age} -- {address}"

def format_name_and_age_to_profile_version_2(name: str | None, age: int | None, address: str | None): return f"{name} -- {age} -- {address}"  # 2回目のコミット

まだコミットはしないでください! (addまではOKです.)

ここで darkerで修正差分を出力してみます.

今回は以下のように、カレントディレクトリ(.)かファイルを直接指定します。どちらでも結果は変わりません.

$ darker --diff .               # カレントディレクトリ
$ darker --diff darker_test.py  # ファイル指定.

f:id:Qtatsu:20220206002304p:plain

今回の変更分だけが整形されていることがわかります。 最初にコミット済みの、一つ目の関数は整形されていません。

また、この時点では元ファイルは変更されていません。修正をファイルに反映したければ、--diffオプションを外します。

$ darker .

整形後は以下のようになります。二つ目の関数(まだコミットしていない分)だけが、整形されています。

def format_name_and_age_to_profile(name: str | None, age: int | None, address: str | None): return f"{name} -- {age} -- {address}"


def format_name_and_age_to_profile_version_2(
    name: str | None, age: int | None, address: str | None
):
    return f"{name} -- {age} -- {address}"  # 2回目のコミット

では一旦コミットしておきましょう。

$ git add .
$ git commit -m "二つ目の関数(整形済み)"

コミットを指定して整形

darkerはgit-diffを利用しているので、例えばあるコミットからあるコミットまでという範囲を指定し、その時の変更をターゲットに整形が可能です。

先程のコミットログは以下のようになっています。

$ git log --oneline
8de1f64 (HEAD -> master) 二つ目の関数(整形済み)
7e25910 一つ目の関数
b6bee90 first

一つ目の関数は整形できていなかったので、こちらを指定して整形を行います。firstコミット(b6bee90)から7e25910の間の変更なので以下のように指定します。

最後にPATHを指定する必要があるのですが、直接ファイル名(darker_test.py)を指定するか、以下のようにワイルドカードを使う必要がありました。

.(カレントディレクトリ)指定は、なぜかできない仕様のようでした。

$ darker --diff --revision b6bee90..7e25910 *  # *.pyやdarker_test.pyでも大丈夫.

またhelpを見ると、コミット間は...(ドット三つ)で区切るように書いてありました。(2つでもできますが、違いは不明です)

結果は、指定したコミット間で記述した一つ目の関数が整形されています。

f:id:Qtatsu:20220206002351p:plain

pre-commitでdarkerを使う

まずpre-commitの導入です。

公式ページがとてもわかりやすいです。

(env)$ pip install pre-commit
(env)$ pre-commit --version
pre-commit 2.17.0

.pre-commit-config.yamlDarkerの公式GitHubにある参考例を元に記述します。

blackのバージョンを落としてください (2022-02-06現在. 理由は前述の通り.)

repos:
-   repo: https://github.com/akaihola/darker
    rev: 1.3.2
    hooks:
    -   id: darker
        additional_dependencies: [black==21.12b0]  # 最新blackだと失敗する.
$ pre-commit install

ではdarkerにまた長い名前の関数を1行で書き、実行してみます。

こちらを追記し...

def format_name_and_age_to_profile3(name: str | None, age: int | None, address: str | None): return f"{name} -- {age} -- {address}"
(env)$ git add .
(env)$ git commit

結果、コミットしていない差分のみがフォーマットされているはずです。

そもそも自分だけコード整形することに意味はあるか?

同僚の方にも相談させていただいたのですが、

  • そもそも自動フォーマットはレビューの負担軽減のためにやっている.
  • チームでコードを統一することが目的.

という指摘をいただきました。

全くその通りで、本当はプロジェクト全体で設定ファイルを共有し、pre-commitでblack/isort/flake8/mypyあたりをかけることが自動フォーマットの目的に即していると思います。

自分の場合、

  • 既存のpythonコードはフォーマッタなどを適応していない。また、クオートなどのスタイルはバラバラ。
  • 新規にコードを追加するのは基本的に自分だけ。レビューは受ける.
  • 既存のpythonスクリプトも、自前のutility関数が大量に入ったライブラリもそこそこの量がある。

という状況でした。

そのため、自分自身が書いた範囲のコードを読みやすくしたく、書くときには余計なことを考えなくて済むようにやはりフォーマッタは欲しいと思いました。そのため、本来の目的とは少し離れてしまうことを念頭に置き、一時しのぎ的にdarkerを利用しようと思っています。

もちろん、これは根本解決にはならない、ということを常に忘れないようにしたいと思います。

まとめ

  • 可能ならプロジェクト立ち上げ時にpre-commitを設定しておいた方がよい。
  • 自動フォーマットの目的は何か見失わないようにする。
  • それでもコミット差分だけをフォーマットしたいなら、darkerは選択肢に入ってくる。

ご意見、ご指摘などいただけると嬉しいです:pray: