modulegraphが上手く動かないのでsys.path_hooksで遊ぶ

modulegraphというパッケージがあり、これでモジュール間の依存関係をグラフにしてみたりできるらしいのだけれど。何だか上手く動かなかった。

残念な気持ちにはなったのだけれど、何か特別なパッケージに頼らずともpythonのimport hookの機能を利用すれば同様のことはそんなに大変じゃなくできるんじゃないかと思ったのでやってみることにした。

import hook

あんまり使われていないけれど。pythonでのimportの方法についてはわりとまじめにPEPが定められている。

これに関してもまじめに読む必要もあんまりなくて、それなりに丁寧なドキュメントが用意されている(和訳もされている)。

重要なのはfinderとloaderという概念があり、finderはをfind_spec()というメソッドでloaderを探し出すという辺り。そしてhookとしてpathを解釈する際の sys.path_hooks がある(sys.meta_path と言うものもあるけれどこれは使わない)。

実際どのようなものが登録されているかというと以下のような2つが登録されていると思う。

import sys


for hook in sys.path_hooks:
    print(hook)

# <class 'zipimport.zipimporter'>
# <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x101242a60>

大抵の場合後者のFileFinderの方のものが使われる。ちなみにこれはどこに定義されているかというと importlib/_bootstrap_external.py というファイルがあるのでそのあたりを見てみると良い(それ意外にもimportlib以下のコードを覗いてみるとけっこう面白いと思う)。

sys.meta_path

いや、やっぱりまじめに説明するには sys.meta_path にも触れる必要があったので触れる。基本的にはimportの解釈はsys.meta_pathに登録されているfinderオブジェクトに委ねられる。

import sys


for finder in sys.meta_path:
    print(finder)

# <class '_frozen_importlib.BuiltinImporter'>
# <class '_frozen_importlib.FrozenImporter'>
# <class '_frozen_importlib_external.PathFinder'>

通常のimportではPathFinderが使われる。このfinderオブジェクトの find_spec() がimportの始まり

まじめなimportの詳細

もう少し真面目に書くと以下の様な形になる。

  1. import <foo> を処理系が読み取る
  2. (bootstrap中の _find_and_load() が呼ばれる)
  3. sys.modulesに既にimportされていないか確認する(importされていれば終了)
  4. sys.meta_pathをiterateして(iterateされるのはfinder)、finder.find_spec() を呼び出す
  5. (spec(importlib._boostrap.ModuleSpec)はloaderを持っている)
  6. specの持つloaderの loader.exec_module() が呼び出される。

すごく雑に書くと以下の様なコード(色々詳細は省いている)。

if ... not in sys.modules:
    for finder in sys.meta_path:
        spec = finder.find_spec(...)
        if spec is not None:
            return spec.loader.exec_module(...)

さてここで sys.path_hooks が現れていない。どこで出てくるかというと、sys.meta_pathからiterateされる通常3つのMetaPathFinderの内のPathFinderの find_spec() で使われる。こういう感じ。

for hook in sys.path_hooks:
    try:
        return hook(path)
    except ImportError:
        continue
else:
    return None

何はなくとも sys.path_hooks で登録された関数が importのタイミング で呼ばれるとさえわかっていれば良い。またImportErrorが見つかったら後続のhookを探すという処理になっている。

テキトウにpath.hooksを追加してみる

テキトウにpath.hooksを追加してみる。先頭にImportErrorを送出するちょっとしたprintを挟んだhookを追加してみよう。その後jsonモジュールをimportしてみる。

def peek(name):
    print("@", name)
    raise ImportError(name)

import sys
sys.path_hooks.insert(0, peek)
import json

例えばこういう風な出力が得られるはず。

@ /opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json

そんなわけでimport時のhookを手にする事ができる。後はこれが呼ばれた時のstack frameをたどればどのファイルからimportされたかが分かる。そしてその依存関係を可視化すれば元のmodulegraphでやろうとしていたことは達成できる。

実際tracebackを出力してみると以下の様なものになる。

import traceback


def peek(name):
    traceback.print_stack()
    raise ImportError(name)


import sys
sys.path_hooks.insert(0, peek)
import json

ちょっと長いtracebackだけれどimportlibの部分を除いてあげれば分かりやすい形になる。

  File "main.py", line 11, in <module>
    import json
  File "<frozen importlib._bootstrap>", line 969, in _find_and_load
  File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 673, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 665, in exec_module
  File "<frozen importlib._bootstrap>", line 222, in _call_with_frames_removed
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/__init__.py", line 106, in <module>
    from .decoder import JSONDecoder, JSONDecodeError
  File "<frozen importlib._bootstrap>", line 969, in _find_and_load
  File "<frozen importlib._bootstrap>", line 954, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 896, in _find_spec
  File "<frozen importlib._bootstrap_external>", line 1139, in find_spec
  File "<frozen importlib._bootstrap_external>", line 1110, in _get_spec
  File "<frozen importlib._bootstrap_external>", line 1082, in _path_importer_cache
  File "<frozen importlib._bootstrap_external>", line 1058, in _path_hooks
  File "qr_51394rjo.py", line 5, in peek
    traceback.print_stack()

こう。

  File "main.py", line 11, in <module>
    import json
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/json/__init__.py", line 106, in <module>
    from .decoder import JSONDecoder, JSONDecodeError
  File "qr_51394rjo.py", line 5, in peek
    traceback.print_stack()

main.pyからimport jsonされたということが分かる。

例えば wsgiref.simple_server の依存関係

上の話を色々まとめると依存関係はそれなりに簡単に取れる(他にも色々説明することがあるので詳細は省略)。 例えばhttp server用にwsgiref.simple_server等があるけれど。

from wsgiref import simple_server

このimportは以下のようなモジュールを読み込んでいる(単に読み込むモジュールを知りたいだけなのであれば python -v で起動しても良いかもしれない)。

load wsgiref (where=main)
load wsgiref.simple_server (where=main)
load http (where=wsgiref.simple_server)
load enum (where=http)
load http.server (where=wsgiref.simple_server)
load html (where=http.server)
load html.entities (where=html)
load http.client (where=http.server)
load email (where=http.client)
load email.parser (where=http.client)
load email.feedparser (where=email.parser)
load email.errors (where=email.feedparser)
load email.message (where=email.feedparser)
load uu (where=email.message)
load lib-dynload.binascii.cpython-35m-darwin.so (where=uu)
load quopri (where=email.message)
load email.utils (where=email.message)
load random (where=email.utils)
load lib-dynload.math.cpython-35m-darwin.so (where=random)
load hashlib (where=random)
load lib-dynload._hashlib.cpython-35m-darwin.so (where=hashlib)
load lib-dynload._random.cpython-35m-darwin.so (where=random)
load socket (where=email.utils)
load lib-dynload._socket.cpython-35m-darwin.so (where=socket)
load selectors (where=socket)
load lib-dynload.select.cpython-35m-darwin.so (where=selectors)
load datetime (where=email.utils)
load lib-dynload._datetime.cpython-35m-darwin.so (where=datetime)
load urllib (where=email.utils)
load urllib.parse (where=email.utils)
load email._parseaddr (where=email.utils)
load calendar (where=email._parseaddr)
load locale (where=calendar)
load email.charset (where=email.utils)
load email.base64mime (where=email.charset)
load base64 (where=email.base64mime)
load struct (where=base64)
load lib-dynload._struct.cpython-35m-darwin.so (where=struct)
load email.quoprimime (where=email.charset)
load string (where=email.quoprimime)
load email.encoders (where=email.charset)
load email._policybase (where=email.message)
load email.header (where=email._policybase)
load email._encoded_words (where=email.message)
load email.iterators (where=email.message)
load ssl (where=http.client)
load ipaddress (where=ssl)
load textwrap (where=ssl)
load lib-dynload._ssl.cpython-35m-darwin.so (where=ssl)
load mimetypes (where=http.server)
load shutil (where=http.server)
load fnmatch (where=shutil)
load tarfile (where=shutil)
load copy (where=tarfile)
load lib-dynload.grp.cpython-35m-darwin.so (where=tarfile)
load bz2 (where=shutil)
load _compression (where=bz2)
load threading (where=bz2)
load traceback (where=threading)
load linecache (where=traceback)
load tokenize (where=linecache)
load token (where=tokenize)
load lib-dynload._bz2.cpython-35m-darwin.so (where=bz2)
load lzma (where=shutil)
load lib-dynload._lzma.cpython-35m-darwin.so (where=lzma)
load socketserver (where=http.server)
load argparse (where=http.server)
load gettext (where=argparse)
load wsgiref.handlers (where=wsgiref.simple_server)
load wsgiref.util (where=wsgiref.handlers)
load wsgiref.headers (where=wsgiref.handlers)
load platform (where=wsgiref.simple_server)
load subprocess (where=platform)
load signal (where=subprocess)
load lib-dynload._posixsubprocess.cpython-35m-darwin.so (where=subprocess)

dot形式で出力してgraphvizなどで可視化すると以下の様になる

https://gist.github.com/podhmo/539854527d1f9b190805873111101414#file-01-svg:cite:pined