mypyやっていったぞ

これは[KLab Advent Calendar 2017](https://qiita.com/advent-calendar/2017/klab)の21日目の記事です。
昨年の[KLab Advent Calendar 2016](https://qiita.com/advent-calendar/2017/klab)で[mypyやっていくぞ](https://qiita.com/k-saka/items/8f05c89f675af219e081)という記事を書きましたが、やっていったのでその続きを書きます。

## typingとmypyの更新

去年の時点ではmypy0.4.6でしたが、12月20日の時点でmypyは0.560, pythonは3.6.4がリリースされています。
やっていったプロジェクトではmypyは更新があるたびにバージョンを上げていました。
更新時に目についた機能を紹介します。

### mypyはmypyになった

mypyは`mypy`というパッケージ名になりました。以前までは名前が衝突していたため`mypy-lang`というパッケージ名でした。

```shell-session
pip install mypy
```

でインストールできます
http://mypy-lang.blogspot.jp/2017/01/mypy-0470-released.html

### PEP-526の変数アノテーション

Python3.6から[PEP 526](https://www.python.org/dev/peps/pep-0526/)が採用され、変数のアノテーション構文が入りました。mypyもこれに対応しています。

変数アノテーション使ってmypyで引っ掛ける例

```python
from typing import ClassVar

#  モジュール変数
module_variable: int = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")


class Sample:

    # クラス変数
    class_variable: ClassVar[int]
    # デフォルト値付クラス変数
    class_variable_default: ClassVar[int] = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
    # インスタンス変数
    instance_variable: int

    def __init__(self) -> None:
        self.instance_variable = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")


Sample.class_variable = ""  # intにstr入れようとしてエラー # error: Incompatible types in assignment (expression has type "str", variable has type "int")
```

### `typing.NamedTuple`

型付き版`namedtuple`として`typing.NamedTuple`がありました、これに変数アノテーションを付ける文法がサポートされています。
[NamedTuple](https://docs.python.org/3.6/library/typing.html#typing.NamedTuple)

3.6未満だと

```python
from typing import NamedTuple

Sample = NamedTuple('Sample', [('var1', int), ('var2', int)])
```

と書いていましたが、3.6.1以降だと

```python
from typing import NamedTuple


class Sample(NamedTuple):
    """type annotated namedtuple sample"""  # 3.6.1以降だとdocstringsが書ける
    var1: int
    var2: int

    def total(self) -> int:  # 3.6.1以降だとメソッドが書ける
        return self.var1 + self.var2
```

このように書けます。ちなみに
3.6以降で変数アノテーションが使え、3.6.1以降でメソッドとdocstringsが使えます。

### Protocols

`Structural subtyping`というものがtypingでサポートされます。[PEP 544](https://www.python.org/dev/peps/pep-0544/)

これは既存の継承を使った方法とは別に、クラスに生えるメソッドのシグネチャを制約するもので、
定義された`Protocol`と同じメソッドを持ったクラスを静的解析ツールが`Protocol`のサブタイプとみなしてチェックできるようになります。
ランタイムに影響を与えず、静的解析で確認できるところが嬉しいですね。mypyではすでにサポートされています。

言葉だけだと説明が難しいのでIterableを実装する例をあげます。

```python
from typing import Iterable, Iterator

class IntIterable:

    def __iter__(self) -> Iterator[int]:
        return iter([1, 2, 3])


def consume_iter(iterable: Iterable) -> None:
    for i in iterable:
        print(i)


consume_iter(IntIterable())
```

`IntIterable`は`Iterable`を継承していませんが、`__iter__`を実装しているため`Iterable`を引数に取る`consume_iter`に渡すことができます。

また自前でProtocolを定義することもできます。

```python
from typing_extensions import Protocol  # 将来的にtypingからインポートできるようになるはず


class Reader(Protocol):

    def read(self) -> str:
        ...

class TextReader:

    def read(self) -> str:
        return "hoge"


def message_reader(reader: Reader) -> None:
    print(reader.read())


message_reader(TextReader())  # OK 明示的にReaderを継承してないけど`read`メソッドを持ってると通る
message_reader(open("somefile"))  # OK ビルトインなクラスでもOK
message_reader(1) # Argument 1 to "message_reader" has incompatible type "int"; expected "Reader"
```

今のところ[PEP 544](https://www.python.org/dev/peps/pep-0544/) のステータスはDraftですが`typing_extentions`をインストールすると使うことができます。
もちろんmypyでもチェックしてくれます。
http://mypy.readthedocs.io/en/latest/class_basics.html#protocols-and-structural-subtyping

### ファイルベースコンフィグ

コマンドライン引数だけではなくファイルで設定を指定できるようになりました。
コマンドラインとは違いモジュール単位で設定できるようになっています。
https://mypy.readthedocs.io/en/latest/config_file.html#the-mypy-configuration-file

## どうやっていったか

[去年](https://qiita.com/k-saka/items/8f05c89f675af219e081)は一旦ビジネスロジックの詰まったサブパッケージにmypyを実行し通るようにしました。
CIに組み込む必要があるなと思いつつも、コードはバリバリと更新されていたので導入するタイミングが難しかったです。
そこで、さらに小分けにして取り込んでいくことにしました。
対象のモジュールを羅列したファイルを指定して実行する方法があったのでこれを採用しました。
https://mypy.readthedocs.io/en/latest/command_line.html#reading-a-list-of-files-from-a-file

mypy_check_filesに

```text
package/module.py
package/sub_package1
package/sub_package2/module.py
```

このように書き

```shell-session
mypy @mypy_check_files
```

と実行するとmypy_check_filesに記載されたファイル・ディレクトリを走査して実行してくれます。
これをそのままCIの設定に突っ込んで、mypy_check_filesに対応したファイルを順次追記して取り込んでいきました。
対応できたモジュールから順次取り込めるので、便利ですね。無理にこの仕組を使わなくても、シェルスクリプトでやってもいいと思います。

## 途中修正したものとか

### warn-no-return

`return`が無いと怒られるパターンが幾つかあったので修正しました。

```python
def hoge() -> Optional[int]: # 行末にreturnがなくてエラー  # error: Missing return statement
    if cond1:
        return 1
    if cond2:
        return 2
```

`return`書いたほうが明確でよさそう
`--no-warn-no-return`オプション付けると抑制することもできます

### error: Return value expected

```python
from typing import Optional


def hoge(s: str) -> Optional[int]:
    if s.startswith("1"):
        return 1
    if s.startswith("2"):
        return 2
    return  # `return expression`と`return`が関数内で混ざってる # error: Return value expected


a = hoge("")
```

関数中で`return expression`と`return`を混ぜるなということらしいです。
https://github.com/python/mypy/issues/1003
PEP8に書いてあるのですが、初めて知りました。

### import but not use

[去年](https://qiita.com/k-saka/items/8f05c89f675af219e081#imported-but-unused)に引き続きこの問題は残っていますが、
PEP 526とflake8(PyFlakes)の更新でだいぶ楽になりました。

```python
from typing import Iterator  # OK
from typing import List  # OK
from typing import Dict  # マジックコメントでしか使ってないのでflake8エラー # error: F401 'typing.Dict' imported but unused

i: Iterator = iter([])


def f() -> None:
    x: List
    x = []
    print(x)
    return None


f()

d = {}  # type: Dict
```

このスクリプトにflake8(3.5.0)を走らせると`Dict`が引っかかりまが、PEP 526で対応した部分は通ります。

### Sqlalchemy

まだ、対応できてません。
なので、Sqlalchemyのテーブル定義やクエリの詰まったパッケージはチェックしてません。
本体のIssueではpluginでなんとかする方向なのですが、pluginの実装は現在Experimentalです。

参考
https://github.com/python/mypy/pull/3517
https://github.com/python/mypy/issues/3538

### 行長過ぎる問題
コメント形式・PEP 526形式どちらを付けるにしても、一行あたりの文字数が増えてしまいflake8に怒られるパターンが多々ありました。
今回は気合で改行して乗り切りましたが、もうすこし条件を緩くしても良かったかもしれません。

## まとめ

去年に引き続きmypyをやってきましたが[typeshed](https://github.com/python/typeshed)の不整合は少なくなり、
mypy自体の細かいバグも目に見えるところでは少なくなっていて、日常で使う分には問題なくやっていけると思います。
3.7向けのtyping関連のPEPも多くあり、これからも新しい仕組みが入ってくるでしょう。
新しくプロジェクトを立ち上げる際には導入を検討していいとおもいます。

参考 3.7向けtyping関連PEP  
https://www.python.org/dev/peps/pep-0560/
https://www.python.org/dev/peps/pep-0561/ 
https://www.python.org/dev/peps/pep-0562/ 
https://www.python.org/dev/peps/pep-0563/ 
https://www.python.org/dev/peps/pep-0544/ 

## おまけ

dropboxがpyannotateというツールを公開しました
https://github.com/dropbox/pyannotate
http://mypy-lang.blogspot.jp/2017/11/dropbox-releases-pyannotate-auto.html

これは勝手にアノテーション付けてくれる君で、アノテーションついてないコードベースへ導入する際に実行するとコメントベースのアノテーションを付けてくれます。
テストコードなどを使い実際に動かしてランタイムで型を判定する仕組みらしいです。
手元で試したところ、既にアノテーションついている箇所にも追加されてしまいました。
既存のコードベースへタイプアノテーションを導入する際は試してみてもいいと思います。

### おまけのおまけ

去年書き忘れてたので言いたかった
[typeshed](https://github.com/python/typeshed)に投げてたPRマージされたよ!!
https://github.com/python/typeshed/pulls?q=is%3Apr+is%3Aclosed+author%3Ak-saka