2016/06/14

Day XXX: schema の使い方

{docopt} は python で作成したコマンドの引数・オプションのパースをするものだが、それらの値が想定した値かを調べることは出来ない。
これを行うのが {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} は任意のクラス・関数に対応してない)

0 件のコメント:

コメントを投稿