Chapter 34
Implementing a TALES Namespaces
Difficulty
Newcomer
Skills
- Be familiar with TAL and TALES (in the context of Page Templates).
- You should feel comfortable with ZCML.
Problem/Task
Zope 3 exposes a fair amount of its API in TAL and TALES through expression
types (path, python, string and sql [add-on]) as well as the TALES namespaces, such
as zope. However, sometimes this is not powerful enough or still requires a lot of
Python view class coding. For this reason Zope 3 allows you to add new TALES
namespace. This chapter will demonstrate on how to write and register a new TALES
namespace.
Solution
TALES namespaces use path adapters to implement the adaptation. Path adapters
are used to adapt an object to another interface while traversing a path. The name of
the adapter is used in the path to select the correct adapter. An example is the zope
TALES namespace.
1 <p tal:content="context/zope:modified" />
In this example, the object is adapted to the IZopeTalesAPI, which provides many
convenience methods, including the exposure of the Dublin Core. Then the
modified() method is called on the adapter, which returns the modification date of
the context object.
While the standard zope TALES namespace deals with retrieving additional data
about the component, it does not handle the output format of the data. So it would
be a good idea to specify a new namespace that deals exclusively with the formatting
of objects. To keep this chapter short, we will only concentrate on the full display
of dates, times and datetimes. The user’s locale functions are used to do the actual
formatting, so that the developer’s effort will be minimal and the output is correctly
localized.
The code for this example is located in book/formatns. Therefore, create this
product and do not forget the __init__.py file in the directory.
34.1 Step I: Defining the Namespace Interface
Let’s start out by defining the interface of the namespace. The interface specifies all
the functions that will be available in the namespace. Note that the code available in
the repository has a lot more functions, but since the code is fairly repetitive, I
decided not to put it all into the text.
1 from zope.interface import Interface
2
3 class IFormatTalesAPI(Interface):
4
5 def fullDate(self):
6 """Returns the full date using the user's locale.
7
8 The context of this namespace must be a datetime object,
9 otherwise an exception is raised.
10 """
11
12 def fullTime(self):
13 """Returns the full time using the user's locale.
14
15 The context of this namespace must be a datetime object,
16 otherwise an exception is raised.
17 """
18
19 def fullDateTime(self):
20 """Returns the full datetime using the user's locale.
21
22 The context of this namespace must be a datetime object,
23 otherwise an exception is raised.
24 """
While every TALES namespace also has to implement ITALESFunctionNamespace,
we do not inherit from this interface here, but simply merge it in in the
implementation. This has the advantage that the IFormatTalesAPI interface can be
reused elsewhere.
34.2 Step II: Implementing the Namespace
The actual code of the namespace is not much harder than the interface, if you have
played with the user locales before. Add the following implementation in the
package’s __init__.py file.
1 from zope.interface import implements
2 from zope.tales.interfaces import ITALESFunctionNamespace
3 from zope.security.proxy import removeSecurityProxy
4 from interfaces import IFormatTalesAPI
5
6
7 class FormatTalesAPI(object):
8
9 implements(IFormatTalesAPI, ITALESFunctionNamespace)
10
11 def __init__(self, context):
12 self.context = context
13
14 def setEngine(self, engine):
15 """See zope.tales.interfaces.ITALESFunctionNamespace"""
16 self.locale = removeSecurityProxy(engine.vars['request']).locale
17
18 def fullDate(self):
19 """See book.formatns.interfaces.IFormatTalesAPI"""
20 return self.locale.dates.getFormatter(
21 'date', 'full').format(self.context)
22
23 def fullTime(self):
24 """See book.formatns.interfaces.IFormatTalesAPI"""
25 return self.locale.dates.getFormatter(
26 'time', 'full').format(self.context)
27
28 def fullDateTime(self):
29 """See book.formatns.interfaces.IFormatTalesAPI"""
30 return self.locale.dates.getFormatter(
31 'dateTime', 'full').format(self.context)
- Line 2: Here you see where to import the ITALESFunctionNamespace
interface from. Note that this interface is not totally necessary, but without
it, the engine will not be set on the namespace, which makes it impossible
to reach the request.
- Line 11-12: All TALES function namespaces must be implemented as
adapters. The object they adapt is the object that was evaluated from the
previous part of the path expression.
- Line 14-16: This method implements the only requirement the
ITALESFunctionNamespace interface poses. This method provides the
some additional context about the user and the entire request. For this
namespace, however, we are only interested in the locale, so we get it.
Interestingly enough, the engine is security-wrapped, so that the request is
automatically security-wrapped, which is unusal and therefore no security
declaration exists for accessing the locale attribute. Therefore we have
to remove all the security proxies from the request before accessing the
locale object. Note that this is safe, since all of the namespace functions
only read data from the locale, but do not do any write operations.
- Line 18-31: Here you can see the trivial implementations of the namespace
functions. The locale object provides all the functionality we need. From
the locale itself we can retrieve the date, time or datetime formatting
objects using locale.dates.getFormat(). The method expect the name
of the format to be specified. The four names supported by ICU (on which
the locale support is based) are “short”, “medium”, “long”, and “full”.
The formatting objects have a method called format(), which converts
the datetime object into a localized string representation.
That was easy, wasn’t it? Next we are going to test the functionality.
34.3 Step III: Testing the Namespace
Here we are only going to test whether the namespace works by itself; we are not
checking whether the namespace will work correctly in TALES, since this should
be tested in the TALES implementation. The tricky part of the test is to
create a sufficient Engine object, so that the code can access the request:
1 import unittest
2 from datetime import datetime
3 from zope.publisher.browser import TestRequest
4 from zope.testing.doctestunit import DocTestSuite
5 from book.formatns import FormatTalesAPI
6
7 class Engine:
8 vars = {'request': TestRequest(environ={'HTTP_ACCEPT_LANGUAGE': 'en'})}
9
10 def getFormatNamespace(context):
11 ns = FormatTalesAPI(context)
12 ns.setEngine(Engine())
13 return ns
14
15 def fullDate():
16 """
17 >>> ns = getFormatNamespace(datetime(2003, 9, 16, 16, 51, 01))
18 >>> ns.fullDate()
19 u'Tuesday, September 16, 2003'
20 """
21
22 def fullTime():
23 """
24 >>> ns = getFormatNamespace(datetime(2003, 9, 16, 16, 51, 01))
25 >>> ns.shortTime()
26 u'4:51:01 PM +000'
27 """
28
29 def fullDateTime():
30 """
31 >>> ns = getFormatNamespace(datetime(2003, 9, 16, 16, 51, 01))
32 >>> ns.fullDateTime()
33 u'Tuesday, September 16, 2003 4:51:01 PM +000'
34 """
35
36 def test_suite():
37 return DocTestSuite()
38
39 if __name__ == '__main__':
40 unittest.main(defaultTest='test_suite')
- Line 7-8: This is the most minimal stub implementation of the engine,
which sufficiently provides the request, as it is required by the namespace.
- Line 10-13: This helper function creates the namespace instance for us and
also sets the engine, so that we are ready to use the returned namespace
object.
- Line 15-34: These are the tests in the Zope “Doc Unit Test” format, which
is easy to read. You see what’s going on. The code inside the doc string is
executed and it is checked whether the returned value equals the expected
value.
- Line 36-40: This is the usual unit doctest boilerplate. The only difference
is that we create a DocTestSuite without passing in the module name. If
no name is specified to the DocTestSuite constructor, the current module
is searched for tests.
You can run the tests as usual, of course, using
1 python test.py -vpu --dir src/book/formatns
34.4 Step IV: Wiring the Namspace into Zope 3
The last task is to hook up the namespace to the rest of the framework. This is
simply done by registering a normal adapter that provides the IPathAdapter
interface. Place the following code into the “configure.zcml” file:
1 <configure
2 xmlns="http://namespaces.zope.org/zope">
3
4 <adapter
5 for="*"
6 provides="zope.app.traversing.interfaces.IPathAdapter"
7 factory=".FormatTalesAPI"
8 name="format" />
9
10 </configure>
- Line 5: Register this namespace as an adapter for all possible objects, even
though in our case we could restrict it to IDateTime instances. However,
the idea is that the format namespace will support many different object
types.
- Line 8: The name of the adapter is extremly important, since it is the
name as which the adapter will be available in the path expressions.
Now hook the configuration into Zope 3 by adding a file called
formatns-configure.zcml to package-includes having the following line as
content:
1 <include package="book.formatns" />
34.5 Step V: Trying the format Namespace
First you need to restart Zope. Then create a “ZPT Page” and add the following
content:
1 <html>
2 <body>
3 <h1 tal:content="context/zope:modified/format:fullDateTime">
4 Tuesday, September 16, 2003 04:51:01 PM +000
5 </h1>
6 </body>
7 </html>
- Line 3-4: This line displays the modification datetime of the root folder
as a “full” date time. The output is even localized to the user’s preferred
language and format.
A great aspect of the function namespace concept is that several namespace calls
can be piped together. In the example above you can see how the zope namespace
extracts the modification datetime of the root folder, and this datetime object is then
passed to the format namespace to create the localized human-readable
representation.