🦊

雑にPythonの関数修正時にそれが呼び出している処理を特定する

2023/04/10に公開

忙しい人向け

pytestとpytest-covを使ってカバレッジを出力し、依存するコードを洗い出す。
カバレッジを出力する方法。

  • ローカル環境やリモート上の開発環境でコマンドライン環境が使用できる
  • データ基盤や分析基盤等Jupyter上でしかコードを実行できない
  • 関数の呼び出し履歴を見たい

こんな感じで

はじめに

プロジェクト途中参加してコードを修正してくださいと言われた時、どこ直せばいいかわからないくてどうしましょうねってなったことがあったので雑にどこ直せばいいか調べるにやった方法の忘備録です。

ぱっと思いつく方法はこれ↓。

  1. (この記事でやる) テストコードを書いてカバレッジを見る
  2. (この記事でやる) サンプルコード書いて関数の呼び出し履歴をトレースする
  3. loggingやブレークポイントぽちぽちしてprintfデバッグやデバッガでステップ実行する
  4. 内容知ってる人を質問攻めする(一番楽)
  5. コード読んで人力で呼び出しの依存関係調べる(辛い)

pytest-covのカバレッジから影響範囲特定する

pytestによるカバレッジ生成

pytestのプラグインであるpytest-covを使用すると、テストカバレッジを出力することができます。

サンプルコード

このコードではchainer.functions.sigmoid()を呼び出した際にchainerソースコードのカバレッジを出力する。
出力結果のカバレッジからどこの処理が影響範囲なのか雑に特定することができる。

import chainer.functions as F
import numpy as np


def test_sigmoid():
    F.sigmoid(np.arange(10).astype(np.float32))
pytest --cov=`python -c "import chainer; print(chainer.__path__[0])"` --cov-report=html tests

ipytestによるjupyter上でカバレッジ生成

ipytestはjupyter(IPython)のセル上で定義したpytestのテストコードを実行するライブラリです。
また、ipytest.runメソッドにpytestのコマンドラインオプションを渡すことでpytestの細かい設定をすることもできます。

上記テストと同じものをipythonを使って実行する例

# %%
import ipytest
import chainer
import chainer.functions as F
import numpy as np
# %%
def test_sigmoid():
    F.sigmoid(np.arange(10).astype(np.float32))
# %%
ipytest.run(
    f"--cov={chainer.__path__[0]}",
    "--cov-report=html",
)

実行結果

実行すると、カレントディレクトリにhtmlcovというディレクトリが生成され、テストコードのカバレッジが出力されます。
※サンプルコードではchainerのソースコードが入ってるパッケージディレクトリを指定したのでchainerソースコードのカバレッジが出力される。
この記事ではsigmoid()関数のテストを書いたので、そのコードを呼び出す際に通過したコードがカバレッジとして出力されます。

traceモジュールを使用する

標準ライブラリのtraceモジュールを使用することで呼び出した関数一覧を取得することができる

サンプルコード

import trace

import chainer.functions as F
import numpy as np

def example():
    test_sigmoid()

def test_sigmoid():
    F.sigmoid(np.arange(10).astype(np.float32))
tracer = trace.Trace(
    countfuncs=1,
)

tracer.runfunc(example)
result = tracer.results()

for key, val in result.calledfuncs.items():
    print(f"{key}: {val}")

出力例

('/var/folders/kp/_p1nx3ld5n70jx5y0qg6m3d40000gn/T/ipykernel_18663/2330830199.py', '2330830199', 'example'): 1
('/var/folders/kp/_p1nx3ld5n70jx5y0qg6m3d40000gn/T/ipykernel_18663/2330830199.py', '2330830199', 'test_sigmoid'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/functions/activation/sigmoid.py', 'sigmoid', 'sigmoid'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/function_node.py', 'function_node', 'FunctionNode.apply'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/function_node.py', 'function_node', '_extract_apply_in_data'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainerx/__init__.py', '__init__', 'is_available'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/function_node.py', 'function_node', '<listcomp>'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/function_node.py', 'function_node', 'label'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/utils/__init__.py', '__init__', '_check_arrays_forward_compatible'): 1
('/Users/user/.pyenv/versions/3.9.7/envs/pytest_cov/lib/python3.9/site-packages/chainer/__init__.py', '__init__', 'is_arrays_compatible'): 1

長いので以下略

Discussion