HoudiniでPythonの静的型検査をやってみる

この記事はHoudini Advent Calendar 2021 10日目の記事です

Calendar for Houdini | Advent Calendar 2021 - Qiita

概要

みなさんは静的型検査は好きですか?

Houdini19から、デフォルトでダウンロードされるHoudiniのPythonがPython2系からPython3系になりました。とてもめでたいですね。 この3系のPythonにはTypeHintsという仕組みがあります(Python3.5から追加)。これを用いることでPythonのコードに対して静的型検査を行うことができます。

今回はHoudini上でPythonの静的型検査ができるか試してみました。

静的型検査とは

型検査とは、記述されたプログラムの変数や式の型に矛盾が無いかを調べることです。

静的型検査とは、プログラムを実行する前に型検査を行うことです。

一般にC言語やVEX等の静的型付き言語では、この型検査はプログラムを実行する前のコンパイル時に網羅的に静的に実行されます。一方、python等の動的型付き言語では型検査は基本的にプログラムの実行時に動的に行われています。そのため、動的型付き言語では実際に処理を動かしてみるまで型の矛盾に気がつくことができません。規模が大きいプログラムの場合、実装の隅々まで処理を実行し型の妥当性を検証することはなかなか大変です。

このような問題を解決するため、動的型付き言語での開発に静的型検査を導入するアプローチがあります。

  • javascript: 静的型検査を型注釈(Type Annotation)により部分的に導入することができる漸進的型付けのtypescript。コンパイル時に静的に検査を行う。
  • ruby: 最新バージョンのruby3で型注釈が記述できるように。
  • python: 同様のアプローチでpython3.5から型注釈をの記述をサポート。外部ツールで型検査を行う。

HoudiniでPythonを記述していると、静的型検査により不安要素を取り除きたくなることがあると思います。今回はHoudini上でどうにかして静的型検査を行うことはできないか調べてみました。

どうやってHoudini上で静的型検査をするか

Pythonで静的型検査を行う際に用いられる機能、ツールについて紹介します。

TypeHints

Python3.5から導入された静的型解析のための仕組みです。

docs.python.org

Python側行ってくれるのは型注釈の構文のチェックにとどまっており、検査を行うためには外部のツールが必要になります。

mypy

github.com

Pythonのコードに記述された型注釈から静的型検査を行うための静的型検査器です。 コマンドライン上から実行します。 こちらはpip経由でインストールができます。

mypyはCLI経由で動作するため、Houdiniから後述するsubprocess moduleを用いてmypyを実行することで静的型検査を行うことにします。 また、Houdini上でPythonのコードを記述できる箇所はいくつかありますが、今回はPython SOPのpython ParameterとHDAのdefinition(PythonModule等)に記述されたpythonのscriptを検査の対象として考えていきます。

Houdini上のPython Scriptの取得

今回検査の対象となるPython SOPのpython ParameterとHDAのdefinitionからpythonのscriptを取得する方法について紹介します。どちらの場合も選択した検査対象になるscriptを持ったnode(hou.Node)からスタートして内部からscriptを取り出していきます。

Python SOP

単純にhou.Node classのインスタンスからpythonのテキストが格納されているパラメータをparm("python").eval()により取り出します。

HDADefinition

こちらはPython SOPに比べて結構深めのところにpythonスクリプトが格納されています。

まず、nodeのtype()からoperatorの雛形を表すclassであるNodeTypeを取得します。

HDAのNodeTypeはHDAの定義を表すHDADefinitionというclassを持っています。

こちらはhou.NodeType.definition()から取得できます。なおHDAではない場合はNoneが返ってくるため選択されたnodeがHDAによるものかどうかをここで判定しています。

さらにこのHDADefinitionには、HDASectionと呼ばれるHDAを構成するための様々な情報が格納されています。

HDAのscriptタブから記述できるPythonのscriptもHDASectionの形式でHDADefinitionに保持されています。このHDASectionはhou.HDADefinition.sections()からsection name -> HDASectionの辞書形式で取得ができるため、HDADefinitionを構成する複数のsectionをPythonのコードを保持するであろうsection nameでフィルタし、対象となるPythonのscriptのテキストを取得しています。なおPythonのコードを持つSection Nameは以下になります。

  • Expressions
  • PythonModule
  • BeforeFirstCreate
  • OnCreated
  • OnLoaded
  • OnUpdated
  • OnDeleted
  • AfterLastDelete
  • OnInputChanged
  • OnNameChanged
  • OnInstall
  • OnUninstall
  • SyncNodeVersion

mypyをCLI上で実行する

mypyは*.pyファイルを引数に渡すことでそのファイルに対して検査を行います。これに加えて-c(--command)optionで直接pythonのコードをテキスト形式で渡すこともできます。

$ mypy -c <PROGRAM_TEXT>

cf. The mypy command line — Mypy 0.910 documentation

今回は上記で取得したhoudini上のpythonのscriptをmypyに-coptionで渡して型検査を行っています。

subprocess module

CGWORLDクリエイティブカンファレンス2021でも紹介した、任意のコマンドを子プロセスとして実行するためのmoduleです。 subprocess.run(args)でargsに指定されたコマンドを実行します。返り値はsubprocess.CompletedProcessclassとなっており、標準出力と標準エラー出力をfieldのstdout、stderrからそれぞれ取得することができます。

今回はmypyをこのsubprocess.runを用いて実行し、型検査を行います。

command += [
    'mypy'
    '-c',
    code
]
proc = subprocess.run(command, stdout=PIPE, stderr=PIPE, text=True)
if proc.returncode == 0:
    print(proc.stdout)
else:
    print(proc.stdout)
    print(proc.stderr)
return

houpytypechecker

Houdini上でtoolshelfから型検査を行うツールを作りました。Houdini Packageの形式となっています。

github.com

機能

  • Python SOPとHDADefinitionのPythonの型検査
  • 環境変数によるmypyへ渡す追加のオプション設定 MYPY_ADDITIONAL_OPTIONSでspace区切りで指定ができます。 f:id:ttata:20211210184140p:plain

導入方法

手順

  1. [mypy]をhoudiniから実行できる場所に追加します。

    • houdiniのshellからmypyに対してpathが通っているか確認して下さい。
  2. [houpytypechecker]をhoudini packageとしてインストールします。

    • cf. Houdiniパッケージ
    • e.g. $HOUDINI_USER_PREF_DIR/packages内にhoupytypecheckerのファイルとhoupytypechecker.jsonを配置する
  3. Houdini上でToolShelfを追加します。

  4. 推奨のmypyの追加のオプションを設定します。

    • Aliaces and Variables Windowsを立ち上げます(Alt + Shift + V)
    • MYPY_ADDITIONAL_OPTIONSキーを追加し値には-v --ignore-missing-importsを指定して下さい。
      • -vはエラー時に詳細を表示するようにするオプションです。
      • ignore-missing-importsはモジュールが見つからない場合にそれを無視するオプションです。
    • 他のオプションについては以下を参照して下さい

試してみる

Python SOPに以下のscriptを記述します。

x = [] # type: List[str]
def add(lhs: str, rhs: str)-> str:
    return lhs + rhs
# ok
add('a', 'b') 

# error
# add(1, 2) 

Operatorを選択した状態でShelfのToolをクリックし型検査を実行します。 Houdini Consoleに以下が型検査の結果が表示されます。

Python SOP
========
Success: no issues found in 1 source file

次は型が矛盾している場合を試してみます。最後の行のadd(1, 2)コメントアウトを外して検査を行ってみます。

x = [] # type: List[str]
def add(lhs: str, rhs: str)-> str:
    return lhs + rhs
# ok
# add('a', 'b') 

# error
add(1, 2) 

Houdini Consoleに型の指定が誤っている箇所が表示されました。

Python SOP
========
<string>:15: error: Argument 1 to "add" has incompatible type "int"; expected "str"
<string>:15: error: Argument 2 to "add" has incompatible type "int"; expected "str"
Found 2 errors in 1 file (checked 1 source file)

課題

まだ試せていませんが、Houdini Object Model (HOM)のscriptは型定義がされていないため、このようなscriptを検査対象に含める場合は別途、以下のような手段で型情報を与える必要があります。

HOMのような型定義されていないライブラリについては、これを元に生成した型定義ファイルを環境変数MYPYPATHに対して指定し、mypyに読ませることで型検査を行うことができると思います。

また、今回作成したツールは、Houdiniで記述されたpythonすべてに対して型検査ができるわけではなく、上記にもあるようにPython SOPとHDADefinitionに記述されたもののみの対応となっています。ToolShelfやTOPのpython等他の部分に記述されたものも検査対象にできると良さそうです。

まとめ

  • mypyによるpythonの静的型検査をHoudini上で行うため実験的にツールを作りました。
  • Python SOPとHDADefinitionに記述されたpythonのみ検査することができます。
  • 型付けされていないmoduleに型定義を追加した場合の検査は未検証です。

いつかHOMに型定義が記述される日が来るのをたのしみにしています。