[Python] colander備忘録

地味にcolander(バリデーションライブラリ)を使う機会があるので、デシリアライズを中心とした使い方をメモしておきます。
使用するバージョンは現時点での最新である1.0です。

インストールはpipで:

$ pip install colander

基本

まずは公式を例に取ってみてみましょう。

>>> import colander
>>> class Friend(colander.TupleSchema):
...     rank = colander.SchemaNode(colander.Int(), validator=colander.Range(0, 9999))
...     name = colander.SchemaNode(colander.String())
 
>>> class Phone(colander.MappingSchema):
...     location = colander.SchemaNode(colander.String(), validator=colander.OneOf(['home', 'work']))
...     number = colander.SchemaNode(colander.String())
 
>>> class Friends(colander.SequenceSchema):
...     friend = Friend()
 
>>> class Phones(colander.SequenceSchema):
...     phone = Phone()
 
>>> class Person(colander.MappingSchema):
...     name = colander.SchemaNode(colander.String())
...     age = colander.SchemaNode(colander.Int(), validator=colander.Range(0, 200))
...     friends = Friends()
...     phones = Phones()
 
>>> person = Person()
>>> # Personの定義に合っていれば、整形されたオブジェクトが返却される
>>> person.deserialize({
...    'name':'keith',
...    'age':'20',
...    'friends':[('1', 'jim'),('2', 'bob'), ('3', 'joe'), ('4', 'fred')],
...    'phones':[
...        {'location':'home', 'number':'555-1212'},
...        {'location':'work', 'number':'555-8989'},
...    ],
... })
{
    'age': 20,
    'friends': [(1, u'jim'), (2, u'bob'), (3, u'joe'), (4, u'fred')],
    'name': u'keith',
    'phones': [
        {'location': u'home', 'number': u'555-1212'},
        {'location': u'work', 'number': u'555-8989'}
    ]
}
 
>>> # わざと形式を崩してみる(phones)と例外が発生する
>>> person.deserialize({
...    'name':'keith',
...    'age':'20',
...    'friends':[('1', 'jim'),('2', 'bob'), ('3', 'joe'), ('4', 'fred')],
...    'phones':'android,iphone,ガラケー',
... })
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 608, in deserialize
    return self._impl(node, cstruct, callback)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 588, in _impl
    raise error
colander.Invalid

スキーマ

colanderでは対象オブジェクトに期待する要素の組み合わせなどをSchemaクラスとして定義します。
オブジェクトがネストされている場合は上記例のように、子要素を定義してクラス変数としてネストを表現します。
スキーマは定義時に型(SchemaType)と結びつく必要があります。

SchemaNode

スキーマの基底クラスです。
末端の要素はこのクラスを利用してSchemaTypeクラスと共に定義します。

コレクション(コンテナ)型と結びつくスキーマは既に以下の様に定義されています。
基本的に継承して利用し、そのまま使う機会は決して多くはありません。

MappingSchema

いわゆる辞書型のスキーマです。
コレクション型スキーマで利用頻度の最も高いものがおそらくこれなんじゃないでしょうか。「Schema」という別名をもちます。

以下は、ユーザのプロパティを検査する例です。

>>> import colander
>>> class User(colander.MappingSchema):
...     id = colander.SchemaNode(colander.Int(), missing=None)
...     name = colander.SchemaNode(colander.Str())
...     age = colander.SchemaNode(colander.Int())
...
>>> user = User()
>>> user.deserialize({
...     'name': 'you',
...     'age': '20',
...     'gender': 'man',
... })
{'age': 20, 'id': None, 'name': u'you'}
 
>>> user.deserialize({
...     'name': 'me',
... })
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 608, in deserialize
    return self._impl(node, cstruct, callback)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 588, in _impl
    raise error
colander.Invalid: {'age': u'Required'}

クラス変数名が辞書のキーに当たります。
定義されていない要素(キー)は出力されません。

SequenceSchema

可変長の配列型スキーマです。正確に言うと一次元のイテラブルオブジェクトを期待します。(なぜか文字列は不可)
可変長であるためすべての要素に対して同じバリデーションを行います。

以下は0~100間の数値を複数受け取る例です。

>>> import colander
>>> class Scores(colander.SequenceSchema):
...     score = colander.SchemaNode(colander.Int(), validator=colander.Range(0, 100))
...
>>> scores = Scores()
>>> scores.deserialize([50, 100, '20'])
[50, 100, 20]
 
>>> scores.deserialize((i for i in range(5)))
[0, 1, 2, 3, 4]
 
>>> scores.deserialize([1, 'a'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 992, in deserialize
    return self._impl(node, cstruct, callback, accept_scalar)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 934, in _impl
    raise error
colander.Invalid: {'1': u'"a" is not a number'}
 
>>> scores.deserialize([-1, 100])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 992, in deserialize
    return self._impl(node, cstruct, callback, accept_scalar)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 934, in _impl
    raise error
colander.Invalid: {'0': u'-1 is less than minimum value 0'}
 
>>> scores.deserialize('123')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 992, in deserialize
    return self._impl(node, cstruct, callback, accept_scalar)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 920, in _impl
    value = self._validate(node, value, accept_scalar)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 908, in _validate
    mapping={'val':value})
colander.Invalid: {'': u'"123" is not iterable'}

MappingSchemaと違ってキーが無いためクラス変数名は不要なわけですが、代入文でないと(式分だと)クラスの値と結びついてくれないため、必要がなくても「xxx = Scheme〜()」のようにする必要があります。この時の変数名はなんであっても構いません(大して検証してないけどね!)
あと1つ目のクラス変数しか使用されないという点に注意しましょう。

もし、「先頭はInt型、2番目はStr型」のように特定のインデックスにバリデーションをかけたい場合は以下のTupleSchemaを利用します。

TupleSchema

固定長の配列型スキーマです。個々の要素に対して柔軟にバリデーションをかけられます。
対象のオブジェクト長は固定であり、要素に過不足があると例外が発生します。
TupleSchemaでは要素にmissingパラメータを指定してもデフォルト値は与えられません。注意しましょう。

TODOを表すスキーマを考えてみました。

>>> import colander
>>> from datetime import datetime
>>> class Todo(colander.TupleSchema):
...     period = colander.SchemaNode(colander.DateTime())
...     detail = colander.SchemaNode(colander.Str())
...     done = colander.SchemaNode(colander.Bool(), missing=False)  # TupleSchemaではmissing指定は意味がない
 
>>> todo = Todo()
>>> todo.deserialize(['2015-01-01T12:30', u'初詣', 'true'])
(datetime.datetime(2015, 1, 1, 12, 30, tzinfo=<iso8601.iso8601.Utc object at 0x7fb873ad1e10>), u'\u521d\u8a63', True)
 
>>> # missingでFalseを指定しても省略できない
>>> todo.deserialize(['2015-02-14', u'チョコをもらう'])  # 色んな意味でInvalid
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 742, in deserialize
    return self._impl(node, cstruct, callback)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 708, in _impl
    value = self._validate(node, value)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 686, in _validate
    mapping={'val':value, 'exp':nodelen, 'was':valuelen})
colander.Invalid: {'': u'"[\'2015-02-14\', u\'\\u30c1\\u30e7\\u30b3\\u3092\\u3082\\u3089\\u3046\']" has an incorrect number of elements (expected 3, was 2)'}

入力値が固定長であるだけで型までタプルである必要はありません。
colanderのスキーマ(SchemaNode及びSchemaNodeの継承クラス)は自身が定義された順番を記憶しています。
そのためTupleSchemaでは、定義したフィールドが上から順に配列のインデックスに対応します。
上記の例だと「period」は0、「detail」は1、「done」は2と言った具合です

スキーマタイプ(SchemaType)

すでに大半を例に出してしまいましたが、主要なものを見て行きましょう。
単体で使用することはできないため、SchemaNodeクラスと共に利用する必要があります。

String(別名:Str)

文字列型です。「Str」というエイリアスを持ちます。
入力値として文字列以外は受け付けません。
また、マルチバイト文字列の場合はユニコード型で指定する必要があります。(ちゃんとしたエラーメッセージが出なくてハマった)

>>> colander.SchemaNode(colander.Str()).deserialize('test')
u'test'
 
>>> colander.SchemaNode(colander.Str()).deserialize(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1145, in deserialize
    mapping={'val':cstruct, 'err':e}))
colander.Invalid: {'': u"1 is not a string: {'': ''}"}
 
>>> colander.SchemaNode(colander.Str()).deserialize('テスト')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1145, in deserialize
    mapping={'val':cstruct, 'err':e}))
colander.Invalid
 
>>> colander.SchemaNode(colander.Str()).deserialize(u'テスト')
u'\u30c6\u30b9\u30c8'

返却値は強制的にunicode型となります。

Integer(別名:Int)

整数型です。「Int」というエイリアスをもちます。

>>> import colander
>>> colander.SchemaNode(colander.Int()).deserialize(1)
1
 
>>> colander.SchemaNode(colander.Int()).deserialize('1')
1
 
>>> colander.SchemaNode(colander.Int()).deserialize(1.0)
1
 
>>> colander.SchemaNode(colander.Int()).deserialize('1.0')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1176, in deserialize
    mapping={'val':cstruct})
colander.Invalid: {'': u'"1.0" is not a number'}

返却値は強制的にint型となります。

Float

浮動小数点数型です。

>>> import colander
>>> colander.SchemaNode(colander.Float()).deserialize(1)
1.0
>>> colander.SchemaNode(colander.Float()).deserialize('1')
1.0
>>> colander.SchemaNode(colander.Float()).deserialize(1.0)
1.0
>>> colander.SchemaNode(colander.Float()).deserialize('1.0')
1.0

返却値は強制的にfloat型となります。

Boolean(別名:Bool)

真偽値型です。「Bool」というエイリアスを持ちます。
Pythonのbool()とは若干動作が違うので注意しましょう。

>>> import colander
>>> colander.SchemaNode(colander.Bool()).deserialize(True)
True
>>> colander.SchemaNode(colander.Bool()).deserialize(False)
False
>>> colander.SchemaNode(colander.Bool()).deserialize(None)
True
>>> colander.SchemaNode(colander.Bool()).deserialize('true')
True
>>> colander.SchemaNode(colander.Bool()).deserialize('false')
False
>>> colander.SchemaNode(colander.Bool()).deserialize('null')
True
>>> colander.SchemaNode(colander.Bool()).deserialize(1)
True
>>> colander.SchemaNode(colander.Bool()).deserialize(0)
False
>>> colander.SchemaNode(colander.Bool()).deserialize(1.0)
True
>>> colander.SchemaNode(colander.Bool()).deserialize(0.0)
True
>>> colander.SchemaNode(colander.Bool()).deserialize('1')
True
>>> colander.SchemaNode(colander.Bool()).deserialize('0')
False
>>> colander.SchemaNode(colander.Bool()).deserialize('a')
True

返却値は強制的にbool型となります。

DateTime/Date/Time

それぞれ「ISO 8601」形式の文字列をパースし、datetime, date, time型を返却します。

>>> import colander
>>> colander.SchemaNode(colander.DateTime()).deserialize('2000-01-01 23:59:30.999999+09:00')
datetime.datetime(2000, 1, 1, 23, 59, 30, 999999, tzinfo=<FixedOffset '+09:00' datetime.timedelta(0, 32400)>)
>>> colander.SchemaNode(colander.Date()).deserialize('2000-01-01 23:59:30.999999+09:00')
datetime.date(2000, 1, 1)
>>> colander.SchemaNode(colander.Time()).deserialize('2000-01-01 23:59:30.999999+09:00')
datetime.time(23, 59, 30, 999999)
 
>>> # DateTimeとDateは「月」以降省略可
>>> colander.SchemaNode(colander.Date()).deserialize('2000')
datetime.date(2000, 1, 1)
 
>>> # Timeは「秒」以降省略可
>>> colander.SchemaNode(colander.Time()).deserialize('23:59')
datetime.time(23, 59)

返却値は強制的にパース後の型となります。

List

配列型です。

>>> colander.SchemaNode(colander.List()).deserialize([1, 2, 3])
[1, 2, 3]
 
>>> # 辞書の場合はkeys
>>> colander.SchemaNode(colander.List()).deserialize({'a': 1, 'b': 2, 'c': 3})
['a', 'c', 'b']
 
>>> colander.SchemaNode(colander.List()).deserialize('abc')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 854, in deserialize
    _('${cstruct} is not iterable', mapping={'cstruct': cstruct})
colander.Invalid: {'': u'abc is not iterable'}

返却値は強制的にlist型となります。

Set

集合(set)型です。

>>> import colander
>>> colander.SchemaNode(colander.Set()).deserialize(['a', 'b', 'c', 'b'])
set(['a', 'c', 'b'])

返却値は強制的にset型となるため、すべての要素がハッシュ化可能オブジェクトである必要があります。

Mapping/Sequence/Tuple

それぞれ、辞書、配列(可変長)、配列(固定長)を表していますが、これらの型は子要素をもつことを前提としているため、型だけをチェックする用途としては向いていません。

>>> import colander
>>> colander.SchemaNode(colander.Mapping()).deserialize({'a': 1})
{}
>>> colander.SchemaNode(colander.Sequence()).deserialize([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 992, in deserialize
    return self._impl(node, cstruct, callback, accept_scalar)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 927, in _impl
    result.append(callback(node.children[0], subval))
IndexError: list index out of range
>>> colander.SchemaNode(colander.Tuple()).deserialize([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 742, in deserialize
    return self._impl(node, cstruct, callback)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 708, in _impl
    value = self._validate(node, value)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 686, in _validate
    mapping={'val':value, 'exp':nodelen, 'was':valuelen})
colander.Invalid: {'': u'"[1, 2, 3]" has an incorrect number of elements (expected 0, was 3)'}

バリデータ(Validator)

スキーマの型だけでは入力値チェックとしては不十分な場合に使用します。

Range

対象の値が特定の範囲に収まっているかを判断するバリデータです。

指定方法:

Range(min=None, max=None, min_err=None, max_err=None)

値比較ができればオブジェクトは数値以外でも構いません。

>>> import colander
>>> i = colander.SchemaNode(colander.Int(), validator=colander.Range(0, 10))
>>> i.deserialize(0)
0
>>> i.deserialize(10)
10
>>> i.deserialize(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 345, in __call__
    raise Invalid(node, min_err)
colander.Invalid: {'': u'-1 is less than minimum value 0'}
>>> i.deserialize(11)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 350, in __call__
    raise Invalid(node, max_err)
colander.Invalid: {'': u'11 is greater than maximum value 10'}
 
>>> s = colander.SchemaNode(colander.Str(), validator=colander.Range('b', 'e'))
>>> s.deserialize('b')
u'b'
>>> s.deserialize('e')
u'e'
>>> s.deserialize('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 345, in __call__
    raise Invalid(node, min_err)
colander.Invalid: {'': u'a is less than minimum value b'}
>>> s.deserialize('f')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 350, in __call__
    raise Invalid(node, max_err)
colander.Invalid: {'': u'f is greater than maximum value e'}

Length

対象の値の長さが特定の範囲に収まっているかどうかを判断するバリデータです。
長さ取得(len())ができれば、コンテナ型でなくても構いません。

指定方法:

Length(self, min=None, max=None)
>>> import colander
>>> l = colander.SchemaNode(colander.List(), validator=colander.Length(3, 5))
>>> l.deserialize([1, 2, 3])
[1, 2, 3]
>>> l.deserialize([1, 2, 3, 4, 5])
[1, 2, 3, 4, 5]
>>> l.deserialize([1, 2])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 365, in __call__
    raise Invalid(node, min_err)
colander.Invalid: {'': u'Shorter than minimum length 3'}
>>> l.deserialize([1, 2, 3, 4, 5, 6])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 371, in __call__
    raise Invalid(node, max_err)
colander.Invalid: {'': u'Longer than maximum length 5'}
 
>>> s = colander.SchemaNode(colander.Str(), validator=colander.Length(3, 5))
>>> s.deserialize('abc')
u'abc'
>>> s.deserialize('abcde')
u'abcde'
>>> s.deserialize('ab')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 365, in __call__
    raise Invalid(node, min_err)
colander.Invalid: {'': u'Shorter than minimum length 3'}
>>> s.deserialize('abcdef')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 371, in __call__
    raise Invalid(node, max_err)
colander.Invalid: {'': u'Longer than maximum length 5'}

OneOf

対象が選択肢に含まれるかを判定するバリデータです。
オブジェクト型は問いません。

>>> import colander
>>> s = colander.SchemaNode(colander.Str(), validator=colander.OneOf(['a', 'b']))
>>> s.deserialize('a')
u'a'
>>> s.deserialize('b')
u'b'
>>> s.deserialize('c')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 384, in __call__
    raise Invalid(node, err)
colander.Invalid: {'': u'"c" is not one of a, b'}

Regex

対象が正規表現にマッチするかを判断するバリデータです。
オブジェクト型は文字列に限られます。

指定方法:

Regex(self, regex, msg=None, flags=0)

flagsは正規表現のフラグです。

>>> import colander
>>> s = colander.SchemaNode(colander.Str(), validator=colander.Regex('\d+'))
>>> s.deserialize('1')
u'1'
>>> s.deserialize('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 289, in __call__
    raise Invalid(node, self.msg)
colander.Invalid: {'': u'String does not match expected pattern'}

Regexの継承クラスとしてEmailバリデータがあります。

>>> import colander
>>> s = colander.SchemaNode(colander.Str(), validator=colander.Email())
>>> s.deserialize('test@example.com')
u'test@example.com'
 
>>> # TOPレベルドメインは必須らしい
>>> s.deserialize('test@example')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 289, in __call__
    raise Invalid(node, self.msg)
colander.Invalid: {'': u'Invalid email address'}

カスタマイズ

上記バリデータだけでは要件を満たせない場合も多いので自分で作ってみます。

以下は特定の値が含まれていないことをチェックするバリデータです。

>>> import colander
 
>>> def dislike(value):
...     return value not in [u'生魚', u'トマト']
 
>>> prejudice = colander.SchemaNode(colander.Str(), validator=colander.Function(dislike))
>>> prejudice.deserialize(u'焼き魚')
u'\u713c\u304d\u9b5a'
>>> prejudice.deserialize(u'トマト')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 255, in __call__
    self.msg, mapping={'val':value}))
colander.Invalid: {'': u'Invalid value'}

もしくは以下のようにバリデーションクラスを定義してもOKです。

>>> import colander
>>> class Any(object):
...     def __init__(self, blacklist):
...         self.blacklist = blacklist
...
...     def __call__(self, node, value):
...         if value in self.blacklist:
...             raise colander.Invalid(node, colander._(
...                 '"${val}" is included in ${blacklist}',
...                 mapping={
...                     'val': value,
]),                     'blacklist': ', '.join(['%s' % x for x in self.blacklist
...                 }
...             ))
 
>>> prejudice = colander.SchemaNode(colander.Str(), validator=Any([u'生魚', u'トマト']))
>>> prejudice.deserialize(u'焼き魚')
u'\u713c\u304d\u9b5a'
>>> prejudice.deserialize(u'トマト')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "<stdin>", line 11, in __call__
colander.Invalid: {'': u'"\u30c8\u30de\u30c8" is included in \u751f\u9b5a, \u30c8\u30de\u30c8'}

上記が全てではありません。ほかにも存在するのでぜひ調べてみてください。

遅延評価

バリデーションで必要な値が常に使うタイミングで手に入っているとは限りません。
deferredとbindという仕組みを組み合わせてパラメータ読み込みを遅延させることができます。

deferred

deferredデコレータにより、遅延対象の関数をデコレートします。
関数自体がバリデータというわけではなく、バリデータ関数を作るファクトリ(関数)であることを理解しましょう。

ファクトリの定義方法:

validator(node, kw)
>>> import colander
>>> @colander.deferred
... def a_validator(node, kw):
...     a = kw['a']
...     def _validator(v):
...         return a
...     return colander.Function(_validator, "a is not True")
 
>>> class TestSchema(colander.MappingSchema):
...     x = colander.SchemaNode(colander.Int(), validator=a_validator)
...     @colander.deferred
...     def validator(node, kw):
...         b = kw['b']
...         def _validator(v):
...             return b
...         return colander.Function(_validator, "b is not True")

大抵の場合、変数を閉じ込めるクロージャとして機能することになると思います。

bind

bindメソッドによりスキーマインスタンスに変数を割り当てます。
キーワード引数で指定したパラメータがファクトリの第2引数に辞書として渡されます(kw)。

>>> test = TestSchema()
>>> # このタイミングでa,bの内容がわかった(ということにしよう)
>>> test2 = test.bind(a=False, b=True)
>>> test2.deserialize({'x': 1})    # aがFalseなのでエラーになるはず
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1921, in deserialize
    appstruct = self.typ.deserialize(self, cstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 608, in deserialize
    return self._impl(node, cstruct, callback)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 588, in _impl
    raise error
colander.Invalid: {'x': u'a is not True'}
 
>>> test3 = test.bind(a=True, b=False)
>>> test3.deserialize({'x': 1})  # bがFalseなのでエラーになるはず
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 1943, in deserialize
    self.validator(self, appstruct)
  File "/usr/lib/python2.7/site-packages/colander/__init__.py", line 255, in __call__
    self.msg, mapping={'val':value}))
colander.Invalid: {'': u'b is not True'}
 
>>> test4 = test.bind(a=True, b=True)
>>> test4.deserialize({'x': 1})  # 両方共TrueなのでOK(deserializeできる)
{'x': 1}

bindメソッドは新たなスキーマインスタンスを返却することに注意しましょう(要代入)。

ちなみにdeferredデコレータは関数ではなくクラスです。バリデータを作成するファクトリ関数ををdeferredインスタンスで覆い、bindメソッドで変数を割り当てた後にファクトリがバリデータを返却するという実装になっています。

以上、半分くらい機能はカバーできたんじゃないかと思います。