pyinspectというものを地味に作っている

github.com

pyinspectというものを地味に作っている。まだrepository上には一切のドキュメントがないし正式なリリースをしたというわけでもないのでしばらくは個人用。

コードリーディングのお供に

元々の動機はコードリーディングをしようとしているときに生まれた。コードリーディングで最初に行いたい事はどのようなモジュールが提供されているか大まかに把握することなのだけれど。ぶっちゃけるとドキュメントに書いてあるモチベーションみたいなものを覗いたあとはすぐにどのようなモジュールが存在しているかの一覧さえ見えれば良い。

そして大まかな構造を把握した後に個々のモジュールがどのような機能を持つかというふうに進んでいく。このときに欲しかったけれど存在しなかったものが作りたいものと言うことになる。

そんなわけで作り方も同様で、コードリーディングをしていきながら、その最中で欲しかった機能を加えつつ使いつつという形で育てていきたい。

最初はモジュールの一覧から

はじめの段階でpip install <package>でインストールした時に全体としてどのようなパッケージがあるか分かりづらい。とりあえず提供されているモジュールの一覧が欲しかったりする。

例として starlette を使うことにする。このパッケージを選んだ理由は特に無いけれど。

$ pip install starlette

pyinspect listで提供されているモジュールの一覧が見れる。ネストした階層のものも見れるので便利(pydoc(python -m pydoc)で隣接した階層のモジュールの一覧は見れるけれど。ネストした階層のモジュールは見ることができない)。

$ pyinspect list starlette
starlette
starlette.applications
starlette.authentication
starlette.background
starlette.concurrency
starlette.config
starlette.convertors
starlette.database
starlette.datastructures
starlette.endpoints
starlette.exceptions
starlette.formparsers
starlette.graphql
starlette.middleware
starlette.middleware.authentication
starlette.middleware.base
starlette.middleware.cors
starlette.middleware.database
starlette.middleware.errors
starlette.middleware.gzip
starlette.middleware.httpsredirect
starlette.middleware.lifespan
starlette.middleware.sessions
starlette.middleware.trustedhost
starlette.middleware.wsgi
starlette.requests
starlette.responses
starlette.routing
starlette.schemas
starlette.staticfiles
starlette.status
starlette.testclient
starlette.types
starlette.websockets

全体のモジュールを一覧で眺めてみて興味のありそうなモジュールの中を覗いていく。

次は物理的な位置を知りたい

今度は中のコードを読むために物理的な位置が知りたくなる。エディタやIDEから頑張って探したり定義位置へのジャンプで移動することもあるけれど。直接シェルから開きたくなることもある。

$ pyinspect resolve starlette.middleware
~/venvs/my/lib/python3.7/site-packages/starlette/middleware

# その他お気に入りのコマンドで
$ alias e
alias e='emacsclient -n'
$ e $(pyinspect resolve starlette.middleware)

特定のクラスに対する関係性を知りたい

pydocやsphinxなどで生成されるAPIドキュメントはそのモジュールの特定のクラスなどに対する情報は事細かに載っているけれど。知りたい情報が存在していないこともある。

具体的には以下の様な情報が知りたくなる。

  • 継承関係の全体
  • 階層的な呼び出し関係

きれいな可視化が欲しいのではなく、地図のようなものが欲しい。

継承関係の全体

これは特にjupyter notebook関係のコードを読んでいるときに思ったことなのだけれど。結構継承関係が深い。そして最終的にはtraitletsという謎のライブラリにたどり着くのだけれど。全体の継承関係が追いきれずにわからなかったりする。もちろん全体の継承関係自体はコードを書けばすぐに調べられることではあるのだけれど。コードを書かずに把握したい。

例えば例としてあげたstarletteで実際に試してみると、OpenAPIResponseはResponseを継承していることが分かる。

$ pyinspect inspect starlette.schemas:OpenAPIResponse
starlette.schemas:OpenAPIResponse <- starlette.responses:Response <- builtins:object

.. 省略 ..

この継承の全体を表示してくれる機能は特に外部のライブラリのクラスを継承しているときなどに便利だったりする。たとえばTestClientはrequestsで提供されているクラスを継承している。

$ pyinspect inspect starlette.testclient:TestClient
starlette.testclient:TestClient <- requests.sessions:Session <- requests.sessions:SessionRedirectMixin <- builtins:object

.. 省略 ..

あー、なるほどSessionを継承して作られているんだな−、なるほどなーと言うことが分かる。

階層的な呼び出し関係

特に責務が分割されていないクラスなど機能を多く持ったクラスは異様に多くのメソッドを持っていたりする。それらのメソッドは内部でしか使われないものであったり、トップレベルから呼ばれるものであったりする。それらの内の幾文かはpublicとprivate(_で始まるメソッド名のもの)の紳士協定で把握は可能なもののもう少し絞り込みたくなる。

自分自身のメソッドのコードからASTを取り出して、自身の持つメソッドを呼んでいるというときの呼び出し関係を可視化してあげれば理解に役立ちそうと感じた。例えば以下のようにあるメソッドの内部で呼ばれているメソッドはインデントされて表示される。

$ pyinspect inspect starlette.responses:Response
starlette.responses:Response <- builtins:object
    [method, OVERRIDE] __call__(self, receive: Callable[[], Awaitable[MutableMapping[str, Any]]], send: Callable[[MutableMapping[str, Any]], Awaitable[NoneType]]) -> None
    [method, OVERRIDE] __init__(self, content: Any = None, status_code: int = 200, headers: dict = None, media_type: str = None, background: starlette.background.BackgroundTask = None) -> None
        [method] init_headers(self, headers: Mapping[str, str] = None) -> None
        [method] render(self, content: Any) -> bytes
    [method] delete_cookie(self, key: str, path: str = '/', domain: str = None) -> None
        [method] set_cookie(self, key: str, value: str = '', max_age: int = None, expires: int = None, path: str = '/', domain: str = None, secure: bool = False, httponly: bool = False) -> None
    [property] headers

ここが先程まで省略されていた部分。例えば上の例ではdelete_cookie()の中でset_cookie()が呼ばれているし。__init__()の中でinit_headers()render()が呼ばれている(さらにネストされるようなものはインデントが深くなっていく)。

実際、init()delete_cookie()は以下の様なコード。

class Response:
# ...
    def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
        self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)

# 例をわかりやすくするためにメソッドの定義の順序とは変えて表示している
    def __init__(
        self,
        content: typing.Any = None,
        status_code: int = 200,
        headers: dict = None,
        media_type: str = None,
        background: BackgroundTask = None,
    ) -> None:
        if content is None:
            self.body = b""
        else:
            self.body = self.render(content)
        self.status_code = status_code
        if media_type is not None:
            self.media_type = media_type
        self.background = background
        self.init_headers(headers)

自分自身のメソッドだけが現れると意外と概観を掴むのに良い。

クラスに対してだけでなくモジュールに対しても同様のことをしたい

先程のクラスに対する構造の可視化を関数に対しても表示するようにしたくなった。以下の様な対応関係。

  • あるクラスの自身が持つメソッド内での呼び出し関係を出力
  • あるモジュールが持つ関数内での呼び出し関係を出力

なので最近ちょっと機能を追加した。雑な追加だったので幾つかバグが残っている。

例えば全てが関数扱いになってしまってたり。内部で呼ばれていた関数もトップレベルに表示されてしまったり。

例えばurllib.parse:urlparseの呼び出し関係は以下の様になっている。

$ pyinspect inspect urllib.parse:urlparse
[function] urlparse(url, scheme='', allow_fragments=True)
    [function] urlsplit(url, scheme='', allow_fragments=True)
        [function] SplitResult(scheme, netloc, path, query, fragment)
            [function] urlunsplit(components)
        [function] clear_cache()
    [function] ParseResult(scheme, netloc, path, params, query, fragment)
        [function] urlunparse(components)

ふわっと状況を把握するのに便利なのでは?という感じ。

その他、不足事項としては以下の様な再帰したコードが再帰とわかってほしい。だとか。そもそもコンソール以外への出力も念頭に置いて良いのではとか色々ある。

$ pyinspect inspect x:f
[function] f(x)
    [function] g(x, ans)

(実際のコードは以下)

def f(x):
    return g(x, 1)


def g(x, ans):
    if x == 0:
        return ans
    else:
        return g(x - 1, x * ans)