Python: コンテキストマネージャの型ヒント

## 概要
with構文で使われる、任意のコンテキストマネージャは[contextlib.AbstractContextManager](https://docs.python.org/3.10/library/contextlib.html#contextlib.AbstractContextManager)型の変数に代入できる。​**AbstractConrextManagerを継承している必要はない**​。(mypy v0.942で確認)

`AbstractConrextManager`を継承していないのであればエラーになるべきで、次のようなプロトコル型が必要になると思うのだがどうなんだろう？

```py
T = TypeVar("T", covariant=True)

class ContextManager(Protocol[T]):
    def __enter__(self) -> T:
        ...

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> Optional[bool]:
        ...
```

## 具体例
[contextlib.nullcontext](https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext)の使用例にある次の関数に型ヒントを付けたい。

```py
from contextlib import nullcontext

def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # file_or_pathが文字列ならファイルを開く
        cm = open(file_or_path)
    else:
        # ファイルオブジェクトなら呼び出し側が閉じるべき
        cm = nullcontext(file_or_path)

    with cm as file:
        # ファイルから読み込み
```

`open`がテキストモードでファイルを開いているので、`file_or_path`は`str | TextIO`だと仮定する。この時`cm`の型ヒントはどうすべきか？

なお、何も型ヒントを与えない場合、

```py
error: Incompatible types in assignment (expression has type "nullcontext[TextIO]", variable has type "TextIOWrapper")
```

というエラーが出る。

## 解決策
[typingパッケージ](https://docs.python.org/3.10/library/typing.html)を見ると、[typing.ContextManager](https://docs.python.org/3.10/library/typing.html#typing.ContextManager)という探していた型が定義されているが、v3.9以降は[contextlib.AbstractContextManager](https://docs.python.org/3.10/library/contextlib.html#contextlib.AbstractContextManager)を使えと書いてある。

`AbstractContextManager`を使って次のようにアノテーションするとエラーは解消された。

```py
from contextlib import AbstractContextManager, nullcontext

def process_file(file_or_path: str | TextIO) -> None:
    cm: AbstractContextManager[TextIO]
    if isinstance(file_or_path, str):
        # file_or_pathが文字列ならファイルを開く
        cm = open(file_or_path)
    else:
        # ファイルオブジェクトなら呼び出し側が閉じるべき
        cm = nullcontext(file_or_path)

    with cm as file:
        # ファイルから読み込み
```

## 疑問
コンテキストマネージャの型に`AbstractContextManager`を使うと、`AbstractContextManager`を継承していない野良コンテキストマネージャ( ​`__enter__`と`__exit__`を実装しているだけのクラス)は代入できないのでは？と思い、実験をしてみた。

```py
# AbstractContextManagerを継承していないコンテキストマネージャ
class MyContextManager:
    def __enter__(self) -> TextIO:
        ...

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> Optional[bool]:
        ...

cm: AbstractContextManager[TextIO]
cm = MyContextManager()
```

予想に反してエラーはでませんでした。`AbstractContextManager`には継承関係にないコンテキストマネージャもなぜか代入できるようです。

そもそも`TextIO`が`AbstractContextManager`を継承していないようなので、この辺りの都合でエラーにならないのかも知れません。

```py:typing.py
class TextIO(IO[str]):
    ...

class IO(Generic[AnyStr]):
    ...

class Generic:
    ...
```


本来であれば、次のようなプロトコルクラスが必要だと思うので、将来挙動が変更された時のために記しておきます。

```py
from types import TracebackType
from typing import Optional, Protocol, TypeVar

T = TypeVar("T", covariant=True)

class ContextManager(Protocol[T]):
    def __enter__(self) -> T:
        ...

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> Optional[bool]:
        ...
```