#!/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 classesOnly 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