【Python】型ヒントの付け忘れをチェックしてくれるスクリプト

どうもです、タドスケです。

僕のいるプロジェクトでは、保守性の向上のため、Python コードに型ヒントを付けるルールがあるのですが、つい付け忘れてしまいます。

型ヒントが「間違っている」場合は mypy(型チェックツール)が指摘してくれますが、「付け忘れている」場合には指摘してくれません。

そこで、型ヒントの付け忘れを検出してくれるツールを作ってみました。
(開発にあたっては、ChatGPT さんにお世話になりました)

目次

コード

コードは以下です。

#!/usr/bin/env python3
import ast
import os
import subprocess
import sys


class TypeHintChecker:
    """型ヒントをチェックするクラス."""

    def check_type_hints_in_file(self, file_path: str) -> bool:
        """Pythonファイル内の型ヒントをチェックする."""
        tree = self._create_ast_tree(file_path)
        issues: list[str] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            self._check_argument_type_hints(node, issues)
            self._check_return_type_hints(node, issues)

        if issues:
            for issue in issues:
                sys.stderr.write(issue)
            return False

        return True

    def _create_ast_tree(self, file_path: str) -> ast.AST:
        """抽象構文木(AST)を生成する."""
        with open(file_path, 'r', encoding='utf-8') as f:
            tree = ast.parse(f.read())
        return tree

    def _check_argument_type_hints(self, node: ast.FunctionDef, issues: list[str]) -> None:
        """引数の型ヒントをチェックする."""
        args = node.args
        for arg in args.args:
            if self._is_python_reserved_arg(arg.arg):
                continue
            if arg.annotation is None:
                issues.append(f"関数 '{node.name}' の引数 '{arg.arg}' に型ヒントがありません。\n")

    def _is_python_reserved_arg(self, arg_name: str) -> bool:
        """引数名がPythonの予約語かどうかチェックする."""
        return arg_name == 'self' or arg_name == 'cls'

    def _check_return_type_hints(self, node: ast.FunctionDef, issues: list[str]) -> None:
        """戻り値の型ヒントをチェックする."""
        returns = node.returns
        if returns is None:
            issues.append(f"関数 '{node.name}' の戻り値に型ヒントがありません。\n")


class FileFinder:
    """ファイルを探すクラス."""

    @staticmethod
    def get_files_from_git_diff() -> list[str]:
        """Gitのdiffからファイル名を取得する."""
        result = subprocess.run(["git", "diff", "--name-only", "HEAD", "HEAD~"], capture_output=True, text=True)
        return result.stdout.strip().split("\n")

    @staticmethod
    def get_files_from_argv() -> list[str]:
        """コマンドライン引数からファイル名を取得する."""
        return sys.argv[1:]


class FileFilter:
    """ファイルをフィルタリングするクラス."""

    @staticmethod
    def is_test_file(file_name: str) -> bool:
        """テスト用のファイルかどうかチェックする."""
        return file_name.startswith('test_')

    @staticmethod
    def is_python_file(file_name: str) -> bool:
        """Pythonファイルかどうかチェックする."""
        return file_name.endswith('.py')


def main() -> None:
    """メイン関数."""
    target_files = FileFinder.get_files_from_argv()
    if not target_files:
        sys.stderr.write('対象ファイルが指定されていません')
        exit(1)

    root_folder = os.getcwd()
    type_checker = TypeHintChecker()
    for file in target_files:
        if not FileFilter.is_python_file(file):
            continue
        if FileFilter.is_test_file(file):
            continue

        full_file_path = os.path.join(root_folder, file)
        if not type_checker.check_type_hints_in_file(full_file_path):
            exit(1)


if __name__ == "__main__":
    main()

使いかた

PyCharm の外部ツールに登録する

PyCharm の外部ツール機能にこのスクリプトを登録すると、編集中のファイルに対して簡単にツールを実行できます。

引数の部分にはマクロ「$FilePath$」を指定しており、これは現在選択中のファイルパスを表します。

ファイル名タブの右クリックメニュー→ External Tools(名前は変更可能)から、追加したツールを実行できます。

git hook pre-push で実行する

プロジェクトを git で管理している場合、
.git/hooks/pre-push(なければファイルを新規作成する)
にこのコードを貼り付けると、git のプッシュ操作時に型ヒントの付け忘れがあったらプッシュが失敗するようにできます。

pre-push 内で使う場合は、get_files_from_argv の代わりに get_files_from_git_diff を呼ぶようにすることで、起動引数ではなく git の diff コマンドを使って push 前の変更コードリストを取得するようになります。

pre-push 時のみ、日本語のメッセージが文字化けする問題が起きています。
メッセージを英語にするか、utf-8 への変換処理が必要です。

実行結果

以下のコードに対してツールを実行します。

def func():
    return 0


class MyClass:

    @classmethod
    def cls_func(cls, name: str) -> None:
        pass

    def member_func(self, name) -> None:
        pass

結果は以下のようになります。

関数 ‘func’ の戻り値に型ヒントがありません。
関数 ‘member_func’ の引数 ‘name’ に型ヒントがありません。

ツール化することのメリット

このツールを作るのに ChatGPT を使うくらいなら、初めから ChatGPT にコードを見てもらえばいいんじゃない?と思うかもしれませんが、ツール化することで以下のメリットがあります。

  • ChatGPT 禁止のプロジェクトでも使える
  • プロンプトの容量制限に関係なく使える
  • 実行が速い

応用アイデア

今回のツール開発には ChatGPT を使い、1時間ほどでサクっと作れました。

もっとチェック項目を追加していけば、プロジェクト内のコーディング規約チェッカーのようなものを作れるかもしれません。

他にもいろいろと試してみて、業務の効率化を進めていきます!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (2件)

コメントする

目次