Yet Another Sakatokuです。今回はPyhonで書かれたRuby on Rails風のフレームワークPylonsの使い方を簡単に紹介したいと思います。
PylonsはPythonのWebフレームワークとしては、現在のところ、Django, TurboGearsにつぐ三番手(以下)と見なされていますが、TurboGears 2がPylonsと合流して、Pylons上に旧TurboGearsのAPIを提供していくことが表明されましたので、今後大きな勢力になっていくと思われます。
サンプル・アプリケーション
サンプルとして、簡易Wiki(CoCoWiki)を作ってみました。以下のURLからダウンロードできるので参考にしてみてください。
cocowiki.tar.gzPylonsの開発サーバ上で動作させるには、ダウンロードしたtar.gzを解凍し、cocowikiディレクトリで"paster serve"コマンドで実行します。
$ tar xzf cocowiki.tar.gz
$ cd cocowiki
$ paster serve development.ini
上手く動作していれば、http://localhost:5000/にアクセスすると次のような画面が表示されると思います。
cocowiki posted by (C)フォト蔵
動作確認はWindows版Python2.5.1, Linux版Python2.4.3で行いました。後述の通り、あらかじめPylonsとSAContextをインストールしている必要があります。また、Python2.4以前のバージョンの場合は、pysqlite2ライブラリも必要です。
Pylonsのインストール
例によってeasy_installを使ってインストールします。ただ、僕の環境ではsetuptoolsのバージョンが古いというエラーが発生したので、最初に以下のURLから最新版のsetuptoolsをダウンロードし、インストールしました。
http://cheeseshop.python.org/pypi/setuptools
次に、Pylonsをインストールします。現在配布されているバージョンは0.9.6RC1です。
$ easy_install -U Pylons
これで、Pylonsの依存するライブラリがすべてインストールされるはずです。
- PasteScript
- PasteDeploy
- SQLAlchemy
- Mako
- decorator
- Beaker
- WebHelpers
- Route
- FormEncode
- simplejson
- nose
Pylons 0.9.6の大きな変更点として、データベース接続にSAContextというライブラリの使用が推奨されるようになったことがあります。今回作成するアプリケーションではこのSAContextを使うので、これもインストールしておきます。
$ easy_install -U SAContext
プロジェクトを作成する
新しくPylonsプロジェクトの作成するには"paster create"コマンドを使います。
プロジェクト名は、"cocowiki"としました。
$ paster create --template=pylons cocowiki
$ cd cocowiki
開発用のサーバを起動するには、"paster serve"コマンドを使います。
$ paster serve development.ini
アプリケーションの設定
まずdevelopment.inにアプリケーションの設定を記述します。
今回はSessionを使わないので、とりあえずはデータベースの設定のみです。
sqliteを使うことにしましたが、SQLAlchemyがサポートするデータベースならば何でも構いません。
sqlalchemy.default.uri = sqlite:///%(here)s/data/cocowiki.sqlite.db
sqlalchemy.default.echo = true
sqlalchemy.default.echo_pool = false
sqlalchemy.default.pool_recycle = 3600
SQLAlchemyのサポートするデータベースとURIの記述の仕方については、こちらのドキュメント(英語)を参照してみてください。
モデル定義とデータベースの初期化
次にモデルの定義をcocowiki/model/__init__.pyに記述します。
# -*- coding: utf-8 -*-
# cocowiki/model/__init__.py
from sqlalchemy import *
from sqlalchemy.ext.assignmapper import assign_mapper
from urllib import quote
from datetime import datetime
from sacontext import PylonsSAContext
# Pylons 0.9.6から以前のpylons.databasesがdeprecated扱いになりました。
# 代わりにSAContextのPylonsSAContextを使うことが推奨されています。
# 詳しくはhttp://sluggo.scrapping.cc/python/sacontext/を参照してください。
sac = PylonsSAContext()
sac.add_engine_from_config(None)
# テーブル定義
pages = Table('pages', sac.metadata,
Column('id', Integer, primary_key=True),
Column('word', Unicode(250), nullable=False, unique=True),
Column('content', Unicode, nullable=False),
Column('created_at', DateTime, default=datetime.now()),
Column('updated_at', DateTime, default=datetime.now(), onupdate=datetime.now()),
mysql_engine='InnoDB',
)
# Wikiのページに相当するドメインオブジェクト
class Page(object):
def __init__(self, word=None, content=None):
self.word = word
self.content = content
def __repr__(self):
return '<cocowiki.model.Page "%s">' % self.word
def get_absolute_url(self):
"""
このオブジェクトのURLを取得します
これはPylonsの流儀ではないかもしれませんが便利です!
"""
return '/%s' % quote(self.word.encode('utf-8'))
# AssignMapperを使ってテーブルとドメインオブジェクトを
# マッピングします
assign_mapper(sac.session_context,
Page,
pages)
モデルの設計は、あっさりとしたもので済ませることにしました。
いくつか注釈を付け加えるならば、
- このような単純な例ならば、ElixirというSQLAlchemyのラッパーライブラリを使う手もあります。このライブラリを使うと、モデル定義をよりRails的に(ActiveRecord的に)行うことができます。ただ、個人的にはあまり好みではないので使っていません。
- Pylonsのサンプルでは、しばしば、"assign_mapper"ではなく"mapper"が使われていますが、assign_mapperの方がコードが簡潔になるのでお勧めです。
- もっと大きなアプリケーションを書く場合、project/model/__init__.pyにドメインオブジェクトの定義も、テーブルの定義も一緒に書いてしまうと収拾が付かなくなります。本来ならば、project/model/tables.py, project/model/domains.py...のように細かくモジュールを分けるのがお勧めです。このアプローチはSQLAlchemyのサンプル(test/zblog)が参考になります。
データベースの初期化のコードは、cocowiki/websetup.pyに記述しておきます。
# -*- coding: utf-8 -*-
"""Setup the cocowiki application"""
from paste.deploy import appconfig
from pylons import config
from cocowiki.config.environment import load_environment
def setup_config(command, filename, section, vars):
"""Place any commands to setup cocowiki here"""
conf = appconfig('config:' + filename)
load_environment(conf.global_conf, conf.local_conf)
from sqlalchemy import exceptions
from cocowiki.model import sac, Page
sac.metadata.create_all(checkfirst=True)
try:
start_page = Page('StartPage', u'ここうぃきの世界にようこそ')
sac.session_context.current.flush()
except exceptions.SQLError, e:
pass
コマンドラインから、"paster setup-app"コマンドを使うと、このコードを実行することができます。
$ paster setup-app development.ini
コントローラの作成, ルーティングの設定
コントローラを作成します。
$ paster controller page
これで、cocowiki/controllers/pages.pyにPagesControllerというコントローラクラスが生成されます。このコントローラのメソッドに各処理(アクション)を実装していきます。
それらのアクションとURLを対応付けているのが、cocowiki/config/routing.pyです。ルーティングについて簡潔にまとまっているページがなかなか見つからないので、Routeのドキュメントにあたるのが一番早そうです。今回は、ドキュメントを参考に次のようなURL設計にしてみました。
- /list
- 登録済みのページ一覧の表示
- /WikiWord
- WikiWordというページの表示
- /WikiWord;edit
- WikiWordというページの編集(新規登録)フォームの表示
"/list"で一覧表示というのが、あまりエレガントではないかもしれません。また、リソースの新規作成と更新をURL上でも、HTTPメソッドの上でも区別しないことにしています。"/WikiWord"というURLにPOSTすると、ページがすでにある場合は更新し、無い場合は新規作成します。
このような動作に対応するrouting.pyは、以下のようになります。
# cocowiki/config/routing.py
def make_map():
# ...略
# 登録されている単語を表示するページです。
map.connect('list', controller='pages', action='index')
# Wikiの編集フォームのページ
map.connect('*(word);edit', controller='pages', action='edit')
# Wiki更新のアクションのマッピングです。
# フォームの内容にエラーがある場合は、再度上のeditページを表示します。
map.connect('*word', controller='pages', action='update', conditions={'method':['POST']})
# Wikiの個々のページです。
# 上記のform, edit, list以外の単語がWikiワードになります。
map.connect('*word', controller='pages', action='show')
return map
Wikiとは違って、リソースをデータベースのinteger型のキーで識別するようなアプリケーションでは、Rails風(REST風)のURLマッピングが使えます。この場合は、コントローラを"paster restcontroller"で作成します。
$ paster restcontroller object objects
"paster restcontroller"に関しては、別の場所でメモを残したことがあるので、興味がある方は参照してみてください。Pylons 0.9.6になっても、この部分は変わっていないと思います。
コントローラの実装
いよいよコントローラにロジックを書いていくわけですが、すべてのアクションについて解説していくと長くなるので、ページの取得の部分(showメソッド)を例にとって、Pylonsの基本を説明していきます。
class PagesController(BaseController):
def show(self, word=None, format='html'):
"""
Wikiワード"Word"を表示します。
WordがNoneの場合、つまりURLが"/"の場合は、/StartPage
にリダイレクトさせます。
"""
if not word:
redirect_to('/StartPage')
c.page = Page.get_by(word=word)
if c.page is None:
# wordでページが登録されていない場合は、
# 新規登録のフォームを表示します。
return self.edit(word)
# 更新順に10件のページを取得します。
# limit=11で最大11件表示しているのは、「もっと見る」の
# リンクの表示する条件を簡略化するためです。
c.words = Page.select(order_by=desc(Page.c.updated_at),
limit=11)
return render('show')
- ビュー(テンプレート)に値を渡すには、"c"を使います。上記の例では、"c.page = ...", "c.words = ...."がそれに当たります。
- リダイレクトさせる時にはredirect_toを使います。これは例外として処理されるので、return redirect_to('/path')のようにする必要はありません。
- HTTPエラー(例えば404 Not Found)を返したい場合は、abort(404)を使います。
- 以前のバージョンのPylonsではテンプレートをレンダリングするのに、render_responseというメソッドを使っていましたが、今はrenderを使うようです。
他のメソッドは、サンプルアプリケーションのコードを参照してください。Pylonsの流儀に慣れてしまえば、難しいことは何もしていないことがお分かりになると思います。
テンプレートの作成
テンプレートは、cocowiki/templatesに配置します。現在のPylonsの標準テンプレートエンジンはMakoで、上述のrenderメソッドを使った場合、render('show')ならばcocowiki/templates/show.makが使用されます。
Makoの構文は以前簡単に紹介したとおりですが、今回は継承機能を使ってみました。Makoの継承機能についてはこちらのメモもご覧下さい。
実際のcocowiki/templates/show.makは次のようになっています。
<%inherit file="base.mak" />
<%def name="title()">${c.page.word | h}</%def>
<h2>${title()}</h2>
${ h.cocowiki_markup(c.page.content) }
<h3>${ _('Menu') }</h3>
<ul>
<li>${ h.link_to(_('Edit this page'), h.url_for(word=c.page.word, action='edit')) }</li>
</ul>
<h3>${ _('Recent changes') }</h3>
% if c.words:
<ul>
% for word in c.words[:10]:
<li><a href="${word.get_absolute_url()}">${word.word | h}</a></li>
% endfor
</ul>
% if len(c.words) > 10:
<span>» ${ h.link_to(_('See more'), h.url_for(word=None, action='index')) }</span>
% endif
% else:
<p>${_('No pages')}</p>
% endif
Pylonsのテンプレートについてまず知っておくべきことは、次のようなことです。
- コントローラで割り当てた変数は、テンプレートからも"c"で参照できます。
- "h"を使うと、cocowiki/lib/helpers.pyに定義されている関数、変数を参照できます。上記の例で言うならば、h.cocowiki_markup, h.link_to, h.url_for等がそれに当たります。
- 今回は使いませんでしたが、"g"を使って、cocowiki/lib/app_globals.pyに定義されている値を参照することができます。これはアプリケーション共通の設定を参照するのに便利です。
- "request"を使って、そのリクエストの根底にあるWSGIRequestオブジェクトを参照することができます。これは、リクエストのPATH_INFOを調べたりする際に使用できます。
独自のビュー・ヘルパー関数を定義する
今回作成したアプリケーションでは、cocowiki/lib/helpers.pyに簡易Wiki記法をHTMLに変換するcocowiki_markupという関数を定義しています。
# -*- coding: utf-8 -*-
from webhelpers import *
from urllib import quote
import re
from cgi import escape
WIKI_WORD_RE = re.compile(r'\[\[(.+?)\]\]')
def cocowiki_markup(value):
"""
WIKIのページデータをHTMLに変換します。
"""
def repl(matcher):
word = matcher.group(1)
return '<a href="%s">%s</a>' % (quote(word.encode('utf-8')),
escape(word))
# markdown記法で変換
value = markdown(value)
# http://example.com, mail@example.comをリンクに変換
value = auto_link(value)
# [[word]]という表記をWikiワードとして扱う
value = WIKI_WORD_RE.sub(repl, value)
return value
静的なファイルの配信
cocowiki/publicに置いたファイルは静的なファイルとして配信することができますので、ここにスタイルシートや画像をおきました。
まとめと謝辞
以上、Pylonsの概要を駆け足で紹介しました。サンプルアプリケーションと合わせて参照していただくことを念頭に執筆しましたので、かなり説明を省略しているところもあるかと思います。
分かりにくいところや、間違いがあった場合は、お気軽にコメントやトラックバックをいただけましたら幸いです。
また、サンプルアプリケーションのテンプレートには、stansさんが配布されているテンプレートを利用させて頂きました。ありがとうございました。