Python Code Reading 01

5/9 19:00 -
行ってきました。予備知識的な資料をまとめて発表する係、ソースコードリーディングの進行をする係でした。へたくそな進行でしたが、柴田さんはじめ皆さんに色々つっこんでいただいて勉強になりました。

資料はこちらに上げてあります。
http://groups.google.co.jp/group/python-code-reading/files

日を改めてもうちょい反省したり消化したりない部分を考えることにします。

関連リンク

会場提供していただいたミラクリナックスのtmorimotoさんによるフォロー。http://blog.miraclelinux.com/asianpen/2008/05/python-code-rea.html#more

グルーフォ

会社で新しいサービスを始めました。

Web集合写真 グルーフォ

まずページを作ります。背景や参加人数、制限時間を決めます。URLが発行されますので、参加者はそこに行って画像をアップロード。人数がいっぱいになるか時間が来ると、合成されて一枚の画像になる仕組みです。

という小ネタっぽいサービスです。例によってTurboGearsで動いてます。
Twitterでやってくれた人もいるみたい。
http://twitter.g.hatena.ne.jp/gorimaru9/20080423

tw.forms

数日前に新しいサーバをセットアップしてて気がついたこと。ToscaWidgetsでのフォームの実装twFormsの名前がtw.formsに変わっていた。だもんで

easy_install twForms

がこけた。

easy_install tw.forms

としましょう。というだけの話。
ただし最新版では色々モジュール設計が変わっていたので、既存のTurboGearsアプリで使うために

easy_install tw.forms==0.3.1

とした。

whichコマンド

which コマンドの実装
id:t2y-1979:20080409#1207749772

whichはPATHを頭から調べて最初に見つけたものを返すので、

  • 順序を保持しないsetに入れるとまずい
  • ループをbreakしないと最後に見つけたものを出力してしまう

と思います。

>>> set(['a', 'b', 'c', 'd'])
set(['a', 'c', 'b', 'd'])

順序が変わってます。
重複を排除するには、リストにuniqのようなメソッドがあればいいけど、ないみたいです。
こんな書き方ができました。ただ、遅いかも。

>>> l = ['a', 'b', 'b', 'c', 'a']
>>> [ x for i, x in enumerate(l) if x not in l[:i] ]
['a', 'b', 'c']

でも、頭から調べてbreakする処理なら、重複を排除しておく必要はないわけですが。
自分なりに書いたのが以下。

which2.py

import os, sys
cmd_name = sys.argv[1]
path_list = [ path for path in os.environ["PATH"].split(":") ]
for path in path_list:
    full_path = os.path.join(path, cmd_name)
    if os.access(full_path, os.X_OK):
        print full_path
        break
else:
    print "No such command. %s" % cmd_name

実行。

$ echo $PATH
/usr/local/java/jdk1.6/bin:/usr/local/flex2/bin:/usr/kerberos/bin:/usr/local/bin:/bin:/usr/bin:/home/michisu/bin

$ where python
/usr/local/bin/python
/usr/bin/python
/home/michisu/bin/python

$ which python
/usr/local/bin/python

$ python which2.py python
/usr/local/bin/python

$ python which2.py nocmd
No such command. nocmd

テストケース生成スクリプト

最近「テスト可能なコードが良いコード」と教わり、そういう観点でコーディングを進めてみようと思ってます。

今考えてる進め方はこんな感じです。
まず、実際に使うためのコードを大まかに書きます。この時点では、ほとんどメソッド名だけ。頭の中でなるべく細かくメソッドに分けていきます。メソッドのつながりを考えているうちに短いコードを書いたりもします。
ある程度まで書けたら、スクリプトを使って空のテストメソッドを自動生成します。このスクリプトは、カレントディレクトリ以下の*.pyから関数定義、メソッド定義を探してきて、対応するテストメソッドを作ります。自分用なので適当なところもありますが晒しときます。

make_tests.py

#!/usr/bin/python
import os, sys
import inspect, _ast

excluded_dir_names = ["tests"]
excluded_file_names = ["setup.py"]

def get_method_names(clsdef):
    method_names = [ obj.name for obj in clsdef.body
        if isinstance(obj, _ast.FunctionDef) ]
    return method_names
    
def get_funcs_of_module(module):
    source = inspect.getsource(module)
    ast = compile(source, "dont_care", "exec", _ast.PyCF_ONLY_AST)

    class_dict = dict()
    classes = [ obj for obj in ast.body
        if isinstance(obj, _ast.ClassDef) ]

    for cls in classes:
        methods = get_method_names(cls)
        class_dict.setdefault(cls.name, methods)

    func_names = [ obj.name for obj in ast.body
        if isinstance(obj, _ast.FunctionDef) ]

    return class_dict, func_names

def make_test_code(test_dir, module_name):
    def write(f, old, new):
        if new not in old:
            f.write(new)

    from_list = module_name.split(".")[:-1]
    module = __import__(module_name, globals(), locals(), from_list)
    class_dict, func_names = get_funcs_of_module(module)

    if not class_dict and not func_names:
        raise Exception("no classes or functions are defined.")
    methods = [ value for value in class_dict.values() if value ]
    if not methods:
        raise Exception("no methods are defined.")

    test_file_name = os.path.join(test_dir, 
        "test_%s.py" % module_name.replace(".", "_"))
    test_file = file(test_file_name, "a+")

    old_code = test_file.read()
    code = ("import unittest\n\n"
        "class Test(unittest.TestCase):\n\n")
    write(test_file, old_code, code)

    wrote_test_method = False
    if class_dict:
        for class_name, method_names in class_dict.iteritems():
            for method_name in method_names:
                code = ("    def test_%s_%s(self):\n"
                    "        pass\n\n") % (class_name.lower(), method_name)
                write(test_file, old_code, code)
                wrote_test_method = True

    if func_names:
        for func_name in func_names:
            code = ("    def test_%s(self):\n"
                "        pass\n\n") % func_name
            write(test_file, old_code, code)
            wrote_test_method = True

    if not wrote_test_method:
        test_file.write("    pass\n")
    test_file.close()

    return test_file_name
    
def proc_module(test_dir, module_name):
    try:
        test_file_name = make_test_code(test_dir, module_name)
        print "[created] %s: %s" % (module_name, test_file_name)
    except Exception, e:
        print "[ignore] %s: %s" % (module_name, e)

def get_module_name(walk_root, root, filename):
    file_path = os.path.join(root, filename)
    file_path = file_path.replace(walk_root, "")
    if filename == "__init__.py":
        module_name = file_path[:-12]
    else:
        module_name = file_path[:-3]
    module_name = module_name.replace(os.path.sep, ".")
    return module_name

def is_target_file(root, filename):
    if (not filename.endswith(".py") or
        filename in excluded_file_names):
        return False
    path_dirs = root.split(os.path.sep)
    ignored_dir = [ excluded 
        for excluded in excluded_dir_names
        if excluded in path_dirs ]
    return (False if ignored_dir else True)

def main():
    target_dir = os.getcwd()
    excluded_file_names.append(os.path.basename(sys.argv[0]))
    test_dir = sys.argv[1] if 1 < len(sys.argv) else "tests"
    walk_root = os.path.join(os.getcwd(), "")
    for root, dirs, files in os.walk(target_dir):
        for filename in files:
            if is_target_file(root, filename):
                module_name = get_module_name(walk_root, root, filename)
                proc_module(test_dir, module_name)

if __name__ == "__main__":
    main()

プロジェクトルートに置いて、以下のように実行します。

$ python make_tests.py [test_cases_dir]

TurboGearsプロジェクトでの実行例。

$ tg-admin quickstart myproj
$ cp make_tests.py myproj
$ cd myproj
# もとのテストケースを削除
$ rm -f myproj/tests/test_*
# 実行。モジュール名と結果が表示されます
$ python make_tests.py myproj/tests
[ignore] start-myproj: no classes or functions are defined.
[created] myproj.controllers: myproj/tests/test_myproj_controllers.py
[ignore] myproj: could not get source code
[ignore] myproj.release: no classes or functions are defined.
[ignore] myproj.commands: no methods are defined.
[ignore] myproj.model: no classes or functions are defined.
[ignore] myproj.json: no classes or functions are defined.
[ignore] myproj.templates: could not get source code
[ignore] myproj.config: could not get source code
$ ls myproj/tests
__init__.py  test_myproj_controllers.py

中身はこんな感じ。「test_クラス名_メソッド名」というメソッドができてます。

$ cat myproj/tests/test_myproj_controllers.py
import unittest

class Test(unittest.TestCase):

    def test_root_index(self):
        pass

できたテストメソッドを眺めて、メソッド単位はこんなもんでいいかなーと考えます。もとのコードを適当に修正したり追加したりします。
上のスクリプトはメソッドが増えた場合でも追記で対応できるようにしてあります。ある程度の機能単位で上記を繰り返します。
変更が大きい場合は、マージが面倒ですがテストケースをいったん作り直したほうがいいかもしれません。もっとちゃんと管理できるようにしたいところです。

Word Stones Log

今年もよろしくお願いします。

国語辞書を読むブログ、下の場所でとりあえず始めてみます。
Word Stones Log
やってみるとなかなか難しいですね。まともにやると時間が足りない。ちゃんと読むと急いでも3ページで10分ぐらいかかってしまうのですが、それ以上に難しいのはどの語をチョイスするかということ。面白い言葉が沢山あって悩んでしまいます。その言葉をWebで調べだすとまた大変です。
やり方は色々変えていくかもしれません。

国語辞書『大辞泉』を全文読む計画

2864ページありますがこれを1日数ページずつ読んでいこうと思います。
1日3ページ読むと、955日、2.62年かかる計算になります。

なぜ大辞泉

大辞泉は私がまだ和歌山の田舎で高校生をやっていたときに発売された国語辞書です。誕生日になんか買ってやると親がいうので買ってもらいました。以来今に至るまで、手持ちのうちでいちばん分厚くいちばん重く字がたくさん詰まった本なので、これにしました。

なんでそんなことするの

恥ずかしいので詳しくは書きませんが賽の河原で石を積むようなことです。たいして意味のないことを長く続けることが目的です。(忘れっぽいので全部読んでもたぶんほとんど身につきません)
以前にも一度やってみたことがありますが132ページで挫折しました。今度は完遂したいと思います。

読んでどうするの

せっかくなので(モチベーション維持の意味もありますが)毎日ブログにアウトプットしようと思います。

  • その日の分で気に入った言葉を一つ取り上げて、連想した言葉と一緒に書き留める(マインドマップにする?)
  • その言葉で検索して気になったページをリンクする

など考えてます。Wikiを使ってもいいと思うのですが、時系列に並んでるだけのほうがより無意味っぽいので、ブログにしようかなと思います。選んだ言葉がURLの一部になるのは良いと思います。なのでブログエンジンはPyblosxomとか使おうかとも考えてます。

お願いです

これを読んだ方で、国語辞書を読んだうえでどう加工してどうアウトプットするか、というところでなにかアドバイスというか、こうすれば面白いんじゃない?というのがあれば、教えていただけるとうれしいです。
ただし以下の点にごりゅういください。

  • 怠け者なので、インプットからアウトプットまでが1日10分以内の作業じゃないとたぶん続きません(インプットに5分かかるとすると残りは5分)
  • プログラマなので、自動化できそうなところは適当に自動化してみます

よろしくお願いします。