# Cheetah Developers' Guide

Mike Orr with assistance from Tavis Rudd

iron@mso.oz.net

## Contents

- Contents
- 1 Introduction
  - 1.1 Who should read this Guide?
  - 1.2 Contents
- 2 .py Template Modules
  - 2.1 An example
  - 2.2 A walk through the example
- 3 Placeholders
  - 3.1 Simple placeholders
  - 3.2 Complex placeholders
- 4 Caching placeholders and #cache
  - 4.1 Dynamic placeholder - no cache
  - 4.2 Static placeholder
  - 4.3 Timed-refresh placeholder
  - 4.4 Timed-refresh placeholder with braces
  - 4.5 #cache
  - 4.6 #cache with timer and id
  - 4.7 #cache with test: expression and method conditions
- 5 Directives: Comments
  - 5.1 Docstring and header comments
- 6 Directives: Output
  - 6.1 #echo
  - 6.2 #silent
  - 6.3 #raw
  - 6.4 #include
  - 6.5 #slurp
  - 6.6 #filter
- 7 Directives: Import, Inheritance, Declaration and Assignment
  - 7.1 #import and #from
  - 7.2 #extends
  - 7.3 #implements
  - 7.4 #set and #set global
  - 7.5 #del
  - 7.6 #attr
  - 7.7 #def
  - 7.8 #block
  - 7.9 #settings
- 8 Directives: Flow Control
  - 8.1 #for
  - 8.2 #repeat
  - 8.3 #while
  - 8.4 #if
  - 8.5 #unless
  - 8.6 #break and #continue
  - 8.7 #pass
  - 8.8 #stop
  - 8.9 #return
- 9 Directives: Error Handling
  - 9.1 #try and #raise
  - 9.2 #assert
  - 9.3 #errorCatcher
- 10 Directives: Parser Instructions
  - 10.1 #breakpoint
  - 10.2 #compiler
- 11 Files
- 12 Template
- 13 The parser
- 14 The compiler
- 15 History of Cheetah
- 16 Design Decisions and Tradeoffs
  - 16.1 Delimiters
  - 16.2 Late binding
  - 16.3 Caching framework
  - 16.4 Webware compatibility and the transaction framework
  - 16.5 Single inheritance
- 17 Patching Cheetah
  - 17.1 File Requirements
  - 17.2 Testing Changes and Building Regression Tests
- 18 Documenting Cheetah
- A. A BNF Grammar of Cheetah
- B. Safe Delegation
- About this document ...

©Copyright 2002, Mike Orr. This document may be copied and modified under the terms of the Open Publication License http://www.opencontent.org/openpub/

# 1 Introduction

## 1.1 Who should read this Guide?

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.

## 1.2 Contents

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.

#
2 .py Template Modules

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.

##
2.1 An example

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.

##
2.2 A walk through the example

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.

#
3 Placeholders

##
3.1 Simple placeholders

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.)

##
3.2 Complex placeholders

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.)

#
4 Caching placeholders and #cache

##
4.1 Dynamic placeholder - no cache

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.

##
4.2 Static 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'):

- but this model is expandable for some of the cache features we'll see below.

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.

##
4.3 Timed-refresh placeholder

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.
      """

The docstring tells only that ``something'' will be refreshed every 60.0 seconds, but doesn't reveal which placeholder that is. Only if you know the relative order of the placeholders in the template can you figure that out.

##
4.4 Timed-refresh placeholder with braces

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!

you get:

      Timed with {}:  ${*.5m*voom}

because `${` is not a valid placeholder, so it's treated as ordinary text.

##
4.5 #cache

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.

##
4.6 #cache with timer and id

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

##
4.7 #cache with test: expression and method conditions

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

The second if-block is the same as in the previous example. If you leave out the `()` 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.

#
5 Directives: Comments

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')

##
5.1 Docstring and header comments

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
      """

#
6 Directives: Output

##
6.1 #echo

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')

##
6.2 #silent

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.

##
6.3 #raw

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.

##
6.4 #include

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))

###
6.4.1 #include raw

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.

###
6.4.2 #include from a string or expression (eval)

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!"

##
6.5 #slurp

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.

##
6.6 #filter

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! <<
      &gt;&gt; Rubber Ducky, you're the one!  You make bathtime so much fun! &lt;&lt;
      >> 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

as set at the top of the main method. Here in lines 3-8 and 11-16 we see the filter being changed. Whoops, I lied. `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.

#
7 Directives: Import, Inheritance, Declaration and Assignment

##
7.1 #import and #from

The template:

      #import math

This construct does not produce any output.

The generated module, at the bottom of the import section:

      import math

##
7.2 #extends

The template:

      #extends SomeClass

The generated import (skipped if `SomeClass` has already been imported):

      from SomeClass import SomeClass

The generated class:

      class x(SomeClass):

##
7.3 #implements

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'

##
7.4 #set and #set global

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.)

##
7.5 #del

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]

##
7.6 #attr

The template:

      #attr $namesList = ['Moe', 'Larry', 'Curly']

In the generated class:

      ## GENERATED ATTRIBUTES

      namesList = ['Moe', 'Larry', 'Curly']

##
7.7 #def

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.

##
7.8 #block

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.

##
7.9 #settings

This directive is undocumented because it's likely to disappear in Cheetah 0.9.14.

#
8 Directives: Flow Control

##
8.1 #for

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(' ')

##
8.2 #repeat

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!

(OK, so the second line should be ``sea'' instead of ``ocean''.)

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')

##
8.3 #while

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

##
8.4 #if

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")

##
8.5 #unless

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.

##
8.6 #break and #continue

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(' - ')

##
8.7 #pass

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')

##
8.8 #stop

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')

##
8.9 #return

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 ""

#
9 Directives: Error Handling

##
9.1 #try and #raise

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.

##
9.2 #assert

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!"

##
9.3 #errorCatcher

###
9.3.1 No error catcher

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')

###
9.3.2 Echo and BigEcho

The template:

      #errorCatcher Echo
      $noValue
      #errorCatcher BigEcho
      $noValue

The output:

      $noValue
      ===============&lt;$noValue could not be found&gt;===============

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')

###
9.3.3 ListErrors

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!

#
10 Directives: Parser Instructions

##
10.1 #breakpoint

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.

##
10.2 #compiler

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.

#
11 Files

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.

#
12 Template

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 .)

#
13 The parser

How templates are compiled: a walk through Parser.py's source. (Also need to look at Lexer.py, but not too closely.)

#
14 The compiler

How templates are compiled: a walk through Compiler.py .

#
15 History of Cheetah

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.

#
16 Design Decisions and Tradeoffs

##
16.1 Delimiters

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.

##
16.2 Late binding

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.

##
16.3 Caching framework

##
16.4 Webware compatibility and the transaction framework

##
16.5 Single inheritance

#
17 Patching Cheetah

How to commit changes to CVS or submit patches, how to run the test suite. Describe distutils and how the regression tests work.

##
17.1 File Requirements

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).

##
17.2 Testing Changes and Building Regression Tests

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:

1. Write a simple Python program that exploits the bug/feature you're working on. You can either write a regression test (see below), or a separate program that writesthe template output to one file and put the expected output in another file; then you can run `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.)
2. Make the change in your Cheetah CVS sandbox or in your installed version of Cheetah. If you make it in the sandbox, you'll have to run `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!
3. Run `cheetah test` to verify you didn't break anything. Then run your little test program.
4. Repeat steps 2-3 until everything is correct.
5. Turn your little program into a regression test as described below.
6. When `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.
7. Announce the change on the cheetahtemplate-discuss list and provide a tutorial if necessary. The documentation maintainer will update the Users' Guide and Developers' Guide based on this message and on the changelog.

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.
  etc...

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.

#
18 Documenting Cheetah

How to build the documentation. Why LaTeX, a minimum LaTeX reference, etc.

#
A. A BNF Grammar of Cheetah

#
B. Safe Delegation

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.

1. Only give untrusted developers access to the .tmpl files. (Verifying what this means. Why can't trusted developers access them?)

2. Disable the `#attr` directive and maybe the `#set` directive.

3. Use Cheetah's directive validation hooks to disallow references to `self`, etc (e.g. `#if $steal(self.thePrivateVar)` )

4. Implement a validator for the $placeholders and use it to disallow '__' in $placeholders so that tricks like `$obj.__class__.__dict__` are not possible.

# About this document ...

Cheetah Developers' Guide

This document was generated using the LaTeX2`HTML` translator.

LaTeX2`HTML` 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 LaTeX2`HTML` to the Python documentation has been heavily tailored by Fred L. Drake, Jr. Original navigation icons were contributed by Christopher Petrilli.