XREAで好きなバージョンのPythonを使う方法

前のエントリーから自作のブログソフトに移行してみたわけだけど、やっぱり自分で作ったものはなんとなく気持ちいいですね。

さて、このブログはXREAでPython2.5で動いているわけですが、もちろん?XREAにPython2.5ははいっていません。ではどうやって動かすか・・・というと答えは簡単。バイナリとして動かしているわけです。

XREAはバイナリCGIが動くのでバイナリにしてしまえばどんなバージョンのPythonでも問題ありません。しかも、たとえばMySQLDBなどの拡張モジュールもきちんとバイナリに含まれるので、多少ファイルサイズは大きいですがアップロードするだけで動くので非常に楽です。

今回はXREAでローカルで作ったPython2.5アプリをバイナリ化して動かすまでを紹介しようかと思います。

バイナリ化ライブラリ:bbfreeze

バイナリ化に使うライブラリですが、俺はbbfreezeを使いました。bbfreezeの特徴は

  • すごく簡単
  • ライブラリの探索に優れる
  • Linux用バイナリが作れる

というところでしょう。

bbfreezeを使う上での注意点

ただ、使う上で色々注意点もあります。書きなぐりなのでちょっと汚いですが、このブログアプリ用のfreezeスクリプトです。

python code
  1. from bbfreeze import Freezer
  2. import sys, os, shutil
  3. from os.path import dirname, abspath, join
  4. root = abspath(dirname(__file__))
  5. sys.path = [join(root, d) for d in ["libs"]] + sys.path
  6. include_lst = []
  7. for r, dirs, files in os.walk(join(root, "libs")): #指定ディレクトリ以下を全部追加
  8.   if "__init__.py" in files:
  9.     include_lst.append(r)
  10.   for file in (f for f in files if f.endswith(".py") and f != "__init__.py"):
  11.     include_lst.append(join(r, file[:-3]))
  12. def replace(f):
  13.   return f[len(join(root, "libs"))+1:].replace("/", ".")
  14. include_lst = map(replace, include_lst)
  15. include_lst.append("_mysql") # MySQLDBの_mysqlが含まれないので手動で追加
  16. shutil.copy(join(root, "index.py"), join(root, "index.cgi.py")) #cgi用にリネーム
  17. f = Freezer("dist", includes=include_lst, excludes=(), )
  18. f.addScript("index.cgi.py", True)
  19.  
  20. f() # starts the freezing process
  21.  
  22. os.remove(join(root, "index.cgi.py"))
  23.  

と、こんな感じです。ほんと汚いな・・・。さてでは注意点を一つ一つ。

  • できる限り自動的に使用しているライブラリを検索してくれるのですが、__import__で動的にインポートする場合はさすがに無理です。なので、そういうモジュールがある場合、自分で追加する必要があります。このブログアプリではPygmentsMarkdownあたりが__import__による動的インポートを行っています。
  • MySQLDBの_mysqlモジュールのように探索されないのもありますので、注意しないといけません。
  • 出力されるバイナリファイル名はメインスクリプトから.pyを除いたものになります。XREAでcgiとして動かす場合はあらかじめindex.cgi.pyみたいにリネームしておくとindex.cgiが吐き出されるので便利です。

また、dirname(__file__)でディレクトリを取得し、テンプレートファイル用ディレクトリなどを設定している場合、注意が必要です。以下のようにします。

python code
  1. root_dir = abspath(dirname(__file__))
  2. if "library.zip" in root_dir:
  3.   root_dir = dirname(root_dir)
  4.  
  5. template_dir = os.path.join(root_dir, "templates")
  6.  

ファイルはlibrary.zipにまとめられますので、実行時は/library.zip/hogehoge.pyのような扱いになります。そのためlibrary.zipがパスに含まれる場合はさらに1個上のディレクトリが求めているディレクトリになります。

アップロード

生成されたファイル群をアップロードするだけです。さすがにサイズは大きいですが。もちろんアップした後index.cgiのパーミッションは設定しましょう。

アクセスしましょう

バッチリ動くはずです。


というわけで、bbfreezeを使えばサクッと任意のバージョンのPythonで作ったアプリがXREAで動かせます。あとは負荷だけが問題です。1日見た限りではweb.pyの場合負荷は大丈夫っぽいです。ぶっちゃけ、Wordpressより速い気がします。web.pyはライトウェイトWEBフレームワークとしてもう少し評価されるべき。Djangoとかだとどうなんですかね。チャレンジャーな方は、ゼヒ。

07.29.08/12am

Ajax.InPlaceEditorでバリデーションに引っかかったら

nil.to_sが"nil"になるような言語は(ry というのは置いといて、まだまだPythonをリハビリ中。
Rubyより関数志向での開発がしやすくて楽しいですよねえ。

リハビリを兼ねてweb.pyで個人的なツールを作ってます。web.pyはシンプルで軽いので、個人用途(非公開でツールとして使う)だったらレンタルサーバーでCGIとして動作させても問題なさそうです。(XREAでは簡単なものなら負荷率0ptでした。Railsとかならいれるだけでダメなんで、素晴らしい)

そこで、scriptaculousのAjax.InPlaceEditorを使っているわけですが、困ったことがひとつ。InPlaceEditorなわけで、値を当然入力します。でその値がサーバー側でバリデーションエラーな時どうしたもんかと。

理想的な動作としては

  • エラーをユーザーに通知する

  • サブミットする前の値のままtextareaを表示しておく

なんですが、コンストラクタの中のoptionsあたりをのぞいてもそれらしきオプションはなさげ。

ということでとりあえず場当たり的に書いてみたものが以下。

javascript code
  1. (function(){
  2. Ajax.RollbackableInPlaceEditor = Class.create();
  3. var dummy = function(){};
  4. dummy.prototype = Ajax.InPlaceEditor.prototype;
  5. Ajax.RollbackableInPlaceEditor.prototype = new dummy;
  6. Ajax.RollbackableInPlaceEditor.prototype.constructor = Ajax.RollbackableInPlaceEditor;
  7. })();
  8. Object.extend(Ajax.RollbackableInPlaceEditor.prototype, {
  9.   initialize : function(element, url, options) {
  10.     var self = this;
  11.     options = Object.extend({
  12.       callback: function(form) {
  13.         self.oldValue = self.editField.value;
  14.         return Form.serialize(form);
  15.       },
  16.       onFailure : Prototype.emptyFunction
  17.     }, options || {});
  18.     Ajax.InPlaceEditor.prototype.initialize.apply(this, [element, url, options]);
  19.   },
  20.   onclickCancel: function() {
  21.     this.onComplete();
  22.     this.element.innerHTML = this._oldInnerHTML;
  23.     this.element.show();
  24.     return false;
  25.   },
  26.   onComplete: function(transport) {
  27.     this.leaveEditMode();
  28.     if(transport && !Ajax.Base.prototype.responseIsSuccess.call({transport:transport})) {
  29.       this.options.onFailure.bind(this)(transport, this.element);
  30.       return this.enterEditMode();
  31.     }else {
  32.       this.oldValue = null;
  33.     }
  34.     this.options.onComplete.bind(this)(transport, this.element);
  35.   },
  36.   onFailure: function(transport) {
  37.     return false;
  38.   },
  39.   getText : function() {
  40.      if(this.oldValue != null && this.oldValue != undefined){var v = this.oldValue; this.oldValue = null; return v;}
  41.     this._oldInnerHTML = this.element.innerHTML;
  42.     return this.element.innerHTML;
  43.   }
  44. });
  45.  

サーバー側でエラーがあったときは適当に500とか406とかでレスポンスを返せばオッケーです。

なのでサーバー側ではたとえばweb.pyなら

python code
  1.     # errors = ['データが長すぎます。'] など
  2.     if len(errors) != 0:
  3.       web.header("Content-Type", "text/javascript"),
  4.       web.ctx["status"] = "406 Not Acceptable"
  5.       print "".join(["alert('", "\n".join(errors), "');"])
  6.       return
  7.  

とすればエラーになって(エラーメッセージがアラートされる)、サブミットした内容のままで再度編集できます。

豆知識ですが、以上のようにサーバー側でtext/javascriptでレスポンスを返すとprototype.jsでは自動的にjavascriptとして評価してくれます。

うーむ、まさに場当たり的。いろいろまずそうだけど、まぁ動いてるっぽいし気にしないことにしとこう・・・・

07.27.08/12am

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages