Flask 入門日誌 - ブログ構築(flaskr)編

Flask 入門日誌目次 - にっき

Quickstartは、すでに他のWAF経験がある人向けの文書で、Flask特有のやりかたが書かれているという印象。まったくの初心者は読んでいってもピンと来ない部分が多いので、チュートリアルを読み進める。Flaskのチュートリアルは、flaskrというブログアプリを作るものである。チュートリアルは(バージョンが古いものの)完全に訳出されているので、それを読んで考えたことをここでは述べるに留める *1

(日本語でFlaskのドキュメントを読めると考えたのが甘えだった、という結論に至った。これからは本家のドキュメントを読む)

(DB周りがぜんぜん分からなかったので、逐一メモりながら読んでたら後半ほぼ翻訳になった。flaskのライセンスは3 clause BSDらしい。どう著作表示したらいいのかよく分からんのでリンクだけ貼っときます

step 0: フォルダ構成

/flaskr
    /flaskr
        /static
        /templates

なんでflaskrが入れ子になっとるの?と思われるはず。上のflaskrは単なるプロジェクト置き場としてのディレクトリで、下のflaskrディレクトリはパッケージ名としてのそれである。このチュートリアルではPythonパッケージとしてflaskrをインストールし実行する(step 3)。パッケージ化はFlaskアプリを作る際に推奨される方法である。

step 1: データベーススキーマ

sqlite3を利用する。DBよく知らないです…。idがプライマリキーでタイトル文字列と本文文字列を格納するようになってる、autoincrementnot nullもそのままの意味だろ、という理解でなんとかなると思っている。

step 2: セットアップ

特に変わったことはしていないが、app.config.from_object(__name__)で自分のファイルから全ての文字がuppercaseな変数を設定値として読み込むらしい。ほえーという感想。

あと以下のコードについて少しだけ確認。

app.config.update(dict(
    DATABASE=os.path.join(app.root_path, 'flaskr.db'),
    SECRET_KEY='development key',
    USERNAME='admin',
    PASSWORD='default'
))
app.config.from_envvar('FLASKR_SETTINGS', silent=True)

Q. OSはプロセスのワーキングディレクトリを把握しているのに、なぜここでわざわざデータベースのパスを指定しなければいけないのか

A. 1つのプロセスで複数のアプリケーションを動かすことがありうるから。ここではapp.root_path属性でアプリのパスを取得している。さらに現実世界で運用するべきアプリを作る場合はInstance Folderを使えとドキュメントは述べている

step 3: パッケージとしてflaskrをインストールする

setup.pyは普通に書けばいいが、 templatestaticディレクトリ、DBスキーマも含めてパッケージングしたい。MANIFEST.inを作成して以下のように記述する。

graft flaskr/templates
graft flaskr/static
include flaskr/schema.sql

graftとかincludeというのはマニフェストファイルのためにsetuptoolsが用意しているコマンドである。詳しくは以下を参照。

4. ソースコード配布物を作成する — Python 2.7.x ドキュメント (3系のドキュメントに説明がないので2.7で許していただきたい。マニフェストについては内容は同じだと思う)

__init__.pyからfrom .flaskr import appする。このimport文はアプリケーションのインスタンスをパッケージのトップレベルに担ぎ込む。アプリケーションをいざ走らせようとなった段で、Flaskの開発サーバはappインスタンスの場所を知る必要がある。このimport文はロケーションプロセスを単純にする。これがないとexport FLASK_APP=flaskr.flaskrなどと環境変数に書く必要が出てくる。

これでアプリをインストールする準備が整った。当然ながら、こうしたアプリのpipでのインストールはvirtualenv内で行うことが推奨される。

pip install --editable .する。--editableを使うとインストールしたものがsite-packagesとやらに入らず今のディレクトリのものを使う。つまり変更がそのまま反映されるので楽。

step 4: データベース接続

DBとの接続はstep 2のconnect_db関数を書いたのだが、これはあまり使い勝手がよくない。毎度データベース接続を確立してクローズするというのは非効率的である。接続はちょっぴり長めに手元に留めておきたいわけだ。データベース接続はトランザクションカプセル化であるために、一度に1つだけのリクエストが接続を使うということを確認する必要がある。このことを実現するための洗練された方法は、アプリケーションコンテクストを利用することである。

Flaskは2種類のコンテクストを提供する。アプリケーションコンテクストリクエストコンテクストである。さしあたりは、それらは特殊な変数を使うものだと覚えておけばよい。例えば、request変数は現行ののリクエストと関連するリクエストオブジェクトである。それに対してgは現行のアプリケーションコンテクストに関連する汎用の変数である。これらについてはチュートリアルの後半で詳細に触れる。

さしあたりは、gオブジェクトに情報を安全に保管できるということを覚えておけばよい。

ヘルパー関数として以下のコードを書く。

def get_db():
    if not hasattr(g, 'sqlite_db'):
        g.sqlite_db = connect_db()
    return g.sqlite_db

これは初回呼び出し時にデータベース接続を現行のコンテクストに立ち上げ、次回以降の呼び出しでは確立された接続をそのまま返す、という処理である。

これで接続の仕方は分かった。では適切に接続を切るにはどうしたらいいだろうか? Flaskはteardown_appcontextというデコレータを提供している。これはアプリケーションコンテクストを解体(tear down)*2するとき毎回呼び出される。

@app.teardown_appcontext
def close_db(error):
    """Closes the database again at the end of the request."""
    if hasattr(g, 'sqlite_db'):
        g.sqlite_db.close()

teardown_appcontextでマークされた関数は、アプリコンテクストを解体するたびに毎回呼び出される。これが何を意味するのか、かいつまんで言うと、アプリコンテクストはリクエストが来る前に作られ、リクエストが終了すると、いつともなく破棄される。(アプリコンテクストの)解体は次の2つの原因により起こりうる:すべてがうまくいった(errorパラメータがNoneであった)場合と、あるいは例外が起こった場合、どちらにしろerrorは解体を行う関数に渡されるのである。

ここで起こっていることのより詳しい説明についてはドキュメントのApplication Contextを参照。

step 5: データベースの作成

sqlite3コマンドでdbを作成できる、

$ sqlite3 /tmp/flaskr.db < schema.sql

が、sqlite3コマンドが必要、エラーを導入するためのパスが必要、などの不都合がある。

dbを初期化する関数を作り、さらにそれをflaskコマンドから扱えるようにすると楽である。

def init_db():
    db = get_db()
    with app.open_resource('schema.sql', mode='r') as f:
        db.cursor().executescript(f.read())
    db.commit()

@app.cli.command('initdb')
def initdb_command():
    """Initializes the database."""
    init_db()
    print('Initialized the database.')

app.cli.comannd()デコレータはflaskスクリプトに新しいコマンドを登録する。コマンドが実行されるとFlaskは自動的にアプリケーションコンテクストを作成し、それをアプリケーションに束縛する。関数の内部ではflask.gなど必要そうなものにだいたいアクセスできる。スクリプトが終了するとアプリケーションコンテクストは解体され、データベース接続は解放される。

アプリケーションオブジェクトが持っているopen_resource()メソッドはアプリケーションが提供するリソースを開くための便利な関数である。この関数はリソースの場所(flaskr/flaskrフォルダ)からファイルを開き、読み出せるようにしてくれる。SQLiteによって提供される接続オブジェクトは、カーソルオブジェクトを与えることができる。カーソルには完全なスクリプトを実行するメソッドがある。そして最後は変更をコミットだけである。SQLite3やその他トランザクショナルDBは、こちら側が明示的に伝えるまでコミットを行わない。

step 6: ビュー関数

ようやくDB接続終わり。楽しいビュー関数だ!ビュー関数は4つ機能をつける。

エントリの表示

ルートにアクセスが来たら、DBからタイトルとテキストをid逆順で全部引っ張って来て表示する。何て分かりやすいんだ。show_entries.htmlはhtmlテンプレート(後で)。

@app.route('/')
def show_entries():
    db = get_db()
    cur = db.execute('select title, text from entries order by id desc')
    entries = cur.fetchall()
    return render_template('show_entries.html', entries=entries)

新しいエントリの追加

ここで書くビューは、ログインしているユーザの新しいエントリを追加するものである。これはPOSTメソッドのみに応答する:実際の投稿フォームはshow_entriesページで表示する。すべての処理がうまくいったら、メッセージをflash(チカッ)としてユーザをshow_entiresページにリダイレクトする。

@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    db = get_db()
    db.execute('insert into entries (title, text) values (?, ?)',
                [request.form['title'], request.form['text']])
    db.commit()
    flash('New entry was successfully posted')
    return redirect(url_for('show_entries'))

ところで、上記のようなユーザの入力からそのままSQLを組み立てるようなコードは、セキュリティ的に問題があるので自分だけが使う場合以外はやめた方がいい。

ログインとログアウト

設定からユーザ名とパスワードをチェックし、セッションのlogged_inキーのフラグをTrueに切り替える。

@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != app.config['USERNAME']:
            error = 'Invalid username'
        elif request.form['password'] != app.config['PASSWORD']:
            error = 'Invalid password'
        else:
            session['logged_in'] = True
            flash('You were logged in')
            return redirect(url_for('show_entries'))
    return render_template('login.html', error=error)

render_templateにはログイン失敗にしたときのエラーも渡している。

次いでlogout関数を書く。

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

pop()dictの持つメソッドで、指定したキーの要素が存在する場合削除し、存在しなければ何も悪いことは起こらない。ログアウトしようとしているユーザが、ログインしているかどうか確認する必要がないので楽である。

step 7: テンプレート

layout.html: ログイン状態に応じて表示するリンクをurl_for('login'), url_for('logout')と変えている。また存在する場合はflashされたメッセージを表示している。

show_entries.html: layout.htmlを拡張し、body部を書いたもの。ログインした状態では投稿フォームを表示したり、エントリの表示をしたりといったことをする。

login.html: ログインフォーム。ログイン失敗時には同ページにリダイレクトされるのでその時表示するエラーメッセージ部などを仕込んでいる。

step 8: スタイルの追加

style.cssstaticフォルダに書いて終わり。

ボーナス: アプリケーションのテスト

やりません

感想

DB知らないのでそのへんの話を咀嚼するのに骨が折れた。というかアプリケーションコンテクストについては未だ内実不明である。知らなくてもさしあたりは困らないものだが。DB周りは追い追い覚えるとして、簡単なwebアプリの動きの全体像が把握できたので、やる意義はそれなりあったと感じる。

*1:新しいドキュメントにはsetuptoolsでのインストール項目などがあり、かなり異なる。また0.11以降でflaskはclickと連携しており、チュートリアルはそれを用いた説明もなされている。古い日本語ドキュメントを読む意義はかなり薄い

*2:データベースでよく使う用語っぽいが日本語にしていいのかよくわからない。とりあえずここでは「解体」と呼称する。