web2py処理を辿ってみる

はじめに

今度はソースで処理をざっと辿ることにする。と言っても、わからないところは飛ばす。

ドキュメント chap 4.2 「ディスパッチ」に掲載されている処理プロセス概念図は下記のように典型的なMVCであるが

web2py処理プロセス:doc 3rd,p.127


実際のプログラムの実行順序についてはchap.1.3「モデル、ビュー、コントローラー」のシーケンス図に詳しい。

web2py処理プロセス:シーケンス図:doc 3rd,p.27


上図は見づらいので、上側のオブジェクトを並べて書き直せば

  1. Browser
  2. Server
  3. Main
  4. Model
  5. Controller
  6. View
  7. Database

となっている。

理解のポイントはMainだろう。

シーケンス図にもとづいて処理を辿る

BrowserからServerへ

nginx

Serverはnginxを使っている。

nginxはリクエストがくると、別のポートで動いているuwsgiを呼び出す。

これはnginxの*confの役目である。

uwsgi

Python対応のuwsgiは

uwsgi:

socket: 127.0.0.1:ポート番号

pythonpath: web2pyのインストールルート

module: wsgihandler

のように設定されているので、

web2pyのインストールルート/wsgihandler.py(c)

を呼び出す。

wsgihandler.py

というわけで、wsgihandler.pyがweb2pyの入り口スクリプトになる。

中を見ると

application = gluon.main.wsgibase

ここで

web2pyのインストールルート/gluon/main.py

が呼び出されていることがわかる。

main.py

というわけで、gluon.main.wsgibase関数の働きを見ればイイ。

wsgibase

Version 1.99.4 (2011-12-14 14:46:14) stableの場合、315行目にある。

grep -n 'def wsgibase' main.py

315:def wsgibase(environ, responder):

tryが3つ入れ子になっているが、気にせず眺める。

最下層のtryで何をやっているのか、コメントにある。

 # handle fcgi missing path_info and query_string
 # select rewrite parameters
 # rewrite incoming URL
 # parse rewritten header variables
 # parse rewritten URL
 # serve file if static

この順序で処理が進む。この段階でrewriteという名前のURLリライト処理が行われているのに注意しておこう。rewrite.pyの働きである。

というわけで、URLをパースして、スタティックファイルへのリクエストがあるかどうか確認したのち、まずはデフォルトルートへのリクエストを行う。

serve_controller(request, response, session)

新しい関数が出てきた。

これがフェイルすれば、except処理で上のスタティックファイルがあった時、それを返す。web2pyはともかく最後にスタティックを返すのである。

ここが肝っぽいので、以下、tryの上層と二層目は省略。

serve_controller

さて、serve_controllerである。171行目にある。

171:def serve_controller(request, response, session):

要するに、web2pyでは、request, response, sessionと名付けられた変数に常に気をくばればいいことになる。

ここでもコードのコメントを引用しておく。やることは2つ。

    # ##################################################
    # build environment for controller and view
    # ##################################################
    # ##################################################
    # process models, controller and view (if required)
    # ##################################################

同関数から呼び出されている関数はすべてcompileapp.pyからインポートされたものである。

from compileapp import build_environment, run_models_in, \
    run_controller_in, run_view_in

上の4つの関数順に処理は進む。

build_environment

request, response, sessionに加え、一切合切の変数をenvironmentの「辞書」に突っ込む。あるいはまた、モジュールを格納しておく。

一部をコピペすれば

 from dal import BaseAdapter, SQLDB, SQLField, DAL, Field
 **snip**
 global __builtins__
 **snip**
 environment['__builtins__'] = __builtins__
 environment['HTTP'] = HTTP
 environment['redirect'] = redirect
 environment['request'] = request
 environment['response'] = response
 environment['session'] = session
 environment['DAL'] = DAL
 environment['Field'] = Field
 environment['SQLDB'] = SQLDB       
 **snip*

こんな感じである。

なお、global __builtins__した上で、 environment['__builtins__'] = __builtins__する意味はよくわからないので今後の課題。

モデルへ:run_models_in:Pythonのお勉強をかねて。

短いものなので、一つ一つたどってみた。しかし、すごく時間がかかった。

def run_models_in(environment):
    """
    Runs all models (in the app specified by the current folder)  
    It tries pre-compiled models first before compiling them.
    """

    folder = environment['request'].folder
    c = environment['request'].controller
    f = environment['request'].function
    cpath = os.path.join(folder, 'compiled')
    if os.path.exists(cpath):
        for model in listdir(cpath, '^models_\w+\.pyc$', 0):
            restricted(read_pyc(model), environment, layer=model)
        path = os.path.join(cpath, 'models')
        models = listdir(path, '^\w+\.pyc$',0,sort=False)
        compiled=True
    else:
        path = os.path.join(folder, 'models')
        models = listdir(path, '^\w+\.py$',0,sort=False)
        compiled=False
    paths = (path, os.path.join(path,c), os.path.join(path,c,f))
    for model in models:
        if not os.path.split(model)[0] in paths and c!='appadmin':
            continue
        elif compiled:
            code = read_pyc(model)
        elif is_gae
            code = getcfs(model, model,
                          lambda: compile2(read_file(model), model))
        else:
            code = getcfs(model, model, None)
        restricted(code, environment, layer=model)

code色がついてるのが、ながめても名前だけでは類推がつかずに、マニュアルを見たりソースを見た部分。初心者感満開だが、こういうのは大切だと思われる。

request.folder/request.applicationの定義場所について

os.path.join自体はパス結合であることは想像つく。

ただ結合しているfolder

folder = environment['request'].folder

がどういう意味かはワカラン。

environment['request'].folder=request.folderである。

で、request.folderはmain.pyのwsgibaseで定義されている。

request.folder = abspath('applications',request.application) + os.sep

abspathは実行ファイルの絶対パスを得るweb2py/gluon/fileutilsモジュールの関数。os組み込みではない。

os.sepはOS依存のディレクトリセパレタだろう。

この場合..../web2py/applications/の絶対パスが得られる。

それではrequest.applicationはどこで定義されているのか。

さがしてみるとrewrite.pydef regex_url_in(request, environ):内にあった。

path = request.env.path_info.replace('\\', '/')
**snip**
match = regex_url.match(path)
**snip**
request.application = \
  regex_space.sub('_', match.group('a') or thread.routes.default_application)
request.controller = \
  regex_space.sub('_', match.group('c') or thread.routes.default_controller)
request.function = \
  regex_space.sub('_', match.group('f') or thread.routes.default_function)

match=regex_url.match の部分は、rewrite.py冒頭で/a/c/f.e/sのパスの上の順序から

regex_url = re.compile(r'''
**snip**
     ''', re.X)

というかたちでre.compileされている(*1)。

re.compileしたもの.正規表現関数というのは使いやすいオブジェクトだな。スマート。

aはapplicationの略のaというわけである。

またregex_spaceはスペース(+記号(*2)か、任意のホワイトスペース\sか、%20か)の正規表現(というか文字列)。

regex_space = re.compile('(\+|\s|%20)+')

である。

したがって

regex_space.sub('_', match.group('a') or **snip**)

は、match.group('a')に空白が有った場合は、それをアンダースコアに置換して返す、ということになる(*3)。

ではmatch.group('a')とはなにかといえば/a/c/f.e/sのURLパスの最初の正規表現

/(?P<a> [\w\s+]+ )

(aはapplicationの意味)

で、[a-zA-Z0-9_]=英数文字+あるいは\s=任意のホワイトスペースかがある文字列の組み合わせを表している(したがってURLパスで言えばスラッシュとスラッシュの間の文字列=しばしばURL segmentと呼ばれる文字列のマッチとなる)。

ここでようやくPythonの正規表現の書き方学習に到達したのであった。ふむ。

というわけで、rewrite.pyにて、application/controller/functionのURLパスが取得され、めでたく request.applicationの「パス名称」がきまる、いうわけである。

listdir

os.listdirではない。gluon/fileutilsモジュールのlistdirである。

restricted

gluon/restricted.pyモジュールにある関数。

def restricted(code, environment=None, layer='Unknown'):
  """
  runs code in environment and returns the output. if an exception occurs
  in code it raises a RestrictedError containing the traceback. layer is
  passed to RestrictedError to identify where the error occurred.
  """     

指定されたcodeがtypes.CodeTypeでcodeならばそれを実行exec。そうでない場合は、コンパイルした上で実行する。というよりむしろ、実行できなかった場合はエラー処理(およびロギング)することなどのほうが主眼だと思われる。

read_pyc

これはdef read_pycとして定義されており、コンパイルされているモデル.pycのチェック、評価を行う。

Pythonのコンパイルファイルのお勉強に最適か。

read_pyc自体は短いコード

    data = read_file(filename, 'rb')
    if not is_gae and data[:4] != imp.get_magic():
        raise SystemError, 'compiled code is incompatible'
    return marshal.loads(data[8:])   

is_gaeはGoogle Application Engineの略みたいだ。GAEを設定してれば、ということになる。その意味は・・・多分使わないから無視しよう。

ここでのポイントは、Pythonのコンパイルファイルには「マジックナンバー」というのがバイナリの冒頭4バイトに付加されていて、これでバージョン管理を行なっているということを理解した(おれが)、ということである。http://linuxfree.ma-to-me.com/archives/000369.html

次の4バイトはタイムスタンプ(http://blog.kzfmix.com/entry/1301308907)で、その後がコードとなる。

コードは、marshalhttp://www.python.jp/doc/2.5/lib/module-marshal.html)でバイナリ化されている。

というわけで、最後のreturnはmarshal化された9バイト目から以降のデータを値に変換して返す、ということになる。

os.path.split

パス分解

from os import path
>>> path.split("/tmp")
('/', 'tmp')
>>> path.split("/tmp/")
('/tmp', '')
>>> path.split("tmp")
('','tmp')
>>> path.split("/tmp/")[0]
'/tmp'
>>> path.split("/tmp")[0]
'/'
>>> path.split("tmp")[0]
''
getcfs

cfs.pyモジュールの関数。

models/*.py(c)ファイルのコードが、同一スレッドですでに動作しているかどうかを判定して、スレッドのキャッシュか、ファイルのコードを読み取り返す・・・みたい(*4)。

この役割を担うのはthreadモジュールhttp://www.python.jp/doc/release/library/thread.html(version 3系では改名されて_threadとなっている)。

Pythonのマルチスレッドについて、詳細は「PY習 threadingモジュール(1)」 (http://pyshu.blog111.fc2.com/blog-entry-78.html)〜「モジュール(6)」までが詳しい(ただしversion3系のthreadingに基づいた記述)。


run_model_inの動作まとめ
  1. rewirte.pyにて正規表現でパースされたURLのセグメント化されたものがrequest変数に入っている。
  2. まず、requestされた当該アプリケーションの絶対パス(request.folder)直下にあるcompiledフォルダを見る
    1. その中に'models_なにか'.pycがあるなら、それをrestricted関数(restricted.py)で実行execする。
    2. さらにcompiled/modelsフォルダー内の*pycがあれば、そのフルパスをmodelsという変数に入れる
  3. request.folder/compiledフォルダがないばあいは、request.folder/models/*pyのフルパスをmodels変数に入れる。
  4. その上でmodelsをループして、pycあるいはpyを実行する。
    1. そのさい、request.folder/compiled,request.folder/models,request.folder/models/コントーローラ名,request.foder/コントローラ名/関数名にセパレタ記号/が存在していなくて(*5)、かつコントローラ名がappadminではない場合はなにもしない。

学習総時間5時間ぐらい。

コントローラーへ:run_controller_in
ビューへ:run_view_in

以上

以上終わり。main.pyエライということで。





*1: http://www.python.jp/Zope/articles/tips/regex_howto/regex_howto_3

*2: スペースをURLエンコードで+に変換ている場合にあたる。

*3: URLにsome thingのように空白があれば、web2pyはフォルダ名をsome_thingにするという規則をもっている、ということになる。

*4: じつは、このcfs.pyのコードは、理解できてないのでした・・・冒頭でcfsを空辞書にして、そのあとdef内でcfs.get(key,None)しているというのはどういう意味なんだろう。。。

*5: os.path.split(パス)[0]が無い、という意味。逆に言えば、appadminならば無くてもいいわけか・・よくわかっていない。

this file --> last modified:2012-03-31 13:57:53