2007-01-20
■ [Python] UnicodeEncodeErrorが発生した場合は、sitecustomize.pyでデフォルトのエンコーディングを指定する。
環境
この記事の内容は、Ubuntu 6.10, Python 2.4で確認しました。
現象
ユニコード文字列をstr関数に与えると、UnicodeEncodeErrorが発生します。
$ python Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02) [GCC 4.1.2 20060928 (prerelease) (Ubuntu 4.1.1-13ubuntu5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> s = u'ほげ' >>> str(s) Traceback (most recent call last): File "<stdin>", line 1, in ? UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
解決方法
以下の/usr/lib/python2.4/site-package/sitecustomize.pyを作成してください。
import sys sys.setdefaultencoding('utf-8')
詳細
デフォルトのエンコーディングは、以下のようにして確認できます。
$ python Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02) [GCC 4.1.2 20060928 (prerelease) (Ubuntu 4.1.1-13ubuntu5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.getdefaultencoding() 'ascii'
sitecustomize.pyは、pythonが起動したときに読み込まれるモジュールです。この中で、python全体の設定を行うことができます。デフォルトのエンコーディングを正しく設定した場合、問題の現象は以下のようになります。
$ python Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02) [GCC 4.1.2 20060928 (prerelease) (Ubuntu 4.1.1-13ubuntu5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.getdefaultencoding() 'utf-8' >>> s = u'ほげ' >>> str(s) '\xe3\x81\xbb\xe3\x81\x92'
なお、sitecustomize.py以外でsys.setdefaultencodingを実行しても、AttributeErrorになります。
$ python Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02) [GCC 4.1.2 20060928 (prerelease) (Ubuntu 4.1.1-13ubuntu5)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.setdefaultencoding('utf-8') Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: 'module' object has no attribute 'setdefaultencoding'
これは、pythonは起動が終了したら、sysモジュールからsetdefaultencoding関数を削除するためです。
ここで、pythonの起動時の処理を、少し詳しく説明します。pythonは起動時に、モジュールの検索パスを設定し、siteモジュールを読み込みます。Ubuntuなら、/usr/lib/python2.4/site.pyになります。sitecustomizeモジュールも、このsite.pyから読み込まれています。
def execsitecustomize(): """Run custom site specific code, if available.""" try: import sitecustomize except ImportError: pass
siteモジュールが読み込まれると、その中のmain関数が呼び出され、main関数の最後でsys.setdefaultencoding関数を削除しています。
def main(): abs__file__() paths_in_sys = removeduppaths() if (os.name == "posix" and sys.path and os.path.basename(sys.path[-1]) == "Modules"): addbuilddir() paths_in_sys = addsitepackages(paths_in_sys) if sys.platform == 'os2emx': setBEGINLIBPATH() setquit() setcopyright() sethelper() aliasmbcs() setencoding() execsitecustomize() # Remove sys.setdefaultencoding() so that users cannot change the # encoding after initialization. The test for presence is needed when # this module is run as a script, because this code is executed twice. if hasattr(sys, "setdefaultencoding"): del sys.setdefaultencoding main()
これにより、sys.setdefaultencodingは、sitecustomize.py内でしか実行できなくなります。
なお、pythonにはsite.pyの読み込みを抑制する-Sオプションがあります。
$ python -h usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ... Options and arguments (and corresponding environment variables): -c cmd : program passed in as string (terminates option list) -d : debug output from parser (also PYTHONDEBUG=x) -E : ignore environment variables (such as PYTHONPATH) -h : print this help message and exit -i : inspect interactively after running script, (also PYTHONINSPECT=x) and force prompts, even if stdin does not appear to be a terminal -m mod : run library module as a script (terminates option list) -O : optimize generated bytecode (a tad; also PYTHONOPTIMIZE=x) -OO : remove doc-strings in addition to the -O optimizations -Q arg : division options: -Qold (default), -Qwarn, -Qwarnall, -Qnew -S : don't imply 'import site' on initialization -t : issue warnings about inconsistent tab usage (-tt: issue errors) -u : unbuffered binary stdout and stderr (also PYTHONUNBUFFERED=x) see man page for details on internal buffering relating to '-u' -v : verbose (trace import statements) (also PYTHONVERBOSE=x) -V : print the Python version number and exit -W arg : warning control (arg is action:message:category:module:lineno) -x : skip first line of source, allowing use of non-Unix forms of #!cmd file : program read from script file - : program read from stdin (default; interactive mode if a tty) arg ...: arguments passed to program in sys.argv[1:] Other environment variables: PYTHONSTARTUP: file executed on interactive startup (no default) PYTHONPATH : ':'-separated list of directories prefixed to the default module search path. The result is sys.path. PYTHONHOME : alternate <prefix> directory (or <prefix>:<exec_prefix>). The default module search path uses <prefix>/pythonX.X. PYTHONCASEOK : ignore case in 'import' statements (Windows).
このオプションを使用すると、site.py以外でもsys.setdefaultencoding関数を使用することができます。
$ python -S Python 2.4.4c1 (#2, Oct 11 2006, 21:51:02) [GCC 4.1.2 20060928 (prerelease) (Ubuntu 4.1.1-13ubuntu5)] on linux2 >>> import sys >>> dir(sys) ['__displayhook__', '__doc__', '__excepthook__', '__name__', '__stderr__', '__stdin__', '__stdout__', '_getframe', 'api_version', 'argv', 'builtin_module_names', 'byteorder', 'call_tracing', 'callstats', 'copyright', 'displayhook', 'exc_clear', 'exc_info', 'exc_type', 'excepthook', 'exec_prefix', 'executable', 'exit', 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags', 'getfilesystemencoding', 'getrecursionlimit', 'getrefcount', 'hexversion', 'maxint', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'setcheckinterval', 'setdefaultencoding', 'setdlopenflags', 'setprofile', 'setrecursionlimit', 'settrace', 'stderr', 'stdin', 'stdout', 'version', 'version_info', 'warnoptions'] >>> sys.setdefaultencoding('utf-8')
■ [TurboGears] ブログを作成する。その5
注意
この記事は、id:SumiTomohiko:20070119:1169220734の続きです。
入力値検証
今回は、フォームで入力された値を検証する機能を追加します。検証には、ウィジェットを使用します。ウィジェットのフィールドには、ヴァリデータを設定することができるのです。また、「その4」ではウィジェットは使うたびに生成していたのですが、入力値検証ではインスタンスが必要なので、その点も修正します。
作成するウィジェットは2種類あります。登録画面のフォームのウィジェットと、編集画面のフォームのウィジェットです。これら2つには、以下の違いがあります。
これら以外の共通点は基底クラスでまとめ、差異だけ派生クラスで実装します。
まず、必要なモジュールをインポートします。具体的には、turbogears.error_handlerとturbogears.validate, turbogears.validatorsです。
from turbogears import controllers, expose, widgets, error_handler, validate from turbogears import validators, identity, redirect
ウィジェットの共通の基底クラスArticleFormWidgetは、次のようになります。
class ArticleFormWidget(widgets.TableForm): def __init__(self): fields = self._create_fields() action = self._get_action() super(ArticleFormWidget, self).__init__('ArticleForm', fields=fields, action=action, submit_text=u'送信') def _create_fields(self): title_field = widgets.TextField('title', label=u'題名', attrs=dict(size='64'), validator=validators.UnicodeString(not_empty=True, max=64, messages={'empty': u'題名を入力してください。', 'tooLong': u'題名は、最大%(max)i文字です。'})) body_field = widgets.TextArea('body', label=u'本文', rows=16, cols=64, validator=validators.UnicodeString(not_empty=True, max=8192, messages={'empty': u'本文を入力してください。', 'tooLong': u'本文は、最大%(max)i文字です。'})) return [title_field, body_field]
ここで、widgets.TextFieldとwidgets.TextAreaに与えているvalidator引数が、ヴァリデータです。両方とも、validators.UnicodeStringを設定しています(他にどんなヴァリデータがあるかは、formencode.validators -- Validator/Converters for use with FormEncodeを参照してください)。validators.UnicodeStringに渡している引数のうち、not_emptyは空文字列を許可するかしないかを設定し、maxは最大長を設定します。messagesでは、どんなエラーが発生したらどんなメッセージを表示するかを設定します。'empty'は、何も入力されなかった場合のメッセージで、ここでは「題名を入力してください。」などにしています。'tooLong'は、最大文字数を越えたときのメッセージで、「題名は、最大%(max)i文字です。」などにしています。%(max)iには、最大文字数が入ります。
登録画面で使用するウィジェットは、次の通りです。_get_action関数で、フォームの送信先を返しています。クラスの定義がすんだら、インスタンスを生成しています。
class ArticleAddFormWidget(ArticleFormWidget): def __init__(self): super(ArticleAddFormWidget, self).__init__() def _get_action(self): return '/doadd' article_add_form_widget = ArticleAddFormWidget()
編集画面で使用するウィジェットは、次の通りです。_create_fields関数をオーバーライドして、編集対象のレコードのidを保持するhiddenフィールドを追加しています。_get_action関数で送信先を返し、クラスの定義後にインスタンスを生成するのは、ArticleAddFormWidgetクラスと同じです。
class ArticleEditFormWidget(ArticleFormWidget): def __init__(self): super(ArticleEditFormWidget, self).__init__() def _create_fields(self): fields = super(ArticleEditFormWidget, self)._create_fields() id_field = widgets.HiddenField('id') fields.append(id_field) return fields def _get_action(self): return '/doedit' article_edit_form_widget = ArticleEditFormWidget()
次に、これらのウィジェットを使用する箇所を作ります。まず、登録画面からです。
@expose(template="tgdiary.templates.add")
@identity.require(identity.not_anonymous())
def add(self):
return dict(article_add_form_widget=article_add_form_widget)
@expose()
@identity.require(identity.not_anonymous())
@error_handler(add)
@validate(form=article_add_form_widget)
def doadd(self, title, body):
# TODO: Validate.
article = Article(title=title, body=body, user=identity.current.user)
self._redirect_to_date(identity.current.user.id, article.created)
フォームの送信先であるdoadd関数に、2つのデコレータを追加します。ひとつ目は、@error_handlerです。これで、エラー発生時の遷移先を設定します。ここでは、もういちど登録画面を表示したいので、add関数を指定しています。ふたつ目は、@validateです。これのform引数で、入力値検証に使用するウィジェットを指定します。
編集画面は、以下のようになります。
@expose(template="tgdiary.templates.edit")
@identity.require(identity.not_anonymous())
def edit(self, id):
# TODO: Validate id and user's id.
article = Article.get(id)
return dict(article=article, article_edit_form_widget=article_edit_form_widget)
@expose()
@identity.require(identity.not_anonymous())
@error_handler(edit)
@validate(form=article_edit_form_widget)
def doedit(self, id, title, body):
# TODO: Validate id, title, body and user's id.
# XXX: Better way? Transaction?
article = Article.get(id)
article.title = title
article.body = body
self._redirect_to_date(identity.current.user.id, article.created)
同様に、doedit関数に@error_handlerデコレータと@validateデコレータを追加しています。
ビューであるtgdiary/templates/add.kidとtgdiary/templates/edit.kidは、以下のように修正します。これは、単に変数の名前を変更したためであり、入力値検証とは関係ありません。tgdiary/templates/add.kidは、
${article_add_form_widget.display()}
であり、tgdiary/templates/edit.kidは、
${article_edit_form_widget.display(article)}
です。
以上により、入力値検証が完成しました。試しに、登録画面で何も入力せずに送信してみます。
メッセージの場所が気に入りませんが、見事にエラーメッセージが表示されました。最大文字数を越えて入力すると、以下のようになります。
■ [TurboGears] 認証関係の例外
環境
この記事の内容は、Ubuntu 6.10, Python 2.4, TurboGears 1.0で確認しました。
認証関係の例外
TurboGearsで使用される認証関係の例外は、/usr/lib/python2.4/site-packages/TurboGears-1.0-py2.4.egg/turbogears/identity/exceptions.pyにあります。以下に、クラス名と説明を抜粋します。
class IdentityException(Exception): ''' Base class for all Identity exceptions. ''' class RequestRequiredException(IdentityException): ''' An attempt was made to use a facility of Identity that requires the presence of an HTTP request. ''' class IdentityManagementNotEnabledException(IdentityException): """ User forgot to enable Identity management """ class IdentityConfigurationException(IdentityException): ''' Exception thrown when the Identity management system hasn't been configured correctly. Mostly, when failure_url is not specified. ''' class IdentityFailure(cherrypy.InternalRedirect, IdentityException): ''' Exception thrown when an access control check fails. '''

