型注釈の品質のテストと保証

注釈の精度のテスト

型注釈を含むパッケージを作成する場合、著者は公開する注釈が期待に沿っていることを検証したいと考えるかもしれません。 これは、公開された注釈がパッケージのパブリック インターフェイスの一部であるライブラリの作成者にとって特に重要です。

この問題にはいくつかのアプローチがあり、このドキュメントではそのいくつかを紹介します。

注釈

簡単のために、型チェックは mypy で行われると仮定します。 これらの戦略の多くは、他の型チェッカーにも適用できます。

assert_type--warn-unused-ignores を使用したテスト

通常の Python ファイルを作成し、typing_tests/ などの専用ディレクトリに配置して、型注釈の特定のプロパティをアサートするというアイデアです。

assert_type (mypy 0.950 以上) を使用すると、型注釈が予期される型を生成することを確認できます。

次のファイルがテスト対象である場合:

# foo.py
def bar(x: int) -> str:
    return str(x)

次のファイルは foo.py をテストします:

from typing_extensions import assert_type

assert_type(bar(42), str)

mypy --warn-unused-ignores を巧妙に使用すると、特定の式が型指定されているかどうかを確認できます。 有効な式と type: ignore コメントで注釈された無効な式を一緒に配置するというアイデアです。 これらのファイルで mypy --warn-unused-ignores を実行すると、パスするはずです。

この戦略は、テスト対象の型に関する強力な保証を提供しませんが、追加のツールは必要ありません。

次のファイルがテスト対象である場合:

# foo.py
def bar(x: int) -> str:
    return str(x)

次のファイルは foo.py をテストします:

bar(42)
bar("42")  # type: ignore [arg-type]
bar(y=42)  # type: ignore [call-arg]
r1: str = bar(42)
r2: int = bar(42)  # type: ignore [assignment]

mypy.api.run からの reveal_type 出力のチェック

mypy は、Python プロセスから mypy を呼び出すための api というサブパッケージを提供します。 reveal_type と組み合わせて、式から reveal_type 出力を取得する関数を記述するために使用できます。 それが得られたら、テストはそれに対して文字列と正規表現の一致をアサートできます。

このアプローチでは、優れたテスト エクスペリエンスを提供するためのヘルパー セットを記述する必要があり、テスト ケースごとに mypy を 1 回実行します (これには時間がかかる場合があります)。 ただし、mypy と選択したテスト フレームワークのみに基づいて構築されます。

次の例は、任意のフレームワークで記述されたテスト スイートに統合できます:

import re
from mypy import api

def get_reveal_type_output(filename):
    result = api.run([filename])
    stdout = result[0]
    match = re.search(r'note: Revealed type is "([^"]+)"', stdout)
    assert match is not None
    return match.group(1)

たとえば、上記を使用して一時ファイルを生成し、それを get_reveal_type_output への入力として使用する run_reveal_type pytest フィクスチャを提供できます:

import os
import pytest

@pytest.fixture
def _in_tmp_path(tmp_path):
    cur = os.getcwd()
    try:
        os.chdir(tmp_path)
        yield
    finally:
        os.chdir(cur)

@pytest.fixture
def run_reveal_type(tmp_path, _in_tmp_path):
    content_path = tmp_path / "reveal_type_test.py"

    def func(code_snippet, *, preamble = ""):
        content_path.write_text(preamble + f"reveal_type({code_snippet})")
        return get_reveal_type_output("reveal_type_test.py")

    return func

詳細については、mypy.api に関するドキュメント を参照してください。

pytest-mypy-plugins

pytest-mypy-plugins は、型テスト ケースを YAML データとして定義する pytest 用のプラグインです。 テスト ケースは mypy を通じて実行され、reveal_type の出力をアサートできます。

このプロジェクトは、pytest パラメータ化テストやテストごとの mypy 構成など、複雑な型の配置をサポートしています。 テストを実行するには pytest を使用する必要があり、テスト ケースごとにサブプロセスで mypy を実行します。

これは、pytest-mypy-plugins を使用したパラメータ化テストの例です:

- case: with_params
  parametrized:
    - val: 1
      rt: builtins.int
    - val: 1.0
      rt: builtins.float
  main: |
    reveal_type({[ val }})  # N: Revealed type is '{{ rt }}'

型の完全性の向上

多くのライブラリの目標の 1 つは、「完全に型注釈されている」ことを確認することです。つまり、すべての関数、クラス、およびオブジェクトに完全かつ正確な型注釈を提供することです。 完全な注釈を持つことは「型の完全性」または「型カバレッジ」と呼ばれます。

ライブラリの型の完全性スコアを向上させるためのヒントをいくつか紹介します。

  • 型の完全性をテスト プロセスの出力にします。 いくつかの型チェッカーには、有用な出力、警告、さらにはレポートを生成するためのオプションがあります。

  • パッケージにテストやサンプル コードが含まれている場合は、それらを配布から削除することを検討してください。 含める正当な理由がある場合は、アンダースコアで始まるディレクトリに配置して、ライブラリのインターフェイスの一部と見なされないようにすることを検討してください。

  • 実装の詳細を意図したサブモジュールがパッケージに含まれている場合は、それらのファイルの名前をアンダースコアで始まるように変更します。

  • シンボルがライブラリのインターフェイスの一部として意図されておらず、実装の詳細と見なされる場合は、アンダースコアで始まるように名前を変更します。 その後、プライベートと見なされ、型の完全性チェックから除外されます。

  • パッケージが他のライブラリの型を公開している場合は、これらの他のライブラリのメンテナーと協力して型の完全性を達成します。

警告

さまざまな型チェッカーが型カバレッジを評価し、より良い型カバレッジを達成するのに役立つ方法は異なる場合があります。 上記の推奨事項の一部は、使用する型チェック ツールによっては役に立たない場合があります。

mypy 不許可オプション

mypy には、型指定されていないコードを検出できるいくつかのオプションがあります。 詳細については、これらのオプションに関する mypy ドキュメント を参照してください。

型指定されていないデータに対して mypy エラーを発生させる基本的な使用法は次のとおりです:

mypy --disallow-untyped-defs
mypy --disallow-incomplete-defs

pyright 型検証

pyright には、型の完全性を検証するための特別なコマンド ライン フラグ --verifytypes があります。 詳細については、型の完全性の検証に関する pyright ドキュメント を参照してください。

mypy レポート

mypy には、分析に関するレポートを生成するためのいくつかのオプションがあります。 詳細については、レポート生成に関する mypy ドキュメント を参照してください。