例外処理

TurboGears?はエラーの管理やリダイレクトに関しては標準Pythonのtry/exceptブロックを拡張したシステムを持っています。またTurboGearsのエラーシステムはバリデーションエラーとPythonの例外を区別しています。これらはそれぞれ @error_handler() ディレクティブや @exception_handler() ディレクティブで処理されます。

バリデーションエラー

バリデーションエラーはTurboGearsのバリデータフレームワークによって発行されます。これはふつうウィジェットライブラリとともに用いられますが、引数が整数かどうかを確認するときにも役に立ちます。バリデーションエラーは @error_handler() デコレータや、 tg_errors キーワードパラメータを伴ったメソッドを使うことによって制御されます。

このデコレータがどのように動作するのかの例を以下に示します:

from turbogears import controllers, expose, validate
from turbogears import error_handler, validators as v

class Root(controllers.RootController)

    @expose()
    def index(self, number=-1, tg_errors=None):
        """displays a 'number' (but actually can be anything)"""
        if tg_errors:
            errors = [(param,inv.msg,inv.value) for param, inv in
                      tg_errors.items()]
            return dict(error_message=errors)
        else:
            return dict(number=number)

    @expose()
    @error_handler(index)
    @validate(validators={"number":v.Int})
    def validated_number(self, number=2):
        """displays an Integer (only)"""
        return dict(valid_number=number)

このコードは2つのコントローラメソッドを生成しています。一つはドメインのルートである / であり、もう一つは /validated_number です。両方とも number をパラメータにとってそれを表示しています。このindexメソッドは number パラメータをチェックしておらず何でも通過させてしまいますが、 validated_number メソッドは、エラーをチェックする Int バリデータを伴っています。このエラーは index コントローラに戻されて、indexコントローラはそれを tg_errors エラーとして受け取り、それをうまい具合に処理します。

以下はこれらがどの様に動作するのかを示す、 twill session からの出力です(一部改変しています):

-= Welcome to twill! =-

current page:  *empty page*
>> go http://localhost:8080/
==> at http://localhost:8080/
>> show
{"tg_flash": null, "number": -1}

>> go http://localhost:8080/?number=42
==> at http://localhost:8080/?number=42
>> show
{"tg_flash": null, "number": "42"}

>> go http://localhost:8080/?number=blue
==> at http://localhost:8080/?number=blue
>> show
{"tg_flash": null, "number": "blue"}

>> go http://localhost:8080/validated_number
==> at http://localhost:8080/validated_number
>> show
{"tg_flash": null, "valid_number": 2}

>> go http://localhost:8080/validated_number?number=42
==> at http://localhost:8080/validated_number?number=42
>> show
{"tg_flash": null, "valid_number": 42}

>> go http://localhost:8080/validated_number?number=blue
==> at http://localhost:8080/validated_number?number=blue
>> show
{"tg_flash": null, "error_messages": [["number", "Please enter an integer
value", "blue"]]}

興味深いのは一番最後のリクエストです。 validated_number コントローラは明らかに整数ではない、間違った値である blue を通してしまいます。しかしバリデータはこれを発見し、期待通り index コントローラへ tg_errors として戻してくれました。同時にふつうの呼び出しも期待通りに動作します。 tg_errors そのものは失敗したパラメータをキーとし、 formencode.Invalid を値とするふつうの辞書です。このindexメソッドはエラーを処理する一つの(そしてちょっと貧弱な)方法を見せていますが、もっとうまくやる方法があります。

ふつう @error_handler() を使うときは TurboGears? widgets を一緒に使います。以下は上記と同じ基本的なセットアップの例です:

from turbogears import controllers, expose, validate, redirect
from turbogears import error_handler, validators as v
from turbogears import widgets as w

number_form = w.ListForm(
    fields = [
        w.TextField(
            name="number",
            label="Enter a Number",
            validator=v.Int(not_empty=True))
    ],
    submit_text = "Submit Number!"
)

## This is what the template looks like
# <html xmlns:py="http://purl.org/kid/ns#">
#     <body>
#         <h1>This is a Form Page!</h1>
#         ${form(value_of('data',None), action=action, method="POST")}
#     </body>
# </html>

class Root(controllers.RootController):

    @expose(template=".templates.welcome")
    def index(self,number=-1,tg_errors=None):
        return dict(data={'number':number},
                    form=number_form,
                    action="/validated_number")

    @expose()
    @error_handler(index)
    @validate(form=number_form)
    def validated_number(self, number=2):
        return dict(valid_number=number)

先ほどのコードとの一番大きな違いは、 index コントローラがもう明示的には tg_errors を処理しないということです。フォームで便利なのは、どのようにバリデーションエラーを処理すればよいかわかっていることです。あなたはエラーハンドラをセットすれば、もうそれを忘れてしまえばいいのです。

例外とルール

例外はエラーとは分離されてしょりされます。何故ならバリデーションエラーはウェブアプリにおいては予測されることだからです。一方、例外はそれよりもやや深刻です。あなたもそれを分離して処理したいのではないでしょうか。使い方は @exception_handlertg_exceptions を使うこと以外はエラーのときと非常に似ています。

気を付けなければならないのは、異なるタイプの例外を異なるハンドラで処理したいときです。例えばSQLObjectの例外はValueErrorsと分離して処理したいでしょう。これは rules パラメータ( @error_handler() 内で利用可能です)を使うことで可能です。 rules  は適切なPython表現を含んだ文字列を取ります。表現が正しいときにルールはマッチします。以下を見てみましょう:

import turbogears
from turbogears import controllers, expose, validate, redirect
from turbogears import exception_handler

class Root(controllers.RootController):
    # Note that the exception handlers don't need to be exposed
    # if they're not meant to be accessed directly. It's always a
    # wise decison to expose only what is absloutely needed to prevent
    # information leaking.

    def value_handler(self,tg_exceptions=None):
        """only called for value errors"""
        return dict(handling_value=True,exception=str(tg_exceptions))

    def index_handler(self,tg_exceptions=None):
        """only called for index errors"""
        return dict(handling_index=True,exception=str(tg_exceptions))

    @expose()
    @exception_handler(value_handler,"isinstance(tg_exceptions,ValueError)")
    @exception_handler(index_handler,rules="isinstance(tg_exceptions,IndexError)")
    def exceptional(self, number=2):
        number = int(number)
        if number < 42:
            raise IndexError("Number too Low!")
        if number == 42:
            raise IndexError("Wise guy, eh?")
        if number > 100:
            raise Exception("This number is exceptionally high!")
        return dict(result="No errors!")

以下はtwillの出力です:

current page:  *empty page* 
>> go http://localhost:8080/exceptional
==> at http://localhost:8080/exceptional
>> show
{"exception": "Number too Low!", "handling_index": true, "tg_flash": null}

>> go http://localhost:8080/exceptional?number=42
==> at http://localhost:8080/exceptional?number=42
>> show
{"exception": "Wise guy, eh?", "handling_index": true, "tg_flash": null}

>> go http://localhost:8080/exceptional?number=blue
==> at http://localhost:8080/exceptional?number=blue
>> show
{"exception": "invalid literal for int(): blue", "tg_flash": null, "handling_value": true}

>> debug http 1
DEBUG: setting http debugging to level 1

>> go http://localhost:8080/exceptional?number=400
# ...
reply: 'HTTP/1.1 500 Internal error\r\n'
# ...

number が42より小さいときに index_handler (出力では handling_index )へ行くことや、整数でない値のときにバリューエラーが起こることに注意してください。また最後のケースは、処理できなかったエラーをCherryPyに組み込まれたハンドリングメカニズムがカバーしていることを示しています。もちろんそういったエラーを rules パラメータ無しの @exception_handler() を使うことでリダイレクトすることも可能です。

最後に注意を

ほとんどの部分において、デコレータの順序はさほど大きな問題ではありません。複合エラー処理とIdentityフレームワークの独自ハンドラメソッドがあるときには、一つの例外が発生します。 @error_handler() デコレータが @identity_require() デコレータよりも下にあったとき、identityのデコレータは完全に素通りになり、セキュリティが危険になってしまいます。そうすることのないように注意してください。

@expose()
@identity.require(identity.not_anonymous())
@error_handler()                             #BAD, DON'T DO IT
def foo(self,tg_errors=None):
    pass

@expose()
@error_handler()                            #OK, be careful
@identity.require(identity.not_anonymous())
def bar(self, tg_errors=None):
    pass

def handler(self, tg_errors=None):
    pass

@expose()
@identity.require(identity.not_anonymous())
@error_handler(handler)                     #GOOD
def baz(self):
    pass