Chapter 29
Registries with Global Utilities

Difficulty

Contributer

Skills

Problem/Task

We need registries all the time. It is a very common pattern. In fact, the Component Architecture itself depends heavily on registered component lookups to provide functionality. These registries, especially the utility service, can be used to provide application-level registries as well.

Solution

29.1 Introduction

The goal of this chapter is to develop a mechanism that provides image-representations of text-based smileys. For example, the text “:-)” should be converted to PIC. To further complicate the problem, it is undesirable to just store smileys without any order. Many applications support multiple themes of smileys, so we want to do that as well. For example, the message board administrator should be able to choose the desired smiley theme for his message board.

Based on the requirements above, we want a registry of smiley themes that the user can choose from. Therefore, we will develop the theme as a utility and register it with a specific interface, ISmileyTheme. Thus, the entire utility service acts as a huge registry, but we can easily simulate sub-registries by specifying a particular interface for a utility. We can then ask the Zope component API to return a list of all utilities providing ISmileyTheme or simply return a single ISmileyTheme having a specific name.

Let’s now take a look on how to handle the smileys themselves. Inside the theme, we simply need a mapping (dictionary) from the text representation to the image. However, should we really store the image data. In fact, it would better to declare the image itself as a resource and only store the URL, so that we can (a) support external links (not done in this chapter) and (b) do not have to worry about publishing the images.

The code will be rounded off by implementing a couple new ZCML directives to make the registration of new smiley themes as easy as possible, so that a message board editor can easily upload his/her favorite theme and use it. We will actually add a final step to the message board example incorporating smiley themes at the end of the chapter.

To allow the smiley theme utility to be distributed independently of the message board application, develop its code in a new package called smileyutility, which you should place into ZOPE3/src/book. Don’t forget to add an __init__.py file to the directory.

29.2 Step I: Defining the Interfaces

Before we start coding away, we need to spend some time thinking about the API that we want to expose. In the next chapter we develop a local/placeful equivalent of the utility, so our base interface, ISmileyTheme, should be general enough to support both implementations and not include any implementation-specific methods. We will then derive another interface, IGlobalSmileyTheme, from the general one that will specify methods to manage smileys for the global implementation. Note: The utility will still be registered as a ISmileyTheme.


1  from zope.interface import Interface
2  
3  class ISmileyTheme(Interface):
4      """A theme is a collection of smileys having a stylistic theme.
5  
6      Themes are intended to be implemented as named utilities, which will be
7      available via a local smiley service.
8      """
9  
10      def getSmiley(text, request):
11          """Returns a smiley for the given text and theme.
12  
13          If no smiley was found, a ComponentLookupError should be raised.
14          """
15  
16      def querySmiley(text, request, default=None):
17          """Returns a smiley for the given text and theme.
18  
19          If no smiley was found, the default value is returned.
20          """
21  
22      def getSmileysMapping(request):
23          """Return a mapping of text to URL.
24  
25          This is incredibly useful when actually attempting to substitute the
26          smiley texts with a URL.
27          """
28  
29  
30  class IGlobalSmileyTheme(ISmileyTheme):
31      """A global smiley theme that also allows management of smileys."""
32  
33      def provideSmiley(text, smiley_path):
34          """Provide a smiley for the utility."""

You might think that this interface seems a bit wordy, but in such widely available components it is extremely important to document the specific semantics of each method and the documentation should leave no question or corner case uncovered. Many people will depend on the correctness of the API.

Now that we have all interfaces defined, let’s look at the implementation, which should be straightforward.

29.3 Step II: Implementing the Utility

The global theme will use simple dictionary that maps a text representation to the path of a smiley. When smileys are requested this path is converted to a URL and returned. The only tricky part of the utility will be to obtain the root URL, since the utility does not know anything about locations.

However, there is an fast solution. We create a containment root component stub that implements the IContainmentRoot interface, for which the traversal mechanism is looking for while generating a path or URL. So here is what I did for obtaining the root URL:


1  from zope.app import zapi
2  from zope.app.traversing.interfaces import IContainmentRoot
3  from zope.interface import implements
4  
5  class Root:
6      implements(IContainmentRoot)
7  
8  def getRootURL(request):
9      return str(zapi.getView(Root(), 'absolute_url', request))

Now we have all the pieces to implement the utility. I just used pyskel.py to create the skeleton and then filled it. Place the following and the getRootURL() code in a file called globaltheme.py:


1  om zope.component.exceptions import ComponentLookupError
2  om interfaces import IGlobalSmileyTheme
3  
4  ass GlobalSmileyTheme(object):
5    """A filesystem based smiley theme."""
6    implements(IGlobalSmileyTheme)
7  
8    def __init__(self):
9        self.__smileys = {}
10  
11    def getSmiley(self, text, request):
12        "See book.smileyutility.interfaces.ISmileyTheme"
13        smiley = self.querySmiley(text, request)
14        if smiley is None:
15            raise ComponentLookupError, 'Smiley not found.'
16        return smiley
17  
18    def querySmiley(self, text, request, default=None):
19        "See book.smileyutility.interfaces.ISmileyTheme"
20        if self.__smileys.get(text) is None:
21            return default
22        return getRootURL(request) + '/' + self.__smileys[text]
23  
24    def getSmileysMapping(self, request):
25        "See book.smileyutility.interfaces.ISmileyTheme"
26        smileys = self.__smileys.copy()
27        root_url = getRootURL(request)
28        for name, smiley in smileys.items():
29            smileys[name] = root_url + '/' + smiley
30        return smileys
31  
32    def provideSmiley(self, text, smiley_path):
33        "See book.smileyutility.interfaces.IGlobalSmileyTheme"
34        self.__smileys[text] = smiley_path
35  

Our utility is now complete. However, we have not created a way to declare a default theme. To make life simple, the default theme is simply available under the name “default”.


1  from interfaces import ISmileyTheme
2  
3  def declareDefaultSmileyTheme(name):
4      """Declare the default smiley theme."""
5      utilities = zapi.getService(zapi.servicenames.Utilities)
6      theme = zapi.getUtility(ISmileyTheme, name)
7      # register the utility simply without a name
8      utilities.provideUtility(ISmileyTheme, theme, 'default')

In the code above we simply look up the utility by its original name and then register it again using the name “default”. By the way, this is totally legal and of practiced. One Utility instance can be registered multiple times using different interfaces and/or names.

Now, let’s test our new utility.

29.4 Step III: Writing Tests

Writing tests for global utilities is usually fairly simple too, since you usually do not have to start up the component architecture. In this case, however, we have to do this, since we are looking up a view when asking for the root URL. We also have to register this view ( absolute_url) in the first place, so it can be found later. In the tests package I created a test_doc.py and inserted the set up and tear down code there:


1  import unittest
2  
3  from zope.interface import Interface
4  from zope.testing.doctestunit import DocTestSuite
5  
6  from zope.app.tests import ztapi, placelesssetup
7  
8  class AbsoluteURL:
9      def __init__(self, context, request):
10          pass
11      def __str__(self):
12          return ''
13  
14  def setUp():
15      placelesssetup.setUp()
16      ztapi.browserView(Interface, 'absolute_url', AbsoluteURL)
17  
18  
19  def test_suite():
20      return unittest.TestSuite((
21          DocTestSuite('book.smileyutility.globaltheme',
22                       setUp=setUp, tearDown=placelesssetup.tearDown),
23          ))
24  
25  if __name__ == '__main__':
26      unittest.main(defaultTest='test_suite')

Now we just have to write the tests. In the docstring of the GlobalSmileyTheme class add the following doctest code:


1  Let's make sure that the global theme implementation actually fulfills the
2  `ISmileyTheme` API.
3  
4  >>> from zope.interface.verify import verifyClass
5  >>> verifyClass(IGlobalSmileyTheme, GlobalSmileyTheme)
6  True
7  
8  Initialize the theme and add a couple of smileys.
9  
10  >>> theme = GlobalSmileyTheme()
11  >>> theme.provideSmiley(':-)', '++resource++plain__smile.png')
12  >>> theme.provideSmiley(';-)', '++resource++plain__wink.png')
13  
14  Let's try to get a smiley out of the registry.
15  
16  >>> from zope.publisher.browser import TestRequest
17  
18  >>> theme.getSmiley(':-)', TestRequest())
19  '/++resource++plain__smile.png'
20  >>> theme.getSmiley(':-(', TestRequest())
21  Traceback (most recent call last):
22  ...
23  ComponentLookupError: 'Smiley not found.'
24  >>> theme.querySmiley(';-)', TestRequest())
25  '/++resource++plain__wink.png'
26  >>> theme.querySmiley(';-(', TestRequest()) is None
27  True
28  
29  And finally we'd like to get a dictionary of all smileys.
30  
31  >>> map = theme.getSmileysMapping(TestRequest())
32  >>> len(map)
33  2
34  >>> map[':-)']
35  '/++resource++plain__smile.png'
36  >>> map[';-)']
37  '/++resource++plain__wink.png'

Run the tests and make sure that they all pass.

29.5 Step IV: Providing a user-friendly UI

While the current API is functional, it is not very practical to the developer, since s/he first needs to look up the theme using the component architecture’s utility API and only then can make use the of the smiley theme features. It would be much nicer, if we would only need a smiley-theme-related API to work with. Thus we create some convenience functions in the package’s __init__.py file:


1  from zope.app import zapi
2  
3  from interfaces import ISmileyTheme
4  
5  def getSmiley(text, request, theme='default'):
6      theme = zapi.getUtility(ISmileyTheme, theme)
7      return theme.getSmiley(text, request)
8  
9  def querySmiley(text, request, theme='default', default=None):
10      theme = zapi.queryUtility(ISmileyTheme, theme)
11      if theme is None:
12          return default
13      return theme.querySmiley(text, request, default)
14  
15  def getSmileyThemes():
16      return [name for name, util in zapi.getUtilitiesFor(ISmileyTheme)
17              if name != 'default']
18  
19  def getSmileysMapping(request, theme='default'):
20      theme = zapi.getUtility(ISmileyTheme, theme)
21      return theme.getSmileysMapping(request)

The functions integrate the theme utility more tightly in the API.

The tests for these functions are very similar to the ones of the theme utility, so I am not going to include them in the text. As always, you can find the complete code including methods in the code repository ( http://svn.zope.org/book/smileyutility).

29.6 Step V: Implement ZCML Directives

You might have already wondered, how this utility can be useful, if it does not even deal with the smiley images. This functionality is reserved for the configuration. When a smiley registration is made, the directive will receive a path to an image, but does not just register it with the smiley theme. Instead, it first creates a resource for the image and then passes the resource’s relative path to the smiley theme.

In case you have not written a ZCML directive yet, there are three steps: creating the directive schema, implementing the directive handlers and writing the meta-ZCML configuration. They are represented by the next three sections (a) through (c).

But first we need to decide what directives we want to create. The first one, smiley:theme, defines a new theme and allows a sub-directive, smiley:smiley, that registers new smileys for this theme. A second directive, smiley:smiley, allows you to register a single smiley for an existing theme, so that other packages can add additional smileys to a theme. The third and final directive, smiley:defaultTheme, let’s you specify the theme that will be known as the default one. The specified theme must exist already.

29.6.1 (a) Declaring the directive schemas

Each ZCML directive is represented by a schema, which defines the type of content for each element/directive attribute. Each field is also responsible for knowing how to convert the attribute value into something that is useful. All the usual schema fields are available. Additionally there are some specific configuration fields that can also be used. They are listed in the “Intorduction to the Zope Configuration Markup Language (ZCML)” chapter.

So now that we know what we can use, let’s define the schemas. By convention they are placed in a file called metadirectives.py:


1  from zope.interface import Interface
2  from zope.configuration.fields import Path
3  from zope.schema import TextLine
4  
5  class IThemeDirective(Interface):
6      """Define a new theme."""
7  
8      name = TextLine(
9          title=u"Theme Name",
10          description=u"The name of the theme.",
11          default=None,
12          required=False)
13  
14  class ISmileySubdirective(Interface):
15      """This directive adds a new smiley using the theme information of the
16      complex smileys directive."""
17  
18      text = TextLine(
19          title=u"Smiley Text",
20          description=u"The text that represents the smiley, i.e. ':-)'",
21          required=True)
22  
23      file = Path(
24          title=u"Image file",
25          description=u"Path to the image that represents the smiley.",
26          required=True)
27  
28  class ISmileyDirective(ISmileySubdirective):
29      """This is a standalone directive registering a smiley for a certain
30      theme."""
31  
32      theme = TextLine(
33          title=u"Theme",
34          description=u"The theme the smiley belongs to.",
35          default=None,
36          required=False)
37  
38  class IDefaultThemeDirective(IThemeDirective):
39      """Specify the default theme."""

29.6.2 (b) Implement ZCML directive handlers

Next we implement the directive handlers themselves, which is the real fun part, since it actually represents some important part of the package’s logic. This code goes by convention into metaconfigure.py:


1  import os
2  
3  from zope.app import zapi
4  from zope.app.component.metaconfigure import utility
5  from zope.app.publisher.browser.resourcemeta import resource
6  
7  from interfaces import ISmileyTheme
8  from globaltheme import GlobalSmileyTheme, declareDefaultSmileyTheme
9  
10  __registered_resources = []
11  
12  def registerSmiley(text, path, theme):
13      theme = zapi.queryUtility(ISmileyTheme, theme)
14      theme.provideSmiley(text, path)
15  
16  class theme(object):
17  
18      def __init__(self, _context, name):
19          self.name = name
20          utility(_context, ISmileyTheme,
21                  factory=GlobalSmileyTheme, name=name)
22  
23      def smiley(self, _context, text, file):
24          return smiley(_context, text, file, self.name)
25  
26      def __call__(self):
27          return
28  
29  def smiley(_context, text, file, theme):
30      name = theme + '__' + os.path.split(file)[1]
31      path = '/++resource++' + name
32  
33      if name not in __registered_resources:
34          resource(_context, name, image=file)
35          __registered_resources.append(name)
36  
37      _context.action(
38          discriminator = ('smiley', theme, text),
39          callable = registerSmiley,
40          args = (text, path, theme),
41          )
42  
43  def defaultTheme(_context, name=None):
44      _context.action(
45          discriminator = ('smiley', 'defaultTheme',),
46          callable = declareDefaultSmileyTheme,
47          args = (name,),
48          )

29.6.3 (c) Writing the meta-ZCML directives

Now that we have completed the Python-side of things, let’s register the new ZCML directives using the meta namespace in ZCML. By convention the ZCML directives are placed into a file named metal.zcml:


1  <configure xmlns:meta="http://namespaces.zope.org/meta">
2  
3    <meta:directives namespace="http://namespaces.zope.org/smiley">
4  
5      <meta:complexDirective
6          name="theme"
7          schema=".metadirectives.IThemeDirective"
8          handler=".metaconfigure.theme">
9  
10        <meta:subdirective
11            name="smiley"
12            schema=".metadirectives.ISmileySubdirective" />
13  
14      </meta:complexDirective>
15  
16      <meta:directive
17          name="smiley"
18          schema=".metadirectives.ISmileyDirective"
19          handler=".metaconfigure.smiley" />
20  
21      <meta:directive
22          name="defaultTheme"
23          schema=".metadirectives.IDefaultThemeDirective"
24          handler=".metaconfigure.defaultTheme" />
25  
26    </meta:directives>
27  
28  </configure>

Each meta directive, whether it is directive, complexDirective or subdirective, specifies the name of the directive and the schema it represents. The first two meta directives also take a handler attribute, which describes the callable object that will execute the directive.

You register this meta ZCML file with the system by placing a file called smileyutility-meta.zcml in the package-includes directory having the following content:


1  <include package="book.smileyutility" file="meta.zcml" />

29.6.4 (d) Test Directives

Now we are ready to test the directives. First we create a test ZCML file in tests called smiley.zcml. We write the directives in a way that we assume we are in the tests directory during its execution:


1  <configure
2      xmlns:zope="http://namespaces.zope.org/zope"
3      xmlns="http://namespaces.zope.org/smiley">
4  
5    <zope:include package="book.smileyutility" file="meta.zcml" />
6  
7    <theme name="yazoo">
8      <smiley text=":(" file="../smileys/yazoo/sad.png"/>
9      <smiley text=":)" file="../smileys/yazoo/smile.png"/>
10    </theme>
11  
12    <theme name="plain" />
13  
14    <smiley
15        theme="plain"
16        text=":("
17        file="../smileys/yazoo/sad.png"/>
18  
19    <defaultTheme name="plain" />
20  
21  </configure>

Now create a module called test_directives.py (the directive tests modules are usually called this way) and add the following test code:


1  import unittest
2  
3  from zope.app import zapi
4  from zope.app.tests.placelesssetup import PlacelessSetup
5  from zope.configuration import xmlconfig
6  
7  from book.smileyutility import tests
8  from book.smileyutility.interfaces import ISmileyTheme
9  
10  class DirectivesTest(PlacelessSetup, unittest.TestCase):
11  
12      def setUp(self):
13          super(DirectivesTest, self).setUp()
14          self.context = xmlconfig.file("smiley.zcml", tests)
15  
16      def test_SmileyDirectives(self):
17          self.assertEqual(
18              zapi.getUtility(ISmileyTheme,
19                              'default')._GlobalSmileyTheme__smileys,
20              {u':(': u'/++resource++plain__sad.png'})
21          self.assertEqual(
22              zapi.getUtility(ISmileyTheme,
23                              'plain')._GlobalSmileyTheme__smileys,
24              {u':(': u'/++resource++plain__sad.png'})
25          self.assertEqual(
26              zapi.getUtility(ISmileyTheme,
27                              'yazoo')._GlobalSmileyTheme__smileys,
28              {u':)': u'/++resource++yazoo__smile.png',
29               u':(': u'/++resource++yazoo__sad.png'})
30  
31      def test_defaultTheme(self):
32          self.assertEqual(zapi.getUtility(ISmileyTheme, 'default'),
33                           zapi.getUtility(ISmileyTheme, 'plain'))
34  
35  def test_suite():
36      return unittest.TestSuite((
37          unittest.makeSuite(DirectivesTest),
38          ))
39  
40  if __name__ == '__main__':
41      unittest.main()

As we can see, directive unittests can be very compact thanks to the xmlconfig.file() call.

29.7 Step VI: Setting up some Smiley Themes

The service functionality is complete and we are now ready to hook it up to the system. We need to define the service and provide an implementation to the component architecture before we add two smiley themes. Therefore, in the configure.zcml file add:


1  <configure
2      xmlns="http://namespaces.zope.org/smiley"
3      i18n_domain="smileyutility">
4  
5    <theme name="plain">
6      <smiley text=":("   file="./smileys/plain/sad.png"/>
7      <smiley text=":-("  file="./smileys/plain/sad.png"/>
8      <smiley text=":)"   file="./smileys/plain/smile.png"/>
9      <smiley text=":-)"  file="./smileys/plain/smile.png"/>
10      ...
11    </theme>
12  
13    <theme name="yazoo">
14      <smiley text=":("   file="./smileys/yazoo/sad.png"/>
15      <smiley text=":-("  file="./smileys/yazoo/sad.png"/>
16      <smiley text=":)"   file="./smileys/yazoo/smile.png"/>
17      <smiley text=":-)"  file="./smileys/yazoo/smile.png"/>
18      ...
19    </theme>
20  
21    <defaultTheme name="plain" />
22  
23  </configure>

You can now activate the configuration by placing a file named smileyutility-configure.zcml in package-includes. It should have the following content:


1  <include package="book.smileyutility" />

29.8 Step VII: Integrate Smiley Themes into the Message Board

Okay, now we have these smiley themes, but we do not use them anywhere. So that it will be easier for us to see the smiley themes in action, I decided to extend the messageboard example by yet another step. The new code consists of two parts: (a) allow the message board to select one of the available themes and (b) use smileys in the “Preview” tab of the message board.

29.8.1 (a) The Smiley Theme Selection Adapter

The additional functionality is best implemented using an Adapter and annotations. The interface that we need is trivially:


1  from zope.schema import Choice
2  
3  class ISmileyThemeSpecification(Interface):
4  
5      theme = Choice(
6          title=u"Smiley Theme",
7          description=u"The Smiley Theme used in message bodies.",
8          vocabulary=u"Smiley Themes",
9          default=u"default",
10          required=True)

Add this interface to the interfaces.py file of the message board. In the interface above we refer to a vocabulary called “Smiley Themes” without having specified it. We expect this vocabulary to provide a list of names of all available smiley themes. Luckily, creating vocabularies for utilities or utility names can be easily done using a single ZCML directive:


1  <vocabulary
2      name="Smiley Themes"
3      factory="zope.app.utility.vocabulary.UtilityVocabulary"
4      interface="book.smileyutility.interfaces.ISmileyTheme"
5      nameOnly="true" />

Next we create the adapter; place the following class into messageboard.py:


1  from zope.app.annotation.interfaces import IAnnotations
2  from book.messageboard.interfaces import ISmileyThemeSpecification
3  
4  class SmileyThemeSpecification(object):
5  
6      implements(ISmileyThemeSpecification)
7      __used_for__ = IMessageBoard
8  
9      def __init__(self, context):
10          self.context = self.__parent__ = context
11          self._annotations = IAnnotations(context)
12          if self._annotations.get(ThemeKey, None) is None:
13              self._annotations[ThemeKey] = 'default'
14  
15      def getTheme(self):
16          return self._annotations[ThemeKey]
17  
18      def setTheme(self, value):
19          self._annotations[ThemeKey] = value
20  
21      # See .interfaces.ISmileyThemeSpecification
22      theme = property(getTheme, setTheme)

As you can see, this is a very straightforward implementation of the interface using annotations and the adapter concept, both of which were introduced in the content components parts before.

The adapter registration and security is a bit tricky, since we must use a trusted adapter. It is not enough to just specify the “permission” attribute in the adapter directive, since it will only affect attribute access, but not mutation. Instead of specifying the “permission” attribute, we need to do a full security declaration using the zope:class and zope:require directives:


1  <class class=".messageboard.SmileyThemeSpecification">
2    <require
3        permission="book.messageboard.View"
4        interface=".interfaces.ISmileyThemeSpecification"
5        />
6    <require
7        permission="book.messageboard.Edit"
8        set_schema=".interfaces.ISmileyThemeSpecification"
9        />
10  </class>
11  
12  <adapter
13      factory=".messageboard.SmileyThemeSpecification"
14      provides=".interfaces.ISmileyThemeSpecification"
15      for=".interfaces.IMessageBoard"
16      trusted="true" />

Last, we need to create a view to set the value. We can simply use the browser:editform. We configure the view with the following directive in browser/configure.zcml:


1  <editform
2      name="smileyTheme.html"
3      schema="book.messageboard.interfaces.ISmileyThemeSpecification"
4      for="book.messageboard.interfaces.IMessageBoard"
5      label="Change Smiley Theme"
6      permission="book.messageboard.Edit"
7      menu="zmi_views" title="Smiley Theme" />

By the way, the editform will automatically know how to look up the adapter and use it instead of the MessageBoard instance. If you now restart Zope 3, you should be able to change the theme to whatever you like.

29.8.2 (b) Using the Smiley Theme

The very final step is to use all this machinery. To do this, add a method called body() to the MessageDetails ( browser/message.py) class:


1  def body(self):
2      """Return the body, but mark up smileys."""
3      body = self.context.body
4  
5      # Find the messageboard and get the theme preference
6      obj = self.context
7      while not IMessageBoard.providedBy(obj) and \
8                obj is not None:
9          obj = zapi.getParent(obj)
10  
11      if obj is None:
12          theme = None
13      else:
14          theme = ISmileyThemeSpecification(obj).theme
15  
16      for text, url in getSmileysMapping(self.request, theme).items():
17          body = body.replace(
18              text,
19              '<img src="%s" label="%s"/>' %(url, text))
20  
21      return body

In the details.pt template, line 33, we now just have to change the call from context/body to view/body so that the above method is being used. Once you have done that you are ready to restart Zope 3 and enjoy the smileys.


PIC

Figure 29.1: The message “Preview” screen featuring the smileys.


Exercises

  1. The GlobalSmileyTheme currently is really only meant to work with resources that are defined while executing the smiley directives. However, it is not very hard to support general URLs as well. To do this, you will have to change the configuration to allow for another attribute called url and you have to adjust the theme so that it does not attempt all the time to add the root URL to the smiley path.
  2. The directive tests do not check whether resources were created by the directives. You should enhance the test_directives.py module to do that.
  3. I only updated one view to use the smileys. Update the skin as well to make use of them.