これを行うのが {schema} である。
使い方は PyPI の schema のページに記載されている。
前回と同様に arith4 という sandbox パッケージ上で実装してみる。
arith4/arith4_command.py...(snip)...
from schema import Use, And, Or, Schema, SchemaError
arith4_command_schema = {
'--head':
Or(None,
And(Use(int),
lambda n: n > 0,
error="--head <num>: 正の整数でなければなりません")),
'--tail':
Or(None,
And(Use(int, error="--tail <num>: 整数でなければなりません"),
lambda n: n > 0,
error="--tail <num>: 正の値でなければなりません")),
'<val>': [
Use(int, error="values <val>: 整数でなければなりません")
],
'<file>': [
And(str, len,
os.path.exists, os.path.isfile, lambda f: os.access(f, os.R_OK),
error="files <file>: ファイルが読み込めません")
],
str: object,
}
def arith4_command():
try:
args = docopt(__doc__, argv=None, help=True, version=arith4.__version__, options_first=False)
except DocoptExit as e:
...(snip)...
try:
args = Schema(arith4_command_schema).validate(args)
except SchemaError as e:
# print(e.autos, file=sys.stderr)
sys.exit(e.code)
...(snip)...
Schema はチェックすべき項目を辞書にしたもの(ここではスキーマ辞書と呼ぶことにする)を引数に持ち、Schema.validate() 関数に与えた辞書(チェック対象辞書)の値をチェックする。{docopt} は引数をパースした結果を辞書の形で出力するので、docopt.docopt() の返り値をそのまま Schema.validate() に与えれば良く、非常に相性が良い。
上記の例におけるスキーマ辞書は arith4_command_schema である。
チェック対象である arith4_command の場合、変数を伴う引数・オプションは '--head', '--tail' ,'<val>', '<file>' の4つ(それぞれ docopt が出力する辞書の key に対応)であるため、その4つのみのスキーマを実装している。
(スキーマ辞書におけるエントリをここではスキーマと呼ぶことにする)
スキーマの書式は以下の通り。
- int, str などの型:key もしくは value がその型であるかチェック
- 文字列:key もしくは value がその文字列と一致するかチェック
- タプル・リスト・セット:key もしくは value がその中に含まれているかチェック
- Schema.Use(型):文字列が型にキャストできるかチェック
Schema.Use(int) なら整数にキャストできるかチェックする
Schema.validate() 後は実際にその型にキャストして出力される - 任意の(lambda)関数:関数の返り値が正の値、もしくは True であるかチェック
- Schema.And(A, B):A かつ B
- Schema.Or(A, B):A もしくは B
docopt.docopt() は key も value も文字列で出力されるため、整数(もしくは実数など)かどうかをチェックするためには Schema.Use(int) を用いる。
Schema.validate() の返り値は自動的にその型にキャストされるため、自分でキャストする必要がなく便利である。
例えば value にファイル名が格納されており、open() によってファイルが open できるかチェックしたい場合、Schema.validate() の返り値はファイル名ではなくファイルハンドルが格納される。
<val>... など value が配列であるものは、チェックする式をリストにする。リストが空の場合はチェックがスキップされる。
Schema.Use() など Schema の関数は error のオプションを与えて、エラー時のメッセージを設定できる。
チェックがエラーになった場合、SchemaError が送出される。
SchemaError.autos は Schema 側で自動的に作成されたエラーメッセージで、実際にエラーした変数が表示される。
error オプションにより自分で設定したエラーメッセージは print(SchemaError.errors) もしくは sys.exit(SchemaError.code) とすることで表示される。
チェックの必要がない変数については、Schema.validate() は各変数についてスキーマ辞書の最初のスキーマから順に評価をしていくため、str:object というスキーマを最後に追加することによって「keyが str型である」「value が object 型」であるという全ての変数に合致するスキーマをの評価を行うことになり、スキーマのチェックが実質的にスキップされる。
もちろん各変数について '--max':bool などのスキーマを設定しても良いし、str:Or(bool, None) も(docopt.docopt() の帰り値ならば)正しい実装だろう。
一般的な辞書について必ず通過するスキーマは object:object である。
docopt.docopt() の返り値ではない、一般的な辞書をスキーマ辞書で評価するために、{Schema} には以下の実装が含まれている。
- Schema.Optional(A):A となる key が存在する場合にのみ評価する
docopt の場合、全引数・オプションが返り値に含まれるため、不要 - Schema.Optional(A, default=B):A となる key が存在しない場合、A:B というエントリを追加する
- Schema(スキーマ辞書, ignore_extra_keys=True):スキーマ辞書に含まれない key がチェック対象辞書に存在した場合、それは返り値には含まれない
Schema.Optional() が存在することによって、任意のクラス・関数の引数に対して {Schema} が対応できるようになっていると考えられる。
(ただし、{docopt} は任意のクラス・関数に対応してない)