vim-quickrunで標準入力が受け取れない

コードの実行 -> 修正というサイクルを早めるのに便利なvim-quickrunだが、実行時に以下のようにすることで<input_file>の中身を標準入力として受け取ることができる。

:QuickRun -input <input file>

https://github.com/thinca/vim-quickrun/blob/877c8d0/doc/quickrun.txt#L110

のだが、自分の環境でやってみたところ入力が受け取れなかった。

以下のようにして実行すると実行時のコマンドや渡した引数などのデバッグ情報が見られるので確認したものの想定どおりの形になっていた。

:QuickRun -input <input file> -debug x
:echo g:x

以下のような設定を言語共通で入れていたのでその辺が悪さをしてるのかと思い無効化して試してみたところ、最初に挙げたコマンドでちゃんと標準入力が受け取れることがわかった。

let g:quickrun_config = {
\   '_': {
\     'input': '=%{b:input}', 'cmdopt': '%{b:cmdopt}', 'args': '%{b:args}',
\     'runner': 'vimproc',
\     'runner/vimproc/updatetime': 50,
\     'outputter' : 'error',
\     'outputter/buffer/opener' : 'new',
\     'outputter/buffer/close_on_empty' : 1,
\     'outputter/buffer/running_mark' : 'running...',
\     'outputter/error/success' : 'buffer',
\     'outputter/error/error'   : 'quickfix',
\     'hook/time/enable': 1,
\   },

なので切り分けつつ特定していくと原因はrunnerとしてvimprocを指定していることだった。vimprocはquickrunの実行を別プロセスで並行して行うために入れていた設定だが、これによってinputが受け取れなくなっているようだった。

runnerをデフォルトのものに直して実行することで問題なく動くようになった。

:QuickRun -input <input file> -runner system

argparse-dataclassからhydraに移行する

argparse-dataclassを少し使っていたのだが、hydraに移行してみたのでメモ。

前回書いたこちらの話に関係するが、解決したもののやはりhydraを試してみるかと思ったため。
https://jsapachehtml.hatenablog.com/entry/2021/10/23/184058

題材としては今年始めくらいにやっていたテキスト感情分類のrepoを使う。hydraを導入した際の差分はこちら。
https://github.com/y-kamiya/emotion-classification/pull/11

元々argparse-dataclassを使っていたためdataclassでオプションのスキーマが定義されている状態からスタートしているのもありStructured Configsの形で入れた。導入に必要な情報はこちらのStructured Configs用のチュートリアルを見ればOK。
https://hydra.cc/docs/tutorials/structured_config/intro

この形にしておくことで、dataclassとしてオプションのスキーマを定義することができ、それによって型による入力値のチェックが可能になる。あとそもそもオプションがクラスの形で定義されている方が実装上便利。

hydra全体の使い方については既にわかりやすく書かれたブログなどたくさん上がっているのでそちらを参考にするとして、ここでは移行する際に調べたり考えたりした点を書いておく。

argparseにおけるchoices

hydraではargparseにおけるchoicesの指定そのままのものはない(特定の文字列のみを引数として許す機能)。ただし、スキーマの型指定が可能であるためenumを利用することで実装的にはより使いやすく定義できる。log levelを取れるようにしていたのでそれに関する部分だけ例として書くと

# argparse-dataclassの場合
class Config:
    loglevel: str = field(
        default="INFO",
        metadata=dict(choices=["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]),
    )
# hydraの場合
class LogLevel(Enum):
    DEBUG = auto()
    INFO = auto()
    WARN = auto()
    ERROR = auto()
    CRITICAL = auto()

class Config:
    loglevel: LogLevel = LogLevel.INFO

hydraでは上記の書き方にしておくことで実行の際は以下のように指定できる。

$ python main.py loglevel=DEBUG

元々choicesを使ったオプションの指定がいくつかあり、その判定のためにオプションの値となる文字列との比較がコード内に存在していた。いずれenumにしようかと思っていたが今回の対応のおかげで必然的に修正されることに。

Noneの扱い

hydraのStructured Configsでは例えば str と指定したオプションはデフォルト値としての指定も含めてNoneを入れることができない。

Noneを取れるようにするには以下のようにする必要がある。

from typing import Optional

class Config:
    example: Optional[str] = None

ただ、元々のオプションの中でNoneを使っていたものを見てみるとそのほとんどが良くない使い方をしてる部分だった。本来的にはconfigに入れるべきではないオブジェクトを取り回ししやすいようにconfigに入れてただけ。こちらのloggerとか。
https://github.com/y-kamiya/emotion-classification/pull/11/files#diff-e9f42701dc7658ddc2df3f56acc27573667c95b86723dcbe10ceb77ece3ac793L54

私は基本的にtrainerやdataset用のクラスなどすべてにconfigオブジェクトを渡してアクセス可能な形で実装するようにしていたため、configに入れておくと手間がかからないというだけだった。これもいずれ、、と思いつつ案の定そのままになっていたものなのでこれを機にconfigから外した。

また、hydraの制約としてもそのような使い方はできないようになっており、Structured Configsとして正しく解釈されるのは以下の型のみで他はエラーとなる。

  • Primitive types (int, bool, float, str, Enums)
  • Nesting of Structured Configs
  • Containers (List and Dict) containing primitives or Structured Configs
  • Optional fields

cf. https://hydra.cc/docs/tutorials/structured_config/intro#structured-configs-supports

post_initについて

dataclassでは __post_init__ というメソッドを定義することで __init__の後にすべき処理を定義できる。
https://docs.python.org/ja/3/library/dataclasses.html#post-init-processing

hydraではconfigが生成される際にこちらの処理は反映されていなかった。少し上で書いたカスタムクラスのオブジェクトを保持する際に、その初期化に用いていたので実質使う必要がなくなったため問題ないが一応メモしておく。

別の設定値を参照

configに存在する値を参照して別の値を設定することができる。
https://hydra.cc/docs/patterns/specializing_config#modified-configyaml

defaults:
  - dataset: imagenet
  - model: alexnet
  - optional dataset_model: ${dataset}_${model}

これはOmegaConfのvariable interpolationという機能をそのまま使ったものなので詳細はこちら。
https://omegaconf.readthedocs.io/en/latest/usage.html#variable-interpolation

注意点としては文字列として挿入されることになるということ。boolとして扱おうとしてうまくいかなかった例
https://github.com/y-kamiya/emotion-classification/commit/e83b8e863fd6bb1c85c0aff89f2c76cea8dae6e2#diff-e9f42701dc7658ddc2df3f56acc27573667c95b86723dcbe10ceb77ece3ac793L57

自動でcwdが変更される

hydraは出力を実行毎に分けるために実行時に current working directory が変更されるようになっている。
https://hydra.cc/docs/tutorials/basic/running_your_app/working_directory

上記のページにある例だが以下のように変化する。

  • 実行前
    • /home/omry/dev/hydra
  • 実行中
    • /home/omry/dev/hydra/outputs/2019-09-25/15-16-17

入力データなどをpathで渡して使っていたため、この挙動によってデータが取得できなくなるという問題が起きた。

出力されるデータは毎回上書きしてしまってOKなのであれば設定でディレクトリがそのままになるようにすることが可能。
https://hydra.cc/docs/configure_hydra/workdir/

hydra:
  run:
    dir: .

output用のディレクトリ構成は使いたいという場合は上記の設定は変更せず、コード上で元のcwdを参照するように変更する。以下のメソッドで元のを取得可能。
https://hydra.cc/docs/tutorials/basic/running_your_app/working_directory#original-working-directory

ちなみに私は基本的にconfig全体を各クラスに引き渡して参照する形で実装していたので、mainの最初の部分でconfig内の値を修正するようにした。
https://github.com/y-kamiya/emotion-classification/blob/6560332/src/main.py#L22-L24

default list

同じ項目の設定が複数の場所に存在する場合にどれを優先するかの設定。
https://hydra.cc/docs/advanced/defaults_list

こちらに2つの設定の順番を変えた場合にどちらが優先されるかが書いてあるが、後ろに設定したもので上書きするというイメージ。
https://hydra.cc/docs/advanced/defaults_list#composition-order

設定はyamlならファイルのパス、スキーマであればCacheStoreの登録時に設定している名前を使う。
https://github.com/y-kamiya/emotion-classification/blob/6560332/src/main.py#L17

yamlで設定している内容を優先して使いたいため、このようなdefault listを定義している。
https://github.com/y-kamiya/emotion-classification/blob/6560332/src/conf/main.yaml#L1-L3

tab completion

シェル上で引数指定する際にtab補完を効かせることができる。
https://hydra.cc/docs/tutorials/basic/running_your_app/tab_completion

これはできると便利なのでやってみたのだが、tabを押すとしばらく動かなくなるくらい重くなってしまったので一旦やめた。

まとめ

今回書いていない機能も当然あるため、argparseに比べるとかなり高機能でありその分複雑であるといえる。今回のように一度やってしまえば特にわかりづらい部分はないので問題ないが初見だと追いづらい部分もある(どの設定が優先されるのかなど)

また、基本的にはOmegaConfの機能を利用してるので何かやったり引っかかったりした場合はOmegaConfの使い方を調べるのがよい。
- https://omegaconf.readthedocs.io/en/2.1_branch/index.html

参考

argparse-dataclass使用時に__future__.annotationsをimportするとエラーが出るので中身を調べてみた

argparse-dataclassはdataclassをargparseに合う形に変換して渡してくれるもの。
https://github.com/mivade/argparse_dataclass

もともとargparseを使っていたのだが、以下の理由から引数を管理するクラスを作りたくなった。

  • package化して外から使える形にする際に便利
  • 引数の設定だけまとめてあると管理しやすい

本当はhydraを使ってみようと思ったのだが、そのときやりたいことからすると過剰な感じだった(dataclassとして定義さえできればOKだったので)

ということで真っ先に目についたのが名前の通りそのままのargparse-dataclassだったのだが、少し使ってみたところタイトルの通り__future__.annotationsをimportすることでエラーが出る。

例えばこちらの設定に対して

@dataclass
class Config:
    cpu: bool = False

以下のエラー

ValueError: 'bool' is not callable

エラーを出力してる箇所はこちら
https://github.com/python/cpython/blob/v3.7.12/Lib/argparse.py#L1364
で、type というキーで登録しているのは元を辿るとこれ
https://github.com/mivade/argparse_dataclass/blob/8051690/argparse_dataclass.py#L185

ということで

  • metadataとしてtypeが定義されていればそれ type として使う
  • なければdataclassの型アノテーションとして書いたものを使う
  • argparse側ではそれがcallableであることが想定されている
  • が、実際はただの文字列として渡っている

今回はmetadataの定義はしてなかったので、型アノテーションとして書いたものが使われている。なので解決策としてはmetadataとして以下のように型を渡せばOK。

@dataclass
class Config:
    cpu: bool = field(default=False, metadata=dict(type=bool)) 

ただ、型アノテーションとして書かれたものだとなぜうまくいかないのか気になるので追ってみる。__future__.annotationsをimportしなければ問題なく通ることから、元々は組み込み型のboolコンストラクタとして使える状態だったはず。

ということで__future__.annotationsが実際に何をしているのかを見る
https://www.python.org/dev/peps/pep-0563/

PEP 563 -- Postponed Evaluation of Annotations

Just like default values, annotations are evaluated at function definition time. This creates a number of issues for the type hinting use case:

  • forward references: when a type hint contains names that have not been defined yet, that definition needs to be expressed as a string literal

  • type hints are executed at module import time, which is not computationally free.

Postponing the evaluation of annotations solves both problems.

現状だとアノテーションの評価タイミングが関数定義の時点だがそれを遅らせるというのが趣旨。それによって定義前の段階の型へのアクセスやimport時に型評価が実行されることによる計算リソースの無駄遣いをなくせる、というメリットがある。

ではいつ評価されるようになるのかというと、

To resolve an annotation at runtime from its string form to the result of the enclosed expression, user code needs to evaluate the string.

For code that uses type hints, the typing.get_type_hints(obj, globalns=None, localns=None) function correctly evaluates expressions back from its string form.

type hintを実行時に型として扱うにはユーザ側で評価の処理を実行する必要があり、それにはtyping.get_type_hintsを使えばよい。

というわけで、 タイトルに書いたようにエラーが出てしまうのは、argparse-dataclass側でそもそも__annotations__に対する対応が入ってないためで、適用するにはtype hintの型を評価するコードを入れる必要があるということだった。

そもそもannotationsはこちらの記事を参考にして型アノテーションをpython3.9以降と同様の形でかけるようにするという目的で入れていた。
https://future-architect.github.io/articles/20201223/

中身についてはちゃんと調べていなかったので今回はとても勉強になった。annotationsをimportすることでlist[str]などのように型名をそのままアノテーションとして使えるようになるというのも、型として評価されなくなるために可能になることだと考えられる。