どうもです、タドスケです。
僕のいるプロジェクトでは、保守性の向上のため、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時間ほどでサクっと作れました。
もっとチェック項目を追加していけば、プロジェクト内のコーディング規約チェッカーのようなものを作れるかもしれません。
他にもいろいろと試してみて、業務の効率化を進めていきます!
コメント
コメント一覧 (2件)
[…] […]
[…] […]