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