Mike Orr with assistance from Tavis Rudd
iron@mso.oz.net
©Copyright 2002, Mike Orr. This document may be copied and modified under the terms of the Open Publication License http://www.opencontent.org/openpub/
The Cheetah Developers' Guide is for those who want to learn how Cheetah works internally, or wish to modify or extend Cheetah. It is assumed that you've read the Cheetah Users' Guide and have an intermediate knowledge of Python.
This Guide takes a behaviorist approach. First we'll look at what the Cheetah compiler generates when it compiles a template definition, and how it compiles the various $placeholder features and #directives. Then we'll stroll through the files in the Cheetah source distribution and show how each file contributes to the compilation and/or filling of templates. Then we'll list every method/attribute inherited by a template object. Finally, we'll describe how to submit bugfixes/enhancements to Cheetah, and how to add to the documentation.
Appendix A will contain a BNF syntax of the Cheetah template language.
This chapter examines the structure of a .py template module. The following few chapters will then show how each placeholder and directive affects the generated Python code.
Our first template follows a long noble tradition in computer tutorials. It produces a familiar, friendly greeting. Here's the template:
Hello, world!
... the output:
Hello, world!
... and the .py template module cheetah-compile produced, with line numbers added:
1 #!/usr/bin/env python 2 """ 3 Autogenerated by CHEETAH: The Python-Powered Template Engine 4 CHEETAH VERSION: 0.9.12 5 Generation time: Sat Apr 20 14:27:47 2002 6 Source file: x.tmpl 7 Source file last modified: Wed Apr 17 22:10:59 2002 8 """ 9 __CHEETAH_genTime__ = 'Sat Apr 20 14:27:47 2002' 10 __CHEETAH_src__ = 'x.tmpl' 11 __CHEETAH_version__ = '0.9.12' 12 ################################################## 13 ## DEPENDENCIES 14 import sys 15 import os 16 import os.path 17 from os.path import getmtime, exists 18 import time 19 import types 20 from Cheetah.Template import Template 21 from Cheetah.DummyTransaction import DummyTransaction 22 from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList 23 import Cheetah.Filters as Filters 24 import Cheetah.ErrorCatchers as ErrorCatchers 25 ################################################## 26 ## MODULE CONSTANTS 27 try: 28 True, False 29 except NameError: 30 True, False = (1==1), (1==0) 31 ################################################## 32 ## CLASSES 33 class x(Template): 34 """ 35 36 Autogenerated by CHEETAH: The Python-Powered Template Engine 37 """
38 ################################################## 39 ## GENERATED METHODS 40 def __init__(self, *args, **KWs): 41 """ 42 43 """ 44 Template.__init__(self, *args, **KWs) 45 self._filePath = 'x.tmpl' 46 self._fileMtime = 1019106659 47 def respond(self, 48 trans=None, 49 dummyTrans=False, 50 VFS=valueFromSearchList, 51 VFN=valueForName, 52 getmtime=getmtime, 53 currentTime=time.time): 54 """ 55 This is the main method generated by Cheetah 56 """ 57 if not trans: 58 trans = DummyTransaction() 59 dummyTrans = True 60 write = trans.response().write 61 SL = self._searchList 62 filter = self._currentFilter 63 globalSetVars = self._globalSetVars 64 65 ######################################## 66 ## START - generated method body 67 68 if exists(self._filePath) and getmtime(self._filePath) > \ self._fileMtime: 69 self.compile(file=self._filePath) 70 write(getattr(self, self._mainCheetahMethod_for_x) (trans=trans)) 71 if dummyTrans: 72 return trans.response().getvalue() 73 else: 74 return "" 75 write('Hello, world!\n') 76 77 ######################################## 78 ## END - generated method body 79 80 if dummyTrans: 81 return trans.response().getvalue() 82 else: 83 return ""
84 85 ################################################## 86 ## GENERATED ATTRIBUTES 87 __str__ = respond 88 _mainCheetahMethod_for_x= 'respond' 89 # CHEETAH was developed by Tavis Rudd, Chuck Esterbrook, Ian Bicking # and Mike Orr; 90 # with code, advice and input from many other volunteers. 91 # For more information visit http://www.CheetahTemplate.org 92 ################################################## 93 ## if run from command line: 94 if __name__ == '__main__': 95 x().runAsMainProgram()
(I added the line numbers for this Guide, and split a few lines to fit the page width. The continuation lines don't have line numbers, and I added indentation, backslashes and '#' as necessary to make the result a valid Python program.)
The examples were generated from CVS versions of Cheetah between 0.9.12 and 0.9.14.
Lines 20-24 are the Cheetah-specific imports. Line 33 introduces our generated
class, x
, a subclass of Template
. It's called x because the
source file was x.tmpl.
Lines 40-46 are the .__init__
method called when the template is
instantiated or used as a Webware servlet, or when the module is run as a
standalone program. We can see it calling its superclass constructor and
setting ._filePath
and ._fileMtime
to the filename and
modification time (in Unix ticks) of the source .tmpl file.
Lines 47-84 are the main method .respond
, the one that fills the
template. Normally you call it without arguments, but Webware calls it with a
Webware Transaction
object representing the current request. Lines
57-59 set up the trans
variable. If a real or dummy transaction is
passed in, the method uses it. Otherwise (if the trans
argument is
None
), the method creates a DummyTransaction
instance.
dummyTrans
is a flag that just tells whether a dummy transaction is in
effect; it'll be used at the end of the method.
The other four .respond
arguments aren't anything you'd ever want to
pass in; they exist solely to speed up access to these frequently-used
global functions. This is a standard Python trick described in question 4.7
of the Python FAQ (http://www.python.org/cgi-bin/faqw.py).
VFS
and VFN
are the functions that give your template the
benefits of NameMapper lookup, such as the ability to use the searchList.
Line 60 initializes the write
variable. This important variable is
discussed below.
Lines 60-63 initialize a few more local variables. SL
is the
searchList. filter
is the current output filter. globalSetVars
are the variables that have been defined with #set global
.
The comments at lines 65 and 78 delimit the start and end of the code that
varies with each template. The code outside this region is identical in all
template modules. That's not quite true - #import
for instance
generates additional import
statements at the top of the module -
but it's true enough for the most part.
Lines 68-74 exist only if the template source was a named file rather than
a string or file object. The stanza recompiles the template if the source
file has changed. Lines 70-74 seem to be redundant with 75-83: both
fill the template and send the output. The reason the first set of lines
exists is because the second set may become invalid when the template is
recompiled. (This is for re compilation only. The initial compilation
happened in the .__init__
method if the template wasn't
precompiled.)
Line 75 is the most interesting line in this module. It's a direct
translation of what we put in the template definition, ``Hello, world!'' Here
the content is a single string literal. write
looks like an ordinary
function call, but remember that line 60 made it an alias to
trans.response().write
, a method in the transaction. The next few
chapters describe how the different placeholders and directives influence this
portion of the generated class.
Lines 80-83 finish the template filling. If trans
is a real Webware
transaction, write
has already sent the output to Webware for handling,
so we return ""
. If trans
is a dummy transaction,
write
has been accumulating the output in a Python StringIO
object rather than sending it anywhere, so we have to return it.
Line 83 is the end of the .respond
method.
Line 87 makes code.__str__ an alias for the main method, so that you
can print
it or apply str
to it and it will fill the template.
Line 88 gives the name of the main method, because sometimes it's not
.respond
.
Lines 94-95 allow the module to be run directly as a script. Essentially, they process the command-line arguments and them make the template fill itself.
Let's add a few $placeholders to our template:
>>> from Cheetah.Template import Template >>> values = {'what': 'surreal', 'punctuation': '?'} >>> t = Template("""\ ... Hello, $what world$punctuation ... One of Python's least-used functions is $xrange. ... """, [values]) >>> print t Hello, surreal world? One of Python's least-used functions is <built-in function xrange>. >>> print t.generatedModuleCode() 1 #!/usr/bin/env python 2 """ 3 Autogenerated by CHEETAH: The Python-Powered Template Engine 4 CHEETAH VERSION: 0.9.12 5 Generation time: Sun Apr 21 00:53:01 2002 6 """ 7 __CHEETAH_genTime__ = 'Sun Apr 21 00:53:01 2002' 8 __CHEETAH_version__ = '0.9.12' 9 ################################################## 10 ## DEPENDENCIES 11 import sys 12 import os 13 import os.path 14 from os.path import getmtime, exists 15 import time 16 import types 17 from Cheetah.Template import Template 18 from Cheetah.DummyTransaction import DummyTransaction 19 from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList 20 import Cheetah.Filters as Filters 21 import Cheetah.ErrorCatchers as ErrorCatchers 22 ################################################## 23 ## MODULE CONSTANTS 24 try: 25 True, False 26 except NameError: 27 True, False = (1==1), (1==0) 28 ################################################## 29 ## CLASSES 30 class GenTemplate(Template): 31 """ 32 33 Autogenerated by CHEETAH: The Python-Powered Template Engine 34 """ 35 ################################################## 36 ## GENERATED METHODS
37 def __init__(self, *args, **KWs): 38 """ 39 40 """ 41 Template.__init__(self, *args, **KWs) 42 def respond(self, 43 trans=None, 44 dummyTrans=False, 45 VFS=valueFromSearchList, 46 VFN=valueForName, 47 getmtime=getmtime, 48 currentTime=time.time): 49 """ 50 This is the main method generated by Cheetah 51 """ 52 if not trans: 53 trans = DummyTransaction() 54 dummyTrans = True 55 write = trans.response().write 56 SL = self._searchList 57 filter = self._currentFilter 58 globalSetVars = self._globalSetVars 59 60 ######################################## 61 ## START - generated method body 62 63 write('Hello, ') 64 write(filter(VFS(SL,"what",1))) # generated from '$what' at # line 1, col 8. 65 write(' world') 66 write(filter(VFS(SL,"punctuation",1))) # generated from # '$punctuation' at line 1, col 19. 67 write("\nOne of Python's least-used methods is ") 68 write(filter(xrange)) # generated from '$xrange' at line 2, # col 39. 69 write('.\n') 70 71 ######################################## 72 ## END - generated method body 73 74 if dummyTrans: 75 return trans.response().getvalue() 76 else: 77 return ""
78 79 ################################################## 80 ## GENERATED ATTRIBUTES 81 __str__ = respond 82 _mainCheetahMethod_for_GenTemplate= 'respond' 83 # CHEETAH was developed by Tavis Rudd, Chuck Esterbrook, Ian Bicking # and Mike Orr; 84 # with code, advice and input from many other volunteers. 85 # For more information visit http://www.CheetahTemplate.org 86 ################################################## 87 ## if run from command line: 88 if __name__ == '__main__': 89 GenTemplate().runAsMainProgram()
(Again, I have added line numbers and split the lines as in the previous chapter.)
This generated template module is different from the previous one in several
trivial respects and one important respect. Trivially,
._filePath
and ._fileMtime
are not updated in
.__init__
, so they inherit the value None
from
Template
. Also, that if-stanza in .respond
that recompiles the
template if the source file changes is missing - because there is no source
file. So this module is several lines shorter than the other one.
But the important way this module is different is that instead of the one
write
call outputting a string literal, this module has a series of
write
calls (lines 63-69) outputting successive chunks of the
template. Regular text has been translated into a string literal, and
placeholders into function calls. Every placeholder is wrapped inside a
filter
call to apply the current output filter. (The default
output filter converts all objects to strings, and None
to ""
.)
Placeholders referring to a Python builtin like xrange
(line 68)
generate a bare variable name. Placeholders to be looked up in the searchList
have a nested function call; e.g.,
write(filter(VFS(SL,"what",1))) # generated from '$what' at line 1, col 8.
VFS
, remember, is a function imported from Cheetah.NameMapper
that looks up a value in a searchList. So we pass it the searchList, the
name to look up, and a boolean (1) indicating we want autocalling. (It's
1
rather than True
because it's generated from an
and
expression, and that's what Python 2.2 outputs for true and
expressions.)
Placeholders can get far more complicated than that. This example shows what
kind of code the various NameMapper features produce. The formulas are
taken from Cheetah's test suite, in the
Cheetah.Tests.SyntaxAndOutput.Placeholders
class.
1 placeholder: $aStr 2 placeholders: $aStr $anInt 2 placeholders, back-to-back: $aStr$anInt 1 placeholder enclosed in {}: ${aStr} 1 escaped placeholder: \$var func placeholder - with (): $aFunc() func placeholder - with (int): $aFunc(1234) func placeholder - with (string): $aFunc('aoeu') func placeholder - with ('''\nstring'\n'''): $aFunc('''\naoeu'\n''') func placeholder - with (string*int): $aFunc('aoeu'*2) func placeholder - with (int*float): $aFunc(2*2.0) Python builtin values: $None $True $False func placeholder - with ($arg=float): $aFunc($arg=4.0) deeply nested argstring: $aFunc( $arg = $aMeth( $arg = $aFunc( 1 ) ) ): function with None: $aFunc(None) autocalling: $aFunc! $aFunc(). nested autocalling: $aFunc($aFunc). list subscription: $aList[0] list slicing: $aList[:2] list slicing and subcription combined: $aList[:2][0] dict - NameMapper style: $aDict.one dict - Python style: $aDict['one'] dict combined with autocalled string method: $aDict.one.upper dict combined with string method: $aDict.one.upper() nested dict - NameMapper style: $aDict.nestedDict.two nested dict - Python style: $aDict['nestedDict']['two'] nested dict - alternating style: $aDict['nestedDict'].two nested dict - NameMapper style + method: $aDict.nestedDict.two.upper nested dict - alternating style + method: $aDict['nestedDict'].two.upper nested dict - NameMapper style + method + slice: $aDict.nestedDict.two.upper[:4] nested dict - Python style, variable key: $aDict[$anObj.meth('nestedDict')].two object method: $anObj.meth1 object method + complex slice: $anObj.meth1[0: ((4/4*2)*2)/$anObj.meth1(2) ] very complex slice: $( anObj.meth1[0: ((4/4*2)*2)/$anObj.meth1(2) ] )
We'll need a big program to set up the placeholder values. Here it is:
#!/usr/bin/env python from ComplexExample import ComplexExample try: # Python >= 2.2.1 True, False except NameError: # Older Python True, False = (1==1), (1==0) class DummyClass: _called = False def __str__(self): return 'object' def meth(self, arg="arff"): return str(arg) def meth1(self, arg="doo"): return arg def meth2(self, arg1="a1", arg2="a2"): return str(arg1) + str(arg2) def callIt(self, arg=1234): self._called = True self._callArg = arg def dummyFunc(arg="Scooby"): return arg defaultTestNameSpace = { 'aStr':'blarg', 'anInt':1, 'aFloat':1.5, 'aList': ['item0','item1','item2'], 'aDict': {'one':'item1', 'two':'item2', 'nestedDict':{1:'nestedItem1', 'two':'nestedItem2' }, 'nestedFunc':dummyFunc, }, 'aFunc': dummyFunc, 'anObj': DummyClass(), 'aMeth': DummyClass().meth1, } print ComplexExample( searchList=[defaultTestNameSpace] )
Here's the output:
1 placeholder: blarg 2 placeholders: blarg 1 2 placeholders, back-to-back: blarg1 1 placeholder enclosed in {}: blarg 1 escaped placeholder: $var func placeholder - with (): Scooby func placeholder - with (int): 1234 func placeholder - with (string): aoeu func placeholder - with ('''\nstring'\n'''): aoeu' func placeholder - with (string*int): aoeuaoeu func placeholder - with (int*float): 4.0 Python builtin values: 1 0 func placeholder - with ($arg=float): 4.0 deeply nested argstring: 1: function with None: autocalling: Scooby! Scooby. nested autocalling: Scooby. list subscription: item0 list slicing: ['item0', 'item1'] list slicing and subcription combined: item0 dict - NameMapper style: item1 dict - Python style: item1 dict combined with autocalled string method: ITEM1 dict combined with string method: ITEM1 nested dict - NameMapper style: nestedItem2 nested dict - Python style: nestedItem2 nested dict - alternating style: nestedItem2 nested dict - NameMapper style + method: NESTEDITEM2 nested dict - alternating style + method: NESTEDITEM2 nested dict - NameMapper style + method + slice: NEST nested dict - Python style, variable key: nestedItem2 object method: doo object method + complex slice: do very complex slice: do
And here - tada! - is the generated module.
To save space, I've included only the lines containing the write
calls.
The rest of the module is the same as in the first example, chapter
2.1. I've split some of the lines to make them fit on
the page.
1 write('1 placeholder: ') 2 write(filter(VFS(SL,"aStr",1))) # generated from '$aStr' at line 1, col 16. 3 write('\n2 placeholders: ') 4 write(filter(VFS(SL,"aStr",1))) # generated from '$aStr' at line 2, col 17. 5 write(' ') 6 write(filter(VFS(SL,"anInt",1))) # generated from '$anInt' at line 2, col 23. 7 write('\n2 placeholders, back-to-back: ') 8 write(filter(VFS(SL,"aStr",1))) # generated from '$aStr' at line 3, col 31. 9 write(filter(VFS(SL,"anInt",1))) # generated from '$anInt' at line 3, col 36. 10 write('\n1 placeholder enclosed in {}: ') 11 write(filter(VFS(SL,"aStr",1))) # generated from '${aStr}' at line 4, # col 31. 12 write('\n1 escaped placeholder: $var\nfunc placeholder - with (): ') 13 write(filter(VFS(SL,"aFunc",0)())) # generated from '$aFunc()' at line 6, # col 29. 14 write('\nfunc placeholder - with (int): ') 15 write(filter(VFS(SL,"aFunc",0)(1234))) # generated from '$aFunc(1234)' at # line 7, col 32. 16 write('\nfunc placeholder - with (string): ') 17 write(filter(VFS(SL,"aFunc",0)('aoeu'))) # generated from "$aFunc('aoeu')" # at line 8, col 35. 18 write("\nfunc placeholder - with ('''\\nstring'\\n'''): ") 19 write(filter(VFS(SL,"aFunc",0)('''\naoeu'\n'''))) # generated from # "$aFunc('''\\naoeu'\\n''')" at line 9, col 46. 20 write('\nfunc placeholder - with (string*int): ') 21 write(filter(VFS(SL,"aFunc",0)('aoeu'*2))) # generated from # "$aFunc('aoeu'*2)" at line 10, col 39. 22 write('\nfunc placeholder - with (int*float): ') 23 write(filter(VFS(SL,"aFunc",0)(2*2.0))) # generated from '$aFunc(2*2.0)' # at line 11, col 38. 24 write('\nPython builtin values: ') 25 write(filter(None)) # generated from '$None' at line 12, col 24. 26 write(' ') 27 write(filter(True)) # generated from '$True' at line 12, col 30. 28 write(' ') 29 write(filter(False)) # generated from '$False' at line 12, col 36. 30 write('\nfunc placeholder - with ($arg=float): ') 31 write(filter(VFS(SL,"aFunc",0)(arg=4.0))) # generated from # '$aFunc($arg=4.0)' at line 13, col 40. 32 write('\ndeeply nested argstring: ') 33 write(filter(VFS(SL,"aFunc",0)( arg = VFS(SL,"aMeth",0)( arg = VFS(SL,"aFunc",0)( 1 ) ) ))) # generated from '$aFunc( $arg = $aMeth( $arg = $aFunc( 1 ) ) )' # at line 14, col 26. 34 write(':\nfunction with None: ') 35 write(filter(VFS(SL,"aFunc",0)(None))) # generated from '$aFunc(None)' at # line 15, col 21. 36 write('\nautocalling: ') 37 write(filter(VFS(SL,"aFunc",1))) # generated from '$aFunc' at line 16, # col 14. 38 write('! ') 39 write(filter(VFS(SL,"aFunc",0)())) # generated from '$aFunc()' at line 16, # col 22.
40 write('.\nnested autocalling: ') 41 write(filter(VFS(SL,"aFunc",0)(VFS(SL,"aFunc",1)))) # generated from # '$aFunc($aFunc)' at line 17, col 21. 42 write('.\nlist subscription: ') 43 write(filter(VFS(SL,"aList",1)[0])) # generated from '$aList[0]' at line # 18, col 20. 44 write('\nlist slicing: ') 45 write(filter(VFS(SL,"aList",1)[:2])) # generated from '$aList[:2]' at # line 19, col 15. 46 write('\nlist slicing and subcription combined: ') 47 write(filter(VFS(SL,"aList",1)[:2][0])) # generated from '$aList[:2][0]' # at line 20, col 40. 48 write('\ndict - NameMapper style: ') 49 write(filter(VFS(SL,"aDict.one",1))) # generated from '$aDict.one' at line # 21, col 26. 50 write('\ndict - Python style: ') 51 write(filter(VFS(SL,"aDict",1)['one'])) # generated from "$aDict['one']" # at line 22, col 22. 52 write('\ndict combined with autocalled string method: ') 53 write(filter(VFS(SL,"aDict.one.upper",1))) # generated from # '$aDict.one.upper' at line 23, col 46. 54 write('\ndict combined with string method: ') 55 write(filter(VFN(VFS(SL,"aDict.one",1),"upper",0)())) # generated from # '$aDict.one.upper()' at line 24, col 35. 56 write('\nnested dict - NameMapper style: ') 57 write(filter(VFS(SL,"aDict.nestedDict.two",1))) # generated from # '$aDict.nestedDict.two' at line 25, col 33. 58 write('\nnested dict - Python style: ') 59 write(filter(VFS(SL,"aDict",1)['nestedDict']['two'])) # generated from # "$aDict['nestedDict']['two']" at line 26, col 29. 60 write('\nnested dict - alternating style: ') 61 write(filter(VFN(VFS(SL,"aDict",1)['nestedDict'],"two",1))) # generated # from "$aDict['nestedDict'].two" at line 27, col 34. 62 write('\nnested dict - NameMapper style + method: ') 63 write(filter(VFS(SL,"aDict.nestedDict.two.upper",1))) # generated from # '$aDict.nestedDict.two.upper' at line 28, col 42. 64 write('\nnested dict - alternating style + method: ') 65 write(filter(VFN(VFS(SL,"aDict",1)['nestedDict'],"two.upper",1))) # generated from "$aDict['nestedDict'].two.upper" at line 29, col 43. 66 write('\nnested dict - NameMapper style + method + slice: ')
67 write(filter(VFN(VFS(SL,"aDict.nestedDict.two",1),"upper",1)[:4])) # generated from '$aDict.nestedDict.two.upper[:4]' at line 30, col 50. 68 write('\nnested dict - Python style, variable key: ') 69 write(filter(VFN(VFS(SL,"aDict",1) [VFN(VFS(SL,"anObj",1),"meth",0)('nestedDict')],"two",1))) # generated from "$aDict[$anObj.meth('nestedDict')].two" at line 31, # col 43. 70 write('\nobject method: ') 71 write(filter(VFS(SL,"anObj.meth1",1))) # generated from '$anObj.meth1' at # line 32, col 16. 72 write('\nobject method + complex slice: ') 73 write(filter(VFN(VFS(SL,"anObj",1),"meth1",1) [0: ((4/4*2)*2)/VFN(VFS(SL,"anObj",1),"meth1",0)(2) ])) # generated from '$anObj.meth1[0: ((4/4*2)*2)/$anObj.meth1(2) ]' # at line 33, col 32. 74 write('\nvery complex slice: ') 75 write(filter(VFN(VFS(SL,"anObj",1),"meth1",1) [0: ((4/4*2)*2)/VFN(VFS(SL,"anObj",1),"meth1",0)(2) ] )) # generated from '$( anObj.meth1[0: ((4/4*2)*2)/$anObj.meth1(2) ] )' # at line 34, col 21. 76 write('\n')
For each placeholder lookup, the the innermost level of nesting is a VFS
call, which looks up the first (leftmost) placeholder component in the
searchList. This is wrapped by zero or more VFN
calls, which perform
Universal Dotted Notation lookup on the next dotted component of the
placeholder, looking for an attribute or key by that name within the previous
object (not in the searchList). Autocalling is performed by VFS
and
VFN
: that's the reason for their third argument.
Explicit function/method arguments, subscripts and keys (which are all expressions) are left unchanged, besides expanding any embedded $placeholders in them. This means they must result in valid Python expressions, following the standard Python quoting rules.
Built-in Python values (None
, True
and False
) are
converted to filter(None)
, etc. They use normal Python variable
lookup rather than VFS
. (Cheetah emulates True
and False
using global variables for Python < 2.2.1, when they weren't builtins yet.)
The template:
Dynamic variable: $voom
The command line and the output:
% voom='Voom!' python x.py --env Dynamic variable: Voom!
The generated code:
write('Dynamic variable: ') write(filter(VFS(SL,"voom",1))) # generated from '$voom' at line 1, col 20. write('\n')
Just what we expected, like any other dynamic placeholder.
The template:
Cached variable: $*voom
The command line and output:
% voom='Voom!' python x.py --env Cached variable: Voom!
The generated code, with line numbers:
1 write('Cached variable: ') 2 ## START CACHE REGION: at line, col (1, 19) in the source. 3 RECACHE = True 4 if not self._cacheData.has_key('19760169'): 5 pass 6 else: 7 RECACHE = False 8 if RECACHE: 9 orig_trans = trans 10 trans = cacheCollector = DummyTransaction() 11 write = cacheCollector.response().write 12 write(filter(VFS(SL,"voom",1))) # generated from '$*voom' at line 1, # col 19. 13 trans = orig_trans 14 write = trans.response().write 15 self._cacheData['19760169'] = cacheCollector.response().getvalue() 16 del cacheCollector 17 write(self._cacheData['19760169']) 18 ## END CACHE REGION 19 write('\n')
That one little star generated a whole lotta code. First, instead of an
ordinary VFS
lookup (searchList) lookup, it converted the
placeholder to a lookup in the ._cacheData
dictionary. Cheetah also
generated a unique key ('19760169'
) for our cached item - this is its
cache ID.
Second, Cheetah put a pair of if-blocks before the write
. The first
(lines 3-7) determine whether the cache value is missing or out of date, and
sets local variable RECHARGE
true or false.
This stanza may look unnecessarily verbose - lines 3-7 could be eliminated if
line 8 was changed to
if not self._cacheData.has_key('19760169'):
The second if-block, lines 8-16, do the cache updating if necessary.
Clearly, the programmer is trying to stick as close to normal (dynamic)
workflow as possible. Remember that write
, even though it looks like a
local function, is actually a method of a file-like object. So we create a
temporary file-like object to divert the write
object into, then read
the result and stuff it into the cache.
The template:
Timed cache: $*.5m*voom
The command line and the output:
% voom='Voom!' python x.py --env Timed cache: Voom!
The generated method's docstring:
""" This is the main method generated by Cheetah This cache will be refreshed every 30.0 seconds. """
The generated code:
1 write('Timed cache: ') 2 ## START CACHE REGION: at line, col (1, 15) in the source. 3 RECACHE = True 4 if not self._cacheData.has_key('55048032'): 5 self.__cache55048032__refreshTime = currentTime() + 30.0 6 elif currentTime() > self.__cache55048032__refreshTime: 7 self.__cache55048032__refreshTime = currentTime() + 30.0 8 else: 9 RECACHE = False 10 if RECACHE: 11 orig_trans = trans 12 trans = cacheCollector = DummyTransaction() 13 write = cacheCollector.response().write 14 write(filter(VFS(SL,"voom",1))) # generated from '$*.5m*voom' at # line 1, col 15. 15 trans = orig_trans 16 write = trans.response().write 17 self._cacheData['55048032'] = cacheCollector.response().getvalue() 18 del cacheCollector 19 write(self._cacheData['55048032']) 20 ## END CACHE REGION 21 write('\n')
This code is identical to the static cache example except for the docstring and the first if-block. (OK, so the cache ID is different and the comment on line 14 is different too. Big deal.)
Each timed-refresh cache item has a corrsponding private attribute
.__cache########__refreshTime
giving the refresh time
in ticks (=seconds since January 1, 1970). The first if-block (lines 3-9)
checks whether the cache value is missing or its update time has passed, and if
so, sets RECHARGE
to true and also schedules another refresh at the next
interval.
The method docstring reminds the user how often the cache will be refreshed. This information is unfortunately not as robust as it could be. Each timed-cache placeholder blindly generates a line in the docstring. If all refreshes are at the same interval, there will be multiple identical lines in the docstring. If the refreshes are at different intervals, you get a situation like this:
""" This is the main method generated by Cheetah This cache will be refreshed every 30.0 seconds. This cache will be refreshed every 60.0 seconds. This cache will be refreshed every 120.0 seconds. """
This example is the same but with the long placeholder syntax. It's here because it's a Cheetah FAQ whether to put the cache interval inside or outside the braces. (It's also here so I can look it up because I frequently forget.) The answer is: outside. The braces go around only the placeholder name (and perhaps some output-filter arguments.)
The template:
Timed with {}: $*.5m*{voom}
The output:
Timed with {}: Voom!
The generated code differs only in the comment. Inside the cache-refresh if-block:
write(filter(VFS(SL,"voom",1))) # generated from '$*.5m*{voom}' at line 1, #col 17.
The reason this example is here is because it's a Cheetah FAQ whether to
put the cache interval inside or outside the {}
. (Also so I can look
it up when I forget, as I frequently do.) The answer is: outside. The
{}
go around only the placeholder name and arguments. If you do it
this way:
Timed with {}: ${*.5m*voom} ## Wrong!
Timed with {}: ${*.5m*voom}
${
is not a valid placeholder, so it's treated as ordinary text.
The template:
#cache This is a cached region. $voom #end cache
The output:
This is a cached region. Voom!
The generated code:
1 ## START CACHE REGION: at line, col (1, 1) in the source. 2 RECACHE = True 3 if not self._cacheData.has_key('23711421'): 4 pass 5 else: 6 RECACHE = False 7 if RECACHE: 8 orig_trans = trans 9 trans = cacheCollector = DummyTransaction() 10 write = cacheCollector.response().write 11 write('This is a cached region. ') 12 write(filter(VFS(SL,"voom",1))) # generated from '$voom' at line 2, # col 27. 13 write('\n') 14 trans = orig_trans 15 write = trans.response().write 16 self._cacheData['23711421'] = cacheCollector.response().getvalue() 17 del cacheCollector 18 write(self._cacheData['23711421']) 19 ## END CACHE REGION
This is the same as the $*voom
example, except that the plain text
around the placeholder is inside the second if-block.
The template:
#cache timer='.5m', id='cache1' This is a cached region. $voom #end cache
The output:
This is a cached region. Voom!
The generated code is the same as the previous example except the first if-block:
RECACHE = True if not self._cacheData.has_key('13925129'): self._cacheIndex['cache1'] = '13925129' self.__cache13925129__refreshTime = currentTime() + 30.0 elif currentTime() > self.__cache13925129__refreshTime: self.__cache13925129__refreshTime = currentTime() + 30.0 else: RECACHE = False
The template:
#cache test=$isDBUpdated This is a cached region. $voom #end cache
(Analysis postponed: bug in Cheetah produces invalid Python.)
The template:
#cache id='cache1', test=($isDBUpdated or $someOtherCondition) This is a cached region. $voom #end cache
The output:
This is a cached region. Voom!
The first if-block in the generated code:
RECACHE = True if not self._cacheData.has_key('36798144'): self._cacheIndex['cache1'] = '36798144' elif (VFS(SL,"isDBUpdated",1) or VFS(SL,"someOtherCondition",1)): RECACHE = True else: RECACHE = False
()
around the test expression, the result is the same, although
it may be harder for the template maintainer to read.
You can even combine arguments, although this is of questionable value.
The template:
#cache id='cache1', timer='30m', test=$isDBUpdated or $someOtherCondition This is a cached region. $voom #end cache
The output:
This is a cached region. Voom!
The first if-block:
RECACHE = True if not self._cacheData.has_key('88939345'): self._cacheIndex['cache1'] = '88939345' self.__cache88939345__refreshTime = currentTime() + 1800.0 elif currentTime() > self.__cache88939345__refreshTime: self.__cache88939345__refreshTime = currentTime() + 1800.0 elif VFS(SL,"isDBUpdated",1) or VFS(SL,"someOtherCondition",1): RECACHE = True else: RECACHE = False
We are planning to add a 'varyBy'
keyword argument in the future that
will allow a separate cache instances to be created for a variety of conditions,
such as different query string parameters or browser types. This is inspired by
ASP.net's varyByParam and varyByBrowser output caching keywords. Since this is
not implemented yet, I cannot provide examples here.
The template:
Text before the comment. ## The comment. Text after the comment. #* A multi-line comment spanning several lines. It spans several lines, too. *# Text after the multi-line comment.
The output:
Text before the comment. Text after the comment. Text after the multi-line comment.
The generated code:
write('Text before the comment.\n') # The comment. write('Text after the comment.\n') # A multi-line comment spanning several lines. # It spans several lines, too. write('\nText after the multi-line comment.\n')
The template:
##doc: .respond() method comment. ##doc-method: Another .respond() method comment. ##doc-class: A class comment. ##doc-module: A module comment. ##header: A header comment.
The output:
The beginning of the generated .respond
method:
def respond(self, trans=None, dummyTrans=False, VFS=valueFromSearchList, VFN=valueForName, getmtime=getmtime, currentTime=time.time): """ This is the main method generated by Cheetah .respond() method comment. Another .respond() method comment. """
The class docstring:
""" A class comment. Autogenerated by CHEETAH: The Python-Powered Template Engine """
The top of the module:
#!/usr/bin/env python # A header comment. """A module comment. Autogenerated by CHEETAH: The Python-Powered Template Engine CHEETAH VERSION: 0.9.13a1 Generation time: Fri Apr 26 22:39:23 2002 Source file: x.tmpl Source file last modified: Fri Apr 26 22:36:23 2002 """
The template:
Here is my #echo ', '.join(['silly']*5) # example
The output:
Here is my silly, silly, silly, silly, silly example
The generated code:
write('Here is my ') write(filter(', '.join(['silly']*5) )) write(' example\n')
The template:
Here is my #silent ', '.join(['silly']*5) # example
The output:
Here is my example
The generated code:
write('Here is my ') ', '.join(['silly']*5) write(' example\n')
OK, it's not quite covert because that extra space gives it away, but it almost succeeds.
The template:
Text before raw. #raw Text in raw. $alligator. $croc.o['dile']. #set $a = $b + $c. #end raw Text after raw.
The output:
Text before raw. Text in raw. $alligator. $croc.o['dile']. #set $a = $b + $c. Text after raw.
The generated code:
write('''Text before raw. Text in raw. $alligator. $croc.o['dile']. #set $a = $b + $c. Text after raw. ''')
So we see that #raw
is really like a quoting mechanism. It says that
anything inside it is ordinary text, and Cheetah joins a #raw
section
with adjacent string literals rather than generating a separate write
call.
The main template:
#include "y.tmpl"
The included template y.tmpl:
Let's go $voom!
The shell command and output:
% voom="VOOM" x.py --env Let's go VOOM!
The generated code:
write(self._includeCheetahSource("y.tmpl", trans=trans, includeFrom="file", raw=0))
The main template:
#include raw "y.tmpl"
The shell command and output:
% voom="VOOM" x.py --env Let's go $voom!
The generated code:
write(self._includeCheetahSource("y.tmpl", trans=trans, includeFrom="fil e", raw=1))
That last argument, raw
, makes the difference.
The template:
#attr $y = "Let's go $voom!" #include source=$y #include raw source=$y #include source="Bam! Bam!"
The output:
% voom="VOOM" x.py --env Let's go VOOM!Let's go $voom!Bam! Bam!
The generated code:
write(self._includeCheetahSource(VFS(SL,"y",1), trans=trans, includeFrom="str", raw=0, includeID="481020889808.74")) write(self._includeCheetahSource(VFS(SL,"y",1), trans=trans, includeFrom="str", raw=1, includeID="711020889808.75")) write(self._includeCheetahSource("Bam! Bam!", trans=trans, includeFrom="str", raw=0, includeID="1001020889808.75"))
Later in the generated class:
y = "Let's go $voom!"
The template:
#for $i in range(5) $i #end for #for $i in range(5) $i #slurp #end for Line after slurp.
The output:
0 1 2 3 4 0 1 2 3 4 Line after slurp.
The generated code:
for i in range(5): write(filter(i)) # generated from '$i' at line 2, col 1. write('\n') for i in range(5): write(filter(i)) # generated from '$i' at line 5, col 1. write(' ') write('Line after slurp.\n')
The space after each number is because of the space before #slurp
in
the template definition.
The template:
#attr $ode = ">> Rubber Ducky, you're the one! You make bathtime so much fun! <<" $ode #filter WebSafe $ode #filter MaxLen ${ode, maxlen=13} #filter None ${ode, maxlen=13}
The output:
>> Rubber Ducky, you're the one! You make bathtime so much fun! << >> Rubber Ducky, you're the one! You make bathtime so much fun! << >> Rubber Duc >> Rubber Ducky, you're the one! You make bathtime so much fun! <<
The WebSafe
filter escapes characters that have a special meaning in
HTML. The MaxLen
filter chops off values at the specified length.
#filter None
returns to the default filter, which ignores the maxlen
argument.
The generated code:
1 write(filter(VFS(SL,"ode",1))) # generated from '$ode' at line 2, col 1. 2 write('\n') 3 filterName = 'WebSafe' 4 if self._filters.has_key("WebSafe"): 5 filter = self._currentFilter = self._filters[filterName] 6 else: 7 filter = self._currentFilter = \ 8 self._filters[filterName] = getattr(self._filtersLib, filterName)(self).filter 9 write(filter(VFS(SL,"ode",1))) # generated from '$ode' at line 4, col 1. 10 write('\n') 11 filterName = 'MaxLen' 12 if self._filters.has_key("MaxLen"): 13 filter = self._currentFilter = self._filters[filterName] 14 else: 15 filter = self._currentFilter = \ 16 self._filters[filterName] = getattr(self._filtersLib, filterName)(self).filter 17 write(filter(VFS(SL,"ode",1), maxlen=13)) # generated from #'${ode, maxlen=13}' at line 6, col 1. 18 write('\n') 19 filter = self._initialFilter 20 write(filter(VFS(SL,"ode",1), maxlen=13)) # generated from #'${ode, maxlen=13}' at line 8, col 1. 21 write('\n')
As we've seen many times, Cheetah wraps all placeholder lookups in a
filter
call. (This also applies to non-searchList lookups: local,
global and builtin variables.) The filter
``function''
is actually an alias to the current filter object:
filter = self._currentFilter
filter
is not an alias to
the filter object itself but to that object's .filter
method. Line 19
switches back to the default filter.
In line 17 we see the maxlen
argument being passed as a keyword
argument to filter
(not to VFS
). In line 20 the same thing
happens although the default filter ignores the argument.
The template:
#import math
This construct does not produce any output.
The generated module, at the bottom of the import section:
import math
The template:
#extends SomeClass
The generated import (skipped if SomeClass
has already been
imported):
from SomeClass import SomeClass
The generated class:
class x(SomeClass):
The template:
#implements doOutput
In the generated class, the main method is .doOutput
instead of
.respond
, and the attribute naming this method is:
_mainCheetahMethod_for_x2= 'doOutput'
The template:
#set $namesList = ['Moe','Larry','Curly'] $namesList #set global $toes = ['eeny', 'meeny', 'miney', 'moe'] $toes
The output:
['Moe', 'Larry', 'Curly'] ['eeny', 'meeny', 'miney', 'moe']
The generated code:
1 namesList = ['Moe','Larry','Curly'] 2 write(filter(namesList)) # generated from '$namesList' at line 2, col 1. 3 write('\n') 4 globalSetVars["toes"] = ['eeny', 'meeny', 'miney', 'moe'] 5 write(filter(VFS(SL,"toes",1))) # generated from '$toes' at line 4, col 1. 6 write('\n')
globalSetVars
is a local variable shadowing ._globalSetVars
.
Writes go into it directly, but reads take advantage of the fact that
._globalSetVars
is on the searchList. (In fact, it's the very first
namespace.)
The template:
#set $a = 1 #del $a #set $a = 2 #set $arr = [0, 1, 2] #del $a, $arr[1]
In the generated class:
1 a = 1 2 del a 3 a = 2 4 arr = [0, 1, 2] 5 del a, arr[1]
The template:
#attr $namesList = ['Moe', 'Larry', 'Curly']
In the generated class:
## GENERATED ATTRIBUTES namesList = ['Moe', 'Larry', 'Curly']
The template:
#def printArg($arg) The argument is $arg. #end def My method returned $printArg(5).
The output:
My method returned The argument is 5. .
Hmm, not exactly what we expected. The method returns a trailing newline
because we didn't end the last line with #slurp
. So the second
period (outside the method) appears on a separate line.
The #def
generates a method .printArg
whose structure is similar
to the main method:
def printArg(self, arg, trans=None, dummyTrans=False, VFS=valueFromSearchList, VFN=valueForName, getmtime=getmtime, currentTime=time.time): """ Generated from #def printArg($arg) at line 1, col 1. """ if not trans: trans = DummyTransaction() dummyTrans = True write = trans.response().write SL = self._searchList filter = self._currentFilter globalSetVars = self._globalSetVars ######################################## ## START - generated method body write('The argument is ') write(filter(arg)) # generated from '$arg' at line 2, col 17. write('.\n') ######################################## ## END - generated method body if dummyTrans: return trans.response().getvalue() else: return ""
When .printArg
is called from a placeholder, only the arguments the user
supplied are passed. The other arguments retain their default values.
The template:
#block content This page is under construction. #end block
The output:
This page is under construction.
This construct generates a method .content
in the same structure
as .printArg
above, containing the write code:
write('This page is under construction.\n')
In the main method, the write code is:
self.content(trans=trans) # generated from ('content', '#block content') # at line 1, col 1.
So a block placeholder implicitly passes the current transaction to the method.
This directive is undocumented because it's likely to disappear in Cheetah 0.9.14.
The template:
#for $i in $range(10) $i #slurp #end for
The output:
0 1 2 3 4 5 6 7 8 9
The generated code:
for i in range(10): write(filter(i)) # generated from '$i' at line 2, col 1. write(' ')
The template:
#repeat 3 My bonnie lies over the ocean #end repeat O, bring back my bonnie to me!
The output:
My bonnie lies over the ocean My bonnie lies over the ocean My bonnie lies over the ocean O, bring back my bonnie to me!
The generated code:
for i in range( 3): write('My bonnie lies over the ocean\n') write('O, bring back my bonnie to me!\n')
The template:
#set $alive = True #while $alive I am alive! #set $alive = False #end while
The output:
I am alive!
The generated code:
alive = True while alive: write('I am alive!\n') alive = False
The template:
#set $size = 500 #if $size >= 1500 It's big #else if $size < 1500 and $size > 0 It's small #else It's not there #end if
The output:
It's small
The generated code:
size = 500 if size >= 1500: write("It's big\n") elif size < 1500 and size > 0: write("It's small\n") else: write("It's not there\n")
The template:
#set $count = 9 #unless $count + 5 > 15 Count is in range. #end unless
The output:
Count is in range.
The generated code:
count = 9 if not (count + 5 > 15): write('Count is in range.\n')
Note: There is a bug in Cheetah 0.9.13. It's forgetting the
parentheses in the if
expression, which could lead to it calculating
something different than it should.
The template:
#for $i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'James', 'Joe', 'Snow'] #if $i == 10 #continue #end if #if $i == 'Joe' #break #end if $i - #slurp #end for
The output:
1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 11 - 12 - James -
The generated code:
for i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'James', 'Joe', 'Snow']: if i == 10: write('') continue if i == 'Joe': write('') break write(filter(i)) # generated from '$i' at line 8, col 1. write(' - ')
The template:
Let's check the number. #set $size = 500 #if $size >= 1500 It's big #elif $size > 0 #pass #else Invalid entry #end if Done checking the number.
The output:
Let's check the number. Done checking the number.
The generated code:
write("Let's check the number.\n") size = 500 if size >= 1500: write("It's big\n") elif size > 0: pass else: write('Invalid entry\n') write('Done checking the number.\n')
The template:
A cat #if 1 sat on a mat #stop watching a rat #end if in a flat.
The output:
A cat sat on a mat
The generated code:
write('A cat\n') if 1: write(' sat on a mat\n') if dummyTrans: return trans.response().getvalue() else: return "" write(' watching a rat\n') write('in a flat.\n')
The template:
1 $test[1] 3 #def test 1.5 #if 1 #return '123' #else 99999 #end if #end def
The output:
1 2 3
The generated code:
def test(self, trans=None, dummyTrans=False, VFS=valueFromSearchList, VFN=valueForName, getmtime=getmtime, currentTime=time.time): """ Generated from #def test at line 5, col 1. """ if not trans: trans = DummyTransaction() dummyTrans = True write = trans.response().write SL = self._searchList filter = self._currentFilter globalSetVars = self._globalSetVars ######################################## ## START - generated method body write('1.5\n') if 1: return '123' else: write('99999\n') ######################################## ## END - generated method body if dummyTrans: return trans.response().getvalue() else: return ""
def respond(self, trans=None, dummyTrans=False, VFS=valueFromSearchList, VFN=valueForName, getmtime=getmtime, currentTime=time.time): """ This is the main method generated by Cheetah """ if not trans: trans = DummyTransaction() dummyTrans = True write = trans.response().write SL = self._searchList filter = self._currentFilter globalSetVars = self._globalSetVars ######################################## ## START - generated method body write('\n1\n') write(filter(VFS(SL,"test",1)[1])) # generated from '$test[1]' at line 3, col 1. write('\n3\n') ######################################## ## END - generated method body if dummyTrans: return trans.response().getvalue() else: return ""
The template:
#import traceback #try #raise RuntimeError #except RuntimeError A runtime error occurred. #end try #try #raise RuntimeError("Hahaha!") #except RuntimeError #echo $sys.exc_info()[1] #end try #try #echo 1/0 #except ZeroDivisionError You can't divide by zero, idiot! #end try
The output:
A runtime error occurred. Hahaha! You can't divide by zero, idiot!
The generated code:
try: raise RuntimeError except RuntimeError: write('A runtime error occurred.\n') write('\n') try: raise RuntimeError("Hahaha!") except RuntimeError: write(filter(VFN(sys,"exc_info",0)()[1])) write('\n') write('\n') try: write(filter(1/0)) write('\n') except ZeroDivisionError: write("You can't divide by zero, idiot!\n")
#finally
works just like in Python.
The template:
#assert False, "You lose, buster!"
The output:
Traceback (most recent call last): File "x.py", line 117, in ? x().runAsMainProgram() File "/local/opt/Python/lib/python2.2/site-packages/Webware/Cheetah/ Template.py", line 331, in runAsMainProgram CmdLineIface(templateObj=self).run() File "/local/opt/Python/lib/python2.2/site-packages/Webware/Cheetah/ TemplateCmdLineIface.py", line 59, in run print self._template File "x.py", line 91, in respond assert False, "You lose, buster!" AssertionError: You lose, buster!
The generated code:
assert False, "You lose, buster!"
The template:
$noValue
The output:
Traceback (most recent call last): File "x.py", line 118, in ? x().runAsMainProgram() File "/local/opt/Python/lib/python2.2/site-packages/Webware/Cheetah/ Template.py", line 331, in runAsMainProgram CmdLineIface(templateObj=self).run() File "/local/opt/Python/lib/python2.2/site-packages/Webware/Cheetah/ TemplateCmdLineIface.py", line 59, in run print self._template File "x.py", line 91, in respond write(filter(VFS(SL,"noValue",1))) # generated from '$noValue' at line 1, col 1. NameMapper.NotFound: noValue
The generated code:
write(filter(VFS(SL,"noValue",1))) # generated from '$noValue' at line 1, # col 1. write('\n')
The template:
#errorCatcher Echo $noValue #errorCatcher BigEcho $noValue
The output:
$noValue ===============<$noValue could not be found>===============
The generated code:
if self._errorCatchers.has_key("Echo"): self._errorCatcher = self._errorCatchers["Echo"] else: self._errorCatcher = self._errorCatchers["Echo"] = ErrorCatchers.Echo(self) write(filter(self.__errorCatcher1(localsDict=locals()))) # generated from '$noValue' at line 2, col 1. write('\n') if self._errorCatchers.has_key("BigEcho"): self._errorCatcher = self._errorCatchers["BigEcho"] else: self._errorCatcher = self._errorCatchers["BigEcho"] = \ ErrorCatchers.BigEcho(self) write(filter(self.__errorCatcher1(localsDict=locals()))) # generated from '$noValue' at line 4, col 1. write('\n')
The template:
#import pprint #errorCatcher ListErrors $noValue $anotherMissingValue.really $pprint.pformat($errorCatcher.listErrors) ## This is really self.errorCatcher().listErrors()
The output:
$noValue $anotherMissingValue.really [{'code': 'VFS(SL,"noValue",1)', 'exc_val': <NameMapper.NotFound instance at 0x8170ecc>, 'lineCol': (3, 1), 'rawCode': '$noValue', 'time': 'Wed May 15 00:38:23 2002'}, {'code': 'VFS(SL,"anotherMissingValue.really",1)', 'exc_val': <NameMapper.NotFound instance at 0x816d0fc>, 'lineCol': (4, 1), 'rawCode': '$anotherMissingValue.really', 'time': 'Wed May 15 00:38:23 2002'}]
The generated import:
import pprint
Then in the generated class, we have our familiar .respond
method
and several new methods:
def __errorCatcher1(self, localsDict={}): """ Generated from $noValue at line, col (3, 1). """ try: return eval('''VFS(SL,"noValue",1)''', globals(), localsDict) except self._errorCatcher.exceptions(), e: return self._errorCatcher.warn(exc_val=e, code= 'VFS(SL,"noValue",1)' , rawCode= '$noValue' , lineCol=(3, 1)) def __errorCatcher2(self, localsDict={}): """ Generated from $anotherMissingValue.really at line, col (4, 1). """ try: return eval('''VFS(SL,"anotherMissingValue.really",1)''', globals(), localsDict) except self._errorCatcher.exceptions(), e: return self._errorCatcher.warn(exc_val=e, code= 'VFS(SL,"anotherMissingValue.really",1)' , rawCode= '$anotherMissingValue.really' , lineCol=(4, 1)) def __errorCatcher3(self, localsDict={}): """ Generated from $pprint.pformat($errorCatcher.listErrors) at line, col (5, 1). """ try: return eval('''VFN(pprint,"pformat",0)(VFS(SL, "errorCatcher.listErrors",1))''', globals(), localsDict) except self._errorCatcher.exceptions(), e: return self._errorCatcher.warn(exc_val=e, code= 'VFN(pprint,"pformat",0)(VFS(SL,"errorCatcher.listErrors",1))' , rawCode= '$pprint.pformat($errorCatcher.listErrors)' , lineCol=(5, 1))
def respond(self, trans=None, dummyTrans=False, VFS=valueFromSearchList, VFN=valueForName, getmtime=getmtime, currentTime=time.time): """ This is the main method generated by Cheetah """ if not trans: trans = DummyTransaction() dummyTrans = True write = trans.response().write SL = self._searchList filter = self._currentFilter globalSetVars = self._globalSetVars ######################################## ## START - generated method body if exists(self._filePath) and getmtime(self._filePath) > self._fileMtime: self.compile(file=self._filePath) write(getattr(self, self._mainCheetahMethod_for_x)(trans=trans)) if dummyTrans: return trans.response().getvalue() else: return "" if self._errorCatchers.has_key("ListErrors"): self._errorCatcher = self._errorCatchers["ListErrors"] else: self._errorCatcher = self._errorCatchers["ListErrors"] = \ ErrorCatchers.ListErrors(self) write(filter(self.__errorCatcher1(localsDict=locals()))) # generated from '$noValue' at line 3, col 1. write('\n') write(filter(self.__errorCatcher2(localsDict=locals()))) # generated from '$anotherMissingValue.really' at line 4, col 1. write('\n') write(filter(self.__errorCatcher3(localsDict=locals()))) # generated from '$pprint.pformat($errorCatcher.listErrors)' at line # 5, col 1. write('\n') # This is really self.errorCatcher().listErrors() ######################################## ## END - generated method body if dummyTrans: return trans.response().getvalue() else: return ""
So whenever an error catcher is active, each placeholder gets wrapped in its own method. No wonder error catchers slow down the system!
The template:
Text before breakpoint. #breakpoint Text after breakpoint. #raise RuntimeError
The output:
Text before breakpoint.
The generated code:
write('Text before breakpoint.\n')
Nothing after the breakpoint was compiled.
The template:
// Not a comment #compiler commentStartToken = '//' // A comment #compiler reset // Not a comment
The output:
// Not a comment // Not a comment
The generated code:
write('// Not a comment\n') # A comment write('// Not a comment\n')
So this didn't affect the generated program, it just affected how the template definition was read.
This chapter will be an overview of the files in the Cheetah package, and how they interrelate in compiling and filling a template. We'll also look at files in the Cheetah tarball that don't get copied into the package.
This chapter will mainly walk through the Cheetah.Template
constructor
and not at what point the template is compiled.
(Also need to look at Transaction,py and Servlet.py .)
How templates are compiled: a walk through Parser.py's source. (Also need to look at Lexer.py, but not too closely.)
How templates are compiled: a walk through Compiler.py .
In spring 2001, several members of the webware-discuss mailing list expressed
the need for a template engine. Webware like Python is great for organizing
analytical logic, but they both suffer when you need to do extensive variable
interpolation into large pieces of text, or to build up a text string from its
nested parts. Python's %
operator gets you only so far, the syntax is
cumbersome, and you have to use a separate format string for each nested part.
Most of us had used template systems from other platforms-chiefly Zope's DTML,
PHPLib's Template object and Java's Velocity-and wanted to port something like
those so it could be used both in Webware servlets and in standalone Python
programs.
Since I (Mike Orr) am writing this history, I'll describe how I encountered Cheetah. I had written a template module called PlowPlate based on PHPLib's Template library. Like PHPLib, it used regular expressions to search and destroy-er, replace-placeholders, behaved like a dictionary to specify placeholder values, contained no directives, but did have BEGIN and END markers which could be used to extract a named block (subtemplate). Meanwhile, Tavis Rudd was also on webware-discuss and interested in templates, and he lived just a few hours away. So 12 May 12, 2001 we met in Vancouver at a gelato shop on Denman Street and discussed Webware, and he drew on a napkin the outline of a template system he was working on.
Instead of filling the template by search-and-replace, he wanted to break it up into parts. This was a primitive form of template compiling: do the time-consuming work once and put it to a state where you can fill the template quickly multiple times. A template without directives happens to break down naturally into a list of alternating text/placeholder pairs. The odd subscript values are literal strings; the even subscripts are string keys into a dictionary of placeholder values. The project was called TemplateServer.
In a couple months, Tavis decided that instead of compiling to a list, he
wanted to compile to Python source code: a series of write
calls that
would output onto a file-like object. This was the nucleus that became
Cheetah. I thought that idea was stupid, but it turned out that this
not-so-stupid idea blew the others out of the water in terms of performance.
Another thing Tavis pushed hard for from near the beginning was ``display
logic'', or simple directives like #for
, #if
and
#echo
. (OK, #echo
came later, but conceptually it belongs
here. I thought display logic was even stupider than compiling to Python
source code because it would just lead to ``DTML hell''-complicated templates
that are hard to read and maintain, and for which you have to learn (and debug)
a whole new language when Python does it just fine. But others (hi Chuck!) had
templates that were maintained by secretaries who didn't know Python, and the
secretaries needed display logic, so that was that. Finally, after working
with Cheetah templates (with display logic) and PlowPlate templates (with just
blocks rather than display logic), I realized Tavis was smarter than I was and
display logic really did belong in the template.
The next step was making directives for all the Python flow-control
statements: #while
, #try
, #assert
, etc. Some of
them we couldn't think of a use for. Nevertheless, they were easy to code,
and ``somebody'' would probably need them ``someday'', so we may as well
implement them now.
During all this, Chuck Esterbrook, Ian Bicking and others offered (and still offer) their support and suggestions, and Chuck gave us feedback about his use of Cheetah-its first deployment in a commercial production environment. Later, Edmund Lian became our #1 bug reporter and suggester as he used Cheetah in his web applications.
A breakthrough came in fall 2001 when Tavis figured out how to implement the name mapper in C. The name mapper is what gives Cheetah its Autocalling and Uniform Dotted Notation features. This raised performance sufficiently to rewrite Cheetah in a totally ``late binding'' manner like Python is. More about this is in the next chapter.
We were going to release 1.0 in January 2002, but we decided to delay it until more people used it in real-world situations and gave us feedback about what is still needed. This has led to many refinements, and we have added (and removed) features according to this feedback. Nevertheless, Cheetah has been changing but stable since the late-binding rewrite in fall 2001, and anybody who keeps up with the cheetah-discuss mailing list will know when changes occur that require modifying one's template, and since most people use point releases rather than CVS, they generally have a few week's warning about any significant changes.
More detail on Cheetah's history and evolution, and why it is the way it is, can be found in our paper for the Python10 conference, http://www.cheetahtemplate.org/Py10.html.
One of the first decisions we encountered was which delimiter syntax to use.
We decided to follow Velocity's $placeholder
and #directive
syntax because the former is widely used in other languages for the same
purpose, and the latter stands out in an HTML or text document. We also
implemented the ${longPlaceholder}
syntax like the shells for cases
where Cheetah or you might be confused where a placeholder ends. Tavis went
ahead and made $(longPlaceholder}
and $[longPlaceholder]
interchangeable with it since it was trivial to implement. Finally,
the #compiler
directive allows you to change the delimiters if you
don't like them or if they conflict with the text in your document.
(Obviously, if your document contains a Perl program listing, you don't
necessarily want to backslash each and every $
and #
, do you?)
The choice of comment delimiters was more arbitrary. ##
and
#* ...*#
doesn't match any language, but it's reminiscent of
Python and C while also being consistent with our ``#
is for
directives'' convention.
We specifically chose not to use pseudo HTML tags for placeholders and directives, as described more thoroughly in the Cheetah Users' Guide introduction. Pseudo HTML tags may be easier to see in a visual editor (supposedly), but in text editors they're hard to distinguish from ``real'' HTML tags unless you look closely, and they're many more keystrokes to type. Also, if you make a mistake, the tag will show up as literal text in the rendered HTML page where it will be easy to notice and eradicate, rather than disappearing as bogus HTML tags do in browsers.
One of Cheetah's unique features is the name mapper, which lets you write
$a.$b
without worrying much about the type of a
or b
.
Prior to version 0.9.7, Cheetah did the entire NameMapper lookup at runtime.
This provided maximum flexibility at the expense of speed. Doing a NameMapper
lookup is intrinsically more expensive than an ordinary Python expression
because Cheetah has to decide what type of container a
is, whether the
the value is a function (autocall it), issue the appropriate Python incantation
to look up b
in it, autocall again if necessary, and then convert the
result to a string.
To maximize run-time (filling-time) performance, Cheetah 0.9.7 pushed much of
this work back into the compiler. The compiler looked up a
in the
searchList at compile time, noted its type, and generated an eval'able Python
expression based on that.
This approach had two significant drawbacks. What if a
later changes
type before a template filling? Answer: unpredictable exceptions occur. What
if a
does not exist in the searchList at compile time? Answer: the
template can't compile.
To prevent these catastrophes, users were required to prepopulate the
searchList before instantiating the template instance, and then not to change
a
's type. Static typing is repugnant in a dynamic language like Python,
and having to prepopulate the searchList made certain usages impossible. For
example, you couldn't instantiate the template object without a searchList and
then set self
attributes to specify the values.
After significant user complaints about the fragility of this system, Tavis
rewrote placeholder handling, and in version 0.9.8a3 (August 2001), Tavis
moved the name mapper lookup back into runtime. Performance wasn't crippled
because he discovered that writing a C version of the name mapper was easier
than anticipated, and the C version completed the lookup quickly. Now Cheetah
had ``late binding'', meaning the compiler does not look up a
or care
whether it exists. This allows users to create a
or change its type
anytime before a template filling.
The lesson we learned is that it's better to decide what you want and then figure out how to do it, rather than assuming that certain goals are unattainable due to performance considerations.
How to commit changes to CVS or submit patches, how to run the test suite. Describe distutils and how the regression tests work.
The codeTemplate class contains not only the Cheetah infrastructure, but also
some convenience methods useful in all templates. More methods may be added if
it's generally agreed among Cheetah developers that the method is sufficiently
useful to all types of templates, or at least to all types of HTML-output
templates. If a method is too long to fit into Template
- especially
if it has helper methods - put it in a mixin class under Cheetah.Utils
and inherit it.
Routines for a specific problem domain should be put under
Cheetah.Tools
, so that it doesn't clutter the namespace unless the user
asks for it.
Remember: Cheetah.Utils
is for objects required by any part of Cheetah's
core. Cheetah.Tools
is for completely optional objects. It should
always be possible to delete Cheetah.Tools
without breaking Cheetah's
core services.
If a core method needs to look up an attribute defined under
Cheetah.Tools
, it should use hasattr()
and gracefully provide a
default if the attribute does not exist (meaning the user has not imported that
subsystem).
Cheetah ships with a regression test suite. To run the built-in tests, execute at the shell prompt:
cheetah test
Before checking any changes in, run the tests and verify they all pass. That way, users can check out the CVS version of Cheetah at any time with a fairly high confidence that it will work. If you fix a bug or add a feature, please take the time to add a test that exploits the bug/feature. This will help in the future, to prevent somebody else from breaking it again without realizing it. Users can also run the test suite to verify all the features work on their particular platform and computer.
The general procedure for modifying Cheetah is as follows:
diff
on the two
outputs. (diff
is a utility included on all Unix-like systems. It
shows the differences between two files line by line. A precompiled
Windows version is at
http://gnuwin32.sourceforge.net/packages/diffutils.htm, and MacOS
sources at http://perso.wanadoo.fr/gilles.depeyrot/DevTools_en.html.)
python setup.py install
before testing it. If you make it in the
installed version, do not run the installer or it will overwrite your
changes!
cheetah test
to verify you didn't break anything. Then run
your little test program.
cheetah test
runs cleanly with your regression test
included, update the CHANGES
file and check in your changes. If you
made the changes in your installed copy of Cheetah, you'll have to copy
them back into the CVS sandbox first. If you added any files that must be
distributed, be sure to cvs add
them before committing.
Otherwise Cheetah will run fine on your computer but fail on anybody
else's, and the test suite can't check for this.
If you add a directory to Cheetah, you have to mention it in setup.py
or
it won't be installed.
The tests are in the Cheetah.Tests
package, aka the src/Tests/
directory of your CVS sandbox. Most of the tests are in
SyntaxAndOutput.py
. You can either run all the tests or choose which
to run:
python Test.py
Run all the tests. (Equivalent to cheetah test
.)
python SyntaxAndOutput.py
Run only the tests in that module.
python SyntaxAndOutput.py CGI
Run only the tests in the class CGI
inside the module. The class
must be a direct or indirect subclass of
unittest_local_copy.TestCase
.
python SyntaxAndOutput.py CGI Indenter
Run the tests in classes CGI
and Indenter
.
python SyntaxAndOutput.py CGI.test1
Run only test test1
, which is a method in the CGI
class.
To make a SyntaxAndOutput test, first see if your test logically fits into one
of the existing classes. If so, simply add a method; e.g., test16
.
The method should not require any arguments except self
, and should
call .verify(source, expectedOutput)
, where the two arguments are
a template definition string and a control string. The tester will complain
if the template output does not match the control string. You have a wide
variety of placeholder variables to choose from, anything that's included in
the defaultTestNameSpace
global dictionary. If that's not enough, add
items to the dictionary, but please keep it from being cluttered with wordy
esoteric items for a single test).
If your test logically belongs in a separate class, create a subclass of
OutputTest
. You do not need to do anything else; the test suite will
automatically find your class in the module. Having a separate class allows
you to define state variables needed by your tests (see the CGI
class)
or override .searchList()
(see the Indenter
class) to provide
your own searchList.
To modify another test module or create your own test module, you'll have to
study the existing modules, the unittest_local_copy
source, and the
unittest
documentation in the Python Library Reference. Note that we
are using a hacked version of unittest
to make a more convenient test
structure for Cheetah. The differences between unittest_local_copy
and Python's standard unittest
are documented at the top of the module.
How to build the documentation. Why LaTeX, a minimum LaTeX reference, etc.
Safe delegation, as provided by Zope and Allaire's Spectra, is not implemented in Cheetah. The core aim has been to help developers and template maintainers get things done, without throwing unnecessary complications in their way. So you should give write access to your templates only to those whom you trust. However, several hooks have been built into Cheetah so that safe delegation can be implemented at a later date.
It should be possible to implement safe delegation via a future configuration
Setting safeDelegationLevel
(0=none, 1=semi-secure, 2-alcatraz). This
is not implemented but the steps are listed here in case somebody wants to try
them out and test them.
Of course, you would also need to benchmark your code and verify it does not impact performance when safe delegation is off, and impacts it only modestly when it is on." All necessary changes can be made at compile time, so there should be no performance impact when filling the same TO multiple times.
#attr
directive and maybe the #set
directive.
self
, etc
(e.g. #if $steal(self.thePrivateVar)
)
$obj.__class__.__dict__
are not possible.
This document was generated using the LaTeX2HTML translator.
LaTeX2HTML is Copyright © 1993, 1994, 1995, 1996, 1997, Nikos Drakos, Computer Based Learning Unit, University of Leeds, and Copyright © 1997, 1998, Ross Moore, Mathematics Department, Macquarie University, Sydney.
The application of LaTeX2HTML to the Python documentation has been heavily tailored by Fred L. Drake, Jr. Original navigation icons were contributed by Christopher Petrilli.