GitHubじゃ!Pythonじゃ!

GitHubからPython関係の優良リポジトリを探したかったのじゃー、でも英語は出来ないから日本語で読むのじゃー、英語社会世知辛いのじゃー

alecthomas

voluptuous – Voluptuousは名前にもかかわらず、Pythonのデータ検証ライブラリ

投稿日:

Voluptuousは名前にもかかわらず、Pythonのデータ検証ライブラリです。 https://pypi.org/project/voluptuous

VoluptuousはPythonのデータ検証ライブラリです

Voluptuousは名前にもかかわらず 、Pythonのデータ検証ライブラリです。 これは主に、PythonにJSON、YAMLなどのデータを入力することを目的としています。

それは3つの目標を持っています:

  1. シンプルさ。
  2. 複雑なデータ構造をサポートします。
  3. 有用なエラーメッセージを提供する。

接触

Voluptuousは今メーリングリストを持っています! にメールを送る 登録するにはvoluptuous@librelist.com 指示が続きます。

また、 メールTwitterで直接私に連絡することもできます

バグを報告するには、問題を複製する方法の簡単な例とともに、GitHubに新しい問題を作成します。

ドキュメンテーション

ドキュメントはここで提供されています

変更ログ

CHANGELOG.mdを参照してください。

私に例を示す

Twitterのユーザー検索APIは次のようなクエリURLを受け入れます:

$ curl 'https://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1'

これを検証するには、次のようなスキーマを使用します。

>>> from voluptuous import Schema
>>> schema = Schema({
...   'q': str,
...   'per_page': int,
...   'page': int,
... })

このスキーマは、APIによって要求されるデータを非常に簡潔かつ大まかに説明しており、正常に動作します。 しかしそれにはいくつかの問題があります。 まず、APIの制約を完全には表現していません。 APIによると、 per_pageは最大20に制限する必要があります(デフォルトは5など)。 APIのセマンティクスをより正確に記述するには、スキーマをより完全に定義する必要があります。

>>> from voluptuous import Required, All, Length, Range
>>> schema = Schema({
...   Required('q'): All(str, Length(min=1)),
...   Required('per_page', default=5): All(int, Range(min=1, max=20)),
...   'page': All(int, Range(min=0)),
... })

このスキーマは、Twitterのドキュメンテーションで定義されているインターフェースを完全に実施しています。

“q”が必要です:

>>> from voluptuous import MultipleInvalid, Invalid
>>> try:
...   schema({})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "required key not provided @ data['q']"
True

…は文字列でなければなりません:

>>> try:
...   schema({'q': 123})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "expected str for dictionary value @ data['q']"
True

…少なくとも1文字は長さでなければなりません:

>>> try:
...   schema({'q': ''})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "length of value must be at least 1 for dictionary value @ data['q']"
True
>>> schema({'q': '#topic'}) == {'q': '#topic', 'per_page': 5}
True

“per_page”は20以下の正の整数です。

>>> try:
...   schema({'q': '#topic', 'per_page': 900})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "value must be at most 20 for dictionary value @ data['per_page']"
True
>>> try:
...   schema({'q': '#topic', 'per_page': -10})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']"
True

“ページ”は整数> = 0です:

>>> try:
...   schema({'q': '#topic', 'per_page': 'one'})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc)
"expected int for dictionary value @ data['per_page']"
>>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1, 'per_page': 5}
True

スキーマの定義

スキーマは、辞書、リスト、スカラー、 バリデーターからなるネストされたデータ構造です。 入力スキーマの各ノードは、入力データ内の対応するノードに対してパターンマッチングされます。

リテラル

スキーマ内のリテラルは、通常の等価チェックを使用してマッチングされます。

>>> schema = Schema(1)
>>> schema(1)
1
>>> schema = Schema('a string')
>>> schema('a string')
'a string'

タイプ

スキーマの型は、対応する値が型のインスタンスであるかどうかをチェックすることによって照合されます。

>>> schema = Schema(int)
>>> schema(1)
1
>>> try:
...   schema('one')
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "expected int"
True

URL

スキーマ内のURLは、 urlparseライブラリを使用して一致します。

>>> from voluptuous import Url
>>> schema = Schema(Url())
>>> schema('http://w3.org')
'http://w3.org'
>>> try:
...   schema('one')
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "expected a URL"
True

リスト

スキーマ内のリストは、有効な値のセットとして扱われます。 スキーマリストの各要素は、入力データの各値と比較されます。

>>> schema = Schema([1, 'a', 'string'])
>>> schema([1])
[1]
>>> schema([1, 1, 1])
[1, 1, 1]
>>> schema(['a', 1, 'string', 1, 'string'])
['a', 1, 'string', 1, 'string']

ただし、空のリスト( [] )はそのまま扱われます。 何かを含むことができるリストを指定する場合は、listとして指定します。

>>> schema = Schema([])
>>> try:
...   schema([1])
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "not a valid value @ data[1]"
True
>>> schema([])
[]
>>> schema = Schema(list)
>>> schema([])
[]
>>> schema([1, 2])
[1, 2]

検証関数

バリデータは、 Invalidなデータに遭遇したときにInvalid例外を発生させる単純な呼び出し可能なものです。 妥当性を判断するための基準は、実装に完全に依存します。 pwd.getpwnam()で値が有効なユーザ名であることを確認したり、値が特定のタイプであるかどうかをチェックしたりすることができます。

バリデータの最も単純な種類は、引数が無効な場合にValueErrorを発生させるPython関数です。 便利なことに、組み込みPython関数の多くはこのプロパティを持っています。 日付バリデータの例を次に示します。

>>> from datetime import datetime
>>> def Date(fmt='%Y-%m-%d'):
...   return lambda v: datetime.strptime(v, fmt)
>>> schema = Schema(Date())
>>> schema('2013-03-03')
datetime.datetime(2013, 3, 3, 0, 0)
>>> try:
...   schema('2013-03')
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "not a valid value"
True

値が有効かどうかを単に決定するだけでなく、バ​​リデータは値を有効な形式に変更することができます。 この例は、 Coerce(type)関数です。この関数は、指定された型に引数を強制する関数を返します。

def Coerce(type, msg=None):
    """Coerce a value to a type.

    If the type constructor throws a ValueError, the value will be marked as
    Invalid.
    """
    def f(v):
        try:
            return type(v)
        except ValueError:
            raise Invalid(msg or ('expected %s' % type.__name__))
    return f

この例では、人間が読めるオプションのメッセージを提供できる共通のイディオムも示しています。 これにより、エラーメッセージの有用性が大幅に向上します。

辞書

スキーマ・ディクショナリの各キー/値ペアは、対応するデータ・ディクショナリの各キー/値ペアに対して検証されます。

>>> schema = Schema({1: 'one', 2: 'two'})
>>> schema({1: 'one'})
{1: 'one'}

追加の辞書キー

デフォルトでは、スキーマではなくデータ内の追加のキーが例外をトリガーします。

>>> schema = Schema({2: 3})
>>> try:
...   schema({1: 2, 2: 3})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "extra keys not allowed @ data[1]"
True

この動作は、スキーマごとに変更できます。 追加キーでSchema(..., extra=ALLOW_EXTRA)使用できるようにするには:

>>> from voluptuous import ALLOW_EXTRA
>>> schema = Schema({2: 3}, extra=ALLOW_EXTRA)
>>> schema({1: 2, 2: 3})
{1: 2, 2: 3}

追加のキーを削除するには、 Schema(..., extra=REMOVE_EXTRA)使用します。

>>> from voluptuous import REMOVE_EXTRA
>>> schema = Schema({2: 3}, extra=REMOVE_EXTRA)
>>> schema({1: 2, 2: 3})
{2: 3}

これは、catch-allマーカートークンextraをキーとして使用して、辞書ごとにオーバーライドすることもできます。

>>> from voluptuous import Extra
>>> schema = Schema({1: {Extra: object}})
>>> schema({1: {'foo': 'bar'}})
{1: {'foo': 'bar'}}

必要な辞書キー

デフォルトでは、スキーマ内のキーはデータ内にある必要はありません。

>>> schema = Schema({1: 2, 3: 4})
>>> schema({3: 4})
{3: 4}

extra_keyの動作と同様に、この動作はスキーマごとにオーバーライドできます:

>>> schema = Schema({1: 2, 3: 4}, required=True)
>>> try:
...   schema({3: 4})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "required key not provided @ data[1]"
True

キーごとのマーカートークンRequired(key)

>>> schema = Schema({Required(1): 2, 3: 4})
>>> try:
...   schema({3: 4})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "required key not provided @ data[1]"
True
>>> schema({1: 2})
{1: 2}

オプションの辞書キー

スキーマがrequired=True場合、マーカートークンを使用してキーを個別にオプションとしてマークすることができます。 Optional(key)

>>> from voluptuous import Optional
>>> schema = Schema({1: 2, Optional(3): 4}, required=True)
>>> try:
...   schema({})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "required key not provided @ data[1]"
True
>>> schema({1: 2})
{1: 2}
>>> try:
...   schema({1: 2, 4: 5})
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "extra keys not allowed @ data[4]"
True
>>> schema({1: 2, 3: 4})
{1: 2, 3: 4}

再帰/ネストされたスキーマ

voluptuous.Selfを使用すると、ネストされたスキーマを定義できます。

>>> from voluptuous import Schema, Self
>>> recursive = Schema({"more": Self, "value": int})
>>> recursive({"more": {"value": 42}, "value": 41}) == {'more': {'value': 42}, 'value': 41}
True

既存のスキーマの拡張

より多くの要件で拡張される基本Schemaを持つことはしばしば便利です。 その場合、 Schema.extendを使用して新しいSchemaを作成することができます:

>>> from voluptuous import Schema
>>> person = Schema({'name': str})
>>> person_with_age = person.extend({'age': int})
>>> sorted(list(person_with_age.schema.keys()))
['age', 'name']

元のSchemaは変更されません。

オブジェクト

スキーマ辞書の各キーと値のペアは、対応するオブジェクトの各属性と値のペアに対して検証されます。

>>> from voluptuous import Object
>>> class Structure(object):
...     def __init__(self, q=None):
...         self.q = q
...     def __repr__(self):
...         return '<Structure(q={0.q!r})>'.format(self)
...
>>> schema = Schema(Object({'q': 'one'}, cls=Structure))
>>> schema(Structure(q='one'))
<Structure(q='one')>

なし値を許可する

値をNoneにするには、Anyを使用します。

>>> from voluptuous import Any

>>> schema = Schema(Any(None, int))
>>> schema(None)
>>> schema(5)
5

エラー報告

バリデータは、無効なデータが渡された場合、 Invalid例外をスローする必要があります。 他のすべての例外はバリデーター内のエラーとして扱われ、捕捉されません。

Invalid例外には、データ構造内で現在検証されている値までのパスを表す関連path属性と、元の例外のメッセージを含むerror_message属性があります。 これは、 Invalid例外をキャッチして、HTTP APIのコンテキストなどでユーザーにフィードバックを与えたい場合に特に便利です。

>>> def validate_email(email):
...     """Validate email."""
...     if not "@" in email:
...         raise Invalid("This email is invalid.")
...     return email
>>> schema = Schema({"email": validate_email})
>>> exc = None
>>> try:
...     schema({"email": "whatever"})
... except MultipleInvalid as e:
...     exc = e
>>> str(exc)
"This email is invalid. for dictionary value @ data['email']"
>>> exc.path
['email']
>>> exc.msg
'This email is invalid.'
>>> exc.error_message
'This email is invalid.'

path属性は、エラー報告時にも使用されますが、一致時にエラーをユーザーに報告するかどうか、または次の一致を試行するかどうかを決定します。 これは、チェックが行われているパスの深さと、エラーが発生したパスの深さを比較することによって判断されます。 エラーが2つ以上のレベルより深い場合、それが報告されます。

これは、 マッチングが深さ優先であり、フェイル・ファーストであることです。

これを説明するために、次にスキーマの例を示します。

>>> schema = Schema([[2, 3], 6])

最上位リストの各値は、最初に順番に一致します。 [[6]]入力データが与えられた場合、内部リストはスキーマの最初の要素と一致しますが、リテラル6はそのリストの要素のいずれとも一致しません。 このエラーはすぐにユーザーに報告されます。 バックトラッキングは試行されません:

>>> try:
...   schema([[6]])
...   raise AssertionError('MultipleInvalid not raised')
... except MultipleInvalid as e:
...   exc = e
>>> str(exc) == "not a valid value @ data[0][0]"
True

データ[6]を渡すと、 6はリスト型ではないため、スキーマの最初の要素に再帰しません。 マッチングはスキーマの2番目の要素に続き、成功します。

>>> schema([6])
[6]

複数フィールド検証

複数のフィールドを含む検証ルールは、カスタムバリデータとして実装できます。 All()を使用して2パス検証を行うことをお勧めします。最初のパスはデータの基本構造をチェックし、それ以降はクロスフィールドバリデータを適用する2パス目です:

def passwords_must_match(passwords):
    if passwords['password'] != passwords['password_again']:
        raise Invalid('passwords must match')
    return passwords

s=Schema(All(
    # First "pass" for field types
    {'password':str, 'password_again':str},
    # Follow up the first "pass" with your multi-field rules
    passwords_must_match
))

# valid
s({'password':'123', 'password_again':'123'})

# raises MultipleInvalid: passwords must match
s({'password':'123', 'password_again':'and now for something completely different'})

この構造では、マルチフィールドバリデーターは、最初の “合格”からの事前検証されたデータで実行されるので、入力に独自のタイプチェックを行う必要はありません。

フリップサイドは、検証の最初の “合格”が失敗した場合、クロスフィールドバリデーターは実行されません:

# raises Invalid because password_again is not a string
# passwords_must_match() will not run because first-pass validation already failed
s({'password':'123', 'password_again': 1337})

テストの実行。

Voluptuousはnosetestsを使用しています:

$ nosetests

なぜVoluptuousを別のバリデーションライブラリよりも使うのですか?

バリデーターは単純な呼び出し可能です:何もサブクラス化する必要はなく、ただ関数を使うだけです。

エラーは単純な例外です。 Validatorはraise Invalid(msg)を送出し、ユーザが有用なメッセージを受け取ることを期待するだけです。

スキーマは基本的なPythonのデータ構造です。 :データを文字列の整数キーの辞書にする必要がありますか? {int: str}はあなたが期待することを行います。 整数、浮動小数点数または文字列のリスト? [int, float, str]

単にフォーム以外のものを検証するために設計されたものです。 :ネストされたデータ構造は、他のタイプと同じ方法で扱われます。 辞書のリストが必要ですか? [{}]

一貫性。 :スキーマの型は型としてチェックされます。 値は値として比較されます。 呼び出し可能オブジェクトが検証されるために呼び出されます。 シンプル。

その他の図書館やインスピレーション

Voluptuousは、 Validinojsonvalidatorjson_schemaのほうが大きく影響を受けています。

pytest-voluptuousは、 assertで容赦のないバリデータを使用するのに役立つpytestプラグインです。

私はFormEncodeのようなライブラリの複雑さに対して、これらのライブラリによって促進される軽量スタイルを非常に好む。







-alecthomas
-

執筆者: