こういうちょっとしたデータの受け渡しどうするんだという話
はじめに
今自分で作っている dictknife というリポジトリについにコマンドを追加してしまった。
色々あるのだけれど。今回は dictknife transform
の話。
transform
何かしらの形状の変換をしたいことがある。
例えば、こういう入力を受け取って、
properties: name: type: string description: name of something age: type: integer minimum: 0
こういう出力を返したい。
definitions: person: properties: name: type: string description: name of something age: type: integer minimum: 0
結局、load,dumpを無視するとコード自体は以下だけなのだけれど。
def transform(d): return {"definisions": {"person": d}}
これを省力な形で提供するのがちょっとだけ面倒。
面倒くさい点
面倒くさい点は2つある
- transform 関数の取得
- transform 関数へ引数を渡したい場合の方法
transform 関数の取得
上の方法で考えた変換(definisions.personでwrapするもの)がもし仮にどこかのpackageで提供されているとする。 すると以下の様に書ける気がする。
package pathを指定する場合
例えば、 foo.bar.transform:lifting
で提供されている場合は以下の様に書ける。
$ dictknife transform --function foo.bar.transform:lifting ...
でも、これはちょっと使いづらい。そもそも何度も使って便利だと分かっているものでなければpackageになっていることが少ない。テキトウにファイルを置いてPYTHONPATHを追加するという方法でできなくもないけれど。やっぱり面倒。
$ PYTHONPATH=../myscript dictknife transform --function transform:lifinting ...
package path or 物理的な pathで指定する場合
直接ファイルを指定出来るようになれば十分か?一応、昔作ったmagicalimport というpackageを使うとそれは出来る。例えば上の例は以下のように書けるように出来る。
$ dictknife transform --function ../myscript/transform.py:lifting ...
eval的な何か
しかし、それでも使いにくい。何かしらのちょっとした処理を行いたいときには、一時的なファイルすら作りたくない場合がある。(というよりも、temporaryなscriptや関数群の置き場を決めるという意思決定がしたくないというような状況)。仕方が無いので禁断の果実であるevalを使うことにする。
$ dictknife transform --code 'lambda d: {"definitions": {"person": d}}' ...
transform 関数へ引数を渡したい場合の方法
trasnform 関数が取得できれば万事OKという訳でもない。冒頭の変換について考えてみても、常に "person" という固定の名前で変換したいという状況はあんまりない。どうにかしてtransform 関数へ情報を受け渡したい(そもそもtransformは関数だけで十分なのかという話もあるけれど。あんまり複雑なことを考えたくはないので今回は関数で良いということにしてみる)。
コマンドライン引数で渡す方法
コマンドライン引数で渡す方法はすごく分かりやすい。個別にコマンドを作るということを念頭に置くならこの形が最適かもしれない。とは言え、これを汎用的に提供する機能を作ろうとすると、もはやtransform コマンドのジェネレーターのようなものを作る事になってしまう。
$ dictknife transform --name person --code 'lambda d, name="NAME": {"definisions": {name: d}}' ...
configファイルで渡す方法
汎用的なtransformということを考えると以下の様な関数を作る事にならざる負えない。
def lifting(data, **kwargs): ...
pythonに限って言えばキーワード引数になっている方がべんりかもしれない。
def lifting(data, name="NAME"): ...
幸い functools.partial
に辞書を渡してあげるとキーワード引数を埋める事ができる。
from functools import partial fn = partial(lifting, **{"name": "person"}) fn(data) # transform!!
dictを受け取って**
で展開してあげれば良いかもしれない。dictを取得する方法を考えてみる。
幸い元々JSONやYAMLを入出力するライブラリ上のコマンドなのでconfig用の情報をこれらのフォーマットで受け取るという形で考えても良いかもしれない。
$ dictknife transform --function "./myscript.py:lifting" --config-file ./config.json ...
とは言え、これは transform 関数を作ったときと同じ状況に陥る。本当に単純な処理に関してはファイルなんて作りたくない。
JSONで受け取れる引数を追加する
基本的にはシェル上のコマンドとJSONを直接扱う方法と言うのはあまり良い方法とは思えないのだけれど。jo やその類型のものを使えば幾分かマシになるだろうということで。JOSNを直接受け取れるようにする。
$ dictknife transform --code 'lambda d,name="foo": {"definisions": {name: d}}' --config '{"name": "person"}' ...
一応ワンライナーで済ませる事が出来るようになった。
その他細々としたこと
パイプで繋げられるようにしたい。パイプで繋げられるようなインターフェイスと言うのは以下のようなもの
$ cat src.yaml | dictknife transform <> > dst.yaml
とは言え、明示的に入出力を指定したい場合もある(go generateで使うときなど)。
$ dictknife transform --src src.yaml --dst dst.yaml <>
現在の状態
現在の状態は以下のようなもの。真面目にdescriptionは書いていないですね。。
$ dictknife transform Usage: dictknife transform [OPTIONS] transform dict Options: --src PATH --dst PATH --config TEXT --config-file PATH --code TEXT --function TEXT --help Show this message and exit.
--src
入力ファイル--dst
出力ファイル--config
transform関数に渡すdictのリテラル的な文字列を受け取る--config-file
transform関数に渡すdictのファイル(config
のファイルversion)--code
transform関数のワンラインナーを書きたいときに使う(eval)--function
transform関数
追記:
環境変数で設定するみたいな方法もあるのかもしれない?とは言えネストした構造がつらそう。
追記:
JSONを文字列で受け取れるような構造は jq
で取り出す形にすると相性が良いかもしれない。