Decorators I: Introduction To Python Decorators: Decorators vs. The Decorator Pattern

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 20

Decorators I: Introduction to Python Decorators

I predict that in time it will be seen as one of the more powerful features in the language. The problem is that all the
introductions to decorators that I have seen have been rather confusing, so I will try to rectify that here.
(This series of articles will be incorporated into the open-source book Python 3 Patterns & Idioms).

Decorators vs. the Decorator Pattern


First, you need to understand that the word "decorator" was used with some trepidation, because there was
concern that it would be completely confused with theDecorator pattern from the Design Patterns book. At one
point other terms were considered for the feature, but "decorator" seems to be the one that sticks.
Indeed, you can use Python decorators to implement the Decorator pattern, but that's an extremely limited use of it.
Python decorators, I think, are best equated to macros.

History of Macros
The macro has a long history, but most people will probably have had experience with C preprocessor macros. The
problems with C macros were (1) they were in a different language (not C) and (2) the behavior was sometimes
bizarre, and often inconsistent with the behavior of the rest of C.
Both Java and C# have added annotations, which allow you to do some things to elements of the language. Both of
these have the problems that (1) to do what you want, you sometimes have to jump through some enormous and
untenable hoops, which follows from (2) these annotation features have their hands tied by the bondage-anddiscipline (or as Martin Fowler gently puts it: "Directing") nature of those languages.
In a slightly different vein, many C++ programmers (myself included) have noted the generative abilities of C++
templates and have used that feature in a macro-like fashion.
Many other languages have incorporated macros, but without knowing much about it I will go out on a limb and say
that Python decorators are similar to Lisp macros in power and possibility.

The Goal of Macros


I think it's safe to say that the goal of macros in a language is to provide a way to modify elements of the language.
That's what decorators do in Python -- they modify functions, and in the case of class decorators, entire classes.
This is why they usually provide a simpler alternative to metaclasses.
The major failings of most language's self-modification approaches are that they are too restrictive and that they
require a different language (I'm going to say that Java annotations with all the hoops you must jump through to
produce an interesting annotation comprises a "different language").
Python falls into Fowler's category of "enabling" languages, so if you want to do modifications, why create a
different or restricted language? Why not just use Python itself? And that's what Python decorators do.

What Can You Do With Decorators?

Decorators allow you to inject or modify code in functions or classes. Sounds a bit like Aspect-Oriented
Programming (AOP) in Java, doesn't it? Except that it's both much simpler and (as a result) much more powerful.
For example, suppose you'd like to do something at the entry and exit points of a function (such as perform some
kind of security, tracing, locking, etc. -- all the standard arguments for AOP). With decorators, it looks like this:
@entryExit
def func1():
print "inside func1()"
@entryExit
def func2():
print "inside func2()"
The @ indicates the application of the decorator.

Function Decorators
A function decorator is applied to a function definition by placing it on the line before that function definition begins.
For example:
@myDecorator
def aFunction():
print "inside aFunction"
When the compiler passes over this code, aFunction() is compiled and the resulting function object is passed to
the myDecorator code, which does something to produce a function-like object that is then substituted for the
original aFunction().
What does the myDecorator code look like? Well, most introductory examples show this as a function, but I've
found that it's easier to start understanding decorators by using classes as decoration mechanisms instead of
functions. In addition, it's more powerful.
The only constraint upon the object returned by the decorator is that it can be used as a function -- which basically
means it must be callable. Thus, any classes we use as decorators must implement __call__.
What should the decorator do? Well, it can do anything but usually you expect the original function code to be used
at some point. This is not required, however:
class myDecorator(object):
def __init__(self, f):
print "inside myDecorator.__init__()"
f() # Prove that function definition has completed
def __call__(self):
print "inside myDecorator.__call__()"
@myDecorator
def aFunction():
print "inside aFunction()"
print "Finished decorating aFunction()"

aFunction()
When you run this code, you see:
inside myDecorator.__init__()
inside aFunction()
Finished decorating aFunction()
inside myDecorator.__call__()
Notice that the constructor for myDecorator is executed at the point of decoration of the function. Since we can
call f() inside __init__(), it shows that the creation off() is complete before the decorator is called. Note also that the
decorator constructor receives the function object being decorated. Typically, you'll capture the function object in
the constructor and later use it in the __call__() method (the fact that decoration and calling are two clear phases
when using classes is why I argue that it's easier and more powerful this way).
When aFunction() is called after it has been decorated, we get completely different behavior;
the myDecorator.__call__() method is called instead of the original code. That's because the act of
decoration replaces the original function object with the result of the decoration -- in our case,
the myDecorator object replacesaFunction. Indeed, before decorators were added you had to do something much
less elegant to achieve the same thing:
def foo(): pass
foo = staticmethod(foo)
With the addition of the @ decoration operator, you now get the same result by saying:
@staticmethod
def foo(): pass
This is the reason why people argued against decorators, because the @ is just a little syntax sugar meaning "pass
a function object through another function and assign the result to the original function."
The reason I think decorators will have such a big impact is because this little bit of syntax sugar changes the way
you think about programming. Indeed, it brings the idea of "applying code to other code" (i.e.: macros) into
mainstream thinking by formalizing it as a language construct.

Slightly More Useful


Now let's go back and implement the first example. Here, we'll do the more typical thing and actually use the code
in the decorated functions:
class entryExit(object):
def __init__(self, f):
self.f = f
def __call__(self):
print "Entering", self.f.__name__
self.f()
print "Exited", self.f.__name__
@entryExit

def func1():
print "inside func1()"
@entryExit
def func2():
print "inside func2()"
func1()
func2()
The output is:
Entering func1
inside func1()
Exited func1
Entering func2
inside func2()
Exited func2
You can see that the decorated functions now have the "Entering" and "Exited" trace statements around the call.
The constructor stores the argument, which is the function object. In the call, we use the __name__ attribute of the
function to display that function's name, then call the function itself.

Using Functions as Decorators


The only constraint on the result of a decorator is that it be callable, so it can properly replace the decorated
function. In the above examples, I've replaced the original function with an object of a class that has
a __call__() method. But a function object is also callable, so we can rewrite the previous example using a function
instead of a class, like this:
def entryExit(f):
def new_f():
print "Entering", f.__name__
f()
print "Exited", f.__name__
return new_f
@entryExit
def func1():
print "inside func1()"
@entryExit
def func2():
print "inside func2()"
func1()
func2()
print func1.__name__

new_f() is defined within the body of entryExit(), so it is created and returned when entryExit() is called. Note
that new_f() is a closure, because it captures the actual value of f.
Once new_f() has been defined, it is returned from entryExit() so that the decorator mechanism can assign the
result as the decorated function.
The output of the line print func1.__name__ is new_f, because the new_f function has been substituted for the
original function during decoration. If this is a problem you can change the name of the decorator function before
you return it:
def entryExit(f):
def new_f():
print "Entering", f.__name__
f()
print "Exited", f.__name__
new_f.__name__ = f.__name__
return new_f
The information you can dynamically get about functions, and the modifications you can make to those functions,
are quite powerful in Python.

Python Decorators II: Decorator Arguments

In part I, I showed how to use decorators without arguments, primarily using classes as decorators because I find
them easier to think about.
If we create a decorator without arguments, the function to be decorated is passed to the constructor, and
the __call__() method is called whenever the decorated function is invoked:
class decoratorWithoutArguments(object):
def __init__(self, f):
"""
If there are no decorator arguments, the function
to be decorated is passed to the constructor.
"""
print "Inside __init__()"
self.f = f
def __call__(self, *args):
"""
The __call__ method is not called until the
decorated function is called.
"""
print "Inside __call__()"
self.f(*args)
print "After self.f(*args)"

@decoratorWithoutArguments
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "After first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "After second sayHello() call"
Any arguments for the decorated function are just passed to __call__(). The output is:
Inside __init__()
After decoration
Preparing to call sayHello()
Inside __call__()
sayHello arguments: say hello argument list
After self.f(*args)
After first sayHello() call
Inside __call__()
sayHello arguments: a different set of arguments
After self.f(*args)
After second sayHello() call
Notice that __init__() is the only method called to perform decoration, and __call__() is called every time you call
the decorated sayHello().

Decorators with Arguments


Now let's modify the above example to see what happens when we add arguments to the decorator:
class decoratorWithArguments(object):
def __init__(self, arg1, arg2, arg3):
"""
If there are decorator arguments, the function
to be decorated is not passed to the constructor!
"""
print "Inside __init__()"
self.arg1 = arg1
self.arg2 = arg2
self.arg3 = arg3
def __call__(self, f):
"""
If there are decorator arguments, __call__() is only called

once, as part of the decoration process! You can only give


it a single argument, which is the function object.
"""
print "Inside __call__()"
def wrapped_f(*args):
print "Inside wrapped_f()"
print "Decorator arguments:", self.arg1, self.arg2, self.arg3
f(*args)
print "After f(*args)"
return wrapped_f
@decoratorWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "after first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "after second sayHello() call"
From the output, we can see that the behavior changes quite significantly:
Inside __init__()
Inside __call__()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
Now the process of decoration calls the constructor and then immediately invokes __call__(), which can only take a
single argument (the function object) and must return the decorated function object that replaces the original.
Notice that __call__() is now only invoked once, during decoration, and after that the decorated function that you
return from __call__() is used for the actual calls.
Although this behavior makes sense -- the constructor is now used to capture the decorator arguments, but the
object __call__() can no longer be used as the decorated function call, so you must instead use __call__() to

perform the decoration -- it is nonetheless surprising the first time you see it because it's acting so much differently
than the no-argument case, and you must code the decorator very differently from the no-argument case.

Decorator Functions with Decorator Arguments


Finally, let's look at the more complex decorator function implementation, where you have to do everything all at
once:
def decoratorFunctionWithArguments(arg1, arg2, arg3):
def wrap(f):
print "Inside wrap()"
def wrapped_f(*args):
print "Inside wrapped_f()"
print "Decorator arguments:", arg1, arg2, arg3
f(*args)
print "After f(*args)"
return wrapped_f
return wrap
@decoratorFunctionWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print 'sayHello arguments:', a1, a2, a3, a4
print "After decoration"
print "Preparing to call sayHello()"
sayHello("say", "hello", "argument", "list")
print "after first sayHello() call"
sayHello("a", "different", "set of", "arguments")
print "after second sayHello() call"
Here's the output:
Inside wrap()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
The return value of the decorator function must be a function used to wrap the function to be decorated. That is,
Python will take the returned function and call it at decoration time, passing the function to be decorated. That's
why we have three levels of functions; the inner one is the actual replacement function.

Because of closures, wrapped_f() has access to the decorator arguments arg1, arg2 and arg3, without having to
explicitly store them as in the class version. However, this is a case where I find "explicit is better than implicit," so
even though the function version is more succinct I find the class version easier to understand and

Python Decorators III: A Decorator-Based Build System

I've used make for many years. I only used ant because it produced faster Java builds. But both build systems
started out thinking the problem was simple, and only later discovered that you really need a programming
language to solve the build problem. By then it was too late. As a result you have to jump through annoying hoops
to get things done.
There have been efforts to create build systems on top of languages. Rake is a fairly successful domain-specific
language (DSL) built atop Ruby. And a number of projects have been created with Python.
For years I've wanted a system that was just a thin veneer on Python, so you get some support for dependencies
but effectively everything else is Python. This way, you don't need to shift back and forth between Python and some
language other than Python; it's less of a mental distraction.
It turns out that decorators are perfect for this purpose. The design I present here is just a first cut, but it's easy to
add new features and I've already started using it as the build system for The Python Book, so I'll probably need to
add more features. Most importantly, I know I'll be able to do anything that I want, which is not always true
withmake or ant (yes, you can extend ant but the cost of entry is often not worth the benefit).
While the rest of the book has a Creative Commons Attribution-Share Alike license, this program only has
a Creative Commons Attribution license, because I'd like people to be able to use it under any circumstances.
Obviously, it would be ideal if you make any improvements that you'd contribute them back to the project, but this is
not a prerequisite for using or modifying the code.

Syntax

The most important and convenient thing provided by a build system is dependencies. You tell it what depends on
what, and how to update those dependencies. Taken together, this is called a rule, so the decorator will also be
called rule. The first argument of the decorator is the target (the thing that needs to be updated) and the remaining
arguments are the dependencies. If the target is out of date with the dependencies, the function code is run to bring
it up to date.
Here's a simple example that shows the basic syntax:
@rule("file1.txt")

def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
The name of the rule is file1 because that's the function name. In this case, the target is "file1.txt" and there are no
dependencies, so the rule only checks to see whetherfile1.txt exists, and if it doesn't it runs the function code,
which brings it up to date.
Note the use of the docstring; this is captured by the build system and describes the rule on the command line
when you say build help (or anything else the builder doesn't understand).
The @rule decorators only affect the functions they are attached to, so you can easily mix regular code with rules in
the same build file. Here's a function that updates the date stamp on a file, or creates the file if it doesn't exist:
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')
A more typical rule is one that associates a target file with one or more dependent files:
@rule("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"Brings target1.txt up to date with its dependencies"
touchOrCreate("target1.txt")
This build system also allows multiple targets, by putting the targets in a list:
@rule(["target1.txt", "target2.txt"], "dependency1.txt", "dependency2.txt")
def multipleBoth():
"Multiple targets and dependencies"
[touchOrCreate(f) for f in ["target1.txt", "target2.txt"]]
If there is no target or dependencies, the rule is always executed:
@rule()
def clean():
"Remove all created files"
[os.remove(f) for f in allFiles if os.path.exists(f)]
The alFiles array is seen in the example, shown later.
You can write rules that depend on other rules:

@rule(None, target1, target2)


def target3():
"Always brings target1 and target2 up to date"
print target3
Since None is the target, there's nothing to compare to but in the process of checking the rules target1 and target2,
those are both brought up to date. This is especially useful when writing "all" rules, as you will see in the example.

Builder Code

By using decorators and a few appropriate design patterns, the code becomes quite succinct. Note that
the __main__ code creates an example build.py file (containing the examples that you see above and more), and
the first time you run a build it creates a build.bat file for Windows and a build command file for Unix/Linux/Cygwin.
A complete explanation follows the code:
# 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"
print "%s:%s\t%s\n" % (r.name, newline, r)
print "(Multiple targets will be updated in order)"
# Create "build" commands for Windows and Unix:
if not os.path.exists("build.bat"):
file("build.bat", 'w').write("python build.py %1 %2 %3 %4 %5 %6 %7")
if not os.path.exists("build"):
# Unless you can detect cygwin independently of Windows
file("build", 'w').write("python build.py $*")
os.chmod("build", stat.S_IEXEC)
############### Test/Usage Examples ###############
if __name__ == "__main__":
if not os.path.exists("build.py"):
file("build.py", 'w').write('''\
# Use cases: both test code and usage examples
from builder import rule
import os
@rule("file1.txt")
def file1():
"File doesn't exist; run rule"
file("file1.txt", 'w')
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')
dependencies = ["dependency1.txt", "dependency2.txt",
"dependency3.txt", "dependency4.txt"]

targets = ["file1.txt", "target1.txt", "target2.txt"]


allFiles = targets + dependencies
@rule(allFiles)
def multipleTargets():
"Multiple files don't exist; run rule"
[file(f, 'w') for f in allFiles if not os.path.exists(f)]
@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("target1.txt","dependency1.txt","dependency2.txt","dependency3.txt")
def target1():
"Brings target1.txt up to date with its dependencies"
touchOrCreate("target1.txt")
@rule()
def updateDependency():
"Updates the timestamp on all dependency.* files"
[touchOrCreate(f) for f in allFiles if f.startswith("dependency")]
@rule()
def clean():
"Remove all created files"
[os.remove(f) for f in allFiles if os.path.exists(f)]
@rule()
def cleanTargets():
"Remove all target files"
[os.remove(f) for f in targets if os.path.exists(f)]
@rule("target2.txt", "dependency2.txt", "dependency4.txt")
def target2():
"Brings target2.txt up to date with its dependencies, or creates it"
touchOrCreate("target2.txt")
@rule(None, target1, target2)
def target3():
"Always brings target1 and target2 up to date"
print target3

@rule(None, clean, file1, multipleTargets, multipleBoth, target1,


updateDependency, target2, target3)
def all():
"Brings everything up to date"
print all
rule.default = all
rule.main() # Does the build, handles command-line arguments
''')
The first group of classes manage dependencies between different types of objects. The base class contains some
common code, including the constructor which you'll note is automatically called if it is not explicitly redefined in a
derived class (a nice, code-saving feature in Python).
Classes derived from Dependency manage particular types of dependency relationships, and redefine
the updated() method to decide whether the target should be brought up to date with the dependent. This is an
example of the Template Method design pattern, where updated() is the template method and _Rule is the context.
If you want to create a new type of dependency -- say, the addition of wildcards on dependencies and/or targets -you define new Dependency subclasses. You'll see that the rest of the code doesn't require changes, which is a
positive indicator for the design (future changes are isolated).
Dependency.create() is what I call a Simple Factory Method, because all it does is localize the creation of all the
subtypes of Dependency. Note that forward referencing is not a problem here as it is in some languages, so using
the full implementation of Factory Method given in GoF is not necessary and also more complex (this doesn't mean
there aren't cases that justify the full-fledged Factory Method).
Note that in FileToDependency we could assert that self.dependency is a subtype of Dependency, but this type
check happens (in effect) when updated() is called.

The rule Decorator

The rule decorator uses the Builder design pattern, which makes sense because the creation of a rule happens in
two steps: the constructor captures the decorator arguments, and the __call__() method captures the function.
The Builder product is a _Rule object, which, like the Dependency classes, contains an updated() method.
Each _Rule object contains a list of dependencies and aruleUpdater() method which is called if any of the
dependencies is out of date. The _Rule also contains a name (which is the decorated function name) and
adescription (the decorated function's docstring). (The _Rule object is an example of the Command pattern).
What's unusual about _Rule is that you don't see any code in the class which
initializes dependencies, ruleUpdater(), name, and description. These are initialized byrule during

the Builder process, using Injection. The typical alternative to this is to create setter methods, but since _Rule is
nested inside rule, rule effectively "owns"_Rule and Injection seems much more straightforward.
The rule constructor first creates the product _Rule object, then handles the decorator arguments. It
converts decoratorArgs to a list because we need it to be modifiable, and decoratorArgs comes in as a tuple. If
there is only one argument it means the user has only specified the target and no dependencies.
BecauseDependency.create() requires two arguments, we append None to the list.
The target is always the first argument, so pop(0) pulls it off and the remainder of the list is dependencies. To
accommodate the possibility that the target is a list, single targets are turned into lists.
Now Dependency.create() is called for each possible target-dependency combination, and the resulting list is
injected into the _Rule object. For the special case when there are no arguments, a None to None Dependency is
created.
Notice that the only thing the rule constructor does is sort out the arguments; it has no knowledge of particular
relationships. This keeps special knowledge within theDependency hierarchy, so adding a new Dependency is
isolated within that hierarchy.
A similar guideline is followed for the __call__() method, which captures the decorated function. We keep
the _Rule object in a static list called rules, and the first thing to check is whether any of the rule names are
duplicated. Then we capture and inject the name, documentation string, and the function itself.
Note that the Builder "product", the _Rule object, is returned as the result of rule.__call__(), which means that this
object -- which doesn't have a __call__() method -- is substituted for the decorated function. This is a slightly
unusual use of decorators; normally the decorated function is called directly, but in this case the decorated function
is never called directly, but only via the _Rule object.

Running a Build

The static method main() in rule manages the build process, using the helper method update(). If you provide no
command-line arguments, main() passes 0 to update(), which calls the default rule if one has been set, otherwise it
calls the first rule that was defined. If you provide command-line arguments, it passes each one (in order)
toupdate().
If you give it an incorrect argument (typically help is reserved for this), it prints each of the rules along with their
docstrings.
Finally, it checks to see that a build.bat and build command file exists, and creates them if it doesn't.
The build.py produced when you run builder.py the first time can act as a starting point for your build file.

Improvements

As it stands, this system only satisfies the basic needs; it doesn't have, for example, all the features that make does
when it comes to manipulating dependencies. On the other hand, because it's built atop a full-powered
programming language, you can do anything else you need quite easily. If you find yourself writing the same code
over and over, you can modify rule() to reduce the duplicated effort. If you have permission, please submit such
modifications back for possible inclusion.

You might also like