2009年3月17日火曜日

Pythonで、特定のディレクトリ以下にあるすべてのモジュールをインポートする方法

Pythonで特定のディレクトリの下にあるすべてのモジュールをインポートしたいときの方法。
これもGAEOのソースから見つけました。
def initPlugins():
"""
Initialize the installed plugins
"""
plugins_root = os.path.join(os.path.dirname(__file__), 'plugins')
if os.path.exists(plugins_root):
plugins = os.listdir(plugins_root)
for plugin in plugins:
if not re.match('^__|^\.', plugin):
try:
exec('from plugins import %s' % plugin)
except ImportError:
logging.error('Unable to import %s' % (plugin))
except SyntaxError:
logging.error('Unable to import name %s' % (plugin))


キモは二つ。
  1. os.listdirで特定のディレクトリ以下のすべてのファイル名・ディレクトリ名を取得する
  2. 取得したディレクトリ名を使ってimport文を生成して、exec()で実行する
exec()で無理矢理実行とか正直嫌な感じです。しかしこれ以外に方法はなさそうですね・・・
ファイル名を使ってコマンドインジェクションとかできそうじゃないかと試してみましたが、そもそもファイル名には改行とかバックスラッシュとかが使えないので意外と安全かも。

GAEOでPythonのunittestモジュールを使って単体テストにチャレンジ

■まとめ
site-packagesにインポートするモジュールとかまとめておくのが使うのがいちばん楽


■何がしたいか
以下のようなGAEOプロジェクトがあります。



ここで、test.controller.forecastareaTest.pyにforecastareaモジュールのテストケースを記述するためには、applicationディレクトリにPYTHONPATHを通す必要があります。
それをいちばんスマートにやるにはどうすればよいかいろいろ悩んでました。


■どうしてそこで悩むの?
forecastareaTest.pyは単体で動作させなくてはいけないからです。
もしここで、jweeklyforecastディレクトリの直下にテストケースをおくことが出来るのであれば、
application_root = os.path.join(os.path.dirname(__file__), 'application')
test_root = os.path.join(os.path.dirname(__file__), 'test')
sys.path.append(application_root)
sys.path.append(test_root)

from application.controller import ForecastareaController
from test.controller import TestForecastarea

こんな風に自分のディレクトリからのパスをsys.pathに追加してからインポートすれば一発です。applicationもtestも自分の子供ディレクトリですから楽です。
しかしforecastareaTest.pyの位置から見ると・・・
application_root = os.path.join(os.path.dirname(__file__), '../../../application')
sys.path.append(application_root)

from application.controller import ForecastareaController

こんな風にインポートしなくちゃいけなくなります。相対指定とかする必要があって、ちょっとやな感じです。
PROJECT_HOME = '/User/akisute/DropBox/Projects/GoogleAppEngine/jweeklyforecast/'
application_root = os.path.join(PROJECT, 'application')
sys.path.append(application_root)

from application.controller import ForecastareaController

これでもいいのですが、これだとPROJECT_HOMEが各テストケースごとにべた書きになってしまいます。これまたディレクトリの移動に弱いです。
ディレクトリが動いたらテストケース100件の定数を全部書き直しとか嫌すぎです。


■最終的に出した結論がこれ
site-packagesディレクトリの中にjweeklyforecast_path.pyなる以下のようなモジュールを配置。
#!/usr/bin/env python
#encoding: utf-8
import os, sys

PROJECT_HOME = '/Users/akisute/Dropbox/Projects/GoogleAppEngine/jweeklyforecast'
sys.path.append(PROJECT_HOME)
sys.path.append(os.path.join(PROJECT_HOME, 'application'))

そしてこれを各テストケースからインポート。
するとapplicationディレクトリにパスが通るからインポートして使えるという仕組みです。
#!/usr/bin/env python
#coding: utf-8
import logging
try:
    import jweeklyforecast_path
except Exception, e:
    logging.warn("jweeklyforecast_path not found.")

import unittest
from controller.forecastarea import Forecastarea

class TestForecastarea(unittest.TestCase):
    """Forecastarea test case.
    """
    #以下テストケースがずらずらと続く・・・

ディレクトリが動いたり環境が変わったときには、このjweeklyforecast_pathをsite-packagesにコピーして、中身のPROJECT_HOMEを書き直すだけですみます。


■最後になりましたが
意味不明な記事になってしまってごめんなさい。自分の理解度不足ゆえグダグダになってしまいました><

GTDで己の無知の知を知る

OmniFocusを導入していちばん大事だと思ったことなのですが、
GTDを実践していると、「実はびっくりするぐらい自分は何も分かっていない」ということが明らかになります。

どういうことか?
と申しますと、

たとえば今開発しているアプリに新機能を追加したので、テストしたいと考えています。
ですからまずはOmniFocusに「〜をテストする」という考えを追加します。

これだけでは行動に落とし込めないので
「テストクラスを書く」
「テストクラスをTextMateから実行する」
といった行動レベルに分解する必要があるのですが、

ここでふと「テストクラスを書くって、どうやるんだっけ」とか
「TextMateからテストを実行するショートカットってどうだったっけ」とか
そういう疑問が次々にわいてくるわけです。

だからそれらの本筋アクションを実行する前に、
「Pythonのunittestの使い方をネットで調べる」
「サンプルコードを動かして学ぶ」
「TextMateのショートカットを調べてEvernoteにメモする」
というような雑用作業が次々に出てきてしまいます。

今までの自分は、こういう「調べる」とか「学ぶ」というアクションを
全く思考に入れずにプログラムを作ったり仕事をしたりしていました。
これが今までずっと「なぜか知らないが、たったこれだけしか仕事の量がないのにどうしてこんなに時間がかかるのか」と思っていた原因なんだなぁと独り合点しています。

おかげさまで、今では「自分は何も知らないのだから時間がかかる」という風に考えられるようになり、仕事が進まないストレスも減ったかなーと。