root/livinglogic.python.xist/src/ll/make.py @ 3478:b55670e2368e

Revision 3478:b55670e2368e, 70.3 KB (checked in by Walter Doerwald <walter@…>, 11 years ago)

Make the database type configurable in TOXICAction and TOXICPrettifyAction.

Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4## Copyright 2002-2008 by LivingLogic AG, Bayreuth/Germany.
5## Copyright 2002-2008 by Walter Dörwald
6##
7## All Rights Reserved
8##
9## See ll/__init__.py for the license
10
11
12"""
13:mod:`ll.make` provides tools for building projects.
14
15Like make it allows you to specify dependencies between files and actions to be
16executed when files don't exist or are out of date with respect to one of their
17sources. But unlike make you can do this in an object oriented way and targets
18are not only limited to files.
19
20Relevant classes are:
21
22*   :class:`Project`, which is the container for all actions in a project and
23
24*   :class:`Action` (and subclasses), which are used to transform input data
25    and read and write files (or other entities like database records).
26
27A simple script that copies a file :file:`foo.txt` to :file:`bar.txt`
28reencoding it from ``"latin-1"`` to ``"utf-8"`` in the process looks like
29this::
30
31    from ll import make, url
32
33    class MyProject(make.Project):
34        def create(self):
35            make.Project.create(self)
36            source = self.add(make.FileAction(url.File("foo.txt")))
37            target = self.add(
38                source /
39                make.DecodeAction("iso-8859-1") /
40                make.EncodeAction("utf-8") /
41                make.FileAction(url.File("bar.txt"))
42            )
43            self.writecreatedone()
44
45    p = MyProject()
46    p.create()
47
48    if __name__ == "__main__":
49        p.build("bar.txt")
50"""
51
52
53from __future__ import with_statement
54
55import sys, os, os.path, optparse, warnings, re, datetime, cStringIO, errno, tempfile, operator, types, cPickle, gc, contextlib
56
57from ll import misc, url
58
59try:
60    import astyle
61except ImportError:
62    from ll import astyle
63
64
65__docformat__ = "reStructuredText"
66
67
68###
69### Constants and helpers
70###
71
72nodata = misc.Const("nodata") # marker object for "no new data available"
73
74bigbang = datetime.datetime(1900, 1, 1) # there can be no timestamp before this one
75bigcrunch = datetime.datetime(3000, 1, 1) # there can be no timestamp after this one
76
77
78def filechanged(key):
79    """
80    Get the last modified date (or :const:`bigbang`, if the file doesn't exist).
81    """
82    try:
83        return key.mdate()
84    except (IOError, OSError):
85        return bigbang
86
87
88class Level(object):
89    """
90    Stores information about the recursive execution of :class:`Action`\s.
91    """
92    __slots__ = ("action", "since", "reportable", "reported")
93
94    def __init__(self, action, since, reportable, reported=False):
95        self.action = action
96        self.since = since
97        self.reportable = reportable
98        self.reported = reported
99
100    def __repr__(self):
101        return "<%s.%s object action=%r since=%r reportable=%r reported=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.action, self.since, self.reportable, self.reported, id(self))
102
103
104def report(func):
105    """
106    Standard decorator for :meth:`Action.get` methods.
107
108    This decorator handles proper reporting of nested action calls. If it isn't
109    used, only the output of calls to :meth:`Project.writestep` will be visible
110    to the user.
111    """
112    def reporter(self, project, since):
113        reported = False
114        reportable = project.showaction is not None and isinstance(self, project.showaction)
115        if reportable:
116            if project.showidle:
117                args = ["Starting ", project.straction(self)]
118                if project.showtimestamps:
119                    args.append(" since ")
120                    args.append(project.strdatetime(since))
121                project.writestack(*args)
122                reported = True
123        level = Level(self, since, reportable, reported)
124        project.stack.append(level)
125        t1 = datetime.datetime.utcnow()
126        try:
127            data = func(self, project, since)
128        except Exception, exc:
129            project.actionsfailed += 1
130            if project.ignoreerrors: # ignore changes in failed subgraphs
131                data = nodata # Return "everything is up to date" in this case
132                error = exc.__class__
133            else:
134                raise
135        else:
136            project.actionscalled += 1
137            error = None
138        finally:
139            project.stack.pop(-1)
140        t2 = datetime.datetime.utcnow()
141        if level.reportable or error is not None:
142            if (not project.showidle and data is not nodata) or error is not None:
143                project._writependinglevels() # Only outputs something if the action hasn't called writestep()
144            if level.reported:
145                if error is not None:
146                    text1 = "Failed"
147                    text2 = " after "
148                else:
149                    text1 = "Finished"
150                    text2 = " in "
151                args = [text1, " ", project.straction(self)]
152                if project.showtime:
153                    args.append(text2)
154                    args.append(project.strtimedelta(t2-t1))
155                if project.showtimestamps:
156                    args.append(" (changed ")
157                    args.append(project.strdatetime(self.changed))
158                    args.append(")")
159                if project.showdata:
160                    args.append(": ")
161                    if error is not None:
162                        if error.__module__ != "exceptions":
163                            text = "%s.%s" % (error.__module__, error.__name__)
164                        else:
165                            text = error.__name__
166                        args.append(s4error(text))
167                    elif data is nodata:
168                        args.append("nodata")
169                    elif isinstance(data, str):
170                        args.append(s4data("str (%db)" % len(data)))
171                    elif isinstance(data, unicode):
172                        args.append(s4data("unicode (%dc)" % len(data)))
173                    else:
174                        dataclass = data.__class__
175                        if dataclass.__module__ != "__builtin__":
176                            text = "%s.%s @ 0x%x" % (dataclass.__module__, dataclass.__name__, id(data))
177                        else:
178                            text = "%s @ 0x%x" % (dataclass.__name__, id(data))
179                        args.append(s4data(text))
180                project.writestack(*args)
181        return data
182    reporter.__dict__.update(func.__dict__)
183    reporter.__doc__ = func.__doc__
184    reporter.__name__ = func.__name__
185    return reporter
186
187
188###
189### exceptions & warnings
190###
191
192class RedefinedTargetWarning(Warning):
193    """
194    Warning that will be issued when a target is added to a project and a target
195    with the same key already exists.
196    """
197
198    def __init__(self, key):
199        self.key = key
200
201    def __str__(self):
202        return "target with key=%r redefined" % self.key
203
204
205class UndefinedTargetError(KeyError):
206    """
207    Exception that will be raised when a target with the specified key doesn't
208    exist within the project.
209    """
210
211    def __init__(self, key):
212        self.key = key
213
214    def __str__(self):
215        return "target %r undefined" % self.key
216
217
218###
219### Actions
220###
221
222def getoutputs(project, since, input):
223    """
224    Recursively iterate through the object :var:`input` (if it's a
225    :class:`tuple`, :class:`list` or :class:`dict`) and return a tuple
226    containing:
227
228    *   An object (:var:`data`) of the same structure as :var:`input` where every
229        action object encountered is replacd with the output of that action;
230
231    *   A timestamp (:var:`changed`) which the newest timestamp among all the
232        change timestamps of the actions encountered.
233
234    *   If none of the actions has any data newer than :var:`since` (i.e. none
235        of the actions produces any new data) :var:`data` will be :const:`nodata`.
236    """
237    if isinstance(input, Action):
238        return (input.get(project, since), input.changed)
239    elif isinstance(input, (list, tuple)):
240        resultdata = []
241        havedata = False
242        resultchanged = bigbang
243        for item in input:
244            (data, changed) = getoutputs(project, since, item)
245            resultchanged = max(resultchanged, changed)
246            if data is not nodata and not havedata: # The first real output
247                since = bigbang # force inputs to produce data for the rest of the loop
248                resultdata = [getoutputs(project, since, item)[0] for item in input[:len(resultdata)]] # refetch data from previous inputs
249                havedata = True
250            resultdata.append(data)
251        if since is bigbang and not input:
252            resultdata = input.__class__()
253        elif not havedata:
254            resultdata = nodata
255        elif isinstance(input, tuple):
256            resultdata = tuple(resultdata)
257        return (resultdata, resultchanged)
258    elif isinstance(input, dict):
259        resultdata = {}
260        havedata = False
261        resultchanged = bigbang
262        for (key, value) in input.iteritems():
263            (data, changed) = getoutputs(project, since, value)
264            resultchanged = max(resultchanged, changed)
265            if data is not nodata and not havedata: # The first real output
266                since = bigbang # force inputs to produce data for the rest of the loop
267                resultdata = dict((key, getoutputs(project, since, input[key])) for key in resultdata) # refetch data from previous inputs
268                havedata = True
269            resultdata[key] = data
270        if since is bigbang and not input:
271            resultdata = {}
272        elif not havedata:
273            resultdata = nodata
274        return (resultdata, resultchanged)
275    else:
276        return (input if since is bigbang else nodata, bigbang)
277
278
279def _ipipe_type(obj):
280    try:
281        return obj.type
282    except AttributeError:
283        return "%s.%s" % (obj.__class__.__module__, obj.__class__.__name__)
284_ipipe_type.__xname__ = "type"
285
286
287def _ipipe_key(obj):
288    return obj.getkey()
289_ipipe_key.__xname__ = "key"
290
291
292class Action(object):
293    """
294    An :class:`Action` is responsible for transforming input data into output
295    data. It may have no, one or many input actions. It fetches, combines and
296    transforms the output data of those actions and returns its own output data.
297    """
298
299    def __init__(self):
300        """
301        Create a new :class:`Action` instance.
302        """
303        self.changed = bigbang
304
305    def __div__(self, output):
306        return output.__rdiv__(self)
307
308    @misc.notimplemented
309    def get(self, project, since):
310        """
311        This method (i.e. the implementations in subclasses) is the workhorse of
312        :mod:`ll.make`. :meth:`get` must return the output data of the action if
313        this data has changed since :var:`since` (which is a
314        :class:`datetime.datetime` object in UTC). If the data hasn't changed
315        since :var:`since` the special object :const:`nodata` must be returned.
316       
317        In both cases the action must make sure that the data is internally
318        consistent, i.e. if the input data is the output data of other actions
319        :var:`self` has to ensure that those other actions update their data too,
320        independent from the fact whether :meth:`get` will return new data or not.
321
322        Two special values can be passed for :var:`since`:
323
324        :const:`bigbang`
325            This timestamp is older than any timestamp that can appear in real
326            life. Since all data is newer than this, :meth:`get` must always
327            return output data.
328
329        :const:`bigcrunch`
330            This timestamp is newer than any timestamp that can appear in real
331            life. Since there can be no data newer than this, :meth:`get` can
332            only return output data in this case if ensuring internal consistency
333            resulted in new data.
334
335        In all cases :meth:`get` must set the instance attribute :attr:`changed`
336        to the timestamp of the last change to the data before returning. In most
337        cases this if the newest :attr:`changed` timestamp of the input actions.
338        """
339
340    def getkey(self):
341        """
342        Get the nearest key from :var:`self` or its inputs. This is used by
343        :class:`ModuleAction` for the filename.
344        """
345        return getattr(self, "key", None)
346
347    @misc.notimplemented
348    def __iter__(self):
349        """
350        Return an iterator over the input actions of :var:`self`.
351        """
352
353    def iterallinputs(self):
354        """
355        Return an iterator over all input actions of :var:`self`
356        (i.e. recursively).
357        """
358        for input in self:
359            yield input
360            for subinput in input.iterallinputs():
361                yield subinput
362
363    def findpaths(self, input):
364        """
365        Find dependency paths leading from :var:`self` to the other action
366        :var:`input`. I.e. if :var:`self` depends directly or indirectly on
367        :var:`input`, this generator will produce all paths ``p`` where
368        ``p[0] is self`` and ``p[-1] is input`` and ``p[i+1] in p[i]`` for all
369        ``i`` in ``xrange(len(p)-1)``.
370        """
371        if input is self:
372            yield [self]
373        else:
374            for myinput in self:
375                for path in myinput.findpaths(input):
376                    yield [self] + path
377
378    def __xattrs__(self, mode="default"):
379        if mode == "default":
380            return (_ipipe_type, _ipipe_key)
381        return dir(self)
382
383    def __xrepr__(self, mode="default"):
384        if mode in ("cell", "default"):
385            name = self.__class__.__name__
386            if name.endswith("Action"):
387                name = name[:-6]
388            yield (s4action, name)
389            if hasattr(self, "key"):
390                yield (astyle.style_default, "(")
391                key = self.key
392                if isinstance(key, url.URL) and key.islocal():
393                    here = url.here()
394                    home = url.home()
395                    s = str(key)
396                    test = str(key.relative(here))
397                    if len(test) < len(s):
398                        s = test
399                    test = "~/%s" % key.relative(home)
400                    if len(test) < len(s):
401                        s = test
402                else:
403                    s = str(key)
404                yield (s4key, s)
405                yield (astyle.style_default, ")")
406        else:
407            yield (astyle.style_default, repr(self))
408
409
410class PipeAction(Action):
411    """
412    A :class:`PipeAction` depends on exactly one input action and transforms
413    the input data into output data.
414    """
415    def __init__(self, input=None):
416        Action.__init__(self)
417        self.input = input
418
419    def __rdiv__(self, input):
420        """
421        Register the action :var:`input` as the input action for :var:`self` and
422        return :var:`self` (which enables "chaining" :class:`PipeAction` objects).
423        """
424        self.input = input
425        return self
426
427    def getkey(self):
428        return self.input.getkey()
429
430    def __iter__(self):
431        yield self.input
432
433    @misc.notimplemented
434    def execute(self, project, data):
435        """
436        Execute the action: transform the input data :var:`data` and return
437        the resulting output data. This method must be implemented in subclasses.
438        """
439
440    @report
441    def get(self, project, since):
442        (data, self.changed) = getoutputs(project, since, self.input)
443        if data is not nodata:
444            data = self.execute(project, data)
445        return data
446
447
448class CollectAction(PipeAction):
449    """
450    A :class:`CollectAction` is a :class:`PipeAction` that simply outputs its
451    input data unmodified, but updates a number of other actions in the process.
452    """
453    def __init__(self, input=None, *otherinputs):
454        PipeAction.__init__(self, input)
455        self.otherinputs = list(otherinputs)
456
457    def addinputs(self, *otherinputs):
458        """
459        Register all actions in :var:`otherinputs` as additional actions that have
460        to be updated before :var:`self` is updated.
461        """
462        self.otherinputs.extend(otherinputs)
463        return self
464
465    def __iter__(self):
466        yield self.input
467        for input in self.otherinputs:
468            yield input
469
470    @report
471    def get(self, project, since):
472        # We don't need the data itself, so don't use getoutputs(), which would collect all inputs in a list.
473        havedata = False
474        changedinputs = bigbang
475        for item in self.otherinputs:
476            (data, changed) = getoutputs(project, since, item)
477            changedinputs = max(changedinputs, changed)
478            if data is not nodata: # The first real output
479                havedata = True
480        if havedata:
481            since = bigbang
482        (data, changedinput) = getoutputs(project, since, self.input)
483        self.changed = max(changedinputs, changedinput)
484        return data
485
486    def __repr__(self):
487        return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
488
489
490class PhonyAction(Action):
491    """
492    A :class:`PhonyAction` doesn't do anything. It may depend on any number of
493    additonal input actions which will be updated when this action gets updated.
494    If there's new data from any of these actions, a :class:`PhonyAction` will
495    return :const:`None` (and :const:`nodata` otherwise as usual).
496    """
497    def __init__(self, doc=None):
498        """
499        Create a :class:`PhonyAction` object. :var:`doc` describes the action and
500        is printed by the method :meth:`Project.writephonytargets`.
501        """
502        Action.__init__(self)
503        self.doc = doc
504        self.inputs = []
505        self.buildno = None
506
507    def addinputs(self, *inputs):
508        """
509        Register all actions in :var:`inputs` as additional actions that have to
510        be updated once :var:`self` is updated.
511        """
512        self.inputs.extend(inputs)
513        return self
514
515    def __iter__(self):
516        return iter(self.inputs)
517
518    @report
519    def get(self, project, since):
520        # Caching the result object of a :class:`PhonyAction` is cheap (it's either :const:`None` or :const:`nodata`),
521        # so we always do the caching as this optimizes away the traversal of a complete subgraph
522        # for subsequent calls to :meth:`get` during the same build round
523        if self.buildno != project.buildno:
524            havedata = False
525            resultchanged = bigbang
526            # We don't need the data itself, so don't use getoutputs(), which would collect all inputs in a list.
527            for item in self.inputs:
528                (data, changed) = getoutputs(project, since, item)
529                resultchanged = max(resultchanged, changed)
530                if data is not nodata: # The first real output
531                    havedata = True
532            self.buildno = project.buildno
533            self.changed = resultchanged
534            return None if havedata else nodata
535        else:
536            return None if self.changed > since else nodata
537
538    def __repr__(self):
539        s = "<%s.%s object" % (self.__class__.__module__, self.__class__.__name__)
540        if hasattr(self, "key"):
541            s += " with key=%r" % self.key
542        s += " at 0x%x>" % id(self)
543        return s
544
545
546class FileAction(PipeAction):
547    """
548    A :class:`FileAction` is used for reading and writing files (and other
549    objects providing the appropriate interface).
550    """
551    def __init__(self, input=None, key=None):
552        """
553        Create a :class:`FileAction` object with :var:`key` as the "filename".
554        :var:`key` must be an object that provides a method :meth:`open` for
555        opening readable and writable streams to the file. :var:`input` is the
556        data written to the file (or the action producing the data).
557       
558        """
559        PipeAction.__init__(self, input)
560        self.key = key
561        self.buildno = None
562
563    def getkey(self):
564        return self.key
565
566    def write(self, project, data):
567        """
568        Write :var:`data` to the file and return it.
569        """
570        project.writestep(self, "Writing data to ", project.strkey(self.key))
571        with contextlib.closing(self.key.open("wb")) as file:
572            file.write(data)
573            project.fileswritten += 1
574
575    def read(self, project):
576        """
577        Read the content from the file and return it.
578        """
579        project.writestep(self, "Reading data from ", project.strkey(self.key))
580        with contextlib.closing(self.key.open("rb")) as file:
581            return file.read()
582
583    @report
584    def get(self, project, since):
585        """
586        If a :class:`FileAction` object doesn't have an input action it reads the
587        input file and returns the content if the file has changed since
588        :var:`since` (otherwise :const:`nodata` is returned).
589
590        If a :class:`FileAction` object does have an input action and the output
591        data from this input action is newer than the file ``self.key`` the data
592        will be written to the file. Otherwise (i.e. the file is up to date) the
593        data will be read from the file.
594        """
595        if self.buildno != project.buildno: # a new build round
596            self.changed = filechanged(self.key) # Get timestamp of the file (or :const:`bigbang` if it doesn't exist)
597            self.buildno = project.buildno
598
599        if self.input is not None:
600            (data, self.changed) = getoutputs(project, self.changed, self.input)
601            if data is not nodata: # We've got new data from our input =>
602                self.write(project, data) # write new data to disk
603                self.changed = filechanged(self.key) # update timestamp
604                return data
605        else: # We have no inputs (i.e. this is a "source" file)
606            if self.changed is bigbang:
607                raise ValueError("source file %r doesn't exist" % self.key)
608        if self.changed > since: # We are up to date now and newer than the output action
609            return self.read(project) # return file data (to output action or client)
610        # else fail through and return :const:`nodata`
611        return nodata
612
613    def __repr__(self):
614        return "<%s.%s object key=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.key, id(self))
615
616
617class UnpickleAction(PipeAction):
618    """
619    This action unpickles a string.
620    """
621    def execute(self, project, data):
622        project.writestep(self, "Unpickling")
623        return cPickle.loads(data)
624
625    def __repr__(self):
626        return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
627
628
629class PickleAction(PipeAction):
630    """
631    This action pickles the input data into a string.
632    """
633    def __init__(self, input=None, protocol=0):
634        """
635        Create a new :class:`PickleAction` instance. :var:`protocol` is used as
636        the pickle protocol.
637        """
638        PipeAction.__init__(self, input)
639        self.protocol = protocol
640
641    @report
642    def get(self, project, since):
643        (data, self.changed) = getoutputs(project, since, (self.input, self.protocol))
644        if data is not nodata:
645            project.writestep(self, "Pickling with protocol %r" % data[1])
646            data = cPickle.dumps(data[0], data[1])
647        return data
648
649    def __repr__(self):
650        if isinstance(self.protocol, Action):
651            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
652        else:
653            return "<%s.%s object with protocol=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.protocol, id(self))
654
655
656class JoinAction(Action):
657    """
658    This action joins the input of all its input actions into one string.
659    """
660    def __init__(self, *inputs):
661        Action.__init__(self)
662        self.inputs = list(inputs)
663
664    def addinputs(self, *inputs):
665        """
666        Register all actions in :var:`inputs` as input actions, whose data gets
667        joined (in the order in which they have been passed to :meth:`addinputs`).
668        """
669        self.inputs.extend(inputs)
670        return self
671
672    def __iter__(self):
673        return iter(self.inputs)
674
675    @report
676    def get(self, project, since):
677        (data, self.changed) = getoutputs(project, since, self.inputs)
678        if data is not nodata:
679            project.writestep(self, "Joining data from %d inputs" % len(data))
680            data = "".join(data)
681        return data
682
683
684class MkDirAction(PipeAction):
685    """
686    This action creates the a directory (passing through its input data).
687    """
688
689    def __init__(self, key, mode=0777):
690        """
691        Create a :class:`MkDirAction` instance. :var:`mode` (which defaults to
692        :const:`0777`) will be used as the permission bit pattern for the new
693        directory.
694        """
695        PipeAction.__init__(self)
696        self.key = key
697        self.mode = mode
698
699    def execute(self, project, data):
700        """
701        Create the directory with the permission bits specified in the
702        constructor.
703        """
704        project.writestep(self, "Making directory ", project.strkey(self.key), " with mode ", oct(self.mode))
705        self.key.makedirs(self.mode)
706
707    def __repr__(self):
708        return "<%s.%s object with mode=0%03o at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.mode, id(self))
709
710
711class CacheAction(PipeAction):
712    """
713    A :class:`CacheAction` is a :class:`PipeAction` that passes through its
714    input data, but caches it, so that it can be reused during the same build
715    round.
716    """
717    def __init__(self, input=None):
718        PipeAction.__init__(self, input)
719        self.since = bigcrunch
720        self.data = nodata
721        self.buildno = None
722
723    @report
724    def get(self, project, since):
725        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
726            (self.data, self.changed) = getoutputs(project, since, self.input)
727            self.since = since
728            self.buildno = project.buildno
729        elif self.data is not nodata:
730            project.writenote(self, "Reusing cached data")
731        return self.data
732
733
734class GetAttrAction(PipeAction):
735    """
736    This action gets an attribute from its input object.
737    """
738
739    def __init__(self, input=None, attrname=None):
740        PipeAction.__init__(self, input)
741        self.attrname = attrname
742
743    @report
744    def get(self, project, since):
745        (data, self.changed) = getoutputs(project, since, (self.input, self.attrname))
746        if data is not nodata:
747            project.writestep(self, "Getting attribute ", self.attrname)
748            data = getattr(data[0], data[1])
749        return data
750
751    def __repr__(self):
752        if isinstance(self.attrname, Action):
753            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
754        else:
755            return "<%s.%s object with attrname=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.attrname, id(self))
756
757
758class PoolAction(Action):
759    """
760    This action collect all its input data into a :class:`ll.misc.Pool` object.
761    """
762
763    def __init__(self, *inputs):
764        """
765        Create an :class:`PoolAction` object. Arguments in :var:`inputs` must be
766        :class:`ImportAction` or :class:`ModuleAction` objects.
767        """
768        Action.__init__(self)
769        self.inputs = list(inputs)
770
771    def addinputs(self, *inputs):
772        """
773        Registers additional inputs.
774        """
775        self.inputs.extend(inputs)
776        return self
777
778    def __iter__(self):
779        return iter(self.inputs)
780
781    def _getpool(self, *data):
782        return misc.Pool(*data)
783
784    @report
785    def get(self, project, since):
786        (data, self.changed) = getoutputs(project, since, self.inputs)
787
788        if data is not nodata:
789            data = self._getpool(*data)
790            project.writestep(self, "Created ", data.__class__.__module__, ".", data.__class__.__name__," object")
791        return data
792
793    def __repr__(self):
794        return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
795
796
797class XISTPoolAction(PoolAction):
798    """
799    This action collect all its input data into an :class:`ll.xist.xsc.Pool`
800    object.
801    """
802
803    def _getpool(self, *data):
804        from ll.xist import xsc
805        return xsc.Pool(*data)
806
807
808class XISTParseAction(PipeAction):
809    """
810    This action parses the input data (a string) into an XIST node.
811    """
812
813    def __init__(self, input=None, builder=None, pool=None, base=None):
814        """
815        Create an :class:`XISTParseAction` object. :var:`builder` must be an
816        instance of :class:`ll.xist.parsers.Builder`. If :var:`builder` is
817        :const:`None` a builder will be created for you. :var:`pool` must be an
818        XIST pool object (or an action returning one). :var:`base` will be the
819        base URL used for parsing.
820        """
821        PipeAction.__init__(self, input)
822        if builder is None:
823            from ll.xist import parsers
824            builder = parsers.Builder()
825        self.builder = builder
826        self.pool = pool
827        self.base = base
828
829    def __iter__(self):
830        yield self.pool
831        yield self.input
832
833    @report
834    def get(self, project, since):
835        (data, self.changed) = getoutputs(project, since, (self.input, self.builder, self.pool, self.base))
836
837        if data is not nodata:
838            # We really have to do some work
839            from ll.xist import xsc
840            (data, builder, pool, base) = data
841            oldpool = builder.pool
842            try:
843                builder.pool = xsc.Pool(pool, oldpool)
844
845                project.writestep(self, "Parsing XIST input with base ", base)
846                data = builder.parsestring(data, base)
847            finally:
848                builder.pool = oldpool # Restore old pool
849        return data
850
851    def __repr__(self):
852        return "<%s.%s object with base=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.base, id(self))
853
854
855class XISTConvertAction(PipeAction):
856    """
857    This action transform an XIST node.
858    """
859
860    def __init__(self, input=None, mode=None, target=None, stage=None, lang=None, root=None):
861        """
862        Create a new :class:`XISTConvertAction` object. :var:`input` is the input
863        none (or an action producing a node). The other arguments will be used to
864        create a :class:`ll.xist.converters.Converter` object for each call to
865        :meth:`get`.
866       
867        During conversion the :attr:`makeaction` attribute of the converter will
868        be set to :var:`self` and the :attr:`makeproject` attribute will be set
869        to the project.
870        """
871        PipeAction.__init__(self, input)
872        self.mode = mode
873        self.target = target
874        self.stage = stage
875        self.lang = lang
876        self.root = root
877
878    @report
879    def get(self, project, since):
880        (data, self.changed) = getoutputs(project, since, (self.input, dict(mode=self.mode, target=self.target, stage=self.stage, lang=self.lang, root=self.root)))
881
882        if data is not nodata:
883            from ll.xist import converters
884            args = []
885            for argname in ("mode", "target", "stage", "lang", "root"):
886                arg = data[1][argname]
887                if arg is not None:
888                    args.append("%s=%r" % (argname, arg))
889            if args:
890                args = " with %s" % ", ".join(args)
891            else:
892                args = ""
893            project.writestep(self, "Converting XIST node", args)
894            converter = converters.Converter(makeaction=self, makeproject=project, **data[1])
895            data = data[0].convert(converter)
896        return data
897
898    def __repr__(self):
899        args = []
900        for argname in ("mode", "target", "stage", "lang", "targetroot"):
901            arg = getattr(self, argname, None)
902            if arg is not None and not isinstance(arg, Action):
903                args.append("%s=%r" % (argname, arg))
904        if args:
905            args = " with %s" % ", ".join(args)
906        else:
907            args = ""
908        return "<%s.%s object%s at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, "".join(args), id(self))
909
910
911class XISTPublishAction(PipeAction):
912    """
913    This action publishes an XIST node as a byte string.
914    """
915
916    def __init__(self, input=None, publisher=None, base=None):
917        """
918        Create an :class:`XISTPublishAction` object. :var:`publisher` must be an
919        instance of :class:`ll.xist.publishers.Publisher`. If :var:`publisher` is
920        :const:`None` a publisher will be created for you. :var:`base` will be
921        the base URL used for publishing.
922        """
923        PipeAction.__init__(self, input)
924        self.publisher = publisher
925        self.base = base
926
927    @report
928    def get(self, project, since):
929        (data, self.changed) = getoutputs(project, since, (self.input, self.publisher, self.base))
930
931        if data is not nodata:
932            project.writestep(self, "Publishing XIST node with base ", data[2])
933            publisher = data[1]
934            if publisher is None:
935                from ll.xist import publishers
936                publisher = publishers.Publisher()
937            data = "".join(publisher.publish(data[0], data[2]))
938        return data
939
940    def __repr__(self):
941        if isinstance(self.base, Action):
942            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
943        else:
944            return "<%s.%s object with base=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.base, id(self))
945
946
947class XISTTextAction(PipeAction):
948    """
949    This action creates a plain text version of an HTML XIST node.
950    """
951    def __init__(self, input=None, encoding="iso-8859-1", width=72):
952        PipeAction.__init__(self, input)
953        self.encoding = encoding
954        self.width = width
955
956    @report
957    def get(self, project, since):
958        (data, self.changed) = getoutputs(project, since, (self.input, self.encoding, self.width))
959
960        if data is not nodata:
961            project.writestep(self, "Converting XIST node to text with encoding=%r, width=%r" % (data[1], data[2]))
962            from ll.xist.ns import html
963            data = html.astext(data[0], encoding=data[1], width=data[2])
964        return data
965
966    def __repr__(self):
967        args = []
968        for argname in ("encoding", "width"):
969            arg = getattr(self, argname, None)
970            if arg is not None and not isinstance(arg, Action):
971                args.append("%s=%r" % (argname, arg))
972        if args:
973            args = " with %s" % ", ".join(args)
974        else:
975            args = ""
976        return "<%s.%s object%s at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, "".join(args), id(self))
977
978
979class FOPAction(PipeAction):
980    """
981    This action transforms an XML string (containing XSL-FO) into PDF. For it
982    to work `Apache FOP`__ is required. The command line is hardcoded but it's
983    simple to overwrite the class attribute :attr:`command` in a subclass.
984
985    __ http://xmlgraphics.apache.org/fop/
986    """
987    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"
988
989    def execute(self, project, data):
990        project.writestep(self, "FOPping input")
991        (infd, inname) = tempfile.mkstemp(suffix=".fo")
992        (outfd, outname) = tempfile.mkstemp(suffix=".pdf")
993        try:
994            infile = os.fdopen(infd, "wb")
995            os.fdopen(outfd).close()
996            infile.write(data)
997            infile.close()
998            os.system(self.command % (inname, outname))
999            data = open(outname, "rb").read()
1000        finally:
1001            os.remove(inname)
1002            os.remove(outname)
1003        return data
1004
1005
1006class DecodeAction(PipeAction):
1007    """
1008    This action decodes an input :class:`str` object into an output
1009    :class:`unicode` object.
1010    """
1011
1012    def __init__(self, input=None, encoding=None):
1013        """
1014        Create a :class:`DecodeAction` object with :var:`encoding` as the name of
1015        the encoding. If :var:`encoding` is :const:`None` the system default
1016        encoding will be used.
1017        """
1018        PipeAction.__init__(self, input)
1019        if encoding is None:
1020            encoding = sys.getdefaultencoding()
1021        self.encoding = encoding
1022
1023    @report
1024    def get(self, project, since):
1025        (data, self.changed) = getoutputs(project, since, (self.input, self.encoding))
1026
1027        if data is not nodata:
1028            project.writestep(self, "Decoding input with encoding ", data[1])
1029            data = data[0].decode(data[1])
1030        return data
1031
1032    def __repr__(self):
1033        if isinstance(self.encoding, Action):
1034            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
1035        else:
1036            return "<%s.%s object encoding=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.encoding, id(self))
1037
1038
1039class EncodeAction(PipeAction):
1040    """
1041    This action encodes an input :class:`unicode` object into an output
1042    :class:`str` object.
1043    """
1044
1045    def __init__(self, input=None, encoding=None):
1046        """
1047        Create an :class:`EncodeAction` object with :var:`encoding` as the name
1048        of the encoding. If :var:`encoding` is :const:`None` the system default
1049        encoding will be used.
1050        """
1051        PipeAction.__init__(self, input)
1052        if encoding is None:
1053            encoding = sys.getdefaultencoding()
1054        self.encoding = encoding
1055
1056    @report
1057    def get(self, project, since):
1058        (data, self.changed) = getoutputs(project, since, (self.input, self.encoding))
1059
1060        if data is not nodata:
1061            project.writestep(self, "Encoding input with encoding ", data[1])
1062            data = data[0].encode(data[1])
1063        return data
1064
1065    def __repr__(self):
1066        if isinstance(self.encoding, Action):
1067            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
1068        else:
1069            return "<%s.%s object encoding=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.encoding, id(self))
1070
1071
1072class EvalAction(PipeAction):
1073    """
1074    This action evaluates an input string and returns the resulting output
1075    object.
1076    """
1077
1078    def execute(self, project, data):
1079        project.writestep(self, "Evaluating input")
1080        return eval(input)
1081
1082
1083class GZipAction(PipeAction):
1084    """
1085    This action compresses the input string via gzip and returns the resulting
1086    compressed output object.
1087    """
1088    def __init__(self, input=None, compresslevel=9):
1089        PipeAction.__init__(self, input)
1090        self.compresslevel = compresslevel
1091
1092    @report
1093    def get(self, project, since):
1094        (data, self.changed) = getoutputs(project, since, (self.input, self.compresslevel))
1095
1096        if data is not nodata:
1097            project.writestep(self, "Compressing input with level %d" % data[1])
1098            import gzip, cStringIO
1099            stream = cStringIO.StringIO()
1100            compressor = gzip.GzipFile(filename="", mode="wb", fileobj=stream, compresslevel=data[1])
1101            compressor.write(data[0])
1102            compressor.close()
1103            data = stream.getvalue()
1104        return data
1105
1106
1107class GUnzipAction(PipeAction):
1108    """
1109    This action uncompresses the input string via gzip and returns the resulting
1110    uncompressed output object.
1111    """
1112    def execute(self, project, data):
1113        project.writestep(self, "Uncompressing input")
1114        import gzip, cStringIO
1115        stream = cStringIO.StringIO(data)
1116        compressor = gzip.GzipFile(filename="", mode="rb", fileobj=stream)
1117        return compressor.read(data)
1118
1119
1120class CallFuncAction(Action):
1121    """
1122    This action calls a function with a number of arguments. Both positional and
1123    keyword arguments are supported and the function and the arguments can be
1124    static objects or actions.
1125    """
1126    def __init__(self, func, *args, **kwargs):
1127        Action.__init__(self)
1128        self.func = func
1129        self.args = args
1130        self.kwargs = kwargs
1131
1132    @report
1133    def get(self, project, since):
1134        (data, self.changed) = getoutputs(project, since, (self.func, self.args, self.kwargs))
1135        if data is not nodata:
1136            project.writestep(self, "Calling function %r" % data[0])
1137            data = data[0](*data[1], **data[2])
1138        return data
1139
1140
1141class CallMethAction(Action):
1142    """
1143    This action calls a method of an object with a number of arguments. Both
1144    positional and keyword arguments are supported and the object, the method
1145    name and the arguments can be static objects or actions.
1146    """
1147    def __init__(self, obj, methname, *args, **kwargs):
1148        Action.__init__(self)
1149        self.obj = obj
1150        self.methname = methname
1151        self.args = args
1152        self.kwargs = kwargs
1153
1154    @report
1155    def get(self, project, since):
1156        (data, self.changed) = getoutputs(project, since, (self.obj, self.methname, self.args, self.kwargs))
1157        if data is not nodata:
1158            meth = getattr(data[0], data[1])
1159            project.writestep(self, "Calling %r" % meth)
1160            data = meth(*data[2], **data[3])
1161        return data
1162
1163
1164class TOXICAction(PipeAction):
1165    """
1166    This action transforms a TOXIC template code into an Oracle or SQL Server
1167    function or procedure body via the TOXIC compiler (:mod:`ll.toxicc`).
1168    """
1169
1170    def __init__(self, input, mode="oracle"):
1171        PipeAction.__init__(self, input)
1172        self.mode = mode
1173
1174    @report
1175    def get(self, project, since):
1176        (data, self.changed) = getoutputs(project, since, (self.input, self.mode))
1177        if data is not nodata:
1178            project.writestep(self, "Compiling TOXIC template with mode %r" % data[1])
1179            from ll import toxicc
1180            return toxicc.compile(data[0], mode=data[1])
1181        return data
1182
1183
1184class TOXICPrettifyAction(PipeAction):
1185    """
1186    This action tries to fix the indentation of a SQL snippet via the
1187    :func:`ll.toxicc.prettify` function.
1188    """
1189
1190    def __init__(self, input, mode="oracle"):
1191        PipeAction.__init__(self, input)
1192        self.mode = mode
1193
1194    @report
1195    def get(self, project, since):
1196        (data, self.changed) = getoutputs(project, since, (self.input, self.mode))
1197        if data is not nodata:
1198            project.writestep(self, "Prettifying SQL code with mode %r" % data[1])
1199            from ll import toxicc
1200            return toxicc.prettify(data[0], mode=data[1])
1201        return data
1202
1203
1204class SplatAction(PipeAction):
1205    """
1206    This action transforms an input string by replacing regular expressions.
1207    """
1208
1209    def __init__(self, patterns):
1210        """
1211        Create a new :class:`SplatAction` object. :var:`patterns` are pattern
1212        pairs. Each first entry will be replaced by the corresponding second
1213        entry.
1214        """
1215        PipeAction.__init__(self)
1216        self.patterns = patterns
1217
1218    def execute(self, project, data):
1219        for (search, replace) in self.patterns:
1220            project.writestep(self, "Replacing ", search, " with ", replace)
1221            data = re.sub(search, replace, data)
1222        return data
1223
1224
1225class XPITAction(PipeAction):
1226    """
1227    This action transform an input string via :mod:`ll.xpit`.
1228    """
1229
1230    def __init__(self, nsinput=None):
1231        PipeAction.__init__(self)
1232        self.nsinput = nsinput
1233
1234    def addnsinput(self, input):
1235        """
1236        Register :var:`input` as the namespace action. This action must return
1237        a namespace to be used in evaluating the input string from the normal
1238        input action.
1239        """
1240        self.nsinput = input
1241        return self
1242
1243    def __iter__(self):
1244        yield self.nsinput
1245        yield self.input
1246
1247    def execute(self, project, data, ns):
1248        from ll import xpit
1249        globals = dict(makeaction=self, makeproject=project)
1250        project.writestep(self, "Converting XPIT input")
1251        return xpit.convert(data, globals, ns)
1252
1253    @report
1254    def get(self, project, since):
1255        (data, self.changed) = getoutputs(project, since, (self.nsinput, self.input))
1256        if data is not nodata:
1257            data = self.execute(project, data[1], data[0])
1258        return data
1259
1260
1261class ULLCompileAction(PipeAction):
1262    """
1263    This action compiles a ULL template into a :class:`ll.ul4c.Template` object.
1264    """
1265
1266    def execute(self, project, data):
1267        project.writestep(self, "Compiling ULL template")
1268        from ll import ul4c
1269        return ul4c.compile(data)
1270
1271
1272class ULLDumpAction(PipeAction):
1273    """
1274    This action dumps an :class:`ll.ul4c.Template` object into a string.
1275    """
1276
1277    def execute(self, project, data):
1278        project.writestep(self, "Dumping ULL template")
1279        return data.dumps()
1280
1281
1282class ULLLoadAction(PipeAction):
1283    """
1284    This action loads a :class:`ll.ul4c.Template` object from a string.
1285    """
1286
1287    def execute(self, project, data):
1288        project.writestep(self, "Loading ULL template")
1289        from ll import ul4c
1290        return ul4c.loads(data)
1291
1292
1293class CommandAction(PipeAction):
1294    """
1295    This action executes a system command (via :func:`os.system`) and passes
1296    through the input data.
1297    """
1298
1299    def __init__(self, command):
1300        """
1301        Create a new :class:`CommandAction` object. :var:`command` is the command
1302        that will executed when :meth:`execute` is called.
1303        """
1304        PipeAction.__init__(self)
1305        self.command = command
1306
1307    def execute(self, project, data):
1308        project.writestep(self, "Executing command ", self.command)
1309        os.system(self.command)
1310
1311    def __repr__(self):
1312        return "<%s.%s object command=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.command, id(self))
1313
1314
1315class ModeAction(PipeAction):
1316    """
1317    :class:`ModeAction` changes file permissions and passes through the input data.
1318    """
1319
1320    def __init__(self, input=None, mode=0644):
1321        """
1322        Create an :class:`ModeAction` object. :var:`mode` (which defaults to
1323        :const:`0644`) will be use as the permission bit pattern.
1324        """
1325        PipeAction.__init__(self, input)
1326        self.mode = mode
1327
1328    @report
1329    def get(self, project, since):
1330        """
1331        Change the permission bits of the file ``self.getkey()``.
1332        """
1333        (data, self.changed) = getoutputs(project, since, (self.input, self.mode))
1334        if data is not nodata:
1335            key = self.getkey()
1336            project.writestep(self, "Changing mode of ", project.strkey(key), " to 0%03o" % data[1])
1337            key.chmod(data[1])
1338            data = data[0]
1339        return data
1340
1341    def __repr__(self):
1342        if isinstance(self.mode, Action):
1343            return "<%s.%s object at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, id(self))
1344        else:
1345            return "<%s.%s object mode=0%03o at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.mode, id(self))
1346
1347
1348class OwnerAction(PipeAction):
1349    """
1350    :class:`OwnerAction` changes the user and/or group ownership of a file and
1351    passes through the input data.
1352    """
1353
1354    def __init__(self, user=None, group=None):
1355        """
1356        Create a new :class:`OwnerAction` object. :var:`user` can either be a
1357        numerical user id or a user name or :const:`None`. If it is :const:`None`
1358        no user ownership will be changed. The same applies to :var:`group`.
1359        """
1360        PipeAction.__init__(self)
1361        self.id = id
1362        self.user = user
1363        self.group = group
1364
1365    @report
1366    def get(self, project, since):
1367        """
1368        Change the ownership of the file ``self.getkey()``.
1369        """
1370        (data, self.changed) = getoutputs(project, since, (self.input, self.user, self.group))
1371        if data is not nodata:
1372            key = self.getkey()
1373            project.writestep(self, "Changing owner of ", project.strkey(key), " to ", data[1], " and group to ", data[2])
1374            key.chown(data[0], data[1])
1375            data = data[0]
1376        return data
1377
1378    def __repr__(self):
1379        args = []
1380        for argname in ("user", "group"):
1381            arg = getattr(self, argname, None)
1382            if arg is not None and not isinstance(arg, Action):
1383                args.append("%s=%r" % (argname, arg))
1384        if args:
1385            args = " with %s" % ", ".join(args)
1386        else:
1387            args = ""
1388        return "<%s.%s object%s at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, "".join(args), id(self))
1389
1390
1391class ModuleAction(PipeAction):
1392    """
1393    This action will turn the input string into a Python module.
1394    """
1395    def __init__(self):
1396        """
1397        Create an :class:`ModuleAction`.
1398
1399        This object must have an input action (which might be a :class:`FileAction`
1400        that creates the source file).
1401        """
1402        PipeAction.__init__(self)
1403        self.inputs = []
1404        self.changed = bigbang
1405        self.data = nodata
1406        self.buildno = None
1407
1408    def addinputs(self, *inputs):
1409        """
1410        Register all actions in :var:`inputs` as modules used by this module.
1411        These actions must be :class:`ModuleAction` objects too.
1412
1413        Normally it isn't neccessary to call the method explicitely. Instead
1414        fetch the required module inside your module like this::
1415
1416            from ll import make
1417
1418            mymodule = make.currentproject.get("mymodule.py")
1419
1420        This will record your module as depending on :mod:`mymodule`, so if
1421        :mod:`mymodule` changes your module will be reloaded too. For this to
1422        work you need to have an :class:`ModuleAction` added to the project with
1423        the key ``"mymodule.py"``.
1424        """
1425        self.inputs.extend(inputs)
1426        return self
1427
1428    def __iter__(self):
1429        yield self.input
1430        for input in self.inputs:
1431            yield input
1432
1433    def execute(self, project, data):
1434        key = self.getkey()
1435        project.writestep(self, "Importing module as ", project.strkey(key))
1436
1437        if key is None:
1438            filename = name = "<string>"
1439        elif isinstance(key, url.URL):
1440            try:
1441                filename = key.local()
1442            except ValueError: # is not local
1443                filename = str(key)
1444            name = key.withoutext().file.encode("ascii", "ignore")
1445        else:
1446            filename = name = str(key)
1447
1448        del self.inputs[:] # The module will be reloaded => drop all dependencies (they will be rebuilt during import)
1449
1450        # Normalize line feeds, so that :func:`compile` works (normally done by import)
1451        data = data.replace("\r\n", "\n")
1452
1453        mod = types.ModuleType(name)
1454        mod.__file__ = filename
1455
1456        try:
1457            project.importstack.append(self)
1458            code = compile(data, filename, "exec")
1459            exec code in mod.__dict__
1460        finally:
1461            project.importstack.pop(-1)
1462        return mod
1463
1464    @report
1465    def get(self, project, since):
1466        # Is this module required by another?
1467        if project.importstack:
1468            if self not in project.importstack[-1].inputs:
1469                project.importstack[-1].addinputs(self) # Append to inputs of other module
1470
1471        # Is this a new build round?
1472        if self.buildno != project.buildno:
1473            (data, changed) = getoutputs(project, self.changed, self.input) # Get the source code
1474            if data is not nodata or self.data is nodata: # The file itself has changed or this is the first call
1475                needimport = True
1476            else: # Only check the required inputs, if ``self.input`` has *not* changed
1477                (data2, changed2) = getoutputs(project, self.changed, self.inputs)
1478                needimport = data2 is not nodata
1479
1480            if needimport:
1481                if data is nodata:
1482                    (data, changed) = getoutputs(project, bigbang, self.input) # We *really* need the source
1483                self.data = self.execute(project, data) # This will (re)create dependencies
1484                gc.collect() # Make sure classes from the previous module (which have cycles via the :attr:`__mro__`) are gone
1485                # Timestamp of import is the timestamp of the newest module file
1486                self.changed = changed
1487                if self.inputs:
1488                    changed = max(changed, max(input.changed for input in self.inputs))
1489                self.changed = changed
1490            self.buildno = project.buildno
1491            if self.changed > since:
1492                return self.data
1493        # Are we newer then the specified date?
1494        elif self.changed > since:
1495            key = self.getkey()
1496            project.writenote(self, "Reusing cached module ", project.strkey(key))
1497            return self.data
1498        return nodata
1499
1500    def __repr__(self):
1501        return "<%s.%s object key=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.getkey(), id(self))
1502
1503
1504class ModuleName(str):
1505    """
1506    :class:`ModuleName` objects are automatically created by
1507    :class:`ImportAction` as keys to be able to distinguish those keys from the
1508    keys for PhonyActions (which are normally :class:`str` objects).
1509    """
1510    def __eq__(self, other):
1511        return self.__class__ is other.__class__ and str.__eq__(self, other)
1512
1513    def __ne__(self, other):
1514        return not self == other
1515
1516    def __repr__(self):
1517        return "%s.%s(%s)" % (self.__class__.__module__, self.__class__.__name__, str.__repr__(self))
1518
1519
1520class AlwaysAction(Action):
1521    """
1522    This action always returns :const:`None` as new data.
1523    """
1524    def __init__(self):
1525        Action.__init__(self)
1526        self.changed = bigbang
1527
1528    def __iter__(self):
1529        if False:
1530            yield None
1531
1532    @report
1533    def get(self, project, since):
1534        project.writestep(self, "Returning None")
1535        return None
1536alwaysaction = AlwaysAction() # this action can be reused as it has no inputs
1537
1538
1539class NeverAction(Action):
1540    """
1541    This action never returns new data.
1542    """
1543    def __iter__(self):
1544        if False:
1545            yield None
1546
1547    @report
1548    def get(self, project, since):
1549        return nodata
1550neveraction = NeverAction() # this action can be reused as it has no inputs
1551
1552
1553###
1554### Classes for target keys (apart from strings for :const:`PhonyAction` objects and URLs for :class:`FileAction` objects)
1555###
1556
1557class DBKey(object):
1558    """
1559    This class provides a unique identifier for database content. This can be
1560    used as an key for :class:`FileAction` objects and other actions that are
1561    not files, but database records, function, procedures etc.
1562    """
1563    name = None
1564
1565    def __init__(self, connection, type, name, key=None):
1566        """
1567        Create a new :class:`DBKey` instance. Arguments are:
1568
1569        :var:`connection` : string
1570            A string that specifies the connection to the database. E.g.
1571            ``"user/pwd@db.example.com"`` for Oracle.
1572
1573        :var:`type` : string
1574            The type of the object. Values may be ``"table"``, ``"view"``,
1575            ``"function"``, ``"procedure"`` etc.
1576
1577        :var:`name` : string
1578            The name of the object
1579
1580        :var:`key` : any object
1581            If :var:`name` refers to a table, :var:`key` can be used to specify
1582            a row in this table.
1583        """
1584        self.connection = connection
1585        self.type = type.lower()
1586        self.name = name.lower()
1587        self.key = key
1588
1589    def __eq__(self, other):
1590        res = self.__class__ == other.__class__
1591        if not res:
1592            res = self.connection==other.connection and self.type==other.type and self.name==other.name and self.key==other.key
1593        return res
1594
1595    def __hash__(self):
1596        return hash(self.connection) ^ hash(self.type) ^ hash(self.name) ^ hash(self.key)
1597
1598    def __repr__(self):
1599        args = []
1600        for attrname in ("connection", "type", "name", "key"):
1601            attrvalue = getattr(self, attrname)
1602            if attrvalue is not None:
1603                args.append("%s=%r" % (attrname, attrvalue))
1604        return "%s(%s)" % (self.__class__.__name__, ", ".join(args))
1605
1606    def __str__(self):
1607        s = "%s:%s|%s:%s" % (self.__class__.name, self.connection, self.type, self.name)
1608        if self.key is not None:
1609            s += "|%s" % (self.key,)
1610        return s
1611
1612
1613class OracleConnection(url.Connection):
1614    def __init__(self, context, connection):
1615        self.context = context
1616        import cx_Oracle
1617        self.cursor = cx_Oracle.connect(connection).cursor()
1618
1619    def open(self, url, mode="rb"):
1620        return OracleResource(self, url, mode)
1621
1622    def mimetype(self, url):
1623        return "text/x-oracle-%s" % url.type
1624
1625    def cdate(self, url):
1626        # FIXME: This isn't the correct time zone, but Oracle doesn't provide anything better
1627        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)
1628        row = self.cursor.fetchone()
1629        if row is None:
1630            raise IOError(errno.ENOENT, "no such %s: %s" % (url.type, url.name))
1631        return row[0]-datetime.timedelta(seconds=60*(row[1]*60+row[2]))
1632
1633    def mdate(self, url):
1634        # FIXME: This isn't the correct time zone, but Oracle doesn't provide anything better
1635        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)
1636        row = self.cursor.fetchone()
1637        if row is None:
1638            raise IOError(errno.ENOENT, "no such %s: %s" % (url.type, url.name))
1639        return row[0]-datetime.timedelta(seconds=60*(row[1]*60+row[2]))
1640
1641    def __repr__(self):
1642        return "<%s.%s to %r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.cursor.connection.connectstring(), id(self))
1643
1644
1645class OracleKey(DBKey):
1646    name = "oracle"
1647
1648    def connect(self, context=None):
1649        context = url.getcontext(context)
1650        if context is url.defaultcontext:
1651            raise ValueError("oracle URLs need a custom context")
1652
1653        # Use one OracleConnection for each connectstring
1654        try:
1655            connections = context.schemes["oracle"]
1656        except KeyError:
1657            connections = context.schemes["oracle"] = {}
1658        try:
1659            connection = connections[self.connection]
1660        except KeyError:
1661            connection = connections[self.connection] = OracleConnection(context, self.connection)
1662        return connection
1663
1664    def __getattr__(self, name):
1665        def realattr(*args, **kwargs):
1666            try:
1667                context = kwargs["context"]
1668            except KeyError:
1669                context = None
1670            else:
1671                kwargs = kwargs.copy()
1672                del kwargs["context"]
1673            connection = self.connect(context=context)
1674            return getattr(connection, name)(self, *args, **kwargs)
1675        return realattr
1676
1677    def mimetype(self):
1678        return "text/x-oracle-%s" % self.type
1679
1680    def open(self, mode="rb", context=None, *args, **kwargs):
1681        connection = self.connect(context=context)
1682        return connection.open(self, mode, *args, **kwargs)
1683
1684
1685class OracleResource(url.Resource):
1686    """
1687    An :class:`OracleResource` wraps a function or procedure in an Oracle
1688    database in a file-like API.
1689    """
1690    def __init__(self, connection, url, mode="rb"):
1691        self.connection = connection
1692        self.url = url
1693        self.mode = mode
1694        self.closed = False
1695        self.name = str(self.url)
1696
1697        if self.url.type not in ("function", "procedure"):
1698            raise ValueError("don't know how to handle %r" % self.url)
1699        if "w" in self.mode:
1700            self.stream = cStringIO.StringIO()
1701            self.stream.write("create or replace %s %s\n" % (self.url.type, self.url.name))
1702        else:
1703            cursor = self.connection.cursor
1704            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)
1705            code = "\n".join((row[0] or "").rstrip() for row in cursor)
1706            if not code:
1707                raise IOError(errno.ENOENT, "no such %s: %s" % (self.url.type, self.url.name))
1708            # drop type
1709            code = code.split(None, 1)[1]
1710            # skip name
1711            for (i, c) in enumerate(code):
1712                if not c.isalpha() and c != "_":
1713                    break
1714            code = code[i:]
1715            self.stream = cStringIO.StringIO(code)
1716
1717    def __getattr__(self, name):
1718        if self.closed:
1719            raise ValueError("I/O operation on closed file")
1720        return getattr(self.stream, name)
1721
1722    def mimetype(self):
1723        return "text/x-oracle-%s" % self.url.type
1724
1725    def cdate(self):
1726        return self.connection.cdate(self.url)
1727
1728    def mdate(self):
1729        return self.connection.mdate(self.url)
1730
1731    def close(self):
1732        if not self.closed:
1733            if "w" in self.mode:
1734                c = self._cursor()
1735                c.execute(self.stream.getvalue())
1736            self.stream = None
1737            self.closed = True
1738
1739
1740###
1741### Colors for output
1742###
1743
1744s4indent = astyle.Style.fromenv("LL_MAKE_REPRANSI_INDENT", "black:black:bold")
1745s4key = astyle.Style.fromenv("LL_MAKE_REPRANSI_KEY", "green:black")
1746s4action = astyle.Style.fromenv("LL_MAKE_REPRANSI_ACTION", "yellow:black")
1747s4time = astyle.Style.fromenv("LL_MAKE_REPRANSI_TIME", "magenta:black")
1748s4data = astyle.Style.fromenv("LL_MAKE_REPRANSI_DATA", "cyan:black")
1749s4size = astyle.Style.fromenv("LL_MAKE_REPRANSI_SIZE", "magenta:black")
1750s4counter = astyle.Style.fromenv("LL_MAKE_REPRANSI_COUNTER", "red:black:bold")
1751s4error = astyle.Style.fromenv("LL_MAKE_REPRANSI_ERROR", "red:black:bold")
1752
1753
1754###
1755### The project class
1756###
1757
1758class Project(dict):
1759    """
1760    A :class:`Project` collects all :class:`Action` objects from a project. It
1761    is responsible for initiating the build process and for generating a report
1762    about the progress of the build process.
1763    """
1764    def __init__(self):
1765        super(Project, self).__init__()
1766        self.actionscalled = 0
1767        self.actionsfailed = 0
1768        self.stepsexecuted = 0
1769        self.fileswritten = 0
1770        self.starttime = None
1771        self.ignoreerrors = False
1772        self.here = None # cache the current directory during builds (used for shortening URLs)
1773        self.home = None # cache the home directory during builds (used for shortening URLs)
1774        self.stack = [] # keep track of the recursion during calls to :meth:`Action.get`
1775        self.importstack = [] # keep track of recursive imports
1776        self.indent = os.environ.get("LL_MAKE_INDENT", "   ") # Indentation string to use for output of nested actions
1777        self.buildno = 0 # Build number; This gets incremented on each call to :meth:`build`. Can be used by actions to determine the start of a new build round
1778
1779        self.showsummary = self._getenvbool("LL_MAKE_SHOWSUMMARY", True)
1780        self.showaction = os.environ.get("LL_MAKE_SHOWACTION", "filephony")
1781        self.showstep = os.environ.get("LL_MAKE_SHOWSTEP", "all")
1782        self.shownote = os.environ.get("LL_MAKE_SHOWNOTE", "none")
1783        self.showidle = self._getenvbool("LL_MAKE_SHOWIDLE", False)
1784        self.showregistration = os.environ.get("LL_MAKE_SHOWREGISTRATION", "phony")
1785        self.showtime = self._getenvbool("LL_MAKE_SHOWTIME", True)
1786        self.showtimestamps = self._getenvbool("LL_MAKE_SHOWTIMESTAMPS", False)
1787        self.showdata = self._getenvbool("LL_MAKE_SHOWDATA", False)
1788
1789    def __repr__(self):
1790        return "<%s.%s with %d targets at 0x%x>" % (self.__module__, self.__class__.__name__, len(self), id(self))
1791
1792    class showaction(misc.propclass):
1793        """
1794        This property specifies which actions should be reported during the build
1795        process. On setting, the value can be:
1796
1797        :const:`None` or ``"none"``
1798            No actions will be reported;
1799
1800        ``"file"``
1801            Only :class:`FileAction`\s will be reported;
1802
1803        ``"phony"``
1804            Only :class:`PhonyAction`\s will be reported;
1805
1806        ``"filephony"``
1807            Only :class:`FileAction`\s and :class:`PhonyAction`\s will be
1808            reported;
1809
1810        a class or tuple of classes
1811            Only actions that are instances of those classes will be reported.
1812        """
1813        def __get__(self):
1814            return self._showaction
1815        def __set__(self, value):
1816            if value == "none":
1817                self._showaction = None
1818            elif value == "file":
1819                self._showaction = FileAction
1820            elif value == "phony":
1821                self._showaction = PhonyAction
1822            elif value == "filephony":
1823                self._showaction = (PhonyAction, FileAction)
1824            elif value == "all":
1825                self._showaction = Action
1826            else:
1827                self._showaction = value
1828
1829    class showstep(misc.propclass):
1830        """
1831        This property specifies which for which actions tranformation steps
1832        should be reported during the build process. For allowed values on
1833        setting see :prop:`showaction`.
1834        """
1835        def __get__(self):
1836            return self._showstep
1837        def __set__(self, value):
1838            if value == "none":
1839                self._showstep = None
1840            elif value == "file":
1841                self._showstep = FileAction
1842            elif value == "phony":
1843                self._showstep = PhonyAction
1844            elif value == "filephony":
1845                self._showstep = (PhonyAction, FileAction)
1846            elif value == "all":
1847                self._showstep = Action
1848            else:
1849                self._showstep = value
1850
1851    class shownote(misc.propclass):
1852        """
1853        This property specifies which for which actions tranformation notes
1854        (which are similar to step, but not that important, e.g. when an
1855        information that is already there gets reused) be reported during the
1856        build process. For allowed values on setting see :prop:`showaction`.
1857        """
1858        def __get__(self):
1859            return self._shownote
1860        def __set__(self, value):
1861            if value == "none":
1862                self._shownote = None
1863            elif value == "file":
1864                self._shownote = FileAction
1865            elif value == "phony":
1866                self._shownote = PhonyAction
1867            elif value == "filephony":
1868                self._shownote = (PhonyAction, FileAction)
1869            elif value == "all":
1870                self._shownote = Action
1871            else:
1872                self._shownote = value
1873
1874    class showregistration(misc.propclass):
1875        """
1876        This property specifies for which actions registration (i.e. call to the
1877        :meth:`add` should be reported. For allowed values on setting see
1878        :prop:`showaction`.
1879        """
1880        def __get__(self):
1881            return self._showregistration
1882        def __set__(self, value):
1883            if value == "none":
1884                self._showregistration = None
1885            elif value == "file":
1886                self._showregistration = FileAction
1887            elif value == "phony":
1888                self._showregistration = PhonyAction
1889            elif value == "filephony":
1890                self._showregistration = (PhonyAction, FileAction)
1891            elif value == "all":
1892                self._showregistration = Action
1893            else:
1894                self._showregistration = value
1895
1896    def _getenvbool(self, name, default):
1897        return bool(int(os.environ.get(name, default)))
1898
1899    def strtimedelta(self, delta):
1900        """
1901        Return a nicely formatted and colored string for the
1902        :class:`datetime.timedelta` value :var:`delta`. :var:`delta`
1903        may also be :const:`None` in with case ``"0"`` will be returned.
1904        """
1905        if delta is None:
1906            text = "0"
1907        else:
1908            rest = delta.seconds
1909   
1910            (rest, secs) = divmod(rest, 60)
1911            (rest, mins) = divmod(rest, 60)
1912            rest += delta.days*24
1913   
1914            secs += delta.microseconds/1000000.
1915            if rest:
1916                text = "%d:%02d:%06.3fh" % (rest, mins, secs)
1917            elif mins:
1918                text = "%02d:%06.3fm" % (mins, secs)
1919            else:
1920                text = "%.3fs" % secs
1921        return s4time(text)
1922
1923    def strdatetime(self, dt):
1924        """
1925        Return a nicely formatted and colored string for the
1926        :class:`datetime.datetime` value :var:`dt`.
1927        """
1928        return s4time(dt.strftime("%Y-%m-%d %H:%M:%S"), ".%06d" % (dt.microsecond))
1929
1930    def strcounter(self, counter):
1931        """
1932        Return a nicely formatted and colored string for the counter value
1933        :var:`counter`.
1934        """
1935        return s4counter("%d." % counter)
1936
1937    def strerror(self, text):
1938        """
1939        Return a nicely formatted and colored string for the error text
1940        :var:`text`.
1941        """
1942        return s4error(text)
1943
1944    def strkey(self, key):
1945        """
1946        Return a nicely formatted and colored string for the action key
1947        :var:`key`.
1948        """
1949        s = str(key)
1950        if isinstance(key, url.URL) and key.islocal():
1951            if self.here is None:
1952                self.here = url.here()
1953            if self.home is None:
1954                self.home = url.home()
1955            test = str(key.relative(self.here))
1956            if len(test) < len(s):
1957                s = test
1958            test = "~/%s" % key.relative(self.home)
1959            if len(test) < len(s):
1960                s = test
1961        return s4key(s)
1962
1963    def straction(self, action):
1964        """
1965        Return a nicely formatted and colored string for the action
1966        :var:`action`.
1967        """
1968        name = action.__class__.__name__
1969        if name.endswith("Action"):
1970            name = name[:-6]
1971
1972        if hasattr(action, "key"):
1973            return s4action(name, "(", self.strkey(action.key), ")")
1974        else:
1975            return s4action(name)
1976
1977    def __setitem__(self, key, target):
1978        """
1979        Add the action :var:`target` to :var:`self` as a target and register it
1980        under the key :var:`key`.
1981        """
1982        if key in self:
1983            self.warn(RedefinedTargetWarning(key), 5)
1984        if isinstance(key, url.URL) and key.islocal():
1985            key = key.abs(scheme="file")
1986        target.key = key
1987        super(Project, self).__setitem__(key, target)
1988
1989    def add(self, target, key=None):
1990        """
1991        Add the action :var:`target` as a target to :var:`self`. If :var:`key`
1992        is not :const:`None`, :var:`target` will be registered under this key
1993        (and ``target.key`` will be set to it), otherwise it will be registered
1994        under its own key (i.e. ``target.key``).
1995        """
1996        if key is None: # Use the key from the target
1997            key = target.getkey()
1998
1999        self[key] = target
2000
2001        self.stepsexecuted += 1
2002        if self.showregistration is not None and isinstance(target, self.showregistration):
2003            self.writestacklevel(0, self.strcounter(self.stepsexecuted), " Registered ", self.strkey(target.key))
2004
2005        return target
2006
2007    def _candidates(self, key):
2008        """
2009        Return candidates for alternative forms of :var:`key`. This is a
2010        generator, so when the first suitable candidate is found, the rest of the
2011        candidates won't have to be created at all.
2012        """
2013        yield key
2014        key2 = key
2015        if isinstance(key, basestring):
2016            yield ModuleName(key)
2017            key2 = url.URL(key)
2018            yield key2
2019        if isinstance(key2, url.URL):
2020            key2 = key2.abs(scheme="file")
2021            yield key2
2022            key2 = key2.real(scheme="file")
2023            yield key2
2024        if isinstance(key, basestring) and ":" in key:
2025            (prefix, rest) = key.split(":", 1)
2026            if prefix == "oracle":
2027                if "|" in rest:
2028                    (connection, rest) = rest.split("|", 1)
2029                    if ":" in rest:
2030                        (type, name) = rest.split(":", 1)
2031                        if "|" in rest:
2032                            (name, key) = rest.split("|")
2033                        else:
2034                            key = None
2035                        yield OracleKey(connection, type, name, key)
2036
2037    def __getitem__(self, key):
2038        """
2039        Return the target with the key :var:`key`. If an key can't be found, it
2040        will be wrapped in a :class:`ll.url.URL` object and retried. If
2041        :var:`key` still can't be found a :exc:`UndefinedTargetError` will be
2042        raised.
2043        """
2044        sup = super(Project, self)
2045        for key2 in self._candidates(key):
2046            try:
2047                return sup.__getitem__(key2)
2048            except KeyError:
2049                pass
2050        raise UndefinedTargetError(key)
2051
2052    def has_key(self, key):
2053        """
2054        Return whether the target with the key :var:`key` exists in the project.
2055        """
2056        return key in self
2057
2058    def __contains__(self, key):
2059        """
2060        Return whether the target with the key :var:`key` exists in the project.
2061        """
2062        sup = super(Project, self)
2063        for key2 in self._candidates(key):
2064            has = sup.has_key(key2)
2065            if has:
2066                return True
2067        return False
2068
2069    def create(self):
2070        """
2071        Create all dependencies for the project. Overwrite in subclasses.
2072
2073        This method should only be called once, otherwise you'll get lots of
2074        :exc:`RedefinedTargetWarning`\s. But you can call :meth:`clear`
2075        to remove all targets before calling :meth:`create`. You can also
2076        use the method :meth:`recreate` for that.
2077        """
2078        self.stepsexecuted = 0
2079        self.starttime = datetime.datetime.utcnow()
2080        self.writeln("Creating targets...")
2081
2082    def recreate(self):
2083        """
2084        Calls :meth:`clear` and :meth:`create` to recreate all project
2085        dependencies.
2086        """
2087        self.clear()
2088        self.create()
2089
2090    def optionparser(self):
2091        """
2092        Return an :mod:`optparse` parser for parsing the command line options.
2093        This can be overwritten in subclasses to add more options.
2094        """
2095        p = optparse.OptionParser(usage="usage: %prog [options] [targets]")
2096        p.add_option("-x", "--ignore", dest="ignoreerrors", help="Ignore errors", action="store_true", default=None)
2097        p.add_option("-X", "--noignore", dest="ignoreerrors", help="Don't ignore errors", action="store_false", default=None)
2098        p.add_option("-c", "--color", dest="color", help="Use colored output", action="store_true", default=None)
2099        p.add_option("-C", "--nocolor", dest="color", help="No colored output", action="store_false", default=None)
2100        p.add_option("-a", "--showaction", dest="showaction", help="Show actions?", choices=["all", "file", "filephony", "none"], default="filephony")
2101        p.add_option("-s", "--showstep", dest="showstep", help="Show steps?", choices=["all", "file", "filephony", "none"], default="all")
2102        p.add_option("-n", "--shownote", dest="shownote", help="Show steps?", choices=["all", "file", "filephony", "none"], default="none")
2103        p.add_option("-i", "--showidle", dest="showidle", help="Show actions that didn't produce data?", action="store_true", default=False)
2104        p.add_option("-d", "--showdata", dest="showdata", help="Show data?", action="store_true", default=False)
2105        return p
2106
2107    def parseoptions(self, commandline=None):
2108        """
2109        Use the parser returned by :meth:`optionparser` to parse the option
2110        sequence :var:`commandline`, modify :var:`self` accordingly and return
2111        the result of the parsers :meth:`parse_args` call.
2112        """
2113        p = self.optionparser()
2114        (options, args) = p.parse_args(commandline)
2115        if options.ignoreerrors is not None:
2116            self.ignoreerrors = options.ignoreerrors
2117        if options.color is not None:
2118            self.color = options.color
2119        if options.showaction is not None:
2120            self.showaction = options.showaction
2121        if options.showstep is not None:
2122            self.showstep = options.showstep
2123        if options.shownote is not None:
2124            self.shownote = options.shownote
2125        self.showidle = options.showidle
2126        self.showdata = options.showdata
2127        return (options, args)
2128
2129    def _get(self, target, since):
2130        """
2131        :var:`target` must be an action registered in :var:`self` (or the id of
2132        one). For this target the :meth:`Action.get` will be called with
2133        :var:`since` as the argument.
2134        """
2135        global currentproject
2136
2137        if not isinstance(target, Action):
2138            target = self[target]
2139
2140        oldproject = currentproject
2141        try:
2142            currentproject = self
2143            data = target.get(self, since)
2144        finally:
2145            currentproject = oldproject
2146        return data
2147
2148    def get(self, target):
2149        """
2150        Get up-to-date output data from the target :var:`target` (which must be
2151        an action registered with :var:`self` (or the id of one). During the call
2152        the global variable ``currentproject`` will be set to :var:`self`.
2153        """
2154        return self._get(target, bigbang)
2155
2156    def build(self, *targets):
2157        """
2158        Rebuild all targets in :var:`targets`. Items in :var:`targets` must be
2159        actions registered with :var:`self` (or their ids).
2160        """
2161        global currentproject
2162
2163        self.starttime = datetime.datetime.utcnow()
2164
2165        with url.Context():
2166            self.stack = []
2167            self.importstack = []
2168            self.actionscalled = 0
2169            self.actionsfailed = 0
2170            self.stepsexecuted = 0
2171            self.fileswritten = 0
2172   
2173            self.buildno += 1 # increment build number so that actions that stored the old one can detect a new build round
2174   
2175            for target in targets:
2176                data = self._get(target, bigcrunch)
2177            now = datetime.datetime.utcnow()
2178   
2179            if self.showsummary:
2180                args = []
2181                self.write(
2182                    "built ",
2183                    s4action(self.__class__.__module__, ".", self.__class__.__name__),
2184                    ": ",
2185                    s4data(str(len(self))),
2186                    " registered targets; ",
2187                    s4data(str(self.actionscalled)),
2188                    " actions called; ",
2189                    s4data(str(self.stepsexecuted)),
2190                    " steps executed; ",
2191                    s4data(str(self.fileswritten)),
2192                    " files written; ",
2193                    s4data(str(self.actionsfailed)),
2194                    " actions failed",
2195                )
2196                if self.showtime:
2197                    self.write(" [t+", self.strtimedelta(now-self.starttime), "]")
2198                self.writeln()
2199
2200    def buildwithargs(self, commandline=None):
2201        """
2202        For calling make scripts from the command line. :var:`commandline`
2203        defaults to ``sys.argv[1:]``. Any positional arguments in the command
2204        line will be treated as target ids. If there are no positional arguments,
2205        a list of all registered :class:`PhonyAction` objects will be output.
2206        """
2207        if not commandline:
2208            commandline = sys.argv[1:]
2209        (options, args) = self.parseoptions(commandline)
2210
2211        if args:
2212            self.build(*args)
2213        else:
2214            self.writeln("Available phony targets are:")
2215            self.writephonytargets()
2216
2217    def write(self, *texts):
2218        """
2219        All screen output is done through this method. This makes it possible
2220        to redirect the output (e.g. to logfiles) in subclasses.
2221        """
2222        astyle.stderr.write(*texts)
2223
2224    def writeln(self, *texts):
2225        """
2226        All screen output is done through this method. This makes it possible to
2227        redirect the output (e.g. to logfiles) in subclasses.
2228        """
2229        astyle.stderr.writeln(*texts)
2230        astyle.stderr.flush()
2231
2232    def writeerror(self, *texts):
2233        """
2234        Output an error.
2235        """
2236        self.write(*texts)
2237
2238    def warn(self, warning, stacklevel):
2239        """
2240        Issue a warning through the Python warnings framework
2241        """
2242        warnings.warn(warning, stacklevel=stacklevel)
2243
2244    def writestacklevel(self, level, *texts):
2245        """
2246        Output :var:`texts` indented :var:`level` levels.
2247        """
2248        self.write(s4indent(level*self.indent), *texts)
2249        if self.showtime and self.starttime is not None:
2250            self.write(" [t+", self.strtimedelta(datetime.datetime.utcnow() - self.starttime), "]")
2251        self.writeln()
2252
2253    def writestack(self, *texts):
2254        """
2255        Output :var:`texts` indented properly for the current nesting of
2256        action execution.
2257        """
2258        count = misc.count(level for level in self.stack if level.reportable)
2259        self.writestacklevel(count, *texts)
2260
2261    def _writependinglevels(self):
2262        for (i, level) in enumerate(level for level in self.stack if level.reportable):
2263            if not level.reported:
2264                args = ["Started  ", self.straction(level.action)]
2265                if self.showtimestamps:
2266                    args.append(" (since ")
2267                    args.append(self.strdatetime(level.since))
2268                    args.append(")")
2269                self.writestacklevel(i, *args)
2270                level.reported = True
2271
2272    def writestep(self, action, *texts):
2273        """
2274        Output :var:`texts` as the description of the data transformation
2275        done by the action :var:`arction`.
2276        """
2277        self.stepsexecuted += 1
2278        if self.showstep is not None and isinstance(action, self.showstep):
2279            if not self.showidle:
2280                self._writependinglevels()
2281            self.writestack(self.strcounter(self.stepsexecuted), " ", *texts)
2282
2283    def writenote(self, action, *texts):
2284        """
2285        Output :var:`texts` as the note for the data transformation done by
2286        the action :var:`action`.
2287        """
2288        self.stepsexecuted += 1
2289        if self.shownote is not None and isinstance(action, self.shownote):
2290            if not self.showidle:
2291                self._writependinglevels()
2292            self.writestack(self.strcounter(self.stepsexecuted), " ", *texts)
2293
2294    def writecreatedone(self):
2295        """
2296        Can be called at the end of an overwritten :meth:`create` to report
2297        the number of registered targets.
2298        """
2299        self.writestacklevel(0, "done: ", s4data(str(len(self))), " registered targets")
2300
2301    def writephonytargets(self):
2302        """
2303        Show a list of all :class:`PhonyAction` objects in the project and
2304        their documentation.
2305        """
2306        phonies = []
2307        maxlen = 0
2308        for key in self:
2309            if isinstance(key, basestring):
2310                maxlen = max(maxlen, len(key))
2311                phonies.append(self[key])
2312        phonies.sort(key=operator.attrgetter("key"))
2313        for phony in phonies:
2314            text = astyle.Text(self.straction(phony))
2315            if phony.doc:
2316                text.append(" ", s4indent("."*(maxlen+3-len(phony.key))), " ", phony.doc)
2317            self.writeln(text)
2318
2319    def findpaths(self, target, source):
2320        """
2321        Find dependency paths leading from :var:`target` to :var:`source`.
2322        :var:`target` and :var:`source` may be actions or the ids of registered
2323        actions. For more info see :meth:`Action.findpaths`.
2324        """
2325        if not isinstance(target, Action):
2326            target = self[target]
2327        if not isinstance(source, Action):
2328            source = self[source]
2329        return target.findpaths(source)
2330
2331
2332# This will be set to the project in :meth:`build` and :meth:`get`
2333currentproject = None
Note: See TracBrowser for help on using the browser.