egoistでモジュールを分割できるようにした

github.com

定義が多くなってくると、1つのファイルで管理するのが厳しくなってくる。そんなわけで、moduleを分割することを考えてみた。似たような機構はいろいろなWAFにも存在する。例えばflaskならblueprint、fastAPIならAPIRouter。

実はデコレーターベースのアノテーションの機構とこの種の機能との相性はあんまり良くない。少しだけ工夫が必要。まぁそのあたりは省略して使い方だけをメモする。

appをsubappに分ける

使い方は今までcreate_app()で作ってきたAppをcreate_subapp()で作られるSubAppにするだけ。おしまい。

例えば、このようなコードを。

from egoist.app import create_app

app = create_app()
app.include("egoist.directives.define_cli")


@app.define_cli("egoist.generators.clikit:walk")
def hello(*, name: str) -> None:
    """hello message"""
    from egoist.generators.clikit import runtime, clikit

    with runtime.generate(clikit):
        runtime.printf("hello %s\n", name)

このように変える。他は全部一緒。

-from egoist.app import create_app
+from egoist.app import create_subapp
 
-app = create_app()
+app = create_subapp()
 app.include("egoist.directives.define_cli")

少しだけ注意点があって、SubAppでのdirective1などは実行されたかのように見えるだけで実態は記録されただけ。App本体からincludeされない限り実行自体はされない。

subappを使った階層構造

そんなわけでapp1つだけだった定義をsubappを使うように分ける事ができるようになる。例えば以下はhelloとbyebyeに分けた。これはcmd/hello/main.goとcmd/byebye/main.goを生成するような定義。

$ tree
.
├── Makefile
├── apps
│   ├── __init__.py
│   ├── byebye.py
│   └── hello.py
├── cmd
│   ├── byebye
│   │   └── main.go
│   └── hello
│       └── main.go
├── definitions.py
└── scan.output

4 directories, 8 files

definitions.py部分のappsの記述はsubappのincludeだけ。

definitions.py

from egoist.app import create_app, SettingsDict, parse_args

settings: SettingsDict = {"rootdir": "cmd/", "here": __file__}
app = create_app(settings)

app.include("apps.hello")
app.include("apps.byebye")

if __name__ == "__main__":
    for argv in parse_args(sep="-"):
        app.run(argv)

はい。

subappを使った依存ファイルの読み込み

subappからの依存ファイルの読み込みについてだけ説明が必要かもしれない。基本的にはそのファイルからの相対的な位置を見る(なので常にエディタ上から直接開けるような形で指定ができる。わりと此処はこだわりポイントだったりする)。

例えば、以下のようなファイルの階層を持つような定義を見てみる。

$ tree
.
├── Makefile
├── apps
│   ├── __init__
│   ├── byebye
│   │   ├── __init__.py
│   │   └── input
│   │       └── byebye.tmpl
│   └── hello
│       ├── __init__.py
│       └── input
│           └── hello.tmpl
├── definitions.py
├── output
│   ├── byebye
│   │   ├── bar.json
│   │   ├── boo.json
│   │   └── foo.json
│   └── hello
│       ├── bar.json
│       ├── boo.json
│       └── foo.json
└── scan.output

8 directories, 14 files

今回のsubappは以下の2つ。

  • apps/hello/init.py
  • apps/byebye/init.py

これらはそれぞれ、hello.tmplとbyebye.tmplを依存として持つわけだけれど。この依存の定義は以下の様な形で記述されている。

apps/hello/init.py

from egoist.app import create_subapp

app = create_subapp()

app.include("egoist.directives.define_dir")


@app.define_dir("egoist.generators.dirkit:walk")
def output__hello(*, source="input/hello.tmpl") -> None:
    from egoist.generators.dirkit import runtime

    with open(source) as rf:
        template = rf.read()

    for name in ["foo", "bar", "boo"]:
        with runtime.create_file(f"{name}.json", depends_on=[source]) as wf:
            print(template.format(name=name), file=wf)

自身のファイルからの相対パスで記述されているので、いろいろなエディタでパスが補完されるような直接開けるような形になっているはず。byebyeの方も概ね同様。

こちらもApp側の定義はincludeするだけ。

definitions.py

from egoist.app import create_app, SettingsDict, parse_args

settings: SettingsDict = {"rootdir": "", "here": __file__}
app = create_app(settings)

app.include("apps.hello")
app.include("apps.byebye")


if __name__ == "__main__":
    for argv in parse_args(sep="-"):
        app.run(argv)

型が付かない

注意点として、directive (ここではdefine_cliなどのこと) app.include("<path>") によって動的に追加されるので型が付かない。このへんはmypy pluginを追加したり、別途.pyiを自分で書いてみたりすればある程度は対応できそうだけれど、そこまでやってはいない。

他の部分は概ね型がつくし、egoist自体はmypyのチェックが通る様になっているけれど、トップレベルの定義はまだuntyped。

まとめ

  • subappを追加した
  • subapp上での依存ファイルは、自身からの相対パスで記述できる
  • まだuntyped

そんなかんじ。

実際の実行例はこの辺にある。

そういえば

ああ、そうそう、 scan --graph --browse で簡単なグラフを見れる様になった。こんな感じ。

00cli 01with-template


  1. app.include("egoist.directives.define_cli")で利用可能になる app.define_cli()などのこと