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))
おしまい。