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

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

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

今回は、初日 (9日) のカンファレンスレポートです。私は基調講演について執筆しました。

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! ですねと言うやり取りが、なかなかに当たらずとも遠からずのような、英語と日本語の違いがあるものの、うまい言葉になったもんだと感心した次第です。