19新卒のものです。
今年からサーバーサイドに触れていろいろやらせて頂いております。
今回はgitと連動してmypyのエラー出力の増減をわかりやすく表示するようにした話をします。
前置き: mypyとは
pythonにおける型アノテーションに対して静的解析を行ってくれるツールです。
先人の記事があるので読むことをお勧めします。
mypyやっていくぞ
mypyやっていったぞ
はじめに
事のはじまりは担当案件でのmypyのバージョンが長いこと更新されていなかったこと(0.540)でした。
リリースノートを見るとFriday, 20 October 2017などと書いてあります。
軽い気持ちでローカルで最新版を導入してみたところ、アップデートで追加されたチェック項目に関連して沢山のエラーが生えてきます。
とりあえず抑制するオプションつければ通るけど…これは…
直そう
そして手元でエラー直せないかなと手を付けてみたのですが、普通に辛くなりました。
結構な規模になっている案件コード全体にエラーが偏在しているため、ちょっと直しただけでは改善が見えないのが精神的につらい…
もぅマヂ無理。可視化しょ…
作った
可視化というのは言いすぎなのですが(そもそも元から見えてる情報なので)、
自分が潰したエラーのリストが分かりやすく表示されてくれたら、気分的にも良いものになるんじゃないか?と考えました。
方針はシンプルで、変更前と変更後でmypyを2回走らせてdiffを取ります。
gitと連動してワークツリーとHEADの差分呼び出してなんやかんやすればできるはずという方針が立ちます。
実行時間が2倍になりますが、一方でmypyの対象を差分ファイルだけに絞ることができるので大丈夫という希望的観測で行きます。
というわけで作りました。
https://github.com/fujita-ma/mymypy
rustで書いていることに重大な理由はないのですが、
行単位のテキスト処理が多かったので、イテレータの処理をストレスなく書けるのが楽だったと思います。
ぶっちゃけpythonでもいいんですが、せっかくだから仕事で使ってない言語でやりたかったというのが正直なところです。
使ってみる
mypyのサンプルにあるコードで試してみます。
http://www.mypy-lang.org/examples.html
class BankAccount:
def __init__(self, initial_balance=0):
self.balance = initial_balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
def overdrawn(self):
return self.balance < 0
my_account = BankAccount(15)
my_account.withdraw(5)
print(my_account.balance)
mypyを走らせましょう。
❯ mypy --strict ./
main.py:2: error: Function is missing a type annotation
main.py:4: error: Function is missing a type annotation
main.py:6: error: Function is missing a type annotation
main.py:8: error: Function is missing a return type annotation
main.py:11: error: Call to untyped function "BankAccount" in typed context
エラーが出ています。
これを一旦コミットしたのち、一部アノテーションを修正してmypyとmymypyをそれぞれ走らせてみます。
class BankAccount:
def __init__(self, initial_balance: int = 0) -> None: # Annotated
self.balance = initial_balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
def overdrawn(self):
return self.balance < 0
my_account = BankAccount(15)
my_account.withdraw(5)
print(my_account.balance)
❯ mypy --strict ./
main.py:4: error: Function is missing a type annotation
main.py:6: error: Function is missing a type annotation
main.py:8: error: Function is missing a return type annotation
main.py:12: error: Call to untyped function "withdraw" in typed context
❯ mymypy
main.py
-main.py:2:_: error: Function is missing a type annotation
main.py:4:4: error: Function is missing a type annotation
main.py:6:6: error: Function is missing a type annotation
main.py:8:8: error: Function is missing a return type annotation
-main.py:11:11: error: Call to untyped function "BankAccount" in typed context
+main.py:12:12: error: Call to untyped function "withdraw" in typed context
潰したエラーが赤くハイライトされています!
また副産物として、逆に追加されたエラーが緑色でハイライトされています。
エラーを修正したことで、隠れていた別のエラーが出てくるというのは良くあることです。ちゃんと全部潰しておきます。
全部のアノテーションを適切につけると↓のようになります。
class BankAccount:
def __init__(self, initial_balance: int = 0) -> None:
self.balance = initial_balance
def deposit(self, amount: int) -> None:
self.balance += amount
def withdraw(self, amount: int) -> None:
self.balance -= amount
def overdrawn(self) -> bool:
return self.balance < 0
my_account = BankAccount(15)
my_account.withdraw(5)
print(my_account.balance)
❯ mypy --strict ./
❯ mymypy
main.py
-main.py:2:_: error: Function is missing a type annotation
-main.py:4:_: error: Function is missing a type annotation
-main.py:6:_: error: Function is missing a type annotation
-main.py:8:_: error: Function is missing a return type annotation
-main.py:11:11: error: Call to untyped function "BankAccount" in typed context
自分の働きで5個のエラーが潰せました。うれしい。
適当なリポジトリで試してみる
ちょっと遊んでみます。
リビジョンの扱いはgit diff
に準じているつもりなので、適当なコミットを指定すればその間の差分も取れます。
mypyのリポジトリを覗いてコミットを漁ってみましょう。
a5005f4aa977e4911bce5c828fd707ca8680d592
The `inner_types` attribute seems to have no effect.
リファクタリングしたらしいコミットを見つけました。
クローンしてmymypyにかけてみましょう。
❯ mymypy a5005f4~ a5005f4
mypy/checker.py
mypy/checker.py:64:64: error: Module 'mypy.semanal' has no attribute 'set_callable_name'
-mypy/checker.py:2813:_: error: Too many arguments for "PartialType"
-mypy/checker.py:2823:_: error: Too many arguments for "PartialType"
mypy/checker.py:2959:2959: error: unused 'type: ignore' comment
-mypy/checker.py:3022:_: error: "PartialType" has no attribute "inner_types"
-mypy/checker.py:3024:_: error: "PartialType" has no attribute "inner_types"
mypy/checker.py:4137:4133: error: unused 'type: ignore' comment
-mypy/checker.py:4311:_: error: "PartialType" has no attribute "inner_types"
mypy/checkexpr.py
mypy/checkexpr.py:203:203: error: unused 'type: ignore' comment
-mypy/checkexpr.py:592:_: error: "PartialType" has no attribute "inner_types"
-mypy/checkexpr.py:606:_: error: "PartialType" has no attribute "inner_types"
mypy/checkexpr.py:2368:2361: error: Returning Any from function declared to return "Optional[str]"
mypy/checkexpr.py:3003:2996: error: unused 'type: ignore' comment
mypy/type_visitor.py
mypy/type_visitor.py:167:167: error: unused 'type: ignore' comment
mypy/type_visitor.py:207:207: error: unused 'type: ignore' comment
mypy/type_visitor.py:229:229: error: unused 'type: ignore' comment
-mypy/type_visitor.py:293:_: error: "PartialType" has no attribute "inner_types"
mypy/types.py
mypy/types.py:190:190: error: unused 'type: ignore' comment
mypy/types.py:497:497: error: Returning Any from function declared to return "T"
mypy/types.py:520:520: error: Returning Any from function declared to return "T"
mypy/types.py:808:808: error: Returning Any from function declared to return "Union[Dict[str, Any], str]"
mypy/types.py:1557:1557: error: Returning Any from function declared to return "T"
mypy/types.py:1669:1669: error: Returning Any from function declared to return "T"
mypy/types.py:1789:1786: error: Returning Any from function declared to return "T"
mypy/types.py:1844:1841: error: unused 'type: ignore' comment
mypy/types.py:1889:1886: error: Returning Any from function declared to return "T"
リファクタリングの効果が見えていますね。
楽しい✌('ω' ✌)三 ✌('ω')✌ 三( ✌'ω') ✌(死語)
おわりに
気分転換がてら適当な方針で作ったのですが、そこそこ良い感じに表示できたので満足しています。
せっかくだから業務に活用していこうと思います。実際に使ってみたらバグも沢山見つかるだろうし