fanstatic による Python パッケージを使った静的リソース管理

最近 Kotti で Web アプリを作ってみようと調査しています。但し、今日は Kotti のお話ではなく、たまたま更新差分を見ていたら、fanstatic という静的リソース管理ツールが新規に使われているのを見つけました。ちょっと調べてみると、とても良さそうに見えたので紹介します。

fanstatic って何?

特にドキュメントでパッケージ名の由来を見つけられなかったのですが、fan + static と区切ってみると名前を覚えやすいです。個人的に fan- という係りが fancy, fantasy, fantasista などを連想させて言葉の響きが良いですね。static リソースの管理の煩雑さを解消してくれる夢のようなツールを連想します。

さて、fanstatic は、スマートな静的リソースパブリッシャーとあります。

テンプレートに静的リソースの記述をインジェクションしてくれるので、パブリッシャーという表現をしているのでしょうが、それよりも javascriptcss といった静的リソースを Python パッケージとして管理できる仕組みに私は驚きました。

パッケージ、ドキュメント、リポジトリはそれぞれ以下になります。

fanstatic で jquery を使ってみよう

fanstatic を使うと、どんなことができるか、クイックスタート を参考にしながら見てみましょう。

まずは fanstatic と js.jquery というパッケージをインストールします。

(fanstatic)$ pip install fanstatic js.jquery
(fanstatic)$ pip freeze
Paste==1.7.5.1
WebOb==1.2
distribute==0.6.24
fanstatic==0.11.4
js.jquery==1.7.1
wsgiref==0.1.2

簡単なサンプルコードから紹介します。

(fanstatic)$ vi quick.py 
# -*- coding: utf-8 -*-
from fanstatic import Fanstatic
from js.jquery import jquery

def app(environ, start_response):
    start_response('200 OK', [])
    jquery.need()
    return ['<html><head></head><body>Hello World</body></html>']

if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    fanstatic_app = Fanstatic(app)
    server = make_server('0.0.0.0', 8080, fanstatic_app)
    server.serve_forever()

通常の WSGI アプリを作成して、2行だけ追加します。jquery.need() は、HTML の <head> セクションに <script> タグを埋め込みます。あとは FanstaticWSGI アプリをラップするだけです。

...
    jquery.need()
...
    fanstatic_app = Fanstatic(app)
...

実行結果を見てみましょう。

(fanstatic)$ python quick.py 

別のターミナルで
(fanstatic)$ telnet localhost 8080 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /

<html><head>
    <script type="text/javascript" src="/fanstatic/jquery/jquery.js"></script>
</head><body>Hello World</body></html>

ちゃんと <head> セクションに jquery の <script> タグが埋め込まれていますね。

作業前に js.jquery を pip でインストールしただけで使えるのが便利です。jquery の最新版をダウンロードしてきて、どこそこに展開して、パス設定はどうしようかな、、、と悩まなくて済むのが凄いところです。また、jquery のような、汎用的なライブラリは、みんなで共有してバージョンアップもパッケージ管理ツール (今回は pip) で行えた方が楽で良いですね。

任意のアプリから静的リソースを管理する

もう少し現実的なアプリケーションについて、リソースライブラリの作成 から見てみましょう。

ここでは静的リソースを含む Python パッケージを作成してみます。まずパッケージディレクトリとひな形を作成します。

(fanstatic)$ mkdir myapp
(fanstatic)$ cd myapp
(fanstatic)$ mkdir -p foo/static
(fanstatic)$ touch foo/__init__.py
(fanstatic)$ touch foo/static/base.css
(fanstatic)$ touch foo/static/base.js

次にパッケージの setup.py を定義します。

(fanstatic)$ vi setup.py
# -*- coding: utf-8 -*-
from setuptools import setup

setup(
    name="myapp",
    version="0.1",
    install_requires=["fanstatic", "js.jquery"],
    entry_points={
        "fanstatic.libraries": [
            "foo = foo.static:lib_foo",
        ],
    },
)

install_requires に fanstatic と使いたい js ライブラリを記述します。ここでは js.jquery のみを記述します。fanstatic がパッケージ内の静的リソースの場所を見つけられるように entry_points を使って定義します。ここが1つのポイントです。

では、先ほど作成した static ディレクトリを fanstatic から見つけられるように static.py に定義します。サンプルコードから紹介します。

(fanstatic)$ vi foo/static.py
from fanstatic import Group, Library, Resource
from js.jquery import jquery

_resources = [jquery]

lib_foo = Library("foo", "static")
foo_js = Resource(lib_foo, "base.js", depends=[jquery], bottom=True)
_resources.append(foo_js)

foo_css = Resource(lib_foo, "base.css", bottom=True)
_resources.append(foo_css)

resources = Group(_resources)

大体見た感じで雰囲気は掴めますが、詳細に見て行きます。

lib_foo = Library("foo", "static")

Library オブジェクトは、名前と静的リソースの置き場所へのパスを引数に取ります。ここで lib_foo は、setup.py の entry_points で定義した名前を使う必要があるのに注意してください。

foo_js = Resource(lib_foo, "base.js", depends=[jquery], bottom=True)

Resource オブジェクトは、実際の静的リソース (js/css) を定義します。base.js は jquery を使うスクリプトなので depends=[jquery] を定義することにより、base.js より前に jquery.js が読み込まれるようにインジェクションされます。

resources = Group(_resources)

Group オブジェクトは、複数のリソースをまとめます。あとで紹介しますが、複数リソースのインジェクションを resources.need() のように実行できて便利です。

主要な点は紹介しました。実際に動かせるように残りのファイルも紹介します。

main プログラムです。せっかくなので css/js が適用されていることを確認できるように html を少し変更します。

(fanstatic)$ vi foo/main.py 
# -*- coding: utf-8 -*-
from fanstatic import Fanstatic
from static import resources as static_resources

def app(environ, start_response):
    start_response('200 OK', [])
    static_resources.need()
    html = """
    <html>
      <head></head>
      <body>
        <button type="button" id="sample_btn">Click Me!</button>
      </body>
    </html>
    """
    return [html]

if __name__ == "__main__":
    from wsgiref.simple_server import make_server
    fanstatic_app = Fanstatic(app)
    server = make_server('0.0.0.0', 8080, fanstatic_app)
    server.serve_forever()

base.css と base.js は、それぞれ以下の通りです。

(fanstatic)$ vi foo/static/base.css 
button {
    border-color: #666666;
    background-color: #E8E6E1;
    font-size: large;
    padding: 10px;
}
(fanstatic)$ vi foo/static/base.js 
$(function() {

    $("#sample_btn").click(function() {
        alert("Hello World");
    });

});

最終的なパッケージの構成です。

(fanstatic)$ tree myapp/
myapp/
├── foo
│&#160;&#160; ├── __init__.py
│&#160;&#160; ├── main.py
│&#160;&#160; ├── static
│&#160;&#160; │&#160;&#160; ├── base.css
│&#160;&#160; │&#160;&#160; └── base.js
│&#160;&#160; └── static.py
└── setup.py

2 directories, 6 files

それでは、パッケージをインストールして、実行してみましょう。

(fanstatic)$ python setup.py develop
(fanstatic)$ python foo/main.py 

ブラウザでアクセスして、ボタンをクリックすると、次のような画面を確認できます。

まとめ

fanstatic の強力さと利便性が分かる簡単なチュートリアルを紹介しました。

ここで紹介したソースは以下に置いてあります。

fanstatic リポジトリ を見ると、js. の名前空間で始まるライブラリがたくさんあります。ここに無ければ、自分でパッケージングして公開するのも良いですね。パッケージ管理の仕組みを、こんな用途にも使えるんだと再発見しました。おもしろいですね。

PyCon Taiwan 2012 イベントレポート Day 0

6月9-10日に開催された PyCon Taiwan 2012 に参加してきました。

初めて行った台湾ですが、羽田空港から飛行機で3時間半と近く、とても良いところですっかり気に入りました。そんな雰囲気が分かるレポートになっていると思います。記事は3人で執筆しています。

DevOps は開運!?

半月以上前のイベントですが、5月26日に DevOps Days Tokyo 2012 が開催されました。私はお手伝いも兼ねて参加していました。

John Wills 氏のスライド

冒頭のセッションで Enstratus 社の John Wills 氏がスライドに「開運」という文字を表示していました。

昨日、Jay(本イベントの司会で中心人物の堀田直孝氏)と、DevOpsの定義ってなんだろうね、という話をしていて、それは「開運」じゃないかということになり、じゃあ開運のスライドからはじめようと。

DevOpsを実践する企業に共通すること。DevOps Day Tokyo 2012 - Publickey

Jay が DevOps の定義を話しているうちに「開運」になったんだと聞いたのですが、私はその詳しい経緯がよく分かっていませんでした。

このときに来日していた John Willis 氏と Damon Edwards 氏が DevOps について語る DevOps Cafe Podcast & Videos - Show Notes という Podcast を提供しています。
その後、先日の DevOps Tokyo 2012 で収録した Podcast がアップロードされていました。

この対談には Jay も出てきますが、24分頃から「DevOps = 開運」になった経緯が語られています。

DevOps って日本語で何だろうね?

  • Develop x Operation = 開発 x 運用
  • Dev x Ops = 開 x 運

それで「開運」かと言えば、そのまんまでしかないのですが、この「開運」に秘められた意味としては、開 = Open, 運 = fortune、それなら open your luck! つまりは better fortune! ですねと言うやり取りが、なかなかに当たらずとも遠からずのような、英語と日本語の違いがあるものの、うまい言葉になったもんだと感心した次第です。

detox で tox テストを並列実行しよう

複数の Python バージョンでテストを実行するツールに tox があります。

tox ツールそのものがとても便利なのですが、この tox テストを並列実行してくれるツールがリリースされました。

既に tox を使っている環境であれば、detox をインストールするだけで良いです。

$ pip install detox

使い方は tox と全く同じで特別な設定は不要です。tox コマンドを実行する代わりに detox コマンドを実行します。試しに実行してみましょう。

(test)$ detox 
py26 create: /Users/t2y/work/repo/littlehttpserver/.tox/py26
GLOB sdist-make: /Users/t2y/work/repo/littlehttpserver/setup.py
py27 create: /Users/t2y/work/repo/littlehttpserver/.tox/py27
py32 create: /Users/t2y/work/repo/littlehttpserver/.tox/py32
py26 installdeps: :pypi:pytest, :pypi:pep8 
py27 installdeps: :pypi:pytest, :pypi:pep8
py32 installdeps: :pypi:pytest, :pypi:pep8
installdeps py26- py27/ py32-         

この例では、Python 2.6, 2.7, 3.2 の tox テストが並列実行され、依存パッケージのインストール中です。

最終的な実行結果です。

(test)$ detox 
GLOB sdist-make: /Users/t2y/work/repo/littlehttpserver/setup.py
py27 sdist-reinst: /Users/t2y/work/repo/littlehttpserver/.tox/dist/LittleHTTPServer-0.1.2.zip
py32 sdist-reinst: /Users/t2y/work/repo/littlehttpserver/.tox/dist/LittleHTTPServer-0.1.2.zip
py26 sdist-reinst: /Users/t2y/work/repo/littlehttpserver/.tox/dist/LittleHTTPServer-0.1.2.zip
py26 runtests: commands[0]    
py27 runtests: commands[0]               
py32 runtests: commands[0]               
_______________________________ summary ________________________________
  py26: commands succeeded
  py27: commands succeeded
  py32: commands succeeded
  congratulations :)

1つの tox 環境でテストが通ったときに素早く他の環境でも実行したい、ちょっとした修正を行ったときに他の環境でも一応テストしてみたい、といった用途にとても便利です。何よりも CPU パワーが有り余ってるマシンで並列処理を実行するのは気持ち良いです。

detox は、並列処理に eventlet を使っていて、detox のソース自体も百数十行というシンプルなものです。eventlet の GreePool から greenthread でプロセスを実行しているようです。私は eventlet を使っているソースを初めて見かけたので興味深かったです (^ ^;;

pytest のドキュメントを翻訳しました

Python のテストツールに pytest があります。ここ最近、徐々にテストを書くのに慣れてきて、さらにテスト設計や知見を高めようという思いが強くなってきました。

pytest は、wikipedia:設定より規約 を設計原則とするツールです。Python は明示する文化なのでやや違和感を感じる人もいるかもしれません。さらに xUnit スタイルではない funcarg という関数の引数をフックしてリソースインジェクションを行う仕組みを提供するなど、私にとっては新たな概念を学ぶのにおもしろそうな題材に見えました。

Holger Krekel 氏と出会い

先日 PyCon US 2012 に参加して、pytest の作者 Holger Krekel 氏と出会い、話すことができました。

最初に会ったのはカンファレンスが始まる前日のチュートリアルデーで、手持ち無沙汰な様子に見えたので、ランダムデータテストのやり方を相談してみると、pytest のパラメーターテストやフックの仕組みを教えてくれました。そこで顔を覚えてもらったせいか、その後のカンファレンスやスプリントを通して、サンプル実装したコードをレビューしてもらったり、アドバイスをしてもらったりして、最終的な成果物が以下のプラグインです。

カンファレンスも彼の発表を聴講しました。

pytest ドキュメント翻訳

日本へ帰国後、すぐに pytest の翻訳を始めました。

本当は2週間ほどで完了し、4月中旬には出来ていました。その後、メーリングリストで Holger 氏と翻訳ドキュメントの公開やメンテナンスについてやり取りをして、最終的には pytest 本体に翻訳ドキュメントを取り込んでもらいました *1 。とはいえ、やはり Sphinxi18n 機能を使った方が良いかどうかを協議中だったりします。

以下で和訳されたドキュメントが公開されています。

今後のバージョンアップに伴うメンテナンスも継続的に行います。誤字/脱字、誤り翻訳などあれば、こちらまで ご連絡頂けると助かります。

*1:2ヶ月近くかかった理由は Holger 氏が5月末まで休暇中だったため

pytest-pep8 を 0.7 から 0.8 にアップグレードしたらテストが失敗するようになった

タイトルは釣りです。

pytest で pep8 のテストを行う pytest-pep8 プラグインがあります。次のようなサンプルコードを用意します。

(test)$ vi sample.py 
x=3
class A(object): pass

def f(x):
    return x

このサンプルコードを pep8 でチェックすると、たくさんのエラーが出ます。

(test)$ pep8 -r sample.py 
sample.py:1:2: E225 missing whitespace around operator
sample.py:2:1: E302 expected 2 blank lines, found 0
sample.py:2:16: E701 multiple statements on one line (colon)
sample.py:4:1: E302 expected 2 blank lines, found 1
sample.py:6:1: W391 blank line at end of file

同じように pytest-pep8 0.7 で実行します。

(test)$ py.test --version
This is py.test version 2.2.4, imported from /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/pytest.pyc
setuptools registered plugins:
  pytest-pep8-0.7 at /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/pytest_pep8.pyc

(test)$ py.test --pep8 sample.py 
========================= test session starts ==========================
platform darwin -- Python 2.7.3 -- pytest-2.2.4
pep8 ignore opts: (performing all available checks)
collected 1 items 

sample.py F

=============================== FAILURES ===============================
______________________________ PEP8-check ______________________________
/Users/t2y/tmp/t/sample.py:1:2: E225 missing whitespace around operator
x=3
 ^
/Users/t2y/tmp/t/sample.py:2:1: E302 expected 2 blank lines, found 0
class A(object): pass
^
/Users/t2y/tmp/t/sample.py:2:16: E701 multiple statements on one line (colon)
class A(object): pass
               ^
/Users/t2y/tmp/t/sample.py:4:1: E302 expected 2 blank lines, found 1
def f(x):
^
/Users/t2y/tmp/t/sample.py:6:1: W391 blank line at end of file

^

======================= 1 failed in 0.01 seconds =======================

全く同じエラーが出ます。何もオプションを付けずに使っている分には問題ありません。

ここで pytest.ini に PEP8 のコーディングスタイルを無視するオプションを追加します。

(test)$ vi pytest.ini 
[pytest]
pep8ignore = E302 E701

E302 と E701 のエラーだけを無視して、E225 と W391 のエラーは出力されるはずなのですが、、、

(test)$ py.test --pep8 sample.py 
========================= test session starts ==========================
platform darwin -- Python 2.7.3 -- pytest-2.2.4
pep8 ignore opts: E302 E701
collected 1 items 

sample.py .

======================= 1 passed in 0.01 seconds =======================

pytest-pep8 0.7 では、テストが成功してしまいます。これは pep8ignore に設定した無視したいエラー総数が、PEP8 違反としたいエラー総数よりも多いときにテストが成功してしまう不具合がありました。

pytest-pep8 0.8 ではこの不具合が修正されています。

(test)$ pip install pytest-pep8==0.8
(test)$ py.test --pep8 sample.py 
========================= test session starts ==========================
platform darwin -- Python 2.7.3 -- pytest-2.2.4
pep8 ignore opts: E302 E701
collected 1 items 

sample.py F

=============================== FAILURES ===============================
______________________________ PEP8-check ______________________________
/Users/t2y/tmp/t/sample.py:1:2: E225 missing whitespace around operator
x=3
 ^
/Users/t2y/tmp/t/sample.py:6:1: W391 blank line at end of file

^

======================= 1 failed in 0.01 seconds =======================

今度は正しく E225 と W391 のエラーが検出されましたね。

おそらく pep8ignore オプションを設定していると、1つ2つのエラーを見逃してしまっている可能性があります。あるとき pytest-pep8 をアップグレードしたら、ソースいじってないのにエラーが出るようになったと不思議に思うことがあるかもしれません。

参考までに修正された内容です。

もしかしたら、あるときに pep8 の API 仕様が変わったのかな?詳しく調べてはいません。