#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
## Copyright 2002-2007 by LivingLogic AG, Bayreuth/Germany.
## Copyright 2002-2007 by Walter Dörwald
##
## All Rights Reserved
##
## See __init__.py for the license
"""
ll.make provides tools for building projects.
Like make it allows you to specify dependencies between files
and actions to be executed when files don't exist or are out of date with
respect to one of their sources. But unlike make you can do this
in an object oriented way and targets are not only limited to files.
Relevant classes are:
- Project, which is the
container for all actions in a project,
- Action (and subclasses),
which are used to transform input data and read and write files (or other
entities like database records).
A simple script that copies a file foo.txt to
bar.txt reencoding it from "latin-1" to
"utf-8" in the process looks like this:
from ll import make, url
class MyProject(make.Project):
def create(self):
make.Project.create(self)
source = self.add(make.FileAction(url.File("foo.txt")))
target = self.add(
source /
make.DecodeAction("iso-8859-1") /
make.EncodeAction("utf-8") /
make.FileAction(url.File("bar.txt"))
)
self.writecreatedone()
p = MyProject()
p.create()
if __name__ == "__main__":
p.build("bar.txt")
"""
__version__ = "$Revision: 1.84.2.1 $"[11:-1]
import sys, os, os.path, optparse, warnings, re, datetime, cStringIO, errno, tempfile, operator, types, cPickle
from ll import misc, url
try:
import astyle
except ImportError:
from ll import astyle
###
### Constants and helpers
###
nodata = misc.Const("nodata") # marker object for "no new data available"
newdata = misc.Const("newdata") # marker object for "new data available"
bigbang = datetime.datetime(1900, 1, 1) # there can be no timestamp before this one
bigcrunch = datetime.datetime(3000, 1, 1) # there can be no timestamp after this one
def filechanged(key):
"""
Get the last modified date (or bigbang, if the file doesn't exist).
"""
try:
return key.mdate()
except (IOError, OSError):
return bigbang
class Level(object):
"""
Stores information about the recursive execution of Actions.
"""
__slots__ = ("action", "since", "infoonly", "reported")
def __init__(self, action, since, infoonly, reported=False):
self.action = action
self.since = since
self.infoonly = infoonly
self.reported = reported
def __repr__(self):
return "<%s.%s object action=%r since=%r infoonly=%r reported=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.action, self.since, self.infoonly, self.reported, id(self))
def report(func):
"""
Standard decorator for Action.get methods.
This decorator handles proper reporting of nested action calls.
If it isn't used, only the output of calls to
Project.writestep
will be visible to the user.
"""
def reporter(self, project, since, infoonly):
reported = False
show = project.showaction is not None and isinstance(self, project.showaction)
if show:
if project.showidle and (not infoonly or project.showinfoonly):
args = ["Starting ", project.straction(self)]
if project.showtimestamps:
args.append(" since ")
args.append(project.strdatetime(since))
if infoonly:
args.append(" (info only)")
project.writestack(*args)
reported = True
project.stack.append(Level(self, since, infoonly, reported))
t1 = datetime.datetime.utcnow()
try:
data = func(self, project, since, infoonly)
except (KeyboardInterrupt, SystemExit):
raise
except Exception, exc:
project.actionsfailed += 1
if project.ignoreerrors: # ignore changes in failed subgraphs
data = nodata # Return "everything is up to date" in this case
error = exc.__class__
else:
raise
else:
project.actionscalled += 1
error = None
t2 = datetime.datetime.utcnow()
if show or error is not None:
if (not project.showidle and data is not nodata and (data is not newdata or project.showinfoonly)) or error is not None:
project._writependinglevels() # Only outputs something if the action hasn't called writestep()
reported = project.stack[-1].reported
if show:
project.stack.pop(-1)
if reported:
if error is not None:
text = "Canceled"
else:
text = "Finished"
args = [text, " ", project.straction(self)]
if project.showtime:
args.append(" in ")
args.append(project.strtimedelta(t2-t1))
if project.showdata:
args.append(": ")
if error is not None:
if error.__module__ != "exceptions":
text = "%s.%s" % (error.__module__, error.__name__)
else:
text = error.__name__
args.append(s4error(text))
elif data is nodata:
args.append("nodata")
elif data is newdata:
args.append("newdata")
elif isinstance(data, str):
args.append(s4data("str (%db)" % len(data)))
elif isinstance(data, unicode):
args.append(s4data("unicode (%dc)" % len(data)))
else:
dataclass = data.__class__
if dataclass.__module__ != "__builtin__":
text = "%s.%s @ 0x%x" % (dataclass.__module__, dataclass.__name__, id(data))
else:
text = "%s @ 0x%x" % (dataclass.__name__, id(data))
args.append(s4data(text))
project.writestack(*args)
return data
reporter.__dict__.update(func.__dict__)
reporter.__doc__ = func.__doc__
reporter.__name__ = func.__name__
return reporter
###
### exceptions & warnings
###
class MakeWarning(Warning):
"""
Base class for all warnings in ll.make.
"""
class RedefinedTargetWarning(MakeWarning):
"""
Warning that will be issued when a target is added to a project and a target
with the same key already exists.
"""
def __init__(self, key):
self.key = key
def __str__(self):
return "target with key=%r redefined" % self.key
class UndefinedTargetError(KeyError):
"""
Exception that will be raised when a target with the specified key doesn't
exist within the project.
"""
def __init__(self, key):
self.key = key
def __str__(self):
return "target %r undefined" % self.key
###
### Actions
###
def _ipipe_type(obj):
try:
return obj.type
except AttributeError:
return "%s.%s" % (obj.__class__.__module__, obj.__class__.__name__)
_ipipe_type.__xname__ = "type"
def _ipipe_key(obj):
return obj.getkey()
_ipipe_key.__xname__ = "key"
class Action(object):
"""
An Action is responsible for transforming input data
into output data.
"""
def __init__(self):
"""
Create a new Action instance.
"""
def __div__(self, output):
return output.__rdiv__(self)
@misc.notimplemented
def get(self, project, since, infoonly):
"""
This method (i.e. the implementations in subclasses) is the workhorse
of ll.make. get must return the output
data of the action if this data has changed since since
(which is a datetime.datetime object in UTC). If the data
hasn't changed since since the special object nodata
must be returned.
In both cases the action must make sure that the data is internally
consistent, i.e. if the input data is the output data of other actions
has to ensure that those other actions update their data too,
independent from the fact whether get will return new
data or not.
Two special values can be passed for since:
bigbang
- This timestamp is older than any timestamp that can appear in
real life. Since all data is newer than this, get must
always return output data.
bigcrunch
- This timestamp is newer than any timestamp that can appear in
real life. Since there can be no data newer than this, get
can only return output data in this case if ensuring internal consistency
resulted in new data.
If infoonly is true get must return the
constant newdata instead of real data, if any new data is
available.
"""
def getkey(self):
"""
Get the nearest key from or its inputs. This is used by
ModuleAction for the
filename.
"""
return getattr(self, "key", None)
@misc.notimplemented
def __iter__(self):
"""
Return an iterator over the input actions of .
"""
def iterallinputs(self):
"""
Return an iterator over all input actions of (i.e. recursively).
"""
for input in self:
yield input
for subinput in input.iterallinputs():
yield subinput
def findpaths(self, input):
"""
Find dependency paths leading from to the other action input.
I.e. if depends directly or indirectly on input, this
generator will produce all paths p where p[0] is
and p[-1] is input and p[i+1] in p[i] for all
i in xrange(len(p)-1).
"""
if input is self:
yield [self]
else:
for myinput in self:
for path in myinput.findpaths(input):
yield [self] + path
def __xattrs__(self, mode="default"):
if mode == "default":
return (_ipipe_type, _ipipe_key)
return dir(self)
def __xrepr__(self, mode="default"):
if mode in ("cell", "default"):
name = self.__class__.__name__
if name.endswith("Action"):
name = name[:-6]
yield (s4action, name)
if hasattr(self, "key"):
yield (astyle.style_default, "(")
key = self.key
if isinstance(key, url.URL) and key.islocal():
here = url.here()
home = url.home()
s = str(key)
test = str(key.relative(here))
if len(test) < len(s):
s = test
test = "~/%s" % key.relative(home)
if len(test) < len(s):
s = test
else:
s = str(key)
yield (s4key, s)
yield (astyle.style_default, ")")
else:
yield (astyle.style_default, repr(self))
class PipeAction(Action):
"""
A PipeAction depends on exactly one input action and transforms
the input data into output data.
"""
def __init__(self, input=None):
Action.__init__(self)
self.input = input
def __rdiv__(self, input):
"""
Register the action input as the input action for and
return (which enables chaining PipeAction objects).
"""
self.input = input
return self
def getkey(self):
return self.input.getkey()
def __iter__(self):
if self.input is not None:
yield self.input
@misc.notimplemented
def execute(self, project, data):
"""
Execute the action: transform the input data data and return
the resulting output data. This method must be implemented in subclasses.
"""
@report
def get(self, project, since, infoonly):
data = self.input.get(project, since, infoonly)
if data is not nodata and not infoonly:
data = self.execute(project, data)
return data
class CollectAction(PipeAction):
"""
A CollectAction is a PipeAction that simply
outputs its input data unmodified, but updates a number of other actions
in the process.
"""
def __init__(self, input=None):
PipeAction.__init__(self, input)
self.inputs = []
def addinputs(self, *inputs):
"""
Register all actions in inputs as additional actions that
have to be updated before is updated.
"""
self.inputs.extend(inputs)
return self
def __iter__(self):
if self.input is not None:
yield self.input
for input in self.inputs:
yield input
@report
def get(self, project, since, infoonly):
inputsince = since
for input in self.inputs:
# We don't need the data itself, so pass True for infoonly
data = input.get(project, since, True)
if data is not nodata:
inputsince = bigbang
data = self.input.get(project, inputsince, infoonly)
return data
def __repr__(self):
return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
class PhonyAction(Action):
"""
A PhonyAction doesn't do anything. It may depend on any
number of additonal input actions which will be updated when this action
gets updated. If there's new data from any of these actions, a
PhonyAction will return None (and nodata
otherwise as usual).
"""
def __init__(self, doc=None):
"""
Create a PhonyAction object. doc describes the
action and is printed by
Project.writephonytargets.
"""
Action.__init__(self)
self.doc = doc
self.inputs = []
self.data = nodata
self.buildno = None
def addinputs(self, *inputs):
"""
Register all actions in inputs as additional actions that
have to be updated once is updated.
"""
self.inputs.extend(inputs)
return self
def __iter__(self):
for input in self.inputs:
yield input
@report
def get(self, project, since, infoonly):
# Caching the result object of a PhonyAction is cheap (it's either None or nodata),
# so we always do the caching as this optimizes away the traversal of a complete subgraph
# for subsequent calls to get() during the same build round
if self.buildno != project.buildno:
result = nodata
for input in self.inputs:
data = input.get(project, since, True)
if data is not nodata:
self.data = None
result = None
if infoonly:
result = newdata
self.buildno = project.buildno
return result
return self.data
def __repr__(self):
s = "<%s.%s object" % (self.__class__.__module__, self.__class__.__name__)
if hasattr(self, "key"):
s += " with key=%r" % self.key
s += " at 0x%x>" % id(self)
return s
class FileAction(PipeAction):
"""
A FileAction is used for reading and writing files
(and other objects providing the appropriate interface).
"""
def __init__(self, key, input=None):
"""
Create a FileAction object with key as the filename.
key must be an object that provides a method open
for opening readable and writable streams to the file.
"""
PipeAction.__init__(self, input)
self.key = key
self.buildno = None
def getkey(self):
return self.key
def write(self, project, data):
"""
Write data to the file and return it.
"""
project.writestep(self, "Writing data to ", project.strkey(self.key))
file = self.key.open("wb")
try:
file.write(data)
self.changed = datetime.datetime.utcnow() # This isn't 100% correct, but that's unproblematic, because nothing relevant happened between the real timestamp and now
project.fileswritten += 1
finally:
file.close()
def read(self, project):
"""
Read the content from the file and return it.
"""
args = ["Reading data from ", project.strkey(self.key)]
if project.showtimestamps:
args.append(" (changed ")
args.append(project.strdatetime(self.changed))
args.append(")")
project.writestep(self, *args)
file = self.key.open("rb")
try:
return file.read()
finally:
file.close()
@report
def get(self, project, since, infoonly):
"""
If a FileAction object doesn't have an input action it reads the input file
and returns the content if the file has changed since since (otherwise nodata is returned).
If a FileAction object does have an input action and the output data from
this input action is newer than the file .key the data will be written
to the file. Otherwise (i.e. the file is up to date) the data will be read from the file.
"""
if self.buildno != project.buildno: # a new build round
# Get timestamp of the file (or bigbang if it doesn't exist)
self.changed = filechanged(self.key)
self.buildno = project.buildno
if self.input is not None:
data = self.input.get(project, self.changed, False)
if data is not nodata: # We've got new data from our input =>
self.write(project, data) # write new data to disk
if infoonly: # no need for the real data
data = newdata
return data
else: # We have no inputs (i.e. this is a "source" file)
if self.changed is bigbang:
raise ValueError("source file %r doesn't exist" % self.key)
if self.changed > since: # We are up to date now and newer than the output action
if infoonly:
if project.showinfoonly:
args = ["Have new data for ", project.strkey(self.key)]
if project.showtimestamps:
args.append(" (changed ")
args.append(project.strdatetime(self.changed))
args.append(")")
project.writestep(self, *args)
return newdata
return self.read(project) # return file data (to output action or client)
# else fail through and return nodata
return nodata
def __repr__(self):
return "<%s.%s object with key=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.key, id(self))
class UnpickleAction(PipeAction):
"""
This action unpickles a string.
"""
def execute(self, project, data):
project.writestep(self, "Unpickling")
return cPickle.loads(data)
def __repr__(self):
return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
class PickleAction(PipeAction):
"""
This action pickles the input data into a string.
"""
def __init__(self, protocol=0, input=None):
"""
Create a new PickleAction instance. protocol
is used as the pickle protocol.
"""
PipeAction.__init__(self, input)
self.protocol = protocol
def execute(self, project, data):
project.writestep(self, "Unpickling")
return cPickle.dumps(data, self.protocol)
def __repr__(self):
return "<%s.%s object with protocol=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.protocol, id(self))
class JoinAction(Action):
"""
This action joins the input of all its input actions.
"""
def __init__(self):
Action.__init__(self)
self.inputs = []
def addinputs(self, *inputs):
"""
Register all actions in inputs as input actions, whose data
gets joined (in the order in which they have been passed to addinputs).
"""
self.inputs.extend(inputs)
return self
def __iter__(self):
for input in self.inputs:
yield input
@report
def get(self, project, since, infoonly):
alldata = []
changed = False
for input in self.inputs:
data = input.get(project, since, infoonly)
if data is not nodata:
changed = True
alldata.append(data)
data = nodata
if changed:
if infoonly:
project.writestep(self, "Have new data for join")
data = newdata
else:
for (i, input) in enumerate(self.inputs):
if alldata[i] is nodata: # we didn't get data before, but we need it now
alldata[i] = input.get(project, bigbang, False)
project.writestep(self, "Joining data")
data = "".join(alldata)
return data
class ExternalAction(PipeAction):
"""
ExternalAction is like its baseclass PipeAction
except that execute will be called even if infoonly
is true.
"""
@misc.notimplemented
def execute(self, project):
"""
Will be called to execute the action (even if infoonly is true).
execute doesn't get passed the data object.
"""
@report
def get(self, project, since, infoonly):
data = self.input.get(project, since, infoonly)
if data is not nodata:
self.execute(project)
return data
class MkDirAction(ExternalAction):
"""
This action creates the a directory (passing through its input data).
"""
def __init__(self, key, mode=0777, input=None):
"""
Create a MkDirAction instance. mode (which defaults
to 0777) will be used as the permission bit pattern for the new directory.
"""
PipeAction.__init__(self, input)
self.key = key
self.mode = mode
def execute(self, project):
"""
Create the directory with the permission bits specified in the constructor.
"""
project.writestep(self, "Making directory ", project.strkey(self.key), " with mode ", oct(self.mode))
self.key.makedirs(self.mode)
def __repr__(self):
return "<%s.%s object with mode=0%03o at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.mode, id(self))
class CacheAction(PipeAction):
"""
A CacheAction is a PipeAction that passes
through its input data, but caches it, so that it can
be reused during the same build round.
"""
def __init__(self, input=None):
PipeAction.__init__(self, input)
self.since = bigcrunch
self.data = nodata
self.buildno = None
@report
def get(self, project, since, infoonly):
if self.buildno != project.buildno or (since < self.since and self.data is nodata): # If this is a new build round or we're asked about an earlier date and didn't return data last time
self.data = self.input.get(project, since, False)
self.since = since
self.buildno = project.buildno
if infoonly:
return newdata
elif self.data is not nodata:
if infoonly:
if project.showinfoonly:
project.writestep(self, "New data is cached")
return newdata
project.writestep(self, "Reusing cached data")
return self.data
class PrefixNS(object):
"""
A PrefixNS object stores an &xist; namespace and a prefix
for this namespace used for parsing.
"""
__slots__ = ("prefix", "ns")
def __init__(self, prefix, ns):
self.prefix = prefix
self.ns = ns
class XISTNSPrefixAction(PipeAction):
def __init__(self, prefix=None, input=None):
PipeAction.__init__(self, input)
self.prefix = prefix
def execute(self, project, data):
project.writestep(self, "Adding prefix ", self.prefix)
return PrefixNS(self.prefix, data)
def __repr__(self):
return "<%s.%s object with prefix=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.prefix, id(self))
class GetAttrAction(PipeAction):
"""
This action gets an attribute from its input object.
"""
def __init__(self, attrname, input=None):
PipeAction.__init__(self, input)
self.attrname = attrname
def execute(self, project, data):
project.writestep(self, "Getting attribute ", self.attrname)
return getattr(data, self.attrname)
def __repr__(self):
return "<%s.%s object with attrname=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.attrname, id(self))
class XISTParseAction(PipeAction):
"""
This action parses the input data (a string) into an
&xist; node.
"""
def __init__(self, parser=None, base=None, input=None):
"""
Create an XISTParseAction object. parser must
be an instance of ll.xist.parsers.Parser.
If parser is None a parser will be created for you.
base will be the base &url; used for parsing.
"""
PipeAction.__init__(self, input)
if parser is None:
from ll.xist import parsers
parser = parsers.Parser()
self.parser = parser
self.base = base
self.inputs = []
def addinputs(self, *inputs):
"""
Register all actions in inputs (which must be
XISTNSPrefixAction
or ModuleAction objects)
as namespaces to use for parsing the input data.
"""
self.inputs.extend(inputs)
return self
def __iter__(self):
if self.input is not None:
yield self.input
for input in self.inputs:
yield input
@report
def get(self, project, since, infoonly):
if infoonly:
data = nodata
if self.input.get(project, since, infoonly) is not nodata:
data = newdata
for input in self.inputs:
if input.get(project, since, infoonly) is not nodata:
data = newdata
return data
# We really have to do some work
from ll.xist import xsc
prefixes = xsc.Prefixes()
def addns(input, since):
output = input.get(project, since, False)
if output is not nodata:
if isinstance(output, PrefixNS):
prefixes[output.prefix].insert(0, output.ns)
output = output.ns
elif isinstance(output, type) and issubclass(output, xsc.Namespace):
prefixes[None].insert(0, output)
else:
raise TypeError("need a PrefixNS or namespace; got %r from %r" % (type(output), input))
return output
data = self.input.get(project, since, False)
haschanged = False
if data is not nodata:
haschanged = True
for input in self.inputs:
addns(input, bigbang)
else:
alldata = []
inputsince = since
for input in self.inputs:
output = addns(input, inputsince)
alldata.append(output)
if output is not nodata:
haschanged = True
inputsince = bigbang # force module to be loaded for the rest
if haschanged:
data = self.input.get(project, bigbang, False)
# Fill in the rest of the modules
for (i, input) in enumerate(self.inputs):
output = alldata[i]
if output is nodata:
addns(input, bigbang)
if haschanged:
oldprefixes = self.parser.prefixes
try:
if prefixes:
for (prefix, nss) in oldprefixes.iteritems():
prefixes[prefix] = nss + prefixes[prefix]
self.parser.prefixes = prefixes
project.writestep(self, "Parsing XIST input with base ", self.base)
data = self.parser.parseString(data, self.base)
finally:
self.parser.prefixes = oldprefixes # Restore old prefixes
return data
def __repr__(self):
return "<%s.%s object with base=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.base, id(self))
class XISTConvertAction(PipeAction):
"""
This action transform an &xist; node.
"""
def __init__(self, mode=None, target=None, stage=None, lang=None, targetroot=None, input=None):
"""
Create a new XISTConvertAction object. The arguments will be
used to create a Converter
for each call to execute.
"""
PipeAction.__init__(self, input)
self.mode = mode
self.target = target
self.stage = stage
self.lang = lang
self.targetroot = targetroot
def converter(self, project):
"""
Create a new Converter
object to be used by this action. The attributes of this new converter (mode, target,
stage, etc.) will correspond to those specified in the constructor.
The makeaction attribute of the converter will be set to and
the makeproject attribute will be set to project.
"""
from ll.xist import converters
return converters.Converter(root=self.targetroot, mode=self.mode, stage=self.stage, target=self.target, lang=self.lang, makeaction=self, makeproject=project)
def execute(self, project, data):
"""
Convert the &xist; node data using a converter provided
by converter and
return the converted node.
"""
args = []
for argname in ("mode", "target", "stage", "lang", "targetroot"):
arg = getattr(self, argname, None)
if arg is not None:
args.append("%s=%r" % (argname, arg))
if args:
args = " with %s" % ", ".join(args)
else:
args = ""
project.writestep(self, "Converting XIST node", args)
return data.convert(self.converter(project))
def __repr__(self):
args = []
for argname in ("mode", "target", "stage", "lang", "targetroot"):
arg = getattr(self, argname, None)
if arg is not None:
args.append("%s=%r" % (argname, arg))
if args:
args = " with %s" % ", ".join(args)
else:
args = ""
return "<%s.%s object%s at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, "".join(args), id(self))
class XISTPublishAction(PipeAction):
"""
This action publishes an &xist; node as a string.
"""
def __init__(self, publisher=None, base=None, input=None):
"""
Create an XISTPublishAction object. publisher must
be an instance of ll.xist.publishers.Publisher.
If publisher is None a publisher will be created for you.
base will be the base &url; used for publishing.
"""
PipeAction.__init__(self, input)
if publisher is None:
from ll.xist import publishers
publisher = publishers.Publisher()
self.publisher = publisher
self.base = base
def execute(self, project, data):
"""
Use the publisher specified in the constructor to publish the input &xist;
node data. The output data is the generated &xml; string.
"""
project.writestep(self, "Publishing XIST node with base ", self.base)
return "".join(self.publisher.publish(data, self.base))
def __repr__(self):
return "<%s.%s object with base=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.base, id(self))
class XISTTextAction(PipeAction):
"""
This action creates a plain text version of an &html;
&xist; node.
"""
def __init__(self, encoding="iso-8859-1", width=72):
self.encoding = encoding
self.width = width
def execute(self, project, data):
project.writestep(self, "Converting XIST node to text with encoding=%r, width=%r" % (self.encoding, self.width))
from ll.xist.ns import html
return html.astext(data, encoding=self.encoding, width=self.width)
class FOPAction(PipeAction):
"""
This action transforms an &xml; string (containing XSL-FO) into &pdf;.
For it to work Apache FOP is required. The command line is hardcoded but
it's simple to overwrite the class attribute command in a subclass.
"""
command = "/usr/local/src/fop-0.20.5/fop.sh -q -c /usr/local/src/fop-0.20.5/conf/userconfig.xml -fo %s -pdf %s"
def execute(self, project, data):
project.writestep(self, "FOPping input")
(infd, inname) = tempfile.mkstemp(suffix=".fo")
(outfd, outname) = tempfile.mkstemp(suffix=".pdf")
try:
infile = os.fdopen(infd, "wb")
os.fdopen(outfd).close()
infile.write(data)
infile.close()
os.system(self.command % (inname, outname))
data = open(outname, "rb").read()
finally:
os.remove(inname)
os.remove(outname)
return data
class DecodeAction(PipeAction):
"""
This action decodes an input str object into an output unicode object.
"""
def __init__(self, encoding=None, input=None):
"""
Create a DecodeAction object with encoding as the name of the encoding.
If encoding is None the system default encoding will be used.
"""
PipeAction.__init__(self, input)
if encoding is None:
encoding = sys.getdefaultencoding()
self.encoding = encoding
def execute(self, project, data):
project.writestep(self, "Decoding input with encoding ", self.encoding)
return data.decode(self.encoding)
def __repr__(self):
return "<%s.%s object with encoding=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.encoding, id(self))
class EncodeAction(PipeAction):
"""
This action encodes an input unicode object into an output str object.
"""
def __init__(self, encoding=None, input=None):
"""
Create an EncodeAction object with encoding as the name of the encoding.
If encoding is None the system default encoding will be used.
"""
PipeAction.__init__(self, input)
if encoding is None:
encoding = sys.getdefaultencoding()
self.encoding = encoding
def execute(self, project, data):
project.writestep(self, "Encoding input with encoding ", self.encoding)
return data.encode(self.encoding)
def __repr__(self):
return "<%s.%s object with encoding=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.encoding, id(self))
class TOXICAction(PipeAction):
"""
This action transforms an &xml; string into an Oracle procedure body via
ll.toxic.
"""
def execute(self, project, data):
project.writestep(self, "Toxifying input")
from ll import toxic
return toxic.xml2ora(data)
class TOXICPrettifyAction(PipeAction):
"""
This action tries to fix the indentation of a PL/SQL snippet via
ll.toxic.prettify.
"""
def execute(self, project, data):
project.writestep(self, "Prettifying input")
from ll import toxic
return toxic.prettify(data)
class SplatAction(PipeAction):
"""
This action transforms an input string by replacing regular expressions.
"""
def __init__(self, patterns, input=None):
"""
Create a new SplatAction object. patterns
are pattern pairs. Each first entry will be replaced by the corresponding
second entry.
"""
PipeAction.__init__(self, input)
self.patterns = patterns
def execute(self, project, data):
for (search, replace) in self.patterns:
project.writestep(self, "Replacing ", search, " with ", replace)
data = re.sub(search, replace, data)
return data
class XPITAction(PipeAction):
"""
This action transform an input string via xpit.
"""
def __init__(self, nsinput=None, input=None):
PipeAction.__init__(self, input)
self.nsinput = nsinput
def addnsinput(self, input):
"""
Register input as the namespace action. This action must return
a namespace to be used in evaluating the input string from the normal
input action.
"""
self.nsinput = input
return self
def __iter__(self):
if self.input is not None:
yield self.input
if self.nsinput is not None:
yield self.nsinput
def execute(self, project, data, ns):
from ll import xpit
globals = dict(makeaction=self, makeproject=project)
project.writestep(self, "Converting XPIT input")
return xpit.convert(data, globals, ns)
@report
def get(self, project, since, infoonly):
data = self.input.get(project, since, infoonly)
if data is not nodata:
ns = self.nsinput
if ns is not None:
ns = self.nsinput.get(project, bigbang, infoonly)
if infoonly:
data = newdata
else:
data = self.execute(project, data, ns)
else:
ns = self.nsinput
if ns is not None:
ns = self.nsinput.get(project, since, infoonly)
if ns is not nodata:
if infoonly:
data = newdata
else:
data = self.input.get(project, bigbang, False) # Refetch input data
data = self.execute(project, data, ns)
return data
class CommandAction(ExternalAction):
"""
This action executes a system command (via os.system)
and passes through the input data.
"""
def __init__(self, command, input=None):
"""
Create a new CommandAction object. command is the command
that will executed when execute is called.
"""
PipeAction.__init__(self, input)
self.command = command
def execute(self, project):
project.writestep(self, "Executing command ", self.command)
os.system(self.command)
def __repr__(self):
return "<%s.%s object with command=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.command, id(self))
class ModeAction(ExternalAction):
"""
ModeAction changes file permissions and passes through the input data.
"""
def __init__(self, mode=0644, input=None):
"""
Create an ModeAction object. mode
(which defaults to 0644) will be use as the permission bit pattern.
"""
PipeAction.__init__(self, input)
self.mode = mode
def execute(self, project):
"""
Change the permission bits of the file .getkey().
"""
key = self.getkey()
project.writestep(self, "Changing mode of ", project.strkey(key), " to 0%03o" % self.mode)
key.chmod(self.mode)
class OwnerAction(ExternalAction):
"""
OwnerAction changes the user and/or group ownership of a file and passes through the input data.
"""
def __init__(self, user=None, group=None, input=None):
"""
Create a new OwnerAction object. user can either be a numerical
user id or a user name or None. If it is None no user ownership will
be changed. The same applies to group.
"""
PipeAction.__init__(self, input)
self.id = id
self.user = user
self.group = group
self.mode = mode
def execute(self, project):
"""
Change the ownership of the file .getkey().
"""
key = self.getkey()
project.writestep(self, "Changing owner of ", project.strkey(key), " to ", self.user, " and group to ", self.user)
key.chown(self.user, self.group)
class ModuleAction(PipeAction):
"""
This action will turn the input string into a Python module.
"""
def __init__(self, input=None):
"""
Create an ModuleAction.
This object must have an input action (which might be a FileAction
that creates the source file).
"""
PipeAction.__init__(self, input)
self.inputs = []
self.changed = bigbang
self.data = nodata
self.buildno = None
def addinputs(self, *inputs):
"""
Register all actions in inputs as modules used by this
module. These actions must be ModuleActions too.
Normally it isn't neccessary to call the method explicitely. Instead
fetch the required module inside your module like this:
from ll import make
mymodule = make.currentproject.get("mymodule.py")
This will record your module as depending on mymodule,
so if mymodule changes your module will be reloaded too.
For this to work you need to have an ModuleAction
added to the project with the key "mymodule.py".
"""
self.inputs.extend(inputs)
return self
def __iter__(self):
if self.input is not None:
yield self.input
for input in self.inputs:
yield input
def execute(self, project, data):
key = self.getkey()
project.writestep(self, "Importing module as ", project.strkey(key))
if key is None:
filename = name = ""
elif isinstance(key, url.URL):
try:
filename = key.local()
except ValueError: # is not local
filename = str(key)
name = key.withoutext().file.encode("ascii", "ignore")
else:
filename = name = str(key)
del self.inputs[:] # The module will be reloaded => drop all dependencies (they will be rebuilt during import)
# Normalized line feeds, so that compile() works (normally done by import)
data = "\n".join(data.splitlines())
oldmod = sys.modules.get(name, None) # get any existing module out of the way
mod = types.ModuleType(name)
mod.__file__ = filename
try:
project.importstack.append(self)
sys.modules[name] = mod # create module and make sure it can find itself in sys.modules
code = compile(data, filename, "exec")
exec code in mod.__dict__
mod = sys.modules.pop(name) # refetch the module if it has replaced itself with a custom object
finally:
if oldmod is not None: # put old module back
sys.modules[name] = oldmod
project.importstack.pop(-1)
return mod
@report
def get(self, project, since, infoonly):
# Is this module required by another?
if project.importstack:
if self not in project.importstack[-1].inputs:
project.importstack[-1].addinputs(self) # Append to inputs of other module
# Is this a new build round?
if self.buildno != project.buildno:
data = self.input.get(project, self.changed, False) # Get the source code
if data is not nodata or self.data is nodata: # The file itself has changed or this is the first call
needimport = True
else:
needimport = False
for input in self.inputs:
if input.get(project, self.changed, False) is not nodata:
needimport = True
if needimport:
if data is nodata:
data = self.input.get(project, bigbang, infoonly) # We *really* need the source
self.data = self.execute(project, data) # This will (re)create dependencies
# Timestamp of import is the timestamp of the newest module file
self.changed = filechanged(self.getkey())
if self.inputs:
self.changed = max(self.changed, max(input.changed for input in self.inputs))
self.buildno = project.buildno
if self.changed > since:
if infoonly:
return newdata
return self.data
# Are we newer then the specified date?
elif self.changed > since:
if not infoonly or project.showinfoonly:
key = self.getkey()
project.writestep(self, "Reusing cached module ", project.strkey(key))
if infoonly:
return newdata
return self.data
return nodata
def __repr__(self):
return "<%s.%s object with 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
class AlwaysAction(Action):
"""
This action always returns None as new data.
"""
def __iter__(self):
if False:
yield None
@report
def get(self, project, since, infoonly):
if infoonly:
return newdata
project.writestep(self, "Returning None")
return None
alwaysaction = AlwaysAction() # this action can be reused as it has no inputs
class NeverAction(Action):
"""
This action never returns new data.
"""
def __iter__(self):
if False:
yield None
@report
def get(self, project, since, infoonly):
return nodata
neveraction = NeverAction() # this action can be reused as it has no inputs
###
### Classes for target keys (apart from strings for PhonyActions and URLs for FileActions)
###
class DBKey(object):
"""
This class provides a unique identifier for database content. This
can be used as an key for Action
objects that are not files, but database records, function, procedures etc.
"""
name = None
def __init__(self, connection, type, name, key=None):
"""
Create a new DBKey instance. Arguments are:
connection
- A string that specifies the connection to the database.
E.g. "user/pwd@db.example.com" for Oracle.
type
- The type of the object. Values may be "table",
"view", "function", "procedure" etc.
name
- The name of the object
key
- If name refers to a table, key can be used
to specify a row in this table.
"""
self.connection = connection
self.type = type.lower()
self.name = name.lower()
self.key = key
def __eq__(self, other):
res = self.__class__ == other.__class__
if not res:
res = self.connection==other.connection and self.type==other.type and self.name==other.name and self.key==other.key
return res
def __hash__(self):
return hash(self.connection) ^ hash(self.type) ^ hash(self.name) ^ hash(self.key)
def __repr__(self):
args = []
for attrname in ("connection", "type", "name", "key"):
attrvalue = getattr(self, attrname)
if attrvalue is not None:
args.append("%s=%r" % (attrname, attrvalue))
return "%s(%s)" % (self.__class__.__name__, ", ".join(args))
def __str__(self):
s = "%s:%s|%s:%s" % (self.__class__.name, self.connection, self.type, self.name)
if self.key is not None:
s += "|%s" % (self.key,)
return s
class OracleConnection(url.Connection):
def __init__(self, context, connection):
self.context = context
import cx_Oracle
self.cursor = cx_Oracle.connect(connection).cursor()
def open(self, url, mode="rb"):
return OracleResource(self, url, mode)
def mimetype(self, url):
return "text/x-oracle-%s" % url.type
def cdate(self, url):
# FIXME: This isn't the correct time zone, but Oracle doesn't provide anything else
self.cursor.execute("select created, to_number(to_char(systimestamp, 'TZH')), to_number(to_char(systimestamp, 'TZM')) from user_objects where lower(object_type)=:type and lower(object_name)=:name", type=url.type, name=url.name)
row = self.cursor.fetchone()
if row is None:
raise IOError(errno.ENOENT, "no such %s: %s" % (url.type, url.name))
return row[0]-datetime.timedelta(seconds=60*(row[1]*60+row[2]))
def mdate(self, url):
# FIXME: This isn't the correct time zone, but Oracle doesn't provide anything else
self.cursor.execute("select last_ddl_time, to_number(to_char(systimestamp, 'TZH')), to_number(to_char(systimestamp, 'TZM')) from user_objects where lower(object_type)=:type and lower(object_name)=:name", type=url.type, name=url.name)
row = self.cursor.fetchone()
if row is None:
raise IOError(errno.ENOENT, "no such %s: %s" % (url.type, url.name))
return row[0]-datetime.timedelta(seconds=60*(row[1]*60+row[2]))
def __repr__(self):
return "<%s.%s to %r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.cursor.connection.connectstring(), id(self))
class OracleKey(DBKey):
name = "oracle"
def connect(self, context=None):
context = url.getcontext(context)
if context is url.defaultcontext:
raise ValueError("oracle URLs need a custom context")
# Use one OracleConnection for each connectstring
try:
connections = context.schemes["oracle"]
except KeyError:
connections = context.schemes["oracle"] = {}
try:
connection = connections[self.connection]
except KeyError:
connection = connections[self.connection] = OracleConnection(context, self.connection)
return connection
def __getattr__(self, name):
def realattr(*args, **kwargs):
try:
context = kwargs["context"]
except KeyError:
context = None
else:
kwargs = kwargs.copy()
del kwargs["context"]
connection = self.connect(context=context)
return getattr(connection, name)(self, *args, **kwargs)
return realattr
def mimetype(self):
return "text/x-oracle-%s" % self.type
def open(self, mode="rb", context=None, *args, **kwargs):
connection = self.connect(context=context)
return connection.open(self, mode, *args, **kwargs)
class OracleResource(url.Resource):
"""
An OracleResource wraps a function or procedure in an Oracle
database in a file-like API.
"""
def __init__(self, connection, url, mode="rb"):
self.connection = connection
self.url = url
self.mode = mode
self.closed = False
self.name = str(self.url)
if self.url.type not in ("function", "procedure"):
raise ValueError("don't know how to handle %r" % self.url)
if "w" in self.mode:
self.stream = cStringIO.StringIO()
self.stream.write("create or replace %s %s\n" % (self.url.type, self.url.name))
else:
cursor = self.connection.cursor
cursor.execute("select text from user_source where lower(name)=lower(:name) and type='%s' order by line" % self.url.type.upper(), name=self.url.name)
code = "\n".join((row[0] or "").rstrip() for row in cursor)
if not code:
raise IOError(errno.ENOENT, "no such %s: %s" % (self.url.type, self.url.name))
# drop type
code = code.split(None, 1)[1]
# skip name
for (i, c) in enumerate(code):
if not c.isalpha() and c != "_":
break
code = code[i:]
self.stream = cStringIO.StringIO(code)
def __getattr__(self, name):
if self.closed:
raise ValueError("I/O operation on closed file")
return getattr(self.stream, name)
def mimetype(self):
return "text/x-oracle-%s" % self.url.type
def cdate(self):
return self.connection.cdate(self.url)
def mdate(self):
return self.connection.mdate(self.url)
def close(self):
if not self.closed:
if "w" in self.mode:
c = self._cursor()
c.execute(self.stream.getvalue())
self.stream = None
self.closed = True
###
### Colors for output
###
s4indent = astyle.Style.fromenv("LL_MAKE_REPRANSI_INDENT", "black:black:bold")
s4key = astyle.Style.fromenv("LL_MAKE_REPRANSI_KEY", "green:black")
s4action = astyle.Style.fromenv("LL_MAKE_REPRANSI_ACTION", "yellow:black")
s4time = astyle.Style.fromenv("LL_MAKE_REPRANSI_TIME", "magenta:black")
s4data = astyle.Style.fromenv("LL_MAKE_REPRANSI_DATA", "cyan:black")
s4size = astyle.Style.fromenv("LL_MAKE_REPRANSI_SIZE", "magenta:black")
s4counter = astyle.Style.fromenv("LL_MAKE_REPRANSI_COUNTER", "red:black:bold")
s4error = astyle.Style.fromenv("LL_MAKE_REPRANSI_ERROR", "red:black:bold")
###
### The project class
###
class Project(dict):
"""
A Project collects all Actions
from a project. It is responsible for initiating the build process and for generating a report
about the progress of the build process.
"""
def __init__(self):
super(Project, self).__init__()
self.actionscalled = 0
self.actionsfailed = 0
self.stepsexecuted = 0
self.fileswritten = 0
self.starttime = None
self.ignoreerrors = False
self.here = None # cache the current directory during builds (used for shortening URLs)
self.home = None # cache the home directory during builds (used for shortening URLs)
self.stack = [] # keep track of the recursion during calls to Action.get()
self.importstack = [] # keep track of recursive imports
self.indent = os.environ.get("LL_MAKE_INDENT", " ") # Indentation string to use for output of nested actions
self.buildno = 0 # Build number; This gets incremented on each call to build(). Can be used by actions to determine the start of a new build round
self.showsummary = self._getenvbool("LL_MAKE_SHOWSUMMARY", True)
self.showaction = os.environ.get("LL_MAKE_SHOWACTION", "filephony")
self.showstep = os.environ.get("LL_MAKE_SHOWSTEP", "all")
self.showregistration = os.environ.get("LL_MAKE_SHOWREGISTRATION", "phony")
self.showtime = self._getenvbool("LL_MAKE_SHOWTIME", True)
self.showtimestamps = self._getenvbool("LL_MAKE_SHOWTIMESTAMPS", False)
self.showdata = self._getenvbool("LL_MAKE_SHOWDATA", True)
self.showidle = self._getenvbool("LL_MAKE_SHOWIDLE", False)
self.showinfoonly = self._getenvbool("LL_MAKE_SHOWINFOONLY", False)
def __repr__(self):
return "<%s.%s with %d targets at 0x%x>" % (self.__module__, self.__class__.__name__, len(self), id(self))
class showaction(misc.propclass):
"""
This property specifies which actions should be reported during the
build process. On setting, the value can be:
None or "none"- No actions will be reported;
"file"- Only FileActions will be reported;
"phony"- Only PhonyActions will be reported;
"filephony"- Only FileActions
and PhonyActions will be reported;
a class or tuple of classes- Only actions that are instances of those classes will be reported.
"""
def __get__(self):
return self._showaction
def __set__(self, value):
if value == "none":
self._showaction = None
elif value == "file":
self._showaction = FileAction
elif value == "phony":
self._showaction = PhonyAction
elif value == "filephony":
self._showaction = (PhonyAction, FileAction)
elif value == "all":
self._showaction = Action
else:
self._showaction = value
class showstep(misc.propclass):
"""
This property specifies which for which actions tranformation steps should be reported
during the build process. For allowed values on setting see
showaction.
"""
def __get__(self):
return self._showstep
def __set__(self, value):
if value == "none":
self._showstep = None
elif value == "file":
self._showstep = FileAction
elif value == "phony":
self._showstep = PhonyAction
elif value == "filephony":
self._showstep = (PhonyAction, FileAction)
elif value == "all":
self._showstep = Action
else:
self._showstep = value
class showregistration(misc.propclass):
"""
This property specifies for which actions registration (i.e. call to the
add should be reported.
For allowed values on setting see
showaction.
"""
def __get__(self):
return self._showregistration
def __set__(self, value):
if value == "none":
self._showregistration = None
elif value == "file":
self._showregistration = FileAction
elif value == "phony":
self._showregistration = PhonyAction
elif value == "filephony":
self._showregistration = (PhonyAction, FileAction)
elif value == "all":
self._showregistration = Action
else:
self._showregistration = value
def _getenvbool(self, name, default):
return bool(int(os.environ.get(name, default)))
def strtimedelta(self, delta):
"""
return a nicely formatted and colored string for
the datetime.timedelta value delta. delta
may also be None in with case "0" will be returned.
"""
if delta is None:
text = "0"
else:
rest = delta.seconds
(rest, secs) = divmod(rest, 60)
(rest, mins) = divmod(rest, 60)
rest += delta.days*24
secs += delta.microseconds/1000000.
if rest:
text = "%d:%02d:%06.3fh" % (rest, mins, secs)
elif mins:
text = "%02d:%06.3fm" % (mins, secs)
else:
text = "%.3fs" % secs
return s4time(text)
def strdatetime(self, dt):
"""
return a nicely formatted and colored string for
the datetime.datetime value dt.
"""
return s4time(dt.strftime("%Y-%m-%d %H:%M:%S"), ".%06d" % (dt.microsecond))
def strcounter(self, counter):
"""
return a nicely formatted and colored string for
the counter value counter.
"""
return s4counter("%d." % counter)
def strerror(self, text):
"""
return a nicely formatted and colored string for
the error text text.
"""
return s4error(text)
def strkey(self, key):
"""
return a nicely formatted and colored string for
the action key key.
"""
s = str(key)
if isinstance(key, url.URL) and key.islocal():
if self.here is None:
self.here = url.here()
if self.home is None:
self.home = url.home()
test = str(key.relative(self.here))
if len(test) < len(s):
s = test
test = "~/%s" % key.relative(self.home)
if len(test) < len(s):
s = test
return s4key(s)
def straction(self, action):
"""
return a nicely formatted and colored string for
the action action.
"""
name = action.__class__.__name__
if name.endswith("Action"):
name = name[:-6]
if hasattr(action, "key"):
return s4action(name, "(", self.strkey(action.key), ")")
else:
return s4action(name)
def __setitem__(self, key, target):
"""
Add the action target to as a target and register it
under the key key.
"""
if key in self:
self.warn(RedefinedTargetWarning(key), 5)
if isinstance(key, url.URL) and key.islocal():
key = key.abs(scheme="file")
target.key = key
super(Project, self).__setitem__(key, target)
def add(self, target, key=None):
"""
Add the action target as a target to . If key
is not None, target will be registered under this key
(and target.key will be set to it), otherwise it will
be registered under its own key (i.e. target.key).
"""
if key is None: # Use the key from the target
key = target.getkey()
self[key] = target
self.stepsexecuted += 1
if self.showregistration is not None and isinstance(target, self.showregistration):
self.writestacklevel(0, self.strcounter(self.stepsexecuted), " Registered ", self.strkey(target.key))
return target
def _candidates(self, key):
"""
Return candidates for alternative forms of key.
This is a generator, so when the first suitable candidate is found,
the rest of the candidates won't have to be created at all.
"""
yield key
key2 = key
if isinstance(key, basestring):
key2 = url.URL(key)
yield key2
if isinstance(key2, url.URL):
key2 = key2.abs(scheme="file")
yield key2
key2 = key2.real(scheme="file")
yield key2
if isinstance(key, basestring) and ":" in key:
(prefix, rest) = key.split(":", 1)
if prefix == "oracle":
if "|" in rest:
(connection, rest) = rest.split("|", 1)
if ":" in rest:
(type, name) = rest.split(":", 1)
if "|" in rest:
(name, key) = rest.split("|")
else:
key = None
yield OracleKey(connection, type, name, key)
def __getitem__(self, key):
"""
return the target with the key key.
If an key can't be found, it will be wrapped in a
URL instance and retried.
If key still can't be found a UndefinedTargetError
will be raised.
"""
sup = super(Project, self)
for key2 in self._candidates(key):
try:
return sup.__getitem__(key2)
except KeyError:
pass
raise UndefinedTargetError(key)
def has_key(self, key):
"""
Return whether the target with the key key exists in the project.
"""
return key in self
def __contains__(self, key):
"""
Return whether the target with the key key exists in the project.
"""
sup = super(Project, self)
for key2 in self._candidates(key):
has = sup.has_key(key2)
if has:
return True
return False
def create(self):
"""
Create all dependencies for the project. Overwrite in subclasses.
This method should only be called once, otherwise you'll get lots of
RedefinedTargetWarnings.
But you can call clear to
remove all targets before calling create. You can also
use the method recreate for that.
"""
self.stepsexecuted = 0
self.starttime = datetime.datetime.utcnow()
self.writeln("Creating targets...")
def recreate(self):
"""
Calls destroy and
create to recreate
all project dependencies.
"""
self.clear()
self.create()
def optionparser(self):
"""
Return an optparse parser for parsing the command line options.
This can be overwritten in subclasses to add more options.
"""
p = optparse.OptionParser(usage="usage: %prog [options] [targets]", version="%%prog %s" % __version__)
p.add_option("-x", "--ignore", dest="ignoreerrors", help="Ignore errors", action="store_true", default=None)
p.add_option("-X", "--noignore", dest="ignoreerrors", help="Don't ignore errors", action="store_false", default=None)
p.add_option("-c", "--color", dest="color", help="Use colored output", action="store_true", default=None)
p.add_option("-C", "--nocolor", dest="color", help="No colored output", action="store_false", default=None)
p.add_option("-a", "--showaction", dest="showaction", help="Show actions?", choices=["all", "file", "filephony", "none"], default="filephony")
p.add_option("-s", "--showstep", dest="showstep", help="Show steps?", choices=["all", "file", "filephony", "none"], default="all")
p.add_option("-i", "--showidle", dest="showidle", help="Show idle actions?", action="store_true", default=False)
p.add_option( "--showinfoonly", dest="showinfoonly", help="Show info only actions?", action="store_true", default=False)
return p
def parseoptions(self, commandline=None):
"""
Use the parser returned by optionparser
to parse the option sequence commandline, modify accordingly and return
the result of optparses parse_args call.
"""
p = self.optionparser()
(options, args) = p.parse_args(commandline)
if options.ignoreerrors is not None:
self.ignoreerrors = options.ignoreerrors
if options.color is not None:
self.color = options.color
if options.showaction is not None:
self.showaction = options.showaction
if options.showstep is not None:
self.showstep = options.showstep
self.showidle = options.showidle
self.showinfoonly = options.showinfoonly
return (options, args)
def _get(self, target, since, infoonly):
"""
target must be an action registered in (or the id of one).
For this target the get
will be called with since and infoonly as the
arguments.
"""
global currentproject
if not isinstance(target, Action):
target = self[target]
oldproject = currentproject
try:
currentproject = self
data = target.get(self, since, infoonly)
finally:
currentproject = oldproject
return data
def get(self, target):
"""
Get uptodate output data from the target target (which must
be an action registered with (or the id of one). During the call
the global variable currentproject will be set to .
"""
return self._get(target, bigbang, False)
def build(self, *targets):
"""
Rebuild all targets in targets. Items in targets must
be actions registered with (or their ids).
"""
global currentproject
self.starttime = datetime.datetime.utcnow()
context = url.Context()
try:
# Use the context manager in a Python 2.4 compatible way.
context.__enter__()
self.stack = []
self.importstack = []
self.actionscalled = 0
self.actionsfailed = 0
self.stepsexecuted = 0
self.fileswritten = 0
self.buildno += 1 # increment build number so that actions that stored the old one can detect a new build round
for target in targets:
data = self._get(target, bigcrunch, True)
now = datetime.datetime.utcnow()
if self.showsummary:
args = []
self.write(
"built ",
s4action(self.__class__.__module__, ".", self.__class__.__name__),
": ",
s4data(str(len(self))),
" registered targets; ",
s4data(str(self.actionscalled)),
" actions called; ",
s4data(str(self.stepsexecuted)),
" steps executed; ",
s4data(str(self.fileswritten)),
" files written; ",
s4data(str(self.actionsfailed)),
" actions failed",
)
if self.showtime and self.starttime is not None:
self.write(" [t+", self.strtimedelta(now-self.starttime), "]")
self.writeln()
finally:
context.__exit__(None, None, None)
def buildwithargs(self, commandline=None):
"""
For calling make scripts from the command line.
commandline defaults to sys.argv[1:]. Any positional
arguments in the command line will be treated as target ids. If there are
no possitional arguments, a list of all registered PhonyAction
objects will be output.
"""
if not commandline:
commandline = sys.argv[1:]
(options, args) = self.parseoptions(commandline)
if args:
self.build(*args)
else:
self.writeln("Available phony targets are:")
self.writephonytargets()
def write(self, *texts):
"""
All screen output is done through this method. This makes it possible to redirect
the output (e.g. to logfiles) in subclasses.
"""
astyle.stderr.write(*texts)
def writeln(self, *texts):
"""
All screen output is done through this method. This makes it possible to redirect
the output (e.g. to logfiles) in subclasses.
"""
astyle.stderr.writeln(*texts)
astyle.stderr.flush()
def writeerror(self, *texts):
"""
Output an error.
"""
self.write(*texts)
def warn(self, warning, stacklevel):
"""
Issue a warning through the Python warnings framework
"""
warnings.warn(warning, stacklevel=stacklevel)
def writestacklevel(self, level, *texts):
"""
Output texts indented level levels.
"""
self.write(s4indent(level*self.indent), *texts)
if self.showtime and self.starttime is not None:
self.write(" [t+", self.strtimedelta(datetime.datetime.utcnow() - self.starttime), "]")
self.writeln()
def writestack(self, *texts):
"""
Output texts indented properly for the current nesting of
action execution.
"""
self.writestacklevel(len(self.stack), *texts)
def _writependinglevels(self):
for (i, level) in enumerate(self.stack):
if not level.reported:
args = ["Started ", self.straction(level.action)]
if self.showtimestamps:
args.append(" since ")
args.append(self.strdatetime(level.since))
if level.infoonly:
args.append(" (info only)")
self.writestacklevel(i, *args)
level.reported = True
def writestep(self, action, *texts):
"""
Output texts as the description of the data transformation
done by the action arction.
"""
self.stepsexecuted += 1
if self.showstep is not None and isinstance(action, self.showstep):
if not self.showidle:
self._writependinglevels()
self.writestack(self.strcounter(self.stepsexecuted), " ", *texts)
def writecreatedone(self):
"""
Can be called at the end of an overwritten create to
report the number of registered targets.
"""
self.writestacklevel(0, "done: ", s4data(str(len(self))), " registered targets")
def writephonytargets(self):
"""
Show a list of all PhonyAction
objects in the project and their documentation.
"""
phonies = []
maxlen = 0
for key in self:
if isinstance(key, basestring):
maxlen = max(maxlen, len(key))
phonies.append(self[key])
phonies.sort(key=operator.attrgetter("key"))
for phony in phonies:
text = astyle.Text(self.straction(phony))
if phony.doc:
text.append(" ", s4indent("."*(maxlen+3-len(phony.key))), " ", phony.doc)
self.writeln(text)
def findpaths(self, target, source):
"""
Find dependency paths leading from target to source.
target and source may be actions
or the ids of registered actions. For more info see
Action.findpaths.
"""
if not isinstance(target, Action):
target = self[target]
if not isinstance(source, Action):
source = self[source]
return target.findpaths(source)
# This will be set to the project in build() and get()
currentproject = None