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