Subversion で svn:externals (外部参照) を設定する

Subversion は、シンボリックリンクをサポートしています。例えば、あるユーティリティモジュールがあり、それを複数のプログラムから使いたい場合、シンボリックリンクを使うと便利です。

シンボリックリンクの作成

試してみましょう。テスト用のリポジトリを作成して Subversion サーバーを起動します。

$ svnadmin create svn-repos
$ vi svn-repos/conf/svnserve.conf 
[general]
anon-access = write  # 匿名書き込みアクセスを許可する
$ svnserve --daemon --foreground --root svn-repos/

サーバーが起動しました。クライアントからチェックアウトします。

$ svn co svn://localhost/ ./myrepo
リビジョン 0 をチェックアウトしました。

$ # 初期ディレクトリとシンボリックリンクを作成
$ cd myrepo
$ mkdir utils tool1 tool2
$ vi utils/myfunc.py
print "do something"
$ (cd tool1; ln -s ../utils/myfunc.py)
$ (cd tool2; ln -s ../utils/myfunc.py)
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:00 myfunc.py -> ../utils/myfunc.py

$ svn add utils tool1 tool2
A         utils
A         utils/myfunc.py
A         tool1
A         tool1/myfunc.py
A         tool2
A         tool2/myfunc.py

$ svn commit -m "added"
追加しています              tool1
追加しています              tool1/myfunc.py
追加しています              tool2
追加しています              tool2/myfunc.py
追加しています              utils
追加しています              utils/myfunc.py
ファイルのデータを送信しています ...
Committed revision 1.

# リポジトリの属性を調べる
$ svn proplist -R .
'tool1/myfunc.py' の属性:
  svn:special
'tool2/myfunc.py' の属性:
  svn:special

シンボリックリンクを作成すると、svn:special という属性が設定されます。

別のクライアントからチェックアウトしてみましょう。

$ svn co svn://172.16.55.131/ ./myrepo2
A    myrepo2/tool2
A    myrepo2/tool2/myfunc.py
A    myrepo2/utils
A    myrepo2/utils/myfunc.py
A    myrepo2/tool1
A    myrepo2/tool1/myfunc.py
リビジョン 1 をチェックアウトしました。

$ cd myrepo2/
$ ls -l tool1 tool2
tool1:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
tool2:
lrwxrwxrwx 1 t2y t2y 18  98 12:09 myfunc.py -> ../utils/myfunc.py
$ cat tool1/myfunc.py 
print "do something"

ちゃんとシンボリックリンクが有効になっていますね。

svn:externals (外部参照) 属性の設定

Unix 系 OS であれば、これで大丈夫ですが、Windows ユーザーも使いたいときに課題があります。

But that doesn't in any way limit the usability of working copies on systems such as Windows that do not support symlinks. On such systems, Subversion simply creates a regular text file whose contents are the path to which the original symlink pointed. While that file can't be used as a symlink on a Windows system, it also won't prevent Windows users from performing their other Subversion-related activities.

シンボリックリンクのバージョン管理

Windowsシンボリックリンクをサポートしていないため、リンク先を指すテキストファイルを作成して、ユーザーの Subversion 操作においては邪魔しないとあります。

実際に Windows 環境でチェックアウトしてみましょう。

チェックアウト結果をみると、ファイルをコピーして作成してくれているように見えますが、ファイルの中身は "link ../utils/myfunc.py" です。ドキュメント等であれば良いですが、ユーティリティモジュールなどでは使えないですね。

そんなとき、Windows 環境でもシンボリックリンクのように扱える仕組みが svn:externals 属性です。

Fortunately, Subversion provides support for externals definitions. An externals definition is a mapping of a local directory to the URL―and ideally a particular revision―of a versioned directory. In Subversion, you declare externals definitions in groups using the svn:externals property. You can create or modify this property using svn propset or svn propedit (see the section called “Manipulating Properties”). It can be set on any versioned directory, and its value describes both the external repository location and the client-side directory to which that location should be checked out.

svn:externals 属性のドキュメント

svn:externals のドキュメントによると、リポジトリ内に外部リポジトリの URL を指定したり、そのリポジトリ特定バージョンを取得するように設定できるとあります。もともとは外部のリポジトリを取り組むための機能のようですが、同じリポジトリ内においても利用できます。そして、svn propsetsvn propedit で設定できると記述されていますが、残念ながら実際の設定コマンドのサンプルがここでは記述されていません。そのため、どうコマンドを実行して良いか全く分かりません (> <)

svn:externals の設定は直感的に分かりにくいので備忘録としてまとめておきます。また、Suversion 1.4 でディレクトリ単位、1.6 からファイル単位の外部参照ができるようになったので svn クライアントのバージョンにも注意してください。

$ mkdir tool3
$ svn add tool3
A         tool3

$ cd tool3
$ svn propset svn:externals "../utils/myfunc.py linked_myfunc.py" .
属性 'svn:externals''.' に設定しました

$ svn update
Updating '.':
Fetching external item into 'linked_myfunc.py':
A    linked_myfunc.py
外部項目をリビジョン 5 に更新しました。
リビジョン 5 です。

$ cat linked_myfunc.py 
print "do something"

$ svn commit -m "set svn:externals"
送信しています              .
Committed revision 6.

svn propset svn:externals の引数が覚えられないです (> <)
1つのやり方としては、以下のように相対パスで指定します。

$ svn propset svn:externals "カレントディレクトリからの相対パス  取得後のファイル名" 属性設定したいディレクトリ

svn:externals 属性は、ディレクトリに対して設定されます。

$ svn proplist tool3/linked_myfunc.py 
$ svn proplist tool3
'tool3' の属性:
  svn:externals

別のやり方として、リポジトリのルートパスからの相対パスを指定する方法もあります。今度は同じファイル名 myfunc.py で取得しています。

$ svn propset svn:externals "^/utils/myfunc.py myfunc.py" tool4
属性 'svn:externals''tool4' に設定しました
$ svn update tool4/
Updating 'tool4':
Fetching external item into 'tool4/myfunc.py':
リビジョン 7 の外部項目です。

リビジョン 7 です。
$ svn commit -m "set svn:externals"
送信しています              tool4
Committed revision 8.

Windows 環境でもチェックアウトしてみます。

Unix 系 OS 環境と同様にファイル本体がチェックアウトできました!

外部リポジトリ特定ディレクトリを取得したいときは URL を指定します。こういうときは取得した後のディレクトリ名を変更できると便利です。

$ mkdir svn
$ svn add svn/
A         svn
$ svn propset svn:externals "http://svn.apache.org/repos/asf/subversion/trunk/tools/examples/ tool-example" svn/
属性 'svn:externals''svn' に設定しました

$ svn update
Updating '.':

Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 10 の外部項目です。

Fetching external item into 'tool4/myfunc.py':
A    tool4/myfunc.py
外部項目をリビジョン 10 に更新しました。

Fetching external item into 'svn/tool-example':
A    svn/tool-example/svnserve-sgid.c
A    svn/tool-example/svnput.c
A    svn/tool-example/dumpprops.py
A    svn/tool-example/svnlog2html.rb
A    svn/tool-example/check-modified.py
A    svn/tool-example/svnlook.py
A    svn/tool-example/svnshell.py
A    svn/tool-example/geturl.py
A    svn/tool-example/getlocks_test.c
A    svn/tool-example/info.rb
A    svn/tool-example/svnlook.rb
A    svn/tool-example/get-location-segments.py
A    svn/tool-example/headrev.c
A    svn/tool-example/putfile.py
A    svn/tool-example/revplist.py
A    svn/tool-example/svnshell.rb
A    svn/tool-example/minimal_client.c
A    svn/tool-example/getfile.py
A    svn/tool-example/blame.py
A    svn/tool-example/SvnCLBrowse
A    svn/tool-example/testwrite.c
外部項目をリビジョン 1382235 に更新しました。

リビジョン 10 です。

svn update を実行する度に fetch 処理が実行されます。仕方ないですが、外部リポジトリを参照すると実行に時間がかかるので、更新をチェックしたくないときは "--ignore-externals" オプションを指定します。

$ svn update
Updating '.':
Fetching external item into 'tool3/linked_myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'tool4/myfunc.py':
リビジョン 11 の外部項目です。
Fetching external item into 'svn/tool-example':
リビジョン 1382235 の外部項目です。

リビジョン 11 です。

$ svn update --ignore-externals
Updating '.':
リビジョン 11 です。

あまり頻繁に使う機能ではありませんが、使いたいときに実際の設定コマンドのサンプルを見つけられなくて苦労しました。

ちなみに Subversion もいまや Apache プロジェクトのトップレベルプロジェクトなんですね。ホームページも刷新されていて驚きました。

2012/9/10 追記

同じディレクトリに複数ファイルの svn:externals 属性を設定するには、svn propedit を使わないと設定できないようです。

$ svn propedit svn:externals tool5 --editor-cmd=vim  # vim エディタで複数ファイルを設定
^/utils/myfunc.py myfunc.py
^/utils/myfunc2.py myfunc2.py

属性 'svn:externals' の新しい値を 'tool5' に設定しました

アリエル・ネットワークに入社しました

9月からアリエル・ネットワーク (以下アリエル) に入社しました。

以前、アルバイトで3ヶ月半ほどお仕事していました。以下はそのときの手記です。

さて、最近こんなツイートを見ました。


どうやら私がアルバイトをしていた頃から少し経って、最近たるんでいるようです。

これはまた締め直さなければ、、、

と思っていたら入社の初日から先制攻撃を受けてしまいました。ほんとに怖い会社です、、、

寝坊したんじゃないです!初日に入館できる手続きをするから9時に来てと言われてたんです (> <)、朝組イメージを払拭するために、朝何時に来ようが本当はどうでも良いです。生産性を上げるには、人によっては (私もそうですが) 朝型で取り組む方が効率的だったりもしますが、それよりも (自分にあった) 規則正しい生活を続ける方が安定したパフォーマンスを発揮できて、結果的に生産性が上がります。

閑話休題。私はこれまでフリーランスの経験もあわせると7社で働いてみました。それぞれの会社の特徴や文化の一長一短があり、どの会社が優れているとは一概に言えませんが、どの会社が自分の考え方やキャリアの方向性と最も一致しているかということは言えます。私はやはりプログラマーとしてのキャリアをもっと突き詰めたいし、チームメンバーよりもコードを書いて、チケットをクローズしまくるマネージャーが普通にいるアリエルのような職場が魅力的でした。

エンタープライズ分野のパッケージ販売という、ソフトウェア開発の王道とも言える業務に対して、これまでの経験を活かして何ができるか。結果的に過去のキャリアでは、最長3年と、中長期的な展望をもって開発に取り組むことができなかったことが私の中での課題であり、未知の領域でした。それは結果論であって本意ではなかったのですが、次に働くところは中長期的な展望をもって開発に取り組める企業にしようと決めていました。

ひとつ自己努力だけで容易に学べないものがあると思っています。良い開発プロセスを経験できるかどうかです。良い開発プロセスとはウォーターフォールできっちりやるという意味ではありません。社内の開発者がどれだけ合理的にプロセスを改善しようとしているかが重要です。会社にそういう文化があるかどうかです。開発プロセスは日々の行動の積み重ねなので、勉強会で話を聞いたり本を読むだけでは限界があります(わかった気にはなれますが)。

2013年度、アリエル新卒募集が始まりました | ありえるえりあ

実際、私が働いた7社の中で最も優れた開発プロセスをもっていたのもアリエルでした。開発プロセスの改善というのは、技術力やその知識以上に、会社の財務基盤や業務としての開発文化、そして上司の理解が必要になります。いわゆる wikipedia:技術的負債 を抱え込まないために必要なことは誰もが分かってますが、中長期的な開発計画がないと、そもそもそんな投資はできません。また、短期的な利益とのトレードオフになるため、そのバランスを取ることが難しいです。

これまで、私は独りで開発してきたことが多かったので、チームでの開発プロセス改善やその在り方を模索していけることも今後の楽しみです。

最後に1つ。

エキスパート職に関する仙石さんの見解は単純かつ明解でした。エキスパート職の職能は、まわりに良い影響を与えられるか、だと言いきりました。

マネージャになりたくないプログラマのキャリアパス | ありえるえりあ

これは私の中の「すごい人」理論にも通じます。

どこの会社にも優秀な方はたくさんいますが、本当にすごい人はあまりいません。職場にすごい人なんか滅多にいません。私が思うすごい人は、会社の枠や多少の損得なんかの概念を超えて、さらなる高みへ行ってしまうような人たちです。別の言い方をすると、自分でリスクを取って行動し、結果を出しても、さらに先を目指すような人たちです。

アリエルにはそういった人たちが何人もいますし、私もそんな人たちを目標に切磋琢磨していきたいと考えています。

結局のところ、何かおもしろそうだ、という直感は生きていく上で大事なものなんだと思います。

Pyramid ベースの軽量 CMS フレームワーク Kotti の紹介

ここ最近 Kotti アドオンの開発を行っていました。

Kotti は、Pyramid の上位でアプリ開発を支援するフレームワークです。いまのところ、軽量 CMS として開発が進められています。

Kotti を紹介するウェブサイトも構築されています。まだ概要程度の情報量ですが、日本語化もされているのでざっとご覧ください。

個人的にちょっと使ってみた感想としては、Kotti の CMS 機能もその完成度もまだまだベータ開発中といったところです。実用的なレベルまではもう少し時間がかかりそうですが、開発が活発に行われているので将来が楽しみです。私の場合、Pyramid を使ったアプリを開発してみたかったことと、普通の Web アプリにユーザー管理やコンテンツ管理の機能を簡単に追加したかったという動機で Kotti を利用してみました。

開発していると Pyramid の知識も必要になりますが、Node/Content という Kotti がもつリソースモデル、pyramid_tm によるリクエスト単位のトランザクション管理など、Pyramid 流の開発のプラクティスとして、フレームワークでどんなことをやっているのかを学ぶことが多かったです。また、後述する Kotti アドオンの開発も容易でした。

私が開発したものは Amazon Elastic MapReduce (Amazon EMR) を操作する Kotti アドオンです。

現時点の EMR の GUI と比較して、

  • EMR 設定をリソースとして保存・再利用
  • サービス単位にジョブフローをまとめる
  • Keep Alive を有効にしたインスタンスへ追加ステップを実行

といった特徴があります。但し、まだまだ開発ステータスがアルファレベルであり、機能や UI は最低限のものですし、運用面の考慮も全くされていません。実験中といった感じです。

Kotti アドオンを開発する上で慣れが必要なのは、複数プロダクトの仕組みやドキュメントを調べないといけないことです。主なライブラリのドキュメントは以下です。

例えば、INI ファイル の設定においても、

kotti.site_title = Kotti with mapreduce
kotti.secret = qwerty

などの kotti で始まる設定は Kotti 本体の設定ですが、

pyramid.default_locale_name = en
pyramid.includes =
    pyramid_debugtoolbar

などの pyramid で始まる設定は Pyramid のドキュメントを調べないといけません。
また、

mail.default_sender = yourname@yourhost

は、pyramid_mailer の設定であり、これは Message オブジェクトの sender 属性 (envelope-from) に利用されます。

開発していて気になることや不具合があったときに Kotti 本体だけ調べても分からないこともよくあります。そんなとき、この機能を実際に提供しているのはどのプロダクトか、それはどういった仕組みかを理解するために、それぞれのプロダクトについての理解を深め、全体像を把握しないと解決策が分からないこともあります。

最初の取っ掛かりとしては、既に PyPI 上にも10個以上の Kotti アドオンが公開されているので、実際に動くサンプルとしてコードを眺めるのも参考になります。

まだまだ荒削りなフレームワークですが、その分、自分で開発したり、改善したりする余地がたくさんあります。Pyramid アプリ入門として Kotti アドオンを作るのでも良いように思います。もう少し経って Kotti の開発が落ち着いたら、ドキュメント翻訳にも挑戦してみたいところです。

ローカル環境で複数の Sphinx ドキュメントを参照する

最近の Python パッケージは、Sphinx でドキュメントが書かれていることが多いです。

Sphinx でドキュメントが書かれているなら、そのパッケージのソースをクローンしてきて、ローカルでドキュメントをビルドするのも簡単です。私の場合、そのときの開発に用いるフレームワーク・ライブラリ・ツール等は、リポジトリからソースをクローンしてきて、最新版ソースを開発環境へインストールして、自分のアプリケーションを開発する上でデバッグしやすい環境を構築しています。また、そのドキュメントもローカルでビルドしています。

例えば、KottiPyramidフレームワークのドキュメントをローカルでビルドしてみます。

$ cd /path/to/Kotti/
$ python setup.py build_sphinx

$ cd /path/to/pyramid/docs/
$ make html

このとき、前者は Kotti/build/sphinx/html/ に、後者は pyramid/docs/_build/html/ に HTML ドキュメントが生成されます。

ローカルで HTML ドキュメントを参照する際に Sphinx の検索機能を使いたいので HTTP サーバーを介して参照できると便利です。とはいえ、そのためにわざわざ HTTP サーバーを設定・構築するのもちょっと面倒です。前置きが長くなりました、そんな用途に「ちょっとだけ」便利な HTTP サーバーを作ってみました。

その名の表す通り、標準ライブラリの SimpleHTTPServer をちょっとだけカスタマイズしたものです。

先ず SimpleHTTPServer を使って、複数のドキュメントを見ようとすると次のようになります。

$ cd /path/to/Kotti/build/sphinx/html/
$ python -m SimpleHTTPServer 8001
Serving HTTP on 0.0.0.0 port 8001 ...

$ cd /path/to/pyramid/docs/_build/html/
$ python -m SimpleHTTPServer 8002
Serving HTTP on 0.0.0.0 port 8002 ...

SimpleHTTPServer は、カレントディレクトリ配下のみを公開ディレクトリとして扱います。2-3のドキュメントならこれでも良いですが、10個のドキュメントをこの方法で見ようとすると、コマンドラインでサーバー起動するのが面倒だったり、どのドキュメントがどのポート番号か分からなくなったりします。

この煩わしさを解決するのが LittleHTTPServer です。次のようにコマンドラインオプションで指定します。

$ littlehttpserver -p 8003 -d /path/to/pyramid/docs/_build/html/ -d /path/to/Kotti/build/sphinx/html/
INFO: Serving HTTP/1.0 on 0.0.0.0, port: 8003 ...

http://localhost:8003 へアクセスすると、次のように表示されます。

これならローカルで見たいドキュメントが複数になっても煩わしくないですね。

2012/8/22 追記

コメントをもらったので -m オプションで実行できるように修正して再リリースしました。

$ python -m littlehttpserver -i path/to/top -d path/to/pkg1

コマンドラインオプション付きの which コマンドを実装してみた

というツイートを見かけました。

以前、勉強がてらに

を作ったことがありましたが、もうあれから3年も経つんだなとコードを見返しながら感慨に浸っていました。

そんなとき、ふと which コマンドを実行したところ、コマンドラインオプションがあることに気付きました!

$ which
usage: which [-as] program ...

$ man which
WHICH(1)                  BSD General Commands Manual                 WHICH(1)

NAME
     which -- locate a program file in the user's path

SYNOPSIS
     which [-as] program ...

DESCRIPTION
     The which utility takes a list of command names and searches the path for each 
     executable file that would be run had these commands actually been invoked.

     The following options are available:

     -a      List all instances of executables found (instead of just the first one of each).

     -s      No output, just return 0 if any of the executables are found, or 1 if none are found.

     Some shells may provide a builtin which command which is similar or identical
     to this utility.  Consult the builtin(1) manual page.

これは、、、

。。。

過去の私の実装に足りないものを発見してしまいました、もう再実装するしかない! (> <)

ということで、コマンドラインオプションを追加するだけなのですが、結構難しくて悩んでしまいました。

# -*- coding: utf-8 -*-
import glob
import os
import sys
import argparse
from itertools import chain
from os.path import join as pathjoin
from operator import itemgetter

def search(cmd, paths, is_all=False):
    for path in paths:
        for match in glob.glob(pathjoin(path, cmd)):
            if os.access(match, os.X_OK):
                yield match
                if not is_all:
                    raise StopIteration

def parse_argument(args=None):
    parser = argparse.ArgumentParser()
    parser.set_defaults(is_all=False, is_silent=False, commands=[])
    parser.add_argument("-a", dest="is_all", action="store_true",
        help="List all instances of executables found "
             "(instead of just the first one of each).")
    parser.add_argument("-s", dest="is_silent", action="store_true",
        help="No output, just return 0 if any of the executables are found, "
             "or 1 if none are found.")
    parser.add_argument("commands", nargs="*")
    args = parser.parse_args(args or sys.argv[1:])
    return args

def main(cmd_args=None):
    args = parse_argument(cmd_args)
    env_paths = os.environ['PATH'].split(':')
    result = []
    for cmd in args.commands:
        founds = list(search(cmd, env_paths, args.is_all))
        result.append((0, founds) if founds else (1, [cmd]))

    status_code = max(map(itemgetter(0), result))
    if not args.is_silent:
        cmd_paths = [paths for ret_val, paths in result if ret_val == 0]
        for cmd_path in chain.from_iterable(cmd_paths):
            print cmd_path
    return status_code

if __name__ == '__main__':
    sys.exit(main())

実行結果はこんな感じです。

$ python which.py -a ls vi unknown
/bin/ls
/opt/local/bin/vi
/usr/bin/vi

$ python which.py -s ls vi unknown; echo $?
1

ついでに本当に which コマンドと動作が一致しているか、簡単なテストを書いてみました。

# -*- coding: utf-8 -*-
import sys
from subprocess import Popen, PIPE

import pytest
import which

FILESYSTEM_ENCODING = sys.getfilesystemencoding()

def get_system_which_result(args):
    cmds = ["which"]
    cmds.extend(args)
    p = Popen(" ".join(cmds), stdout=PIPE, stderr=PIPE, shell=True)
    out, err = p.communicate()
    return p.returncode, out, err

@pytest.mark.parametrize("args", [
    ["ls"],
    ["cd", "pwd"],
    ["non_existence"],
    ["-a", "vi"],
    ["-a", "ls", "vi"],
    ["-s", "non_existence"],
    ["-a", "-s", "ls", "vi"],
    ["-a", "-s", "ls", "vi", "non_existence"],
])
def test_which_command(args, capsys):
    my_ret = which.main(args)
    my_out, my_err = capsys.readouterr()
    sys_ret, sys_out, sys_err = get_system_which_result(args)
    assert sys_ret == my_ret
    assert sys_out == my_out.encode(FILESYSTEM_ENCODING)
    assert sys_err == my_err.encode(FILESYSTEM_ENCODING)

ソースは以下にあります。

こういったシンプルなツールを作るのはおもしろいですね。

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

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

前回の記事からちょっと間があきましたが、2日目 (10日) のカンファレンスレポートです。私は基調講演について執筆しました。

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