Skip to content

Commit

Permalink
Add @Renderer, plus docs and tests
Browse files Browse the repository at this point in the history
This decorator was suggested by tomprince:
  #522 (comment)
and has been modified, tested, and documented here.

This commit also reorganizes some of the properties documentation,
splitting that in "Customization" between the "Properties" section and
the "Classes" section.
  • Loading branch information
djmitche committed Sep 21, 2012
1 parent 5dc19c5 commit bc6bf17
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 59 deletions.
9 changes: 8 additions & 1 deletion master/buildbot/process/properties.py
Expand Up @@ -22,7 +22,7 @@
from buildbot.interfaces import IRenderable, IProperties
from twisted.internet import defer
from twisted.python.components import registerAdapter
from zope.interface import implements
from zope.interface import implements, implementer, provider

class Properties(util.ComparableMixin):
"""
Expand Down Expand Up @@ -570,6 +570,13 @@ def checkDefault(rv):
else:
return props.render(self.default)

def renderer(f):
@implementer(IRenderable)
@provider(IRenderable)
class _renderer(object):
getRenderingFor = staticmethod(f)
return _renderer

class _DefaultRenderer(object):
"""
Default IRenderable adaptor. Calls .getRenderingFor if availble, otherwise
Expand Down
31 changes: 30 additions & 1 deletion master/buildbot/test/unit/test_process_properties.py
Expand Up @@ -20,7 +20,7 @@
from twisted.python import components
from buildbot.process.properties import Properties, WithProperties
from buildbot.process.properties import Interpolate
from buildbot.process.properties import Property, PropertiesMixin
from buildbot.process.properties import Property, PropertiesMixin, renderer
from buildbot.interfaces import IRenderable, IProperties
from buildbot.test.util.config import ConfigErrorsMixin
from buildbot.test.util.properties import ConstantRenderable
Expand Down Expand Up @@ -1211,3 +1211,32 @@ def test_dict(self):
k2.callback("dict")
r2.callback("lookup")
return d

class Renderer(unittest.TestCase):

def setUp(self):
self.props = Properties()
self.build = FakeBuild(self.props)


def test_renderer(self):
self.props.setProperty("x", "X", "test")
d = self.build.render(
renderer(lambda p : 'x%sx' % p.getProperty('x')))
d.addCallback(self.failUnlessEqual, 'xXx')
return d

def test_renderer_deferred(self):
self.props.setProperty("x", "X", "test")
d = self.build.render(
renderer(lambda p : defer.succeed('y%sy' % p.getProperty('x'))))
d.addCallback(self.failUnlessEqual, 'yXy')
return d

def test_renderer_fails(self):
self.props.setProperty("x", "X", "test")
d = self.build.render(
renderer(lambda p : defer.fail(RuntimeError("oops"))))
self.failUnlessFailure(d, RuntimeError)
return d

2 changes: 2 additions & 0 deletions master/docs/developer/classes.rst
Expand Up @@ -15,6 +15,8 @@ The sections contained here document classes that can be used or subclassed.
cls-remotecommands
cls-buildsteps
cls-forcesched
cls-irenderable
cls-iproperties

.. todo::

Expand Down
26 changes: 26 additions & 0 deletions master/docs/developer/cls-iproperties.rst
@@ -0,0 +1,26 @@
.. index:: single; Properties; IProperties

IProperties
===========

.. class:: buildbot.interfaces.IProperties::

Providers of this interface allow get and set access to a build's properties.

.. method:: getProperty(propname, default=None)

Get a named property, returning the default value if the property is not found.

.. method:: hasProperty(propname)

Determine whether the named property exists.

.. method:: setProperty(propname, value, source)

Set a property's value, also specifying the source for this value.

.. method:: getProperties()

Get a :class:`buildbot.process.properties.Properties` instance. The
interface of this class is not finalized; where possible, use the other
``IProperties`` methods.
14 changes: 14 additions & 0 deletions master/docs/developer/cls-irenderable.rst
@@ -0,0 +1,14 @@
.. index:: single; Properties; IRenderable

IRenderable
===========

.. class:: buildbot.interfaces.IRenderable::

Providers of this class can be "rendered", based on available properties, when a build is started.

.. method:: getRenderingFor(iprops)

:param iprops: the :class:`~buildbot.interfaces.IProperties` provider supplying the properties of the build.

Reeturns the interpretation of the given properties, optionally in a Deferred.
59 changes: 57 additions & 2 deletions master/docs/manual/cfg-properties.rst
Expand Up @@ -266,15 +266,40 @@ Here, ``%s`` is used as a placeholder, and the substitutions (which may themselv
dictionary-style interpolation, not both. Thus you cannot use a string
like ``Interpolate("foo-%(src::revision)s-%s", "branch")``.
.. index:: single; Properties; Renderer

.. _Renderer:

Renderer
++++++++

While Interpolate can handle many simple cases, and even some common conditionals, more complex cases are best handled with Python code.
The ``renderer`` decorator creates a renderable object that will be replaced with the result of the function, called when each build begins.
The function receives an :class:`~buildbot.interfaces.IProperties` object, which it can use to examine the values of any and all properties. For example::

@properties.renderer
def makeCommand(props):
command = [ 'make' ]
cpus = props.getProperty('CPUs')
if cpus:
command += [ '-j', str(cpus+1) ]
else:
command += [ '-j', '2' ]
command += [ 'all' ]
return command
f.addStep(ShellCommand(command=makeCommand))

.. index:: single; Properties; WithProperties

.. _WithProperties:

WithProperties
++++++++++++++

This placeholder is deprecated. It is an older version of :ref:`Interpolate`.
It exists for compatability with older configs.
.. warning::

This placeholder is deprecated. It is an older version of :ref:`Interpolate`.
It exists for compatability with older configs.

The simplest use of this class is with positional string interpolation. Here,
``%s`` is used as a placeholder, and property names are given as subsequent
Expand Down Expand Up @@ -334,3 +359,33 @@ above cannot contain more substitutions.
Note: like python, you can use either positional interpolation *or*
dictionary-style interpolation, not both. Thus you cannot use a string like
``WithProperties("foo-%(revision)s-%s", "branch")``.

Custom Renderables
++++++++++++++++++

If the options described above are not sufficient, more complex substitutions can be achieved by writting custom renderables.

Renderables are objects providing the :class:`~buildbot.interfaces.IRenderable` interface.
That interface is simple - objects must provide a `getRenderingFor` method.
The method should take one argument - an :class:`~buildbot.interfaces.IProperties` provider - and should return a string.
Pass instances of the class anywhere other renderables are accepted.
For example::

class DetermineFoo(object):
implements(IRenderable)
def getRenderingFor(self, props)
if props.hasProperty('bar'):
return props['bar']
elif props.hasProperty('baz'):
return props['baz']
return 'qux'
ShellCommand(command=['echo', DetermineFoo()])

or, more practically, ::

class Now(object):
implements(IRenderable)
def getRenderingFor(self, props)
return time.clock()
ShellCommand(command=['make', Interpolate('TIME=%(kw:now)', now=Now())])

55 changes: 0 additions & 55 deletions master/docs/manual/customization.rst
Expand Up @@ -507,61 +507,6 @@ parts of the repo URL in the sourcestamp, or lookup in a lookup table
based on repo URL. As long as there is a permanent 1:1 mapping between
repos and workdir, this will work.

Advanced Property Interpolation
-------------------------------

If the simple string substitutions described in :ref:`Properties` are not sufficent, more complex substitutions can be achieved by writting custom renderers.

Define a class with method `getRenderingFor`.
The method should take one argument - a properties object, described below - and should return a string.
Pass instances of the class anywhere other renderables are accepted.
For example::

class DetermineFoo(object):
implements(IRenderable)
def getRenderingFor(self, props)
if props.hasProperty('bar'):
return props['bar']
elif props.hasProperty('baz'):
return props['baz']
return 'qux'
ShellCommand(command=['echo', DetermineFoo()])

or, more practically, ::

class Now(object):
implements(IRenderable)
def getRenderingFor(self, props)
return time.clock()
ShellCommand(command=['make', Interpolate('TIME=%(kw:now)', now=Now())])

Properties Objects
~~~~~~~~~~~~~~~~~~

.. class:: buildbot.interfaces.IProperties

The available methods on a properties object are those described by the
``IProperties`` interface. Specifically:


.. method:: getProperty(propname, default=None)

Get a named property, returning the default value if the property is not found.

.. method:: hasProperty(propname)

Determine whether the named property exists.

.. method:: setProperty(propname, value, source)

Set a property's value, also specifying the source for this value.

.. method:: getProperties()

Get a :class:`buildbot.process.properties.Properties` instance. The
interface of this class is not finalized; where possible, use the other
``IProperties`` methods.

Writing New BuildSteps
----------------------

Expand Down

0 comments on commit bc6bf17

Please sign in to comment.