Package buildbot :: Package process :: Module properties
[frames] | no frames]

Source Code for Module buildbot.process.properties

  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   
 16  import collections 
 17  import re 
 18  import warnings 
 19  import weakref 
 20  from buildbot import config, util 
 21  from buildbot.util import json 
 22  from buildbot.interfaces import IRenderable, IProperties 
 23  from twisted.internet import defer 
 24  from twisted.python.components import registerAdapter 
 25  from zope.interface import implements 
26 27 -class Properties(util.ComparableMixin):
28 """ 29 I represent a set of properties that can be interpolated into various 30 strings in buildsteps. 31 32 @ivar properties: dictionary mapping property values to tuples 33 (value, source), where source is a string identifing the source 34 of the property. 35 36 Objects of this class can be read like a dictionary -- in this case, 37 only the property value is returned. 38 39 As a special case, a property value of None is returned as an empty 40 string when used as a mapping. 41 """ 42 43 compare_attrs = ('properties',) 44 implements(IProperties) 45
46 - def __init__(self, **kwargs):
47 """ 48 @param kwargs: initial property values (for testing) 49 """ 50 self.properties = {} 51 # Track keys which are 'runtime', and should not be 52 # persisted if a build is rebuilt 53 self.runtime = set() 54 self.build = None # will be set by the Build when starting 55 if kwargs: self.update(kwargs, "TEST")
56 57 @classmethod
58 - def fromDict(cls, propDict):
59 properties = cls() 60 for name, (value, source) in propDict.iteritems(): 61 properties.setProperty(name, value, source) 62 return properties
63
64 - def __getstate__(self):
65 d = self.__dict__.copy() 66 d['build'] = None 67 return d
68
69 - def __setstate__(self, d):
70 self.__dict__ = d 71 if not hasattr(self, 'runtime'): 72 self.runtime = set()
73
74 - def __contains__(self, name):
75 return name in self.properties
76
77 - def __getitem__(self, name):
78 """Just get the value for this property.""" 79 rv = self.properties[name][0] 80 return rv
81
82 - def __nonzero__(self):
83 return not not self.properties
84
85 - def getPropertySource(self, name):
86 return self.properties[name][1]
87
88 - def asList(self):
89 """Return the properties as a sorted list of (name, value, source)""" 90 l = [ (k, v[0], v[1]) for k,v in self.properties.iteritems() ] 91 l.sort() 92 return l
93
94 - def asDict(self):
95 """Return the properties as a simple key:value dictionary""" 96 return dict(self.properties)
97
98 - def __repr__(self):
99 return ('Properties(**' + 100 repr(dict((k,v[0]) for k,v in self.properties.iteritems())) + 101 ')')
102
103 - def update(self, dict, source, runtime=False):
104 """Update this object from a dictionary, with an explicit source specified.""" 105 for k, v in dict.items(): 106 self.setProperty(k, v, source, runtime=runtime)
107
108 - def updateFromProperties(self, other):
109 """Update this object based on another object; the other object's """ 110 self.properties.update(other.properties) 111 self.runtime.update(other.runtime)
112
113 - def updateFromPropertiesNoRuntime(self, other):
114 """Update this object based on another object, but don't 115 include properties that were marked as runtime.""" 116 for k,v in other.properties.iteritems(): 117 if k not in other.runtime: 118 self.properties[k] = v
119 120 # IProperties methods 121
122 - def getProperty(self, name, default=None):
123 return self.properties.get(name, (default,))[0]
124
125 - def hasProperty(self, name):
126 return self.properties.has_key(name)
127 128 has_key = hasProperty 129
130 - def setProperty(self, name, value, source, runtime=False):
131 try: 132 json.dumps(value) 133 except TypeError: 134 warnings.warn( 135 "Non jsonable properties are not explicitly supported and" + 136 "will be explicitly disallowed in a future version.", 137 DeprecationWarning, stacklevel=2) 138 139 self.properties[name] = (value, source) 140 if runtime: 141 self.runtime.add(name)
142
143 - def getProperties(self):
144 return self
145
146 - def getBuild(self):
147 return self.build
148
149 - def render(self, value):
150 renderable = IRenderable(value) 151 return defer.maybeDeferred(renderable.getRenderingFor, self)
152
153 154 -class PropertiesMixin:
155 """ 156 A mixin to add L{IProperties} methods to a class which does not implement 157 the interface, but which can be coerced to the interface via an adapter. 158 159 This is useful because L{IProperties} methods are often called on L{Build} 160 and L{BuildStatus} objects without first coercing them. 161 162 @ivar set_runtime_properties: the default value for the C{runtime} 163 parameter of L{setProperty}. 164 """ 165 166 set_runtime_properties = False 167
168 - def getProperty(self, propname, default=None):
169 props = IProperties(self) 170 return props.getProperty(propname, default)
171
172 - def hasProperty(self, propname):
173 props = IProperties(self) 174 return props.hasProperty(propname)
175 176 has_key = hasProperty 177
178 - def setProperty(self, propname, value, source='Unknown', runtime=None):
179 # source is not optional in IProperties, but is optional here to avoid 180 # breaking user-supplied code that fails to specify a source 181 props = IProperties(self) 182 if runtime is None: 183 runtime = self.set_runtime_properties 184 props.setProperty(propname, value, source, runtime=runtime)
185
186 - def getProperties(self):
187 return IProperties(self)
188
189 - def render(self, value):
190 props = IProperties(self) 191 return props.render(value)
192
193 194 195 -class _PropertyMap(object):
196 """ 197 Privately-used mapping object to implement WithProperties' substitutions, 198 including the rendering of None as ''. 199 """ 200 colon_minus_re = re.compile(r"(.*):-(.*)") 201 colon_tilde_re = re.compile(r"(.*):~(.*)") 202 colon_plus_re = re.compile(r"(.*):\+(.*)")
203 - def __init__(self, properties):
204 # use weakref here to avoid a reference loop 205 self.properties = weakref.ref(properties) 206 self.temp_vals = {}
207
208 - def __getitem__(self, key):
209 properties = self.properties() 210 assert properties is not None 211 212 def colon_minus(mo): 213 # %(prop:-repl)s 214 # if prop exists, use it; otherwise, use repl 215 prop, repl = mo.group(1,2) 216 if prop in self.temp_vals: 217 return self.temp_vals[prop] 218 elif properties.has_key(prop): 219 return properties[prop] 220 else: 221 return repl
222 223 def colon_tilde(mo): 224 # %(prop:~repl)s 225 # if prop exists and is true (nonempty), use it; otherwise, use repl 226 prop, repl = mo.group(1,2) 227 if prop in self.temp_vals and self.temp_vals[prop]: 228 return self.temp_vals[prop] 229 elif properties.has_key(prop) and properties[prop]: 230 return properties[prop] 231 else: 232 return repl
233 234 def colon_plus(mo): 235 # %(prop:+repl)s 236 # if prop exists, use repl; otherwise, an empty string 237 prop, repl = mo.group(1,2) 238 if properties.has_key(prop) or prop in self.temp_vals: 239 return repl 240 else: 241 return '' 242 243 for regexp, fn in [ 244 ( self.colon_minus_re, colon_minus ), 245 ( self.colon_tilde_re, colon_tilde ), 246 ( self.colon_plus_re, colon_plus ), 247 ]: 248 mo = regexp.match(key) 249 if mo: 250 rv = fn(mo) 251 break 252 else: 253 # If explicitly passed as a kwarg, use that, 254 # otherwise, use the property value. 255 if key in self.temp_vals: 256 rv = self.temp_vals[key] 257 else: 258 rv = properties[key] 259 260 # translate 'None' to an empty string 261 if rv is None: rv = '' 262 return rv 263
264 - def add_temporary_value(self, key, val):
265 'Add a temporary value (to support keyword arguments to WithProperties)' 266 self.temp_vals[key] = val
267
268 -class WithProperties(util.ComparableMixin):
269 """ 270 This is a marker class, used fairly widely to indicate that we 271 want to interpolate build properties. 272 """ 273 274 implements(IRenderable) 275 compare_attrs = ('fmtstring', 'args') 276
277 - def __init__(self, fmtstring, *args, **lambda_subs):
278 self.fmtstring = fmtstring 279 self.args = args 280 if not self.args: 281 self.lambda_subs = lambda_subs 282 for key, val in self.lambda_subs.iteritems(): 283 if not callable(val): 284 raise ValueError('Value for lambda substitution "%s" must be callable.' % key) 285 elif lambda_subs: 286 raise ValueError('WithProperties takes either positional or keyword substitutions, not both.')
287
288 - def getRenderingFor(self, build):
289 pmap = _PropertyMap(build.getProperties()) 290 if self.args: 291 strings = [] 292 for name in self.args: 293 strings.append(pmap[name]) 294 s = self.fmtstring % tuple(strings) 295 else: 296 for k,v in self.lambda_subs.iteritems(): 297 pmap.add_temporary_value(k, v(build)) 298 s = self.fmtstring % pmap 299 return s
300 301 302 303 _notHasKey = object() ## Marker object for _Lookup(..., hasKey=...) default
304 -class _Lookup(util.ComparableMixin):
305 implements(IRenderable) 306
307 - def __init__(self, value, index, default=None, 308 defaultWhenFalse=True, hasKey=_notHasKey, 309 elideNoneAs=None):
310 self.value = value 311 self.index = index 312 self.default = default 313 self.defaultWhenFalse = defaultWhenFalse 314 self.hasKey = hasKey 315 self.elideNoneAs = elideNoneAs
316 317 @defer.inlineCallbacks
318 - def getRenderingFor(self, build):
319 value = build.render(self.value) 320 index = build.render(self.index) 321 value, index = yield defer.gatherResults([value, index]) 322 if not value.has_key(index): 323 rv = yield build.render(self.default) 324 else: 325 if self.defaultWhenFalse: 326 rv = yield build.render(value[index]) 327 if not rv: 328 rv = yield build.render(self.default) 329 elif self.hasKey is not _notHasKey: 330 rv = yield build.render(self.hasKey) 331 elif self.hasKey is not _notHasKey: 332 rv = yield build.render(self.hasKey) 333 else: 334 rv = yield build.render(value[index]) 335 if rv is None: 336 rv = yield build.render(self.elideNoneAs) 337 defer.returnValue(rv)
338
339 340 -def _getInterpolationList(fmtstring):
341 # TODO: Verify that no positial substitutions are requested 342 dd = collections.defaultdict(str) 343 fmtstring % dd 344 return dd.keys()
345
346 347 -class _PropertyDict(object):
348 implements(IRenderable)
349 - def getRenderingFor(self, build):
350 return build.getProperties()
351 _thePropertyDict = _PropertyDict()
352 353 -class _SourceStampDict(object):
354 implements(IRenderable)
355 - def __init__(self, codebase):
356 self.codebase = codebase
357 - def getRenderingFor(self, build):
358 ss = build.getBuild().getSourceStamp(self.codebase) 359 if ss: 360 return ss.asDict() 361 else: 362 return {}
363
364 365 -class _Lazy(object):
366 implements(IRenderable)
367 - def __init__(self, value):
368 self.value = value
369 - def getRenderingFor(self, build):
370 return self.value
371
372 373 -class Interpolate(util.ComparableMixin):
374 """ 375 This is a marker class, used fairly widely to indicate that we 376 want to interpolate build properties. 377 """ 378 379 implements(IRenderable) 380 compare_attrs = ('fmtstring', 'args', 'kwargs') 381 382 identifier_re = re.compile('^[\w-]*$') 383
384 - def __init__(self, fmtstring, *args, **kwargs):
385 self.fmtstring = fmtstring 386 self.args = args 387 self.kwargs = kwargs 388 if self.args and self.kwargs: 389 config.error("Interpolate takes either positional or keyword " 390 "substitutions, not both.") 391 if not self.args: 392 self.interpolations = {} 393 self._parse(fmtstring) 394 395 @staticmethod
396 - def _parse_prop(arg):
397 try: 398 prop, repl = arg.split(":", 1) 399 except ValueError: 400 prop, repl = arg, None 401 if not Interpolate.identifier_re.match(prop): 402 config.error("Property name must be alphanumeric for prop Interpolation '%s'" % arg) 403 prop = repl = None 404 return _thePropertyDict, prop, repl
405 406 @staticmethod
407 - def _parse_src(arg):
408 ## TODO: Handle changes 409 try: 410 codebase, attr, repl = arg.split(":", 2) 411 except ValueError: 412 try: 413 codebase, attr = arg.split(":",1) 414 repl = None 415 except ValueError: 416 config.error("Must specify both codebase and attribute for src Interpolation '%s'" % arg) 417 codebase = attr = repl = None 418 if not Interpolate.identifier_re.match(codebase): 419 config.error("Codebase must be alphanumeric for src Interpolation '%s'" % arg) 420 codebase = attr = repl = None 421 if not Interpolate.identifier_re.match(attr): 422 config.error("Attribute must be alphanumeric for src Interpolation '%s'" % arg) 423 codebase = attr = repl = None 424 return _SourceStampDict(codebase), attr, repl
425
426 - def _parse_kw(self, arg):
427 try: 428 kw, repl = arg.split(":", 1) 429 except ValueError: 430 kw, repl = arg, None 431 if not Interpolate.identifier_re.match(kw): 432 config.error("Keyword must be alphanumeric for kw Interpolation '%s'" % arg) 433 kw = repl = None 434 return _Lazy(self.kwargs), kw, repl
435
436 - def _parseSubstitution(self, fmt):
437 try: 438 key, arg = fmt.split(":", 1) 439 except ValueError: 440 config.error("invalid Interpolate substitution without selector '%s'" % fmt) 441 return 442 443 fn = getattr(self, "_parse_" + key, None) 444 if not fn: 445 config.error("invalid Interpolate selector '%s'" % key) 446 return None 447 else: 448 return fn(arg)
449 450 @staticmethod
451 - def _splitBalancedParen(delim, arg):
452 parenCount = 0 453 for i in range(0, len(arg)): 454 if arg[i] == "(": 455 parenCount += 1 456 if arg[i] == ")": 457 parenCount -= 1 458 if parenCount < 0: 459 raise ValueError 460 if parenCount == 0 and arg[i] == delim: 461 return arg[0:i], arg[i+1:] 462 return arg
463
464 - def _parseColon_minus(self, d, kw, repl):
465 return _Lookup(d, kw, 466 default=Interpolate(repl, **self.kwargs), 467 defaultWhenFalse=False, 468 elideNoneAs='')
469
470 - def _parseColon_tilde(self, d, kw, repl):
471 return _Lookup(d, kw, 472 default=Interpolate(repl, **self.kwargs), 473 defaultWhenFalse=True, 474 elideNoneAs='')
475
476 - def _parseColon_plus(self, d, kw, repl):
477 return _Lookup(d, kw, 478 hasKey=Interpolate(repl, **self.kwargs), 479 default='', 480 defaultWhenFalse=False, 481 elideNoneAs='')
482
483 - def _parseColon_ternary(self, d, kw, repl, defaultWhenFalse=False):
484 delim = repl[0] 485 if delim == '(': 486 config.error("invalid Interpolate ternary delimiter '('") 487 return None 488 try: 489 truePart, falsePart = self._splitBalancedParen(delim, repl[1:]) 490 except ValueError: 491 config.error("invalid Interpolate ternary expression '%s' with delimiter '%s'" % (repl[1:], repl[0])) 492 return None 493 return _Lookup(d, kw, 494 hasKey=Interpolate(truePart, **self.kwargs), 495 default=Interpolate(falsePart, **self.kwargs), 496 defaultWhenFalse=defaultWhenFalse, 497 elideNoneAs='')
498
499 - def _parseColon_ternary_hash(self, d, kw, repl):
500 return self._parseColon_ternary(d, kw, repl, defaultWhenFalse=True)
501
502 - def _parse(self, fmtstring):
503 keys = _getInterpolationList(fmtstring) 504 for key in keys: 505 if not self.interpolations.has_key(key): 506 d, kw, repl = self._parseSubstitution(key) 507 if repl is None: 508 repl = '-' 509 for pattern, fn in [ 510 ( "-", self._parseColon_minus ), 511 ( "~", self._parseColon_tilde ), 512 ( "+", self._parseColon_plus ), 513 ( "?", self._parseColon_ternary ), 514 ( "#?", self._parseColon_ternary_hash ) 515 ]: 516 junk, matches, tail = repl.partition(pattern) 517 if not junk and matches: 518 self.interpolations[key] = fn(d, kw, tail) 519 break 520 if not self.interpolations.has_key(key): 521 config.error("invalid Interpolate default type '%s'" % repl[0])
522
523 - def getRenderingFor(self, props):
524 props = props.getProperties() 525 if self.args: 526 d = props.render(self.args) 527 d.addCallback(lambda args: 528 self.fmtstring % tuple(args)) 529 return d 530 else: 531 d = props.render(self.interpolations) 532 d.addCallback(lambda res: 533 self.fmtstring % res) 534 return d 535
536 -class Property(util.ComparableMixin):
537 """ 538 An instance of this class renders a property of a build. 539 """ 540 541 implements(IRenderable) 542 543 compare_attrs = ('key','default', 'defaultWhenFalse') 544
545 - def __init__(self, key, default=None, defaultWhenFalse=True):
546 """ 547 @param key: Property to render. 548 @param default: Value to use if property isn't set. 549 @param defaultWhenFalse: When true (default), use default value 550 if property evaluates to False. Otherwise, use default value 551 only when property isn't set. 552 """ 553 self.key = key 554 self.default = default 555 self.defaultWhenFalse = defaultWhenFalse
556
557 - def getRenderingFor(self, props):
558 if self.defaultWhenFalse: 559 d = props.render(props.getProperty(self.key)) 560 @d.addCallback 561 def checkDefault(rv): 562 if rv: 563 return rv 564 else: 565 return props.render(self.default)
566 return d 567 else: 568 if props.hasProperty(self.key): 569 return props.render(props.getProperty(self.key)) 570 else: 571 return props.render(self.default)
572
573 -class _Renderer(object):
574 implements(IRenderable)
575 - def __init__(self, fn):
576 self.getRenderingFor = fn
577
578 -def renderer(fn):
579 return _Renderer(fn)
580
581 -class _DefaultRenderer(object):
582 """ 583 Default IRenderable adaptor. Calls .getRenderingFor if availble, otherwise 584 returns argument unchanged. 585 """ 586 587 implements(IRenderable) 588
589 - def __init__(self, value):
590 try: 591 self.renderer = value.getRenderingFor 592 except AttributeError: 593 self.renderer = lambda _: value
594
595 - def getRenderingFor(self, build):
596 return self.renderer(build)
597 598 registerAdapter(_DefaultRenderer, object, IRenderable)
599 600 601 -class _ListRenderer(object):
602 """ 603 List IRenderable adaptor. Maps Build.render over the list. 604 """ 605 606 implements(IRenderable) 607
608 - def __init__(self, value):
609 self.value = value
610
611 - def getRenderingFor(self, build):
612 return defer.gatherResults([ build.render(e) for e in self.value ])
613 614 registerAdapter(_ListRenderer, list, IRenderable)
615 616 617 -class _TupleRenderer(object):
618 """ 619 Tuple IRenderable adaptor. Maps Build.render over the tuple. 620 """ 621 622 implements(IRenderable) 623
624 - def __init__(self, value):
625 self.value = value
626
627 - def getRenderingFor(self, build):
628 d = defer.gatherResults([ build.render(e) for e in self.value ]) 629 d.addCallback(tuple) 630 return d
631 632 registerAdapter(_TupleRenderer, tuple, IRenderable)
633 634 635 -class _DictRenderer(object):
636 """ 637 Dict IRenderable adaptor. Maps Build.render over the keya and values in the dict. 638 """ 639 640 implements(IRenderable) 641
642 - def __init__(self, value):
643 self.value = _ListRenderer([ _TupleRenderer((k,v)) for k,v in value.iteritems() ])
644
645 - def getRenderingFor(self, build):
646 d = self.value.getRenderingFor(build) 647 d.addCallback(dict) 648 return d
649 650 registerAdapter(_DictRenderer, dict, IRenderable) 651