Python で Rake を真似るとしたら という反応を頂いたので、それにまつわるお話を。
まず、CodeReposにコミットしてあるtasktoolsは distutils及びsetuptoolsを拡張する ということを念頭に置いています。ですのである程度distutils.core.Commandの思想というか、インターフェイスを残しています。
- タスクをクラスで定義すること
- オプションの定義方法
sub_commands
やfinalize_options
といったメソッド
などなどは元のまんまです。
じゃあ、 distutils
や setuptools
を抜いて好きにRakeをPythonで真似るとしたら、というのが今回メインのお話。最初にこんな感じでコマンドを定義しますよ、というのを出してしまいます。
1from __future__ import with_statement
2from 今回作成したモジュール import *
3from subprocess import call
4
5# ここからタスク定義
6
7with namespace("test"):
8
9 @task
10 def build(t, depends = ["test:make"]):
11 """builds this modules. """
12 call(["ls"])
13 print t.name, ": build done"
14
15 @task
16 def make(t):
17 print "make"
18
19# 実行
20run()
Rakeだとこうなる。
1namespace "test" do
2
3 desc "builds this modules"
4 task "build" => "make" do |t|
5 sh("ls")
6 puts "#{t.name} done."
7 end
8
9 task "make" do |t|
10 puts "make"
11 end
12
13end
Python版もなかなかシンプルで見やすいと思うんですがどうでしょう。あとちょっとtips。さっきの記事だと os.system
を使ってますが、 subprocess
モジュールの call
や check_call
のほうが便利じゃないかな、と思います。
タスクのメタ情報は引数のデフォルト値を使います。ここで @task(depends = ["test:make"])
ってしたほうがいいんじゃないの?と思う人もいるかもしれません。真っ当な意見だと思いますし、それがPythonicなやり方と思います。しかしそうするとこうなります。
1with namespace("test"):
2
3 @task(depends = ["test:make"])
4 def build(t):
5 """builds this modules. """
6 call(["ls"])
7 print t.name, ": build done"
8
9 @task() #<= ここがキモくないですか?
10 def make(t):
11 print "make"
Pythonではproperty以外では ()
をつけないとメソッドが呼び出せません。だから引数が何もない場合は空のカッコがつくわけです(まぁDescriptorでhogehogeすればできるんですがね)。これ、イケてないですよね。というわけで引数のデフォルト値を使います。
ではこのタスク定義でタスクを実行するためのコードです。
1from __future__ import with_statement
2import inspect,new,sys
3from contextlib import contextmanager
4from optparse import OptionParser
5from itertools import *
6
7class _Tasks(dict):
8 def __getitem__(self,key):
9 if key in self: return dict.__getitem__(self, key)
10 _abort("Error: Unknown task: %s"%key)
11
12class Task(object):
13 def __init__(self, run, name, description,
14 depends = None, options = None):
15 self._run = new.instancemethod(run, self, self.__class__)
16 self.name = _get_current_ns()+name
17 self.description = description
18 self.depends = depends or []
19 self.options = options or []
20 tasks[self.name] = self
21
22 def run(self):
23 for depend in self.depends:
24 if tasks[depend].run() is False:
25 _abort("Error: faild %s"%depend)
26 return self._run()
27
28_namespace = []
29tasks = _Tasks()
30
31@contextmanager
32def namespace(name):
33 _namespace.append(name)
34 yield
35 _namespace.pop()
36
37def task(f):
38 names, _, _, values = inspect.getargspec(f)
39 Task(f, f.__name__, f.__doc__ or "no description", **dict(izip(names[1:], values or [])))
40
41def print_tasks():
42 maxlength = max(imap(len, tasks.iterkeys()))
43 for name in sorted(tasks.iterkeys()):
44 print name.ljust(maxlength), "#", tasks[name].description.strip()
45
46def run():
47 usage = "usage :%prog [options] targets"
48 parser = OptionParser(usage)
49 parser.add_option("-T", "--tasks", action="store_true",
50 dest="print_tasks",
51 help="Display the tasks (matching optional PATTERN) with descriptions, then exit.")
52 (options, args) = parser.parse_args()
53 if options.print_tasks: _abort(print_tasks())
54 if not args: _abort(parser.get_usage())
55
56 map(lambda t:t.run(), imap(tasks.__getitem__, args))
57
58def _abort(msg):
59 if msg : print msg
60 sys.exit(1)
61
62def _get_current_ns(): return _namespace and ":".join(_namespace)+":" or ""
結構短いかな?。一応ちゃんと動きます。
tasks.py -T
1test:build # builds this modules.
2test:make # no description
tasks.py test:build
1make
2tasks.py
3test:build : build done
うむ。
結局、Rubyの言語内DSL構築能力は
()
が省略できる- ブロック(pythonではデコレータとwith_statementがありますが)
によるものが多いと思っています。ことPythonとの対比では。「シンプルさ」「書きやすさ」なんかは好みかな、と(Rakeの例だとdocstringを持っている分Pythonの方がいい感じさえしてきます)。例えば end
ってなんだよ、って言う人にはPythonのほうがウケがいいかもしれませんしそうじゃないかもしれません。
そんなこんなで試しに書いてみたらいい感じだったんで、ちゃんと本格的にPython版Rakeとして開発を続けてみようかなあ。
ところで言語内DSLといえばScalaでしょう。Rubyなんてちょろいもんですよ。なんてったってScalaは(ry