Python Decorators III: A Decorator-Based Build System
October 26, 2008
我使用make已有很多年了。我只使用ant的原因是它可以创建速度更快的java build。但这两个构建系统都是以“问题是简单的”这个出发点考虑问题的,所以此后不久就发现真正需要的是一个程序设计语言来解决构建问题。可为时已晚,作为代价你需要付出大量艰辛来搞定问题。
在语言之上实现构建系统已有一些成果。Rake是一个十分成功的基于Ruby的领域特定语言(domain specific language, DSL)。还有不少项目也是用Python完成的,
多年来我一直想能有一个Python上的系统,其作用类似于一个薄薄的“胶合板”。这样可以得到一些依赖性上的支持,即使都是Python上的。因为如此一来你就不必在Python和Python以外的语言之间来回奔波,减少了思维上的分心。
最终发现decorators是解决该问题的最佳选择。我这里设计的仅是一个雏形,但很容易添加新特性,而且我已经开始将它作为The Python Book的构建系统了,也需要增加更多特性。更重要的是,我知道我能够做任何想做而make和ant做不到的事(没错,你可以扩展ant,但常常是得不偿失)。
尽管书中其余部分有一个Creative Commons Attribution-Share Alike license(Creative Commons相同方式共享署名许可),可该程序仅有一个Creative Commons Attribution许可,因为我想要人们能够在任何环境下使用它。很显然,如果你做了任何回馈于项目的改进方面的贡献,那就再好不过了。不过这不是使用和修改代码的前提条件。
语法
该构建系统提供的最重要和最便捷的特性就是依赖(dependencies)。你告诉它什么依赖于什么,如何更新这些依赖,这就叫做一个规则(rule)。因此decorator也被称为rule。decorator第一个参数是目标对象(target,需要更新的变量),其余参数都属于依赖(dependencies)。如果目标对象相对于依赖过时,函数就要对它进行更新。
下面是一个反映基本语法的简单例子:
@rule("file1.txt")
def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
规则名称是file1,因为这也是函数名称。在这里例子里,目标对象是"file1.txt",不含依赖,因此规则仅检查file1.txt是否存在,如果不存在则运行起更新作用的函数。
注意docstring的使用:它由构建系统获取,当你输入build help,则以命令行方式显示规则描述。
@rule decorators仅对其绑定的函数产生影响,因此你可以很容易地在同一构建文件中将普通代码和规则混合使用。这是一个更新文件时间戳的函数,如果文件不存在则创建一份:
def touchOrCreate(f): # Ordinary function
"Bring file up to date; creates it if it doesn't exist"
if os.path.exists(f):
os.utime(f, None)
else:
file(f, 'w')
更典型的规则是将目标文件与一个或多个依赖文件进行关联:
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"Brings target1.txt up to date with its dependencies"
touchOrCreate("target1.txt")
构建系统也允许加入多个目标,方法是将这些目标放入一个列表中:
@rule(["target1.txt", "target2.txt"], "dependency1.txt", "dependency2.txt")
def multipleBoth():
"Multiple targets and dependencies"
[touchOrCreate(f) for f in ["target1.txt", "target2.txt"]]
如果目标对象和依赖都不存在,规则通常这样运行:
@rule()
def clean():
"Remove all created files"
[os.remove(f) for f in allFiles if os.path.exists(f)]
这个例子中出现了alFiles数组,稍后作以介绍。
你也可以编写依赖于其他规则的规则:
@rule(None, target1, target2)
def target3():
"Always brings target1 and target2 up to date"
print target3
由于None是目标对象,这里不存在比较,但在检查target1和target2的规则过程中,它们将被更新。这在编写“所有(all)”规则时十分有用,下面例子中将涉及到。
构建器代码
通过使用decorators和一些恰当的设计模式,代码变得十分简洁。需要注意的是,代码__main__创建了一个名为“build.by”的示例文件(包含了你上面看到的例子)。当你第一次运行构建时,它创建了一个build.bat文件(在windows中)或build命令文件(在Unix/Linux/Cygwin中)。完整的解释说明附在代码后面:
# builder.py
import sys, os, stat
"""
Adds build rules atop Python, to replace make, etc.
by Bruce Eckel
License: Creative Commons with Attribution.
"""
def reportError(msg):
print >> sys.stderr, "Error:", msg
sys.exit(1)
class Dependency(object):
"Created by the decorator to represent a single dependency relation"
changed = True
unchanged = False
@staticmethod
def show(flag):
if flag: return "Updated"
return "Unchanged"
def __init__(self, target, dependency):
self.target = target
self.dependency = dependency
def __str__(self):
return "target: %s, dependency: %s" % (self.target, self.dependency)
@staticmethod
def create(target, dependency): # Simple Factory
if target == None:
return NoTarget(dependency)
if type(target) == str: # String means file name
if dependency == None:
return FileToNone(target, None)
if type(dependency) == str:
return FileToFile(target, dependency)
if type(dependency) == Dependency:
return FileToDependency(target, dependency)
reportError("No match found in create() for target: %s, dependency: %s"
% (target, dependency))
def updated(self):
"""
Call to determine whether this is up to date.
Returns 'changed' if it had to update itself.
"""
assert False, "Must override Dependency.updated() in derived class"
class NoTarget(Dependency): # Always call updated() on dependency
def __init__(self, dependency):
Dependency.__init__(self, None, dependency)
def updated(self):
if not self.dependency:
return Dependency.changed # (None, None) -> always run rule
return self.dependency.updated() # Must be a Dependency or subclass
class FileToNone(Dependency): # Run rule if file doesn't exist
def updated(self):
if not os.path.exists(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToFile(Dependency): # Compare file datestamps
def updated(self):
if not os.path.exists(self.dependency):
reportError("%s does not exist" % self.dependency)
if not os.path.exists(self.target):
return Dependency.changed # If it doesn't exist it needs to be made
if os.path.getmtime(self.dependency) > os.path.getmtime(self.target):
return Dependency.changed
return Dependency.unchanged
class FileToDependency(Dependency): # Update if dependency object has changed
def updated(self):
if self.dependency.updated():
return Dependency.changed
if not os.path.exists(self.target):
return Dependency.changed # If it doesn't exist it needs to be made
return Dependency.unchanged
class rule(object):
"""
Decorator that turns a function into a build rule. First file or object in
decorator arglist is the target, remainder are dependencies.
"""
rules = []
default = None
class _Rule(object):
"""
Command pattern. name, dependencies, ruleUpdater and description are
all injected by class rule.
"""
def updated(self):
if Dependency.changed in [d.updated() for d in self.dependencies]:
self.ruleUpdater()
return Dependency.changed
return Dependency.unchanged
def __str__(self): return self.description
def __init__(self, *decoratorArgs):
"""
This constructor is called first when the decorated function is
defined, and captures the arguments passed to the decorator itself.
(Note Builder pattern)
"""
self._rule = rule._Rule()
decoratorArgs = list(decoratorArgs)
if decoratorArgs:
if len(decoratorArgs) == 1:
decoratorArgs.append(None)
target = decoratorArgs.pop(0)
if type(target) != list:
target = [target]
self._rule.dependencies = [Dependency.create(targ, dep)
for targ in target for dep in decoratorArgs]
else: # No arguments
self._rule.dependencies = [Dependency.create(None, None)]
def __call__(self, func):
"""
This is called right after the constructor, and is passed the function
object being decorated. The returned _rule object replaces the original
function.
"""
if func.__name__ in [r.name for r in rule.rules]:
reportError("@rule name %s must be unique" % func.__name__)
self._rule.name = func.__name__
self._rule.description = func.__doc__ or ""
self._rule.ruleUpdater = func
rule.rules.append(self._rule)
return self._rule # This is substituted as the decorated function
@staticmethod
def update(x):
if x == 0:
if rule.default:
return rule.default.updated()
else:
return rule.rules[0].updated()
# Look up by name
for r in rule.rules:
if x == r.name:
return r.updated()
raise KeyError
@staticmethod
def main():
"""
Produce command-line behavior
"""
if len(sys.argv) == 1:
print Dependency.show(rule.update(0))
try:
for arg in sys.argv[1:]:
print Dependency.show(rule.update(arg))
except KeyError:
print "Available rules are:/n"
for r in rule.rules:
if r == rule.default:
newline = " (Default if no rule is specified)/n"
else:
newline = "/n"