1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
47 """
48 @param kwargs: initial property values (for testing)
49 """
50 self.properties = {}
51
52
53 self.runtime = set()
54 self.build = None
55 if kwargs: self.update(kwargs, "TEST")
56
57 @classmethod
63
65 d = self.__dict__.copy()
66 d['build'] = None
67 return d
68
70 self.__dict__ = d
71 if not hasattr(self, 'runtime'):
72 self.runtime = set()
73
76
78 """Just get the value for this property."""
79 rv = self.properties[name][0]
80 return rv
81
84
87
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
95 """Return the properties as a simple key:value dictionary"""
96 return dict(self.properties)
97
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
112
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
121
124
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
145
148
152
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
171
175
176 has_key = hasProperty
177
178 - def setProperty(self, propname, value, source='Unknown', runtime=None):
185
188
192
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"(.*):\+(.*)")
207
209 properties = self.properties()
210 assert properties is not None
211
212 def colon_minus(mo):
213
214
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
225
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
236
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
254
255 if key in self.temp_vals:
256 rv = self.temp_vals[key]
257 else:
258 rv = properties[key]
259
260
261 if rv is None: rv = ''
262 return rv
263
265 'Add a temporary value (to support keyword arguments to WithProperties)'
266 self.temp_vals[key] = val
267
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
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()
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
338
341
342 dd = collections.defaultdict(str)
343 fmtstring % dd
344 return dd.keys()
345
351 _thePropertyDict = _PropertyDict()
363
371
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
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
425
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
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
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
465 return _Lookup(d, kw,
466 default=Interpolate(repl, **self.kwargs),
467 defaultWhenFalse=False,
468 elideNoneAs='')
469
471 return _Lookup(d, kw,
472 default=Interpolate(repl, **self.kwargs),
473 defaultWhenFalse=True,
474 elideNoneAs='')
475
477 return _Lookup(d, kw,
478 hasKey=Interpolate(repl, **self.kwargs),
479 default='',
480 defaultWhenFalse=False,
481 elideNoneAs='')
482
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
500 return self._parseColon_ternary(d, kw, repl, defaultWhenFalse=True)
501
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
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
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
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
577
580
582 """
583 Default IRenderable adaptor. Calls .getRenderingFor if availble, otherwise
584 returns argument unchanged.
585 """
586
587 implements(IRenderable)
588
594
597
598 registerAdapter(_DefaultRenderer, object, IRenderable)
602 """
603 List IRenderable adaptor. Maps Build.render over the list.
604 """
605
606 implements(IRenderable)
607
610
612 return defer.gatherResults([ build.render(e) for e in self.value ])
613
614 registerAdapter(_ListRenderer, list, IRenderable)
618 """
619 Tuple IRenderable adaptor. Maps Build.render over the tuple.
620 """
621
622 implements(IRenderable)
623
626
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)
636 """
637 Dict IRenderable adaptor. Maps Build.render over the keya and values in the dict.
638 """
639
640 implements(IRenderable)
641
643 self.value = _ListRenderer([ _TupleRenderer((k,v)) for k,v in value.iteritems() ])
644
649
650 registerAdapter(_DictRenderer, dict, IRenderable)
651