1# This file is part of Buildbot.  Buildbot is free software: you can
2# redistribute it and/or modify it under the terms of the GNU General Public
3# License as published by the Free Software Foundation, version 2.
4#
5# This program is distributed in the hope that it will be useful, but WITHOUT
6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
8# details.
9#
10# You should have received a copy of the GNU General Public License along with
11# this program; if not, write to the Free Software Foundation, Inc., 51
12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13#
14# Copyright Buildbot Team Members
15
16import collections
17import json
18import re
19import weakref
20
21from twisted.internet import defer
22from twisted.python.components import registerAdapter
23from zope.interface import implementer
24
25from buildbot import config
26from buildbot import util
27from buildbot.interfaces import IProperties
28from buildbot.interfaces import IRenderable
29from buildbot.util import flatten
30
31
32@implementer(IProperties)
33class Properties(util.ComparableMixin):
34
35    """
36    I represent a set of properties that can be interpolated into various
37    strings in buildsteps.
38
39    @ivar properties: dictionary mapping property values to tuples
40        (value, source), where source is a string identifying the source
41        of the property.
42
43    Objects of this class can be read like a dictionary -- in this case,
44    only the property value is returned.
45
46    As a special case, a property value of None is returned as an empty
47    string when used as a mapping.
48    """
49
50    compare_attrs = ('properties',)
51
52    def __init__(self, **kwargs):
53        """
54        @param kwargs: initial property values (for testing)
55        """
56        self.properties = {}
57        # Track keys which are 'runtime', and should not be
58        # persisted if a build is rebuilt
59        self.runtime = set()
60        self.build = None  # will be set by the Build when starting
61        self._used_secrets = {}
62        if kwargs:
63            self.update(kwargs, "TEST")
64        self._master = None
65        self._sourcestamps = None
66        self._changes = None
67
68    @property
69    def master(self):
70        if self.build is not None:
71            return self.build.master
72        return self._master
73
74    @master.setter
75    def master(self, value):
76        self._master = value
77
78    @property
79    def sourcestamps(self):
80        if self.build is not None:
81            return [b.asDict() for b in self.build.getAllSourceStamps()]
82        elif self._sourcestamps is not None:
83            return self._sourcestamps
84        raise AttributeError('neither build nor _sourcestamps are set')
85
86    @sourcestamps.setter
87    def sourcestamps(self, value):
88        self._sourcestamps = value
89
90    def getSourceStamp(self, codebase=''):
91        for source in self.sourcestamps:
92            if source['codebase'] == codebase:
93                return source
94        return None
95
96    @property
97    def changes(self):
98        if self.build is not None:
99            return [c.asChDict() for c in self.build.allChanges()]
100        elif self._changes is not None:
101            return self._changes
102        raise AttributeError('neither build nor _changes are set')
103
104    @changes.setter
105    def changes(self, value):
106        self._changes = value
107
108    @property
109    def files(self):
110        if self.build is not None:
111            return self.build.allFiles()
112        files = []
113        # self.changes, not self._changes to raise AttributeError if unset
114        for chdict in self.changes:
115            files.extend(chdict['files'])
116        return files
117
118    @classmethod
119    def fromDict(cls, propDict):
120        properties = cls()
121        for name, (value, source) in propDict.items():
122            properties.setProperty(name, value, source)
123        return properties
124
125    def __getstate__(self):
126        d = self.__dict__.copy()
127        d['build'] = None
128        return d
129
130    def __setstate__(self, d):
131        self.__dict__ = d
132        if not hasattr(self, 'runtime'):
133            self.runtime = set()
134
135    def __contains__(self, name):
136        return name in self.properties
137
138    def __getitem__(self, name):
139        """Just get the value for this property."""
140        rv = self.properties[name][0]
141        return rv
142
143    def __bool__(self):
144        return bool(self.properties)
145
146    def getPropertySource(self, name):
147        return self.properties[name][1]
148
149    def asList(self):
150        """Return the properties as a sorted list of (name, value, source)"""
151        ret = sorted([(k, v[0], v[1]) for k, v in self.properties.items()])
152        return ret
153
154    def asDict(self):
155        """Return the properties as a simple key:value dictionary,
156        properly unicoded"""
157        return self.properties.copy()
158
159    def __repr__(self):
160        return ('Properties(**' +
161                repr(dict((k, v[0]) for k, v in self.properties.items())) +
162                ')')
163
164    def update(self, dict, source, runtime=False):
165        """Update this object from a dictionary, with an explicit source specified."""
166        for k, v in dict.items():
167            self.setProperty(k, v, source, runtime=runtime)
168
169    def updateFromProperties(self, other):
170        """Update this object based on another object; the other object's """
171        self.properties.update(other.properties)
172        self.runtime.update(other.runtime)
173
174    def updateFromPropertiesNoRuntime(self, other):
175        """Update this object based on another object, but don't
176        include properties that were marked as runtime."""
177        for k, v in other.properties.items():
178            if k not in other.runtime:
179                self.properties[k] = v
180
181    # IProperties methods
182
183    def getProperty(self, name, default=None):
184        return self.properties.get(name, (default,))[0]
185
186    def hasProperty(self, name):
187        return name in self.properties
188
189    has_key = hasProperty
190
191    def setProperty(self, name, value, source, runtime=False):
192        name = util.bytes2unicode(name)
193        if not IRenderable.providedBy(value):
194            json.dumps(value)  # Let the exception propagate ...
195        source = util.bytes2unicode(source)
196
197        self.properties[name] = (value, source)
198        if runtime:
199            self.runtime.add(name)
200
201    def getProperties(self):
202        return self
203
204    def getBuild(self):
205        return self.build
206
207    def render(self, value):
208        renderable = IRenderable(value)
209        return defer.maybeDeferred(renderable.getRenderingFor, self)
210
211    # as the secrets are used in the renderable, they can pretty much arrive anywhere
212    # in the log of state strings
213    # so we have the renderable record here which secrets are used that we must remove
214    def useSecret(self, secret_value, secret_name):
215        if secret_value.strip():
216            self._used_secrets[secret_value] = "<" + secret_name + ">"
217
218    # This method shall then be called to remove secrets from any text that could be logged
219    # somewhere and that could contain secrets
220    def cleanupTextFromSecrets(self, text):
221        # Better be correct and inefficient than efficient and wrong
222        secrets = self._used_secrets
223        for k in sorted(secrets, key=len, reverse=True):
224            text = text.replace(k, secrets[k])
225        return text
226
227
228class PropertiesMixin:
229
230    """
231    A mixin to add L{IProperties} methods to a class which does not implement
232    the full interface, only getProperties() function.
233
234    This is useful because L{IProperties} methods are often called on L{Build}
235    objects without first coercing them.
236
237    @ivar set_runtime_properties: the default value for the C{runtime}
238    parameter of L{setProperty}.
239    """
240
241    set_runtime_properties = False
242
243    def getProperty(self, propname, default=None):
244        return self.getProperties().getProperty(propname, default)
245
246    def hasProperty(self, propname):
247        return self.getProperties().hasProperty(propname)
248
249    has_key = hasProperty
250
251    def setProperty(self, propname, value, source='Unknown', runtime=None):
252        # source is not optional in IProperties, but is optional here to avoid
253        # breaking user-supplied code that fails to specify a source
254        props = self.getProperties()
255        if runtime is None:
256            runtime = self.set_runtime_properties
257        props.setProperty(propname, value, source, runtime=runtime)
258
259    def render(self, value):
260        return self.getProperties().render(value)
261
262
263@implementer(IRenderable)
264class RenderableOperatorsMixin:
265
266    """
267    Properties and Interpolate instances can be manipulated with standard operators.
268    """
269
270    def __eq__(self, other):
271        return _OperatorRenderer(self, other, "==", lambda v1, v2: v1 == v2)
272
273    def __ne__(self, other):
274        return _OperatorRenderer(self, other, "!=", lambda v1, v2: v1 != v2)
275
276    def __lt__(self, other):
277        return _OperatorRenderer(self, other, "<", lambda v1, v2: v1 < v2)
278
279    def __le__(self, other):
280        return _OperatorRenderer(self, other, "<=", lambda v1, v2: v1 <= v2)
281
282    def __gt__(self, other):
283        return _OperatorRenderer(self, other, ">", lambda v1, v2: v1 > v2)
284
285    def __ge__(self, other):
286        return _OperatorRenderer(self, other, ">=", lambda v1, v2: v1 >= v2)
287
288    def __add__(self, other):
289        return _OperatorRenderer(self, other, "+", lambda v1, v2: v1 + v2)
290
291    def __sub__(self, other):
292        return _OperatorRenderer(self, other, "-", lambda v1, v2: v1 - v2)
293
294    def __mul__(self, other):
295        return _OperatorRenderer(self, other, "*", lambda v1, v2: v1 * v2)
296
297    def __truediv__(self, other):
298        return _OperatorRenderer(self, other, "/", lambda v1, v2: v1 / v2)
299
300    def __floordiv__(self, other):
301        return _OperatorRenderer(self, other, "//", lambda v1, v2: v1 // v2)
302
303    def __mod__(self, other):
304        return _OperatorRenderer(self, other, "%", lambda v1, v2: v1 % v2)
305
306    # we cannot use this trick to overload the 'in' operator, as python will force the result
307    # of __contains__ to a boolean, forcing it to True all the time
308    # so we mimic sqlalchemy and make a in_ method
309    def in_(self, other):
310        return _OperatorRenderer(self, other, "in", lambda v1, v2: v1 in v2)
311
312
313@implementer(IRenderable)
314class _OperatorRenderer(RenderableOperatorsMixin, util.ComparableMixin):
315    """
316    An instance of this class renders a comparison given by a operator
317    function with v1 and v2
318
319    """
320
321    compare_attrs = ('fn',)
322
323    def __init__(self, v1, v2, cstr, comparator):
324        self.v1, self.v2, self.comparator, self.cstr = v1, v2, comparator, cstr
325
326    @defer.inlineCallbacks
327    def getRenderingFor(self, props):
328        v1, v2 = yield props.render((self.v1, self.v2))
329        return self.comparator(v1, v2)
330
331    def __repr__(self):
332        return '%r %s %r' % (self.v1, self.cstr, self.v2)
333
334
335class _PropertyMap:
336
337    """
338    Privately-used mapping object to implement WithProperties' substitutions,
339    including the rendering of None as ''.
340    """
341    colon_minus_re = re.compile(r"(.*):-(.*)")
342    colon_tilde_re = re.compile(r"(.*):~(.*)")
343    colon_plus_re = re.compile(r"(.*):\+(.*)")
344
345    def __init__(self, properties):
346        # use weakref here to avoid a reference loop
347        self.properties = weakref.ref(properties)
348        self.temp_vals = {}
349
350    def __getitem__(self, key):
351        properties = self.properties()
352        assert properties is not None
353
354        def colon_minus(mo):
355            # %(prop:-repl)s
356            # if prop exists, use it; otherwise, use repl
357            prop, repl = mo.group(1, 2)
358            if prop in self.temp_vals:
359                return self.temp_vals[prop]
360            elif prop in properties:
361                return properties[prop]
362            return repl
363
364        def colon_tilde(mo):
365            # %(prop:~repl)s
366            # if prop exists and is true (nonempty), use it; otherwise, use
367            # repl
368            prop, repl = mo.group(1, 2)
369            if prop in self.temp_vals and self.temp_vals[prop]:
370                return self.temp_vals[prop]
371            elif prop in properties and properties[prop]:
372                return properties[prop]
373            return repl
374
375        def colon_plus(mo):
376            # %(prop:+repl)s
377            # if prop exists, use repl; otherwise, an empty string
378            prop, repl = mo.group(1, 2)
379            if prop in properties or prop in self.temp_vals:
380                return repl
381            return ''
382
383        for regexp, fn in [
384            (self.colon_minus_re, colon_minus),
385            (self.colon_tilde_re, colon_tilde),
386            (self.colon_plus_re, colon_plus),
387        ]:
388            mo = regexp.match(key)
389            if mo:
390                rv = fn(mo)
391                break
392        else:
393            # If explicitly passed as a kwarg, use that,
394            # otherwise, use the property value.
395            if key in self.temp_vals:
396                rv = self.temp_vals[key]
397            else:
398                rv = properties[key]
399
400        # translate 'None' to an empty string
401        if rv is None:
402            rv = ''
403        return rv
404
405    def add_temporary_value(self, key, val):
406        'Add a temporary value (to support keyword arguments to WithProperties)'
407        self.temp_vals[key] = val
408
409
410@implementer(IRenderable)
411class WithProperties(util.ComparableMixin):
412
413    """
414    This is a marker class, used fairly widely to indicate that we
415    want to interpolate build properties.
416    """
417
418    compare_attrs = ('fmtstring', 'args', 'lambda_subs')
419
420    def __init__(self, fmtstring, *args, **lambda_subs):
421        self.fmtstring = fmtstring
422        self.args = args
423        if not self.args:
424            self.lambda_subs = lambda_subs
425            for key, val in self.lambda_subs.items():
426                if not callable(val):
427                    raise ValueError(
428                        'Value for lambda substitution "{}" must be callable.'.format(key))
429        elif lambda_subs:
430            raise ValueError(
431                'WithProperties takes either positional or keyword substitutions, not both.')
432
433    def getRenderingFor(self, build):
434        pmap = _PropertyMap(build.getProperties())
435        if self.args:
436            strings = []
437            for name in self.args:
438                strings.append(pmap[name])
439            s = self.fmtstring % tuple(strings)
440        else:
441            for k, v in self.lambda_subs.items():
442                pmap.add_temporary_value(k, v(build))
443            s = self.fmtstring % pmap
444        return s
445
446
447class _NotHasKey(util.ComparableMixin):
448
449    """A marker for missing ``hasKey`` parameter.
450
451    To withstand ``deepcopy``, ``reload`` and pickle serialization round trips,
452    check it with ``==`` or ``!=``.
453    """
454    compare_attrs = ()
455
456
457# any instance of _NotHasKey would do, yet we don't want to create and delete
458# them all the time
459_notHasKey = _NotHasKey()
460
461
462@implementer(IRenderable)
463class _Lookup(util.ComparableMixin):
464
465    compare_attrs = (
466        'value', 'index', 'default', 'defaultWhenFalse', 'hasKey', 'elideNoneAs')
467
468    def __init__(self, value, index, default=None,
469                 defaultWhenFalse=True, hasKey=_notHasKey,
470                 elideNoneAs=None):
471        self.value = value
472        self.index = index
473        self.default = default
474        self.defaultWhenFalse = defaultWhenFalse
475        self.hasKey = hasKey
476        self.elideNoneAs = elideNoneAs
477
478    def __repr__(self):
479        return '_Lookup({}, {}{}{}{}{})'.format(
480            repr(self.value),
481            repr(self.index),
482            ', default={}'.format(repr(self.default)) if self.default is not None else '',
483            ', defaultWhenFalse=False' if not self.defaultWhenFalse else '',
484            ', hasKey={}'.format(repr(self.hasKey)) if self.hasKey != _notHasKey else '',
485            ', elideNoneAs={}'.format(repr(self.elideNoneAs))
486            if self.elideNoneAs is not None else ''
487            )
488
489    @defer.inlineCallbacks
490    def getRenderingFor(self, build):
491        value = build.render(self.value)
492        index = build.render(self.index)
493        value, index = yield defer.gatherResults([value, index])
494        if index not in value:
495            rv = yield build.render(self.default)
496        else:
497            if self.defaultWhenFalse:
498                rv = yield build.render(value[index])
499                if not rv:
500                    rv = yield build.render(self.default)
501                elif self.hasKey != _notHasKey:
502                    rv = yield build.render(self.hasKey)
503            elif self.hasKey != _notHasKey:
504                rv = yield build.render(self.hasKey)
505            else:
506                rv = yield build.render(value[index])
507        if rv is None:
508            rv = yield build.render(self.elideNoneAs)
509        return rv
510
511
512def _getInterpolationList(fmtstring):
513    # TODO: Verify that no positional substitutions are requested
514    dd = collections.defaultdict(str)
515    fmtstring % dd
516    return list(dd)
517
518
519@implementer(IRenderable)
520class _PropertyDict:
521
522    def getRenderingFor(self, build):
523        return build.getProperties()
524
525
526_thePropertyDict = _PropertyDict()
527
528
529@implementer(IRenderable)
530class _WorkerPropertyDict:
531
532    def getRenderingFor(self, build):
533        return build.getBuild().getWorkerInfo()
534
535
536_theWorkerPropertyDict = _WorkerPropertyDict()
537
538
539@implementer(IRenderable)
540class _SecretRenderer:
541
542    def __init__(self, secret_name):
543        self.secret_name = secret_name
544
545    @defer.inlineCallbacks
546    def getRenderingFor(self, properties):
547        secretsSrv = properties.master.namedServices.get("secrets")
548        if not secretsSrv:
549            error_message = "secrets service not started, need to configure" \
550                            " SecretManager in c['services'] to use 'secrets'" \
551                            "in Interpolate"
552            raise KeyError(error_message)
553        credsservice = properties.master.namedServices['secrets']
554        secret_detail = yield credsservice.get(self.secret_name)
555        if secret_detail is None:
556            raise KeyError("secret key {} is not found in any provider".format(self.secret_name))
557        properties.useSecret(secret_detail.value, self.secret_name)
558        return secret_detail.value
559
560
561class Secret(_SecretRenderer):
562
563    def __repr__(self):
564        return "Secret({0})".format(self.secret_name)
565
566
567class _SecretIndexer:
568
569    def __contains__(self, password):
570        return True
571
572    def __getitem__(self, password):
573        return _SecretRenderer(password)
574
575
576@implementer(IRenderable)
577class _SourceStampDict(util.ComparableMixin):
578
579    compare_attrs = ('codebase',)
580
581    def __init__(self, codebase):
582        self.codebase = codebase
583
584    def getRenderingFor(self, props):
585        ss = props.getSourceStamp(self.codebase)
586        if ss:
587            return ss
588        return {}
589
590
591@implementer(IRenderable)
592class _Lazy(util.ComparableMixin):
593
594    compare_attrs = ('value',)
595
596    def __init__(self, value):
597        self.value = value
598
599    def getRenderingFor(self, build):
600        return self.value
601
602    def __repr__(self):
603        return '_Lazy(%r)' % self.value
604
605
606@implementer(IRenderable)
607class Interpolate(RenderableOperatorsMixin, util.ComparableMixin):
608
609    """
610    This is a marker class, used fairly widely to indicate that we
611    want to interpolate build properties.
612    """
613
614    compare_attrs = ('fmtstring', 'args', 'kwargs')
615
616    identifier_re = re.compile(r'^[\w._-]*$')
617
618    def __init__(self, fmtstring, *args, **kwargs):
619        self.fmtstring = fmtstring
620        self.args = args
621        self.kwargs = kwargs
622        if self.args and self.kwargs:
623            config.error("Interpolate takes either positional or keyword "
624                         "substitutions, not both.")
625        if not self.args:
626            self.interpolations = {}
627            self._parse(fmtstring)
628
629    def __repr__(self):
630        if self.args:
631            return 'Interpolate(%r, *%r)' % (self.fmtstring, self.args)
632        elif self.kwargs:
633            return 'Interpolate(%r, **%r)' % (self.fmtstring, self.kwargs)
634        return 'Interpolate(%r)' % (self.fmtstring,)
635
636    @staticmethod
637    def _parse_prop(arg):
638        try:
639            prop, repl = arg.split(":", 1)
640        except ValueError:
641            prop, repl = arg, None
642        if not Interpolate.identifier_re.match(prop):
643            config.error(
644                "Property name must be alphanumeric for prop Interpolation '{}'".format(arg))
645            prop = repl = None
646
647        return _thePropertyDict, prop, repl
648
649    @staticmethod
650    def _parse_secret(arg):
651        try:
652            secret, repl = arg.split(":", 1)
653        except ValueError:
654            secret, repl = arg, None
655        return _SecretIndexer(), secret, repl
656
657    @staticmethod
658    def _parse_src(arg):
659        # TODO: Handle changes
660        try:
661            codebase, attr, repl = arg.split(":", 2)
662        except ValueError:
663            try:
664                codebase, attr = arg.split(":", 1)
665                repl = None
666            except ValueError:
667                config.error(("Must specify both codebase and attribute for "
668                              "src Interpolation '{}'").format(arg))
669                return {}, None, None
670
671        if not Interpolate.identifier_re.match(codebase):
672            config.error(
673                "Codebase must be alphanumeric for src Interpolation '{}'".format(arg))
674            codebase = attr = repl = None
675        if not Interpolate.identifier_re.match(attr):
676            config.error(
677                "Attribute must be alphanumeric for src Interpolation '{}'".format(arg))
678            codebase = attr = repl = None
679        return _SourceStampDict(codebase), attr, repl
680
681    def _parse_worker(self, arg):
682        try:
683            prop, repl = arg.split(":", 1)
684        except ValueError:
685            prop, repl = arg, None
686        return _theWorkerPropertyDict, prop, repl
687
688    def _parse_kw(self, arg):
689        try:
690            kw, repl = arg.split(":", 1)
691        except ValueError:
692            kw, repl = arg, None
693        if not Interpolate.identifier_re.match(kw):
694            config.error(
695                "Keyword must be alphanumeric for kw Interpolation '{}'".format(arg))
696            kw = repl = None
697        return _Lazy(self.kwargs), kw, repl
698
699    def _parseSubstitution(self, fmt):
700        try:
701            key, arg = fmt.split(":", 1)
702        except ValueError:
703            config.error(
704                "invalid Interpolate substitution without selector '{}'".format(fmt))
705            return None
706
707        fn = getattr(self, "_parse_" + key, None)
708        if not fn:
709            config.error("invalid Interpolate selector '{}'".format(key))
710            return None
711        return fn(arg)
712
713    @staticmethod
714    def _splitBalancedParen(delim, arg):
715        parenCount = 0
716        for i, val in enumerate(arg):
717            if arg[i] == "(":
718                parenCount += 1
719            if arg[i] == ")":
720                parenCount -= 1
721                if parenCount < 0:
722                    raise ValueError
723            if parenCount == 0 and arg[i] == delim:
724                return arg[0:i], arg[i + 1:]
725        return arg
726
727    def _parseColon_minus(self, d, kw, repl):
728        return _Lookup(d, kw,
729                       default=Interpolate(repl, **self.kwargs),
730                       defaultWhenFalse=False,
731                       elideNoneAs='')
732
733    def _parseColon_tilde(self, d, kw, repl):
734        return _Lookup(d, kw,
735                       default=Interpolate(repl, **self.kwargs),
736                       defaultWhenFalse=True,
737                       elideNoneAs='')
738
739    def _parseColon_plus(self, d, kw, repl):
740        return _Lookup(d, kw,
741                       hasKey=Interpolate(repl, **self.kwargs),
742                       default='',
743                       defaultWhenFalse=False,
744                       elideNoneAs='')
745
746    def _parseColon_ternary(self, d, kw, repl, defaultWhenFalse=False):
747        delim = repl[0]
748        if delim == '(':
749            config.error("invalid Interpolate ternary delimiter '('")
750            return None
751        try:
752            truePart, falsePart = self._splitBalancedParen(delim, repl[1:])
753        except ValueError:
754            config.error("invalid Interpolate ternary expression '{}' with delimiter '{}'".format(
755                repl[1:], repl[0]))
756            return None
757        return _Lookup(d, kw,
758                       hasKey=Interpolate(truePart, **self.kwargs),
759                       default=Interpolate(falsePart, **self.kwargs),
760                       defaultWhenFalse=defaultWhenFalse,
761                       elideNoneAs='')
762
763    def _parseColon_ternary_hash(self, d, kw, repl):
764        return self._parseColon_ternary(d, kw, repl, defaultWhenFalse=True)
765
766    def _parse(self, fmtstring):
767        keys = _getInterpolationList(fmtstring)
768        for key in keys:
769            if key not in self.interpolations:
770                d, kw, repl = self._parseSubstitution(key)
771                if repl is None:
772                    repl = '-'
773                for pattern, fn in [
774                    ("-", self._parseColon_minus),
775                    ("~", self._parseColon_tilde),
776                    ("+", self._parseColon_plus),
777                    ("?", self._parseColon_ternary),
778                    ("#?", self._parseColon_ternary_hash)
779                ]:
780                    junk, matches, tail = repl.partition(pattern)
781                    if not junk and matches:
782                        self.interpolations[key] = fn(d, kw, tail)
783                        break
784                if key not in self.interpolations:
785                    config.error("invalid Interpolate default type '{}'".format(repl[0]))
786
787    def getRenderingFor(self, build):
788        props = build.getProperties()
789        if self.args:
790            d = props.render(self.args)
791            d.addCallback(lambda args:
792                          self.fmtstring % tuple(args))
793        else:
794            d = props.render(self.interpolations)
795            d.addCallback(lambda res:
796                          self.fmtstring % res)
797        return d
798
799
800@implementer(IRenderable)
801class Property(RenderableOperatorsMixin, util.ComparableMixin):
802
803    """
804    An instance of this class renders a property of a build.
805    """
806
807    compare_attrs = ('key', 'default', 'defaultWhenFalse')
808
809    def __init__(self, key, default=None, defaultWhenFalse=True):
810        """
811        @param key: Property to render.
812        @param default: Value to use if property isn't set.
813        @param defaultWhenFalse: When true (default), use default value
814            if property evaluates to False. Otherwise, use default value
815            only when property isn't set.
816        """
817        self.key = key
818        self.default = default
819        self.defaultWhenFalse = defaultWhenFalse
820
821    def __repr__(self):
822        return "Property({0})".format(self.key)
823
824    def getRenderingFor(self, props):
825        if self.defaultWhenFalse:
826            d = props.render(props.getProperty(self.key))
827
828            @d.addCallback
829            def checkDefault(rv):
830                if rv:
831                    return rv
832                return props.render(self.default)
833            return d
834
835        if props.hasProperty(self.key):
836            return props.render(props.getProperty(self.key))
837        return props.render(self.default)
838
839
840@implementer(IRenderable)
841class FlattenList(RenderableOperatorsMixin, util.ComparableMixin):
842
843    """
844    An instance of this class flattens all nested lists in a list
845    """
846
847    compare_attrs = ('nestedlist')
848
849    def __init__(self, nestedlist, types=(list, tuple)):
850        """
851        @param nestedlist: a list of values to render
852        @param types: only flatten these types. defaults to (list, tuple)
853        """
854        self.nestedlist = nestedlist
855        self.types = types
856
857    def getRenderingFor(self, props):
858        d = props.render(self.nestedlist)
859
860        @d.addCallback
861        def flat(r):
862            return flatten(r, self.types)
863        return d
864
865    def __add__(self, b):
866        if isinstance(b, FlattenList):
867            b = b.nestedlist
868        return FlattenList(self.nestedlist + b, self.types)
869
870
871@implementer(IRenderable)
872class _Renderer(util.ComparableMixin):
873
874    compare_attrs = ('fn',)
875
876    def __init__(self, fn):
877        self.fn = fn
878        self.args = []
879        self.kwargs = {}
880
881    def withArgs(self, *args, **kwargs):
882        new_renderer = _Renderer(self.fn)
883        new_renderer.args = self.args + list(args)
884        new_renderer.kwargs = dict(self.kwargs)
885        new_renderer.kwargs.update(kwargs)
886        return new_renderer
887
888    @defer.inlineCallbacks
889    def getRenderingFor(self, props):
890        args = yield props.render(self.args)
891        kwargs = yield props.render(self.kwargs)
892
893        # We allow the renderer fn to return a renderable for convenience
894        result = yield self.fn(props, *args, **kwargs)
895        result = yield props.render(result)
896        return result
897
898    def __repr__(self):
899        if self.args or self.kwargs:
900            return 'renderer(%r, args=%r, kwargs=%r)' % (self.fn, self.args,
901                                                         self.kwargs)
902        return 'renderer(%r)' % (self.fn,)
903
904
905def renderer(fn):
906    return _Renderer(fn)
907
908
909@implementer(IRenderable)
910class _DefaultRenderer:
911
912    """
913    Default IRenderable adaptor. Calls .getRenderingFor if available, otherwise
914    returns argument unchanged.
915    """
916
917    def __init__(self, value):
918        try:
919            self.renderer = value.getRenderingFor
920        except AttributeError:
921            self.renderer = lambda _: value
922
923    def getRenderingFor(self, build):
924        return self.renderer(build)
925
926
927registerAdapter(_DefaultRenderer, object, IRenderable)
928
929
930@implementer(IRenderable)
931class _ListRenderer:
932
933    """
934    List IRenderable adaptor. Maps Build.render over the list.
935    """
936
937    def __init__(self, value):
938        self.value = value
939
940    def getRenderingFor(self, build):
941        return defer.gatherResults([build.render(e) for e in self.value])
942
943
944registerAdapter(_ListRenderer, list, IRenderable)
945
946
947@implementer(IRenderable)
948class _TupleRenderer:
949
950    """
951    Tuple IRenderable adaptor. Maps Build.render over the tuple.
952    """
953
954    def __init__(self, value):
955        self.value = value
956
957    def getRenderingFor(self, build):
958        d = defer.gatherResults([build.render(e) for e in self.value])
959        d.addCallback(tuple)
960        return d
961
962
963registerAdapter(_TupleRenderer, tuple, IRenderable)
964
965
966@implementer(IRenderable)
967class _DictRenderer:
968
969    """
970    Dict IRenderable adaptor. Maps Build.render over the keys and values in the dict.
971    """
972
973    def __init__(self, value):
974        self.value = _ListRenderer(
975            [_TupleRenderer((k, v)) for k, v in value.items()])
976
977    def getRenderingFor(self, build):
978        d = self.value.getRenderingFor(build)
979        d.addCallback(dict)
980        return d
981
982
983registerAdapter(_DictRenderer, dict, IRenderable)
984
985
986@implementer(IRenderable)
987class Transform:
988
989    """
990    A renderable that combines other renderables' results using an arbitrary function.
991    """
992
993    def __init__(self, function, *args, **kwargs):
994        if not callable(function) and not IRenderable.providedBy(function):
995            config.error(
996                "function given to Transform neither callable nor renderable")
997
998        self._function = function
999        self._args = args
1000        self._kwargs = kwargs
1001
1002    @defer.inlineCallbacks
1003    def getRenderingFor(self, iprops):
1004        rfunction = yield iprops.render(self._function)
1005        rargs = yield iprops.render(self._args)
1006        rkwargs = yield iprops.render(self._kwargs)
1007        return rfunction(*rargs, **rkwargs)
1008