marshmallowでschemaに制限を加えてみる
marshmallowでschemaに制限を加えてみる。この制限をprotocolと呼ぶとしてこのprotocolの実装をどうするかと言うとこんな感じ。 marshmallowの流儀に従うならSchemaOptsを拡張するのが自然そう。
marshmallow_protocol.py
class CustomOpts(SchemaOpts): protocol = None class CustomMeta(SchemaMeta): def __init__(self, name, bases, attrs): super().__init__(name, bases, attrs) k = "_protocol_verified" protocol = self.Meta.protocol if protocol is not None and k not in self.__dict__: protocol(self) setattr(self, k, protocol) class ProtocolError(Exception): pass class Schema(BaseSchema, metaclass=CustomMeta): class Meta: protocol = None
こんな感じ。marshmallowは何らかの設定(Option)を取る時にMetaというクラスに書くことが多いのだけれど。 (このような実装になっているパッケージは結構多く存在している。一番身近な例でいうとdjangoかもしれない)
このmetaにprotocolという属性を追加する。
protocol
protocolは、schemaを引数として呼べるcallable。やばかったらProtocolErrorの例外が発生する。
ついでに、何度も呼ばれることが無いようcacheする意味合いとどのprotocolで検証されたかを把握するために、_protocol_verified
というフィールドに利用されたprotocolを埋め込む。
呼ばれるタイミングは、クラス定義時。
利用例
metadataに制限を加えてみようというprotocol
例えば、marshmallowのfields.Fieldはmetadataを付加できる。このmetadataに制限を加えてみようというprotocol。
もう少し問題認識を共有しておくと、まず、marshmallow.fields.Fieldの__init__()
は以下の様になっている。
class Field(FieldABC): def __init__(self, default=missing_, attribute=None, load_from=None, dump_to=None, error=None, validate=None, required=False, allow_none=None, load_only=False, dump_only=False, missing=missing_, error_messages=None, **metadata): # do something() self.metadata = metadata
metadataは自由につけられるので例えば以下の様なSchemaを定義してしまえる。
from marshmallow import Schema, fields, validate class S(Schema): v = fields.Integer(validator=validate.Range(max=10))
ちなみに、これはvalidateをvalidatorとtypoしている。このschemaはmetadataとしてvalidate.Rangeのオブジェクトが格納されたvalidな定義。 もちろん、設定したRangeによるvalidateはload時には動かないわけだけれど。
S().load({"v": 1000000000000000}) # エラーは発生しない
このとき、metadataに制限を加えてあげてみることにする。例えば、swaggerなどのようにmetadata的なものをx_
ではじまるものだけに絞るというprotocolを定義する。(swaggerの場合はx-
ではじまるもの)
from marshmallow.schema import BaseSchema from marshmallow_protocol import CustomMeta, ProtocolError def x_vendor_prefix_only_metadata(cls): for f in cls._declared_fields.values(): for k in f.metadata.keys(): if not k.startswith("x_"): raise ProtocolError(k) class Schema(BaseSchema, metaclass=CustomMeta): class Meta: protocol = x_vendor_prefix_only_metadata
定義したprotocolがdefaultのSchemaを作り、これを利用してみる。
from marshmallow import fields, validate class S(Schema): # marshmallow_protocol.ProtocolError: validator v = fields.Integer(validator=validate.Range(max=10))
validatorはx_
で始まっていないのでエラーになる(エラーメッセージが少し雑すぎるかもしれない)。
もちろん、typoしていなければエラーにはならないし。protocolを無効にしてあげればエラーにならない。
from marshmallow import fields, validate class S2(Schema): v = fields.Integer(validate=validate.Range(max=10), x_ja="値") class Ignored(Schema): class Meta: protocol = None v = fields.Integer(validator=validate.Range(max=10))
おしまい。
kamidanaにバッチモードをつけてみた
kamidanaにバッチモードをつけた。これは実験的な機能でまだmasterにはマージされていない。 やっぱり一回のコマンドで終わらせたほうが早いので欲しくなってしまった。
ただまだ見通しが良い感じにできていない(そういう意味でもまだ実験的)。
実行時間の比較
雑に単純なテンプレートを3回実行する処理の時間を測る。
個別にレンダリングした場合
個別にレンダリングした場合の時間は以下。
$ time make 00 echo '{"name": "foo"}' | kamidana -i json hello.j2 --dst 00out/foo.hello echo '{"name": "bar"}' | kamidana -i json hello.j2 --dst 00out/bar.hello kamidana hello.j2 --data me.json --dst 00out/me.hello real 0m0.727s user 0m0.696s sys 0m0.030s
この時のテンプレートは以下のようなもの。
hello {{name}}
まとめてレンダリングした場合
まとめてバッチモードでレンダリングした場合は以下の様な感じ。kamidanaではなくkamidana-batchコマンドを使う。 jinja2の読み込みなどに時間が掛かるのでやっぱり一回で終わらせたほうが早い。
$ time make 01 kamidana-batch 01batch.json --outdir=01out INFO:kamidana.driver:out: 01out/foo.hello INFO:kamidana.driver:out: 01out/bar.hello INFO:kamidana.driver:out: 01out/me.hello real 0m0.250s user 0m0.233s sys 0m0.017s
この時渡したファイルは以下のようなもの。
01batch.json
[ {"template": "./hello.j2", "data": {"name": "foo"}, "outfile": "foo.hello"}, {"template": "./hello.j2", "data": [{"name": "bar"}], "outfile": "bar.hello"}, {"template": "./hello.j2", "data": "me.json", "outfile": "me.hello"} ]
以下のフィールドは必須
- template
- outfile
makefile
作業を行った時のmakefile
default: $(MAKE) 00 $(MAKE) 01 diff -ur 00out 01out # onefile 00: echo '{"name": "foo"}' | kamidana -i json hello.j2 --dst 00out/foo.hello echo '{"name": "bar"}' | kamidana -i json hello.j2 --dst 00out/bar.hello kamidana hello.j2 --data me.json --dst 00out/me.hello # batch 01: kamidana-batch 01batch.json --outdir=01out clean: rm -rf 00out 01out