型情報がないこと由来のエラーに耐えられなかったので、`typeshed`にコントリビュートしたお話

## きっかけ
私が参画しているプロジェクトではCOMをPythonから操作しているスクリプトがあります。

そこで発生するエラーハンドリングを行うため、よく次のようなコードを書いています。

```py
from _ctypes import COMError

try:
    ...
except COMError as e:
    ...
```
ですが、**`_ctypes`には型スタブがなかったので、importした時点でVSCodeのpylance(pyright)がエラーを出していて、それを抑制したとしても`COMError`が`Unknown`扱いになっていました。**
![comerror.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364180/20cab9e9-532e-b1e7-cecd-8bd5ecd03ee0.png)


これによって型チェッカーに例外である情報が伝わっていなかったり、`COMError`があると型チェッカーから警告が出てやりづらい状況でした。

この状態を長らくしょうがないものとして放置していましたが、そのプロジェクトに参画してから2年近くがたち、いよいよ我慢ならなくなりました。

そこでPythonライブラリの型スタブをまとめている`typeshed`へのコントリビュートを決意しました。

## `typeshed`について
PEP484に役割が書いてあるのでご参照ください。
https://peps.python.org/pep-0484/#the-typeshed-repo

### 翻訳はこちら

https://qiita.com/t2y/items/f95f6efe163b29be59af#typeshed-%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA

## issueでの議論
さて、いきなりモジュールのスタブを追加する、まして標準ライブラリにあるものを他のコントリビューターへの情報共有もなしにいきなりPRすることは流石にためらわれました。
そこで、issueを立ててでまず私がやりたいことを伝えて、それに関連するものはどういう状況なのかを聞きました。

https://github.com/python/typeshed/issues/8571

> 私のプロジェクトでは、いくつかの COM-ffi パッケージを使用しています。
> 
> `_ctypes.COMError` や `_ctypes.CopyComPointer` も使っていますが、これらは型スタブが定義されていないため、 `mypy` タイプチェッカーは `Cannot find implementation or library stub for module named "_ctypes"` エラーを発生させることがあります。
> 
> このような型スタブを追加したいです。
> 
> ```python
> # githubのissueカンバンにあるスニペット
> ```
> しかし、これら以外のAPIはよくわかりません。
> 
> * そして、`if sys.platform == "win32":` が必要な場所はどこなのか。
> 
> 何かご意見があればお願いします。

その後、`typeshed`のコントリビューターから様々なアドバイスが寄せられました。
下記が要約です。

> - お気軽にPRしていいですよ :smile:　`stdlib/`以下に`_foo.pyi`みたいなモジュールがすでにたくさんあります
>   - `CONTRIBUTING.md`を読んで、`_ctypes`がincompleteであることをマークしてください
>   - COMの要素はLinux環境では存在していないことを確認したから、COM関係は`if sys.platform == "win32":`の下になります
> 
> - 今まで`stdlib/ctypes/__init__.pyi`に定義してきた要素は、実際ランタイムでは`_ctypes`に定義されています。もし`_ctypes`のスタブが追加されたらランタイムに追随するように移動したほうがいいと思います(とはいっても最初のPRは最小限でOKです)
> 
> - `stdlib/VERSIONS`にも`_ctypes`の情報を入れてください

## PR mergeまでの顛末
アドバイスを元にスタブを作り、PRしました。

https://github.com/python/typeshed/pull/8582

スタブ中のdocstringが不要だったのでそれを取り除くようにレビューを受けて修正されました。

**だがしかし、修正後もCIが通らない!**

CIのエラーメッセージを見ていると、本来ランタイムで定義されている`_ctypes._Pointer`などがないことを怒られていました。

「incompleteなスタブを作ったときには、モジュールレベルで`def __getattr__(name: str) -> Any: ...  # incomplete`を定義する」と`CONTRIBUTING.md`には書いてあったのにこれはおかしい!

途方にくれていると、レビュアーが諸々修正箇所を指摘してくれました。

### `stubtest`と`allowlist`の存在

`stubtest`は`mypy`などの型チェッカーにバンドルされているテストで、ランタイム実装の名前空間と型スタブを比べて、型スタブに不足があれば失敗するテストです。

下記issueの"1) Use stubtest to find things missing from or problematic with stubs."に概要が書かれていますが、`CONTRIBUTING.md`には書いていなかったので見落としていました。

https://github.com/python/typeshed/issues/4641

そんなこんなでレビュアーによって`allowlist`にまだスタブに追加していない各オブジェクトが追加されて、CIが通るようになりmergeされました。

## 現在の状況

`mypy`や`pylance`にスタブの更新が取り込まれ、`Unknown`ではなくなりました!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/364180/9406de6a-5039-9f51-3343-d304c24c853b.png)

## その後
スタブをランタイムの定義に追随させるべく、コントリビュートを続けています。

その中でどうしても循環importが発生するのでissueを立てて相談しました。

https://github.com/python/typeshed/issues/8633

[**型スタブでは循環importしていてもランタイムとは違ってエラーにならず問題ない**](https://github.com/python/typeshed/issues/8633#issuecomment-1229650944)と回答をもらったので安心して、少しづつ定義を移動させるPRを出そうとしています。

ただし現在(2022/09/20時点)では`_ctypes`に定義を書いて`ctypes`へimportするスタブを書いても、**[`pytype`](https://github.com/google/pytype)を使うテストが失敗してCIが通りません**。
これに関しては`pytype`側にissueを立ててあり、そのうち修正してもらう予定になっています。

https://github.com/google/pytype/issues/1281

(上記が解決しだい)**みなさんのコントリビュートもお待ちしております。**