Tizen 2.1 base tizen_2.1 tizen_2.2 2.1b_release 2.2.1_release 2.2_release accepted/tizen_2.1/20130425.021251 submit/tizen_2.1/20130424.235907 submit/tizen_2.1/20130425.060507 submit/tizen_2.2/20130714.153550
authorJinkun Jang <jinkun.jang@samsung.com>
Tue, 12 Mar 2013 17:21:25 +0000 (02:21 +0900)
committerJinkun Jang <jinkun.jang@samsung.com>
Tue, 12 Mar 2013 17:21:25 +0000 (02:21 +0900)
85 files changed:
CHANGES [new file with mode: 0644]
Cheetah.egg-info/PKG-INFO [new file with mode: 0644]
Cheetah.egg-info/SOURCES.txt [new file with mode: 0644]
Cheetah.egg-info/dependency_links.txt [new file with mode: 0644]
Cheetah.egg-info/requires.txt [new file with mode: 0644]
Cheetah.egg-info/top_level.txt [new file with mode: 0644]
LICENSE [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
PKG-INFO [new file with mode: 0644]
README.markdown [new file with mode: 0644]
SetupConfig.py [new file with mode: 0644]
SetupTools.py [new file with mode: 0644]
TODO [new file with mode: 0644]
bin/cheetah [new file with mode: 0755]
bin/cheetah-analyze [new file with mode: 0644]
bin/cheetah-compile [new file with mode: 0644]
cheetah/CacheRegion.py [new file with mode: 0644]
cheetah/CacheStore.py [new file with mode: 0644]
cheetah/CheetahWrapper.py [new file with mode: 0644]
cheetah/Compiler.py [new file with mode: 0644]
cheetah/DirectiveAnalyzer.py [new file with mode: 0644]
cheetah/Django.py [new file with mode: 0644]
cheetah/DummyTransaction.py [new file with mode: 0644]
cheetah/ErrorCatchers.py [new file with mode: 0644]
cheetah/FileUtils.py [new file with mode: 0644]
cheetah/Filters.py [new file with mode: 0644]
cheetah/ImportHooks.py [new file with mode: 0755]
cheetah/ImportManager.py [new file with mode: 0755]
cheetah/Macros/I18n.py [new file with mode: 0644]
cheetah/Macros/__init__.py [new file with mode: 0644]
cheetah/NameMapper.py [new file with mode: 0644]
cheetah/Parser.py [new file with mode: 0644]
cheetah/Servlet.py [new file with mode: 0644]
cheetah/SettingsManager.py [new file with mode: 0644]
cheetah/SourceReader.py [new file with mode: 0644]
cheetah/Template.py [new file with mode: 0644]
cheetah/TemplateCmdLineIface.py [new file with mode: 0644]
cheetah/Templates/SkeletonPage.py [new file with mode: 0644]
cheetah/Templates/SkeletonPage.tmpl [new file with mode: 0644]
cheetah/Templates/_SkeletonPage.py [new file with mode: 0644]
cheetah/Templates/__init__.py [new file with mode: 0644]
cheetah/Tests/Analyzer.py [new file with mode: 0644]
cheetah/Tests/CheetahWrapper.py [new file with mode: 0644]
cheetah/Tests/Cheps.py [new file with mode: 0644]
cheetah/Tests/Filters.py [new file with mode: 0644]
cheetah/Tests/Misc.py [new file with mode: 0644]
cheetah/Tests/NameMapper.py [new file with mode: 0644]
cheetah/Tests/Parser.py [new file with mode: 0644]
cheetah/Tests/Performance.py [new file with mode: 0644]
cheetah/Tests/Regressions.py [new file with mode: 0644]
cheetah/Tests/SyntaxAndOutput.py [new file with mode: 0644]
cheetah/Tests/Template.py [new file with mode: 0644]
cheetah/Tests/Test.py [new file with mode: 0755]
cheetah/Tests/Unicode.py [new file with mode: 0644]
cheetah/Tests/__init__.py [new file with mode: 0644]
cheetah/Tests/xmlrunner.py [new file with mode: 0644]
cheetah/Tools/CGITemplate.py [new file with mode: 0644]
cheetah/Tools/MondoReport.py [new file with mode: 0644]
cheetah/Tools/MondoReportDoc.txt [new file with mode: 0644]
cheetah/Tools/RecursiveNull.py [new file with mode: 0644]
cheetah/Tools/SiteHierarchy.py [new file with mode: 0644]
cheetah/Tools/__init__.py [new file with mode: 0644]
cheetah/Tools/turbocheetah/__init__.py [new file with mode: 0644]
cheetah/Tools/turbocheetah/cheetahsupport.py [new file with mode: 0644]
cheetah/Tools/turbocheetah/tests/__init__.py [new file with mode: 0644]
cheetah/Tools/turbocheetah/tests/test_template.py [new file with mode: 0644]
cheetah/Unspecified.py [new file with mode: 0644]
cheetah/Utils/Indenter.py [new file with mode: 0644]
cheetah/Utils/Misc.py [new file with mode: 0644]
cheetah/Utils/WebInputMixin.py [new file with mode: 0644]
cheetah/Utils/__init__.py [new file with mode: 0644]
cheetah/Utils/htmlDecode.py [new file with mode: 0644]
cheetah/Utils/htmlEncode.py [new file with mode: 0644]
cheetah/Utils/statprof.py [new file with mode: 0644]
cheetah/Version.py [new file with mode: 0644]
cheetah/__init__.py [new file with mode: 0644]
cheetah/c/Cheetah.h [new file with mode: 0644]
cheetah/c/_namemapper.c [new file with mode: 0644]
cheetah/c/cheetah.h [new file with mode: 0644]
cheetah/convertTmplPathToModuleName.py [new file with mode: 0644]
packaging/python-cheetah.changes [new file with mode: 0644]
packaging/python-cheetah.manifest [new file with mode: 0644]
packaging/python-cheetah.spec [new file with mode: 0644]
setup.cfg [new file with mode: 0644]
setup.py [new file with mode: 0755]

diff --git a/CHANGES b/CHANGES
new file mode 100644 (file)
index 0000000..92f713a
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,1663 @@
+2.4.2 (February 8th, 2010)
+  - Fix issue where subclasses of Template failed to pick up attributes in the
+    searchlist
+  - Remove old/outdated bundled memcached python client
+  - Allow for #encoding directives to exist after a comment (i.e. not the first
+    line in a module)
+  - Remove support for WebWare servlets (which caused significant performance
+    slowdowns on Mac OS X)
+  - Old/stale code pruned in preparation for Python 3 support
+
+2.4.1 (December 19th, 2009)
+  - --quiet flag added to `cheetah` to silence printing to stdout (abbeyj)
+  - Refactoring to minimize the amount of forked code for Python3 (rtyler)
+  - Template.compile() will no longer create class names with numerous leading
+    underscores (rtyler; reported by Kirill Uhanov)
+  - DirectiveAnalyzer (cheetah-analyze script) added to report directive usage in templates (rtyler)
+  - Older LaTeX docs converted to rst for Sphinx (rtyler)
+  - Prevent #raw blocks from evaluating $-placeholders and escaped strings (karmix0)
+  - New tests added to verify PSP behavior and other untested internals (rtyler)
+
+2.4.0 (October 24th, 2009)
+  - Fix a major performance regression in Template.__init__()
+  - More graceful handling of unicode when calling .respond() to render a template
+  - Minor code updates
+  - Update the default filter (thanks mikeb!)
+
+2.3.0 (October 24th, 2009) (loosely equivalent to 2.4.0)
+  - Fix a major performance regression in Template.__init__()
+  - More graceful handling of unicode when calling .respond() to render a template
+  - Minor code updates
+  - Update the default filter (thanks mikeb!)
+
+2.2.2 (September 10th, 2009)
+  - Prevent _namemapper.c from segfaulting when PyImport_ImportModule fails for some reason (Bogdano Arendartchuk <debogdano@gmail.com>)
+  - Removal of the contrib/markdown module (in favor of a setuptools dependency)
+  - Default setup.py to use setuptools by default, failing that, fall back to distutils
+  - Improvements to setup.py to support building for Windows (thanks abbeyj!)
+  - Improvements to C-based NameMapper for Windows
+  - Fixes for a swath of unit tests on Windows
+  - Re-enabling the EOL tests (whoops)
+  - Fix for unicode/utf-8 dynamic compilation error (thanks mikeb!) (Test.Unicode.JBQ_UTF8_Test8)
+  - 0000010: [Templates] Failure to execute templates on Google App Engine (rtyler) 
+  - 0000026: [Compiler] Support multiple inheritance (rtyler) 
+
+
+2.2.1 (June 1st, 2009)
+  - 0000020: [Templates] Builtin support for using Cheetah with Django (rtyler) 
+  - 0000021: [Compiler] @static and @classmethod don't properly define the _filter local (rtyler) 
+  - 0000023: [Compiler] Update Template super calls to use super() (rtyler) 
+  - Update all references to communitycheetah.org to point back at cheetahtemplate.org
+
+2.2.0 (May 17th, 2009)
+  - Switch all internal representations of template code to unicode objects instead of str() objects
+  - Convert unicode compiled template to an utf8 char buffer when writing to a file (Jean-Baptiste Quenot <jbq@caraldi.com>)
+  - 0000011: [Templates] Calling a function with arguments calls the function with None (rtyler) 
+  - 0000015: [Tests] Resolve test failures in 'next' branch (rtyler)
+  - 0000019: [Templates] Properly warn when joining unicode and non-unicode objects in DummyTransaction (rtyler)
+
+2.1.2 (May 5, 2009)
+  - 0000006: [Templates] Support @staticmethod and @classmethod (rtyler) 
+
+2.1.1 (April 16, 2009)
+  - Support __eq__() and __ne__() the way you might expect in src/Tools/RecursiveNull (patch suggested by Peter Warasin <peter@endian.com>)
+  - Applied patch to avoid hitting the filesystem to get the file modification time everytime a #include directive is processed (Jean-Baptiste Quenot <jbq@caraldi.com>)
+  - Applied patch to fix some annoying cases when Cheetah writes to stderr instead of propagating the exception (Jean-Baptiste Quenot <jbq@caraldi.com>)
+  - Added KDE editor support
+  - Applied patch to correct importHook behavior on Python 2.6 (reported/patched by Toshio Ernie Kuratomi <a.badger@gmail.com>)
+  - Correct unicode issue when calling/embedding unicode templates inside of other templtes (testcase Tests.Unicode.JPQ_UTF8_Test3. reported by Jean-Baptiste Quenot <jbq@caraldi.com>)
+  - Added --shbang option (e.g. "cheetah compile --shbang '#!/usr/bin/python2.6' ")
+  - Removed dependency on optik OptionParser in favor of builtin Python optparse module
+  - Introduction of the #transform directive for whole-document filtering
+  - Introduction of Cheetah.contrib.markdown and Cheetah.Filters.Markdown for outputting a markdown processed template (meant for #transform)
+  - Cheetah.Filters.CodeHighlighter, pygments-based code highlighting filter for use with #transform
+  - Addition of "useLegacyImportMode" compiler setting (defaulted to True) to allow for older (read: broken) import behavior
+
+2.1.0.1 (March 27, 2009)
+  - Fix inline import issue introduced in v2.1.0
+
+2.1.0 (March 16, 2009)
+  - Quiet DeprecationWarnings being printed to stderr when using Cheetah on Python 2.6 and up. Patch suggested by Satoru SATOH <satoru.satoh@gmail.com>
+  - Apply patch to support parallel compilation of templates courtesy of Evan Klitzke <evan@eklitzke.org>
+  - Corrected issue when __getattr__ calls on searchList objects raise exceptions (tyler@slide.com)
+  - make autocalling in valueForName correctly ignore newstyle classes and instances
+    that are callable, as it does for oldstyle classes and instances.  Patch
+    from lucas@endian.com
+    [TR]
+  - made it possible to chain multiple decorators to a method #def [TR with
+    patch from Graham Dennis]
+  - fixed a bug in _eatMultiLineDef that Graham Dennis reported. [TR] 
+  - fixed 'module.__init__() argument 1 must be string, not unicode' bug in
+    Template.py reported by Erwin Ambrosch [TR]
+
+2.0.1 (Nov 16, 2007)
+  - fixed a deadlock Christoph Zwerschke found in Cheetah.ImportHooks.
+    [TR]
+
+2.0 (Oct 12, 2007)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  
+  - fixed exception handling issue in the C implemenation of NameMapper 
+    [patch from Eric Huss]
+
+  - fixed filtering of #included subtemplates
+    [patch from Brian Bird]
+
+  See the release notes from 2.0b1-5 and 2.0rc1-8 for other changes since
+  Cheetah 1.0.
+
+
+2.0rc8 (April 11, 2007)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+  
+    - added a '#unicode <encoding>' directive to indicate that the output of the
+      template should be a unicode string even if the template source is a
+      normal byte string.
+          
+          - #unicode and #encoding are mutually exclusive.  Use one or the other. 
+          - #unicode must be on a line by itself.
+          - Strings in embedded code must be explictly marked as unicode if they 
+            contain non-ascii chars:
+          
+            #unicode latin-1
+            $f(u"<some non-ascii char>") ## right
+            $f("<some non-ascii char>") ## wrong
+          
+            However, this works fine:
+          
+            #unicode latin-1 
+            blah blah <some non-ascii char> blah blah
+          
+    - fixed several unicode bugs in the compiler.
+
+    - fixed some unicode issues in the standard filters.
+  
+    - fixed a few minor bugs in code that never gets called.  Thanks to
+      Alejandro Dubrovsky for pointing them out.
+  
+    - make RawOrEncodedUnicode the baseclass of all filters and remove some
+      unused/redudant filters
+
+    - added new compiler setting 'addTimestampsToCompilerOutput'. See Brian
+      Bird's post about it.  He stores his cheetah generated .py files in
+      subversion and needed to disable the timestamp code so svn wouldn't care
+      when he recompiles those .py modules.
+  
+    - added the #super directive, which calls the method from the parent class
+      which has the same as the current #def or #block method.  
+  
+       #def foo
+                  ... child output
+          #super  ## includes output of super(<CurrentClass>, self).foo()
+          ... child output
+       #end def 
+
+
+       #def bar(arg)
+                  ... child output
+          #super(arg)  ## includes output of super(<CurrentClass>, self).bar(arg)
+          ... child output
+       #end def 
+
+    - added some unit tests for the new directives
+
+
+2.0rc7 (July 4, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - extended the #implements directive so an arguments list can be declared in
+      the same fashion as #def and #block.
+    
+    - made the parser raise ParseError when $*placeholder, $*5*placeholder,
+      $(placeholder), etc. are found within expressions.  They are only valid in
+      top-level text. 
+
+    - tweaked the parser so it's possible to place a comment on the same line as
+      a directive without needing to explicitly close the directive first. This
+      works regardless of whether or not you added a colon.
+      
+              self.verify("#if 1:\n$aStr\n#end if\n",
+                          "blarg\n")
+      
+              self.verify("#if 1:   \n$aStr\n#end if\n",
+                          "blarg\n")
+      
+              self.verify("#if 1: ##comment \n$aStr\n#end if\n",
+                              "blarg\n")
+      
+              self.verify("#if 1 ##comment \n$aStr\n#end if\n",
+                              "blarg\n")
+      
+      Previously, that last test would have required an extra # to close the #if 
+      directive before the comment directive started:
+              self.verify("#if 1 ###comment \n$aStr\n#end if\n",
+                              "blarg\n")
+      
+      Code that makes use of explicit directive close tokens immediately followed by 
+      another directive will still work as expected:
+      #if test##for i in range(10)# foo $i#end for##end if
+
+    - safer handling of the baseclass arg to Template.compile().  It now does
+      the right thing if the user passes in an instance rather than a class.
+
+  ImportHooks: [TR]
+    - made it possible to specify a list of template filename extentions that are
+      looped through while searching for template modules. E.g.:
+        import Cheetah.ImportHooks
+       Cheetah.ImportHooks.install(templateFileExtensions=('.tmpl','.cheetah'))
+
+  Core changes by MO:
+    - Filters are now new-style classes.
+    - WebSafe and the other optional filters in Filters.py now use
+      RawOrEncodedUnicode instead of Filter as a base class.  This allows them
+      to work with Unicode values containing non-ASCII characters.
+          User-written custom filters should inherit from 
+      RawOrEncodedUnicode and call the superclass .filter() instead of str().
+      str() as of Python 2.4.2 still converts Unicode to string using
+      ASCII codec, which raises UnicodeEncodeError if it contains non-ASCII
+      characters.
+
+2.0rc6 (Feb 4, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - added a Cheetah version dependency check that raises an assertion if a
+      template was compiled with a previous version of Cheetah whose templates
+      must be recompiled.
+
+    - made the Cheetah compilation metadata accessible via class attributes in
+      addition to module globals
+
+    - major improvement to exception reporting in cases where bad Python syntax
+      slips past the Cheetah parser:
+      """
+         File "/usr/lib/python2.4/site-packages/Cheetah/Template.py", line 792, in compile
+           raise parseError
+       Cheetah.Parser.ParseError: 
+       
+       Error in the Python code which Cheetah generated for this template:
+       ================================================================================
+       
+       invalid syntax (DynamicallyCompiledCheetahTemplate.py, line 86)
+       
+       Line|Python Code
+       ----|-------------------------------------------------------------
+       84  |        
+       85  |        write('\n\n')
+       86  |        for i an range(10): # generated from line 4, col 1
+                           ^
+       87  |            _v = i # '$i' on line 5, col 3
+       88  |            if _v is not None: write(_filter(_v, rawExpr='$i')) # from line 5, col 3.
+       89  |            write('\n')
+       
+       ================================================================================
+       
+       Here is the corresponding Cheetah code:
+       
+       Line 4, column 1
+       
+       Line|Cheetah Code
+       ----|-------------------------------------------------------------
+       2   |#compiler useNameMapper=False
+       3   |
+       4   |#for i an range(10)
+            ^
+       5   |  $i
+       6   |#end for
+       7   |
+       """
+       
+2.0rc5 (Feb 3, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - fixed a memory leak in Template.compile(), reported by Andrea Arcangeli
+    - simplified concurrency locking and compile caching in Template.compile()
+
+  The command line tool (CheetahWrapper.py): 
+    - added new option --settings for supplying compiler settings
+    - added new option --templateAPIClass to replace the environment var
+      CHEETAH_TEMPLATE_CLASS lookup I added in 2.0b1
+
+2.0rc4 (Jan 31, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - fixed a typo-bug in the compile hashing code in Template.compile() 
+    - improved the macros framework and made it possible to implement macros in
+      Python code so they can be shared between templates
+    - more work on the #i18n directive.  It's now a macro directive.
+    - added new Cheetah.Macros package
+    - more tests
+2.0rc3 (Jan 29, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - added short-form single line versions of all directives that have an #end
+      tag, except for #errorCatcher:
+         #if, #else, #elif, #unless,
+         #for, #while, #repeat,  
+        #try, #except, #finally, 
+        #cache, #raw
+        #call, #capture
+
+      The #def and #block directives already had single-line versions.
+          #if cond: foo
+         #elif cond2: bar
+         #else: blarg
+         
+         #for i, val in enumerate(vals): $i-$val
+
+      Note that if you accidentally leave a colon at the end of one of these
+      directives but nothing else follows it, aside from whitespace, the parser
+      will treat it as a normal multi-line directive.
+
+      The first leading space after the colon is discarded. Any additional
+      spaces will be included in the output.
+      
+      Also note, if you use the short form versions of #if/#else/#elif you must
+      it for all three.  The following is not valid:
+          #if cond: foo
+         #elif cond2
+            bar
+         #else: blarg
+
+    - added support for $!silentModePlaceholders
+      This is the same as quiet mode in Velocity: 
+       http://jakarta.apache.org/velocity/docs/user-guide.html#Quiet%20Reference%20Notation
+
+    - added support for function/method @decorators.  It also works with blocks.
+      As in vanilla Python, the @decorator statement must be followed with a
+      function/method definition (i.e. #def or #block).
+
+        #from xxx import aDecorator
+        ...  
+        ...
+        #@aDecorator
+        #def func
+          foo
+        #end def
+
+        #@aDecorator
+        #def singleLineShortFormfunc: foo
+
+        #@aDecorator
+        #block func2
+          bar
+        #end block
+
+    - added a new callback hook 'handlerForExtendsDirective' to the compiler settings. It
+      can be used to customize the handling of #extends directives.  The
+      callback can dynamically add import statements or rewrite the baseclass'
+      name if needed:
+         baseClassName = handler(compiler, baseClassName)
+      See the discussion on the mailing list on Jan 25th for more details.
+
+    - changed the default filter to the one that doesn't try to encode Unicode
+      It was 'EncodeUnicode' and is now 'RawOrEncodedUnicode'.
+
+    - added optional support for parsing whitespace between the directive start
+      token (#) and directive names, per Christophe Eymard's request.  For the
+      argument behind this see the mailing list archives for Jan 29th.  This is
+      off by default.  You must turn it on using the compiler setting
+      allowWhitespaceAfterDirectiveStartToken=True
+
+        #for $something in $another
+        #  for $somethin2 in $another2
+              blahblah $something in $something2
+        #  end for
+        #end for
+        
+    - made the handling of Template.compile()'s preprocessors arg simpler and
+      fixed a bug in it.
+
+    - fixed attribute name bug in the .compile() method (it affected the feature
+      that allows generated module files to be cached for better exception
+      tracebacks)
+
+    - refactored the #cache/CacheRegions code to support abitrary backend cache
+      data stores.
+
+    - added MemcachedCacheStore, which allows cache data to be stored in a
+      memcached backend.  See http://www.linuxjournal.com/article/7451 and
+      http://www.danga.com/memcached/.  This is only appropriate for systems
+      running many Python server processes that need to share cached data to
+      reduce memory requirements. Don't bother with this unless you actually
+      need it. If you have a limited number of Python server processes it is
+      much faster, simpler, and more secure to just cache in the memory of each
+      process.
+
+      KEEP MEMCACHED'S LIMITED SECURITY IN MIND!!  It has no authentication or
+      encryption and will introduce a gaping hole in your defenses unless you
+      are careful.  If you are caching sensitive data you should take measures
+      to ensure that a) untrusted local system users cannot connect to memcached
+      server, b) untrusted external servers cannot connect, and c) untrusted
+      users on trusted external servers cannot connect.  Case (a) can be dealt
+      with via iptable's owner match module for one way to do this: "iptables -A
+      ... -m owner ..."  Cases (b) and (c) can be handled by tunnelling
+      memcached network connections over stunnel and implementing stunnel
+      authentication with mandatory peer/client certs.
+
+    - some under-the-hood refactoring of the parser
+    
+    - made it possible to add custom directives, or customize the
+      parsing/handling of existing ones, via the compiler settings
+      'directiveNamesAndParsers' and 'endDirectiveNamesAndHandlers'
+      
+    - added a compile-time macro facility to Cheetah. These macros are very
+      similar to macros in Lisp:
+      http://www.apl.jhu.edu/~hall/Lisp-Notes/Macros.html.  
+
+      As with Lisp macros, they take source code (Cheetah source) as input and
+      return source code (again Cheetah source) as output. They are executed at
+      compile-time, just like in Lisp and C. The resultant code
+      gets executed at run-time.
+
+      The new #defmacro directive allows users to create macros inside the
+      source of their templates.  Macros can also be provided via the compiler
+      setting 'macroDirectives'. The 'macroDirectives' setting allows you to
+      share common macros between templates.  
+
+      The syntax for the opening tag of #defmacro is the same as for #def and
+      #block.  It expects a macro name followed by an optional argument list in
+      brackets.  A `src` argument is automatically added to the beginning of
+      every macro's argument list.  The value of the `src` is the block of
+      input source code that is provided during a macro call (see below).  
+
+        #defmacro <macroname>[(argspec)]
+         <macrobody>
+       #end defmacro
+
+      All of Cheetah's syntax is available for use inside macros, but the
+      placeholderStartToken is @ instead of $ and the
+      directiveStartToken/EndToken is % instead of #.  Any syntax using the
+      standard $/# tokens will be treated as plain text and included in the output
+      of the macro.
+
+      Here are some examples:
+        #defmacro addHeaderFooter
+         header 
+         @src 
+         footer
+       #end defmacro
+
+        #defmacro addHeaderFooter(header='h', footer='f')
+         @header 
+         @src 
+         @footer
+       #end defmacro
+
+      There is a single-line short form like for other directives:
+               
+        #defmacro addHeaderFooter: header @src footer
+        #defmacro addHeaderFooter(header='h', footer='f'): @header @src @footer
+     
+      The syntax for calling a macro is similar to the simplest usage of the
+      #call directive:
+
+        #addHeaderFooter
+         Source $code to wrap
+       #end addHeaderFooter
+
+        #addHeaderFooter: Source $code to wrap
+
+        #addHeaderFooter header='header', footer='footer: Source $code to wrap
+
+
+      In Elisp you write
+        (defmacro inc (var)
+           (list 'setq var (list '1+ var)))
+      to define the macro `inc` and write
+        (inc x)  
+      which expands to 
+        (setq x (1+ x))
+
+      In Cheetah you'd write
+        #defmacro inc: #set @src +=1
+       #inc: $i
+      which expands to 
+        #set $i += 1
+      
+        print Template("""\
+       #defmacro inc: #set @src +=1
+        #set i = 1
+        #inc: $i
+        $i""").strip()==2
+  
+    - fixed some bugs related to advanced usage of Template.compile(). These
+      were found via new unit tests. No one had actually run into them yet.
+
+    - added the initial bits of an #i18n directive.  It has the same semantics
+      as 
+        #call self.handleI18n
+         Some $var cheetah source
+       #end call
+      but has a simpler syntax:
+       #i18n
+         Some $var cheetah source
+       #end i18n  
+
+       ## single-line short form:
+       #i18n: Some $var cheetah source
+
+      The method it calls, self.handleI18n, is just a stub at the moment, but it
+      will soon be a wrapper around gettext.  It currently has one required
+      positional argument `message`. I anticipate supporting the following
+      optional arguments:
+
+          id = msgid in the translation catalog
+          domain = translation domain
+          source = source lang
+          target = a specific target lang
+          comment = a comment to the translation team
+
+          plural = the plural form of the message
+          n = a sized argument to distinguish between single and plural forms
+          
+      #i18n is executed at runtime, but it can also be used in conjunction with
+      a Cheetah preprocessor or macro (see above) to support compile time
+      translation of strings that don't have to deal with plural forms.
+          
+    - added Cheetah.Utils.htmlEncode and Cheetah.Utils.htmlDecode    
+
+    - more docstring text
+
+  Unit tests: [TR]
+    - extended the caching tests
+    - added tests for the various calling styles of Template.compile()
+    - added copies of all the SyntaxAndOutput tests that use a template
+      baseclass other than `Template`. This ensures that all syntax & core
+      features work with 2.0's support for arbitrary baseclasses.
+    - added tests for all the new directives and the new single-line short forms
+  
+2.0rc2 (Jan 13th, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]      
+    - fixed some python 2.4isms that slipped in.  All the tests pass with Python
+      2.2 now
+    - added lots more docstring content in the Template class
+    - made multiline comments gobble whitespace like other directives, per JJ's
+      request.  The rather longwinded compiler setting
+      gobbleWhitespaceAroundMultiLineComments can be used to go back to the old
+      non-gobbling behaviour if needed.
+    - added #capture directive to complement the #call directive.  
+      #call executes a region of Cheetah code and passes its output into a function call
+      #capture executes a region of Cheetah code and assigns its output to a variable
+    - extended the compile caching code in Template.compile so it works with the
+     'file' arg.
+    - added cacheModuleFilesForTracebacks and cacheDirForModuleFiles args to
+      Template.compile(). See the docstring for details.
+    - misc internal refactoring in the parser
+    - improved handling of keyword args in the __init__ method and fixed a
+      potential clash between the namespaces and searchList args
+
+  WWW: [TR]
+    - added the source for the new Cheetah website layout/content
+
+2.0rc1 (Jan 10, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]    
+    - made it possible nest #filter directives
+    - added lots more docstring content in the Template class
+    - added Template.subclass() classmethod for quickly creating subclasses of
+      existing Cheetah template classes. It takes the same args as the
+      .compile() classmethod and returns a template that is a subclass of the
+      template .subclass() is called from:
+          T1 = Template.compile(' foo - $meth1 - bar\n#def meth1: this is T1.meth1')
+          T2 = T1.subclass('#implements meth1\n this is T2.meth1')
+
+    - added baseclass arg to Template.compile(). It simplifies the reuse of
+      dynamically compiled templates:
+      # example 1, quickly subclassing a normal Python class and using its
+      #  __init__ call signature:
+        dictTemplate = Template.compile('hello $name from $caller', baseclass=dict)
+        print dictTemplate(name='world', caller='me')
+
+      # example 2, mixing a Cheetah method into a class definition:
+        class Foo(dict):
+            def meth1(self): 
+                return 'foo'
+            def meth2(self): 
+                return 'bar'        
+        Foo = Template.compile('#implements meth3\nhello $name from $caller', 
+                               baseclass=Foo)
+       print Foo(name='world', caller='me')  
+
+      A side-benefit is the possibility to use the same Cheetah source with
+      several baseclass, as the baseclass is orthogonal to the source code,
+      unlike the #extends directive.
+    
+    - added 'namespaces' as an alias for 'searchList' in Template.__init__
+    - made it possible to pass in a single namespace to 'searchList', which will
+      automatically be converted into a list.
+    - fixed issue with buffering and use of #call when template is used as a
+       webkit servlet
+    - added Cheetah.Utils.htmlEncode and htmlDecode
+   
+  The command line tool (CheetahWrapper.py): 
+    - changed insertion order for the --env and --pickle options so they match the
+      commandline UI of the compiled template modules themselves [TR]
+
+2.0b5 (Jan 7, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]    
+    - made Cheetah.Template a new-style class by inserting 'object' into its'
+      inheritance tree.  Templates can now use super(), properties and all the
+      other goodies that come with new-style classes.
+    - removed the WebInputMixin by placing its one method directly in the
+      Template class.
+    - removed the SettingsManager Mixin. It wasn't being used by anything
+      anymore.
+    - added a framework for caching the results of compilations in
+      Template.compile().  This is on by default and protects against bad
+      performance issues that are due to programmers misguidedly compiling
+      templates inside tight loops.  It also saves on memory usage.
+    - misc attr name changes to avoid namespace pollution
+    - more + improved docstrings
+    - replaced the oldstyle dynamic compile hacks with a wrapper around
+      Template.compile().  The old usage pattern Template(src) now benefits from
+      most of the recent changes.  
+         Template(src).__class__ == Template.compile(src)
+    - removed all the extra imports required by oldstyle dynamic compile hacks
+    - converted the cheetah #include mechanism to newstyle compilation and made it
+      more flexible
+    - made the #include mechanism work with file objects in addition to file names
+    - made the handling of args to Template.compile() more flexible.  You can now
+      provide defaults via class attributes.
+    - made preprocessors for Template.compile() work with file arguments
+    - added support for specifying a __metaclass__ on cheetah template classes
+    - refactored both the class and instance initialization processes
+    - improved the handling of __str__ in _assignRequiredMethodsToClass
+
+  The command line tool (CheetahWrapper.py):  [TR]
+    - improved error output in CheetahWrapper
+    - switched fill command over to new style compile usage
+
+  Unit tests: [TR]
+    - fixed format string bug in unittest_local_copy.py 
+
+2.0b4 (Jan 6, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]
+    - fixed up parsing of target lists in for loops. This was previously limited 
+      to fairly simple target lists.
+      #for ($i, $j) in [('aa','bb'),('cc','dd')]
+        $i.upper,$j.upper
+      #end for"
+      #for (i, j) in [('aa','bb'),('cc','dd')]
+        $i.upper,$j.upper
+      #end for"
+      #for i,(j, k) in enumerate([('aa','bb'),('cc','dd')])
+        $j.upper,$k.upper
+      #end for"
+- refactored the class initialization process
+    - improved handling of target lists in #set directive. This was previously 
+      limited to fairly simple target lists.
+      #set i,j = [1,2] ... #set $i,$j = [1,2]
+      #set (i,j) = [1,2] ... #set ($i,$j) = [1,2]
+      #set i, (j,k) = [1,(2,3)] ... #set $i, ($j,$k) = [1,(2,3)]
+
+    - made it possible for the expressionFilter hooks to modify the code chunks
+      they are fed. Also documented the hooks in a docstring.  Thus the hooks
+      can be used as preprocessors for expressions, 'restricted execution', or
+      even enforcement of style guidelines.
+
+    - removed cheetah junk from docstrings and placed it all in comments or
+      __moduleVars__. Per JJ's suggestion.
+  
+    - made it possible to nest #cache directives to any level
+    - made it possible to nest #call directives to any level
+
+  Unit Tests [TR]
+    - extended tests for #for directive
+    - expanded tests for #set directive
+    - expanded tests for #call directive
+    - expanded tests for #cache directive
+    - added basic tests for the new $placeholder string expressions:
+      c'text $placeholder text'
+
+2.0b3 (Jan 5, 2006)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+  Core Changes: [TR]
+    - added #yield statement
+    - added ability to create nested scopes/functions via nested #def statements
+    - added new #call directive and related #arg directive, per Ian Bicking's
+      suggestion.        
+    - added new expression syntax c"text $placeholder text"
+        
+        for those basic function calling cases where you just need to pass in a
+        small bit of cheetah output as an argument:
+        
+        c'a string with $placeholders', 
+        c'''a string with $placeholders''', 
+        c"a string with $placeholders", 
+        c"""a string with $placeholders"""
+        
+        - They can't contain #directives, but accept any valid $placeholder syntax 
+        except caching placeholders. Caching placeholders don't make any sense in 
+        this context.
+        - They can be used *any* place where a python expression is expected.
+        - They can be nested to any depth.
+        
+        $func(c'<li>$var1-$var2</li>')
+        $func(c'<li>$var1-$var2</li>', doSomething=True)
+        $func(content=c'<li>$var1-$var2</li>', doSomething=True)
+        $func(lambda x,y: c'<li>$x-$y</li>')
+        $func(callback=lambda x,y: c'<li>$x-$y</li>')
+        $func(lambda x,y: c'<li>$x-$y-$varInSearchList</li>')
+        $func(c'<li>$var1-$var2-$(var3*10)-$(94.3*58)</li>')
+        $func(c'<li>$var1-$var2-$func2(c"a nested expr $var99")</li>')
+        #if $cond then c'<li>$var1-$var2</li>' else c'<p>$var1-$var2</p>'
+        #def foo(arg1=c'$var1<span class="foo">$var2</span>'):  blah $arg1 blah
+        $foo(c'$var1<i>$var2</i>')
+
+    - added preprocessor hooks to Template.compile()
+      can be used for partial completion or 'compile-time-caching'
+      ... more details and examples coming.  It's very useful, but takes a bit
+      of explaining.
+    - added '#set module varName = expr' for adding module globals. JJ's suggestion
+    - improved generated docstring notes about cached vars
+    - fixed silly bug related to """ in docstring comments and statements like
+      this '#def foo: $str("""foo""")'.  Reported by JJ.
+    - changed the handling of single-line defs so that 
+      '#def xxx:<just whitespace>\n' will be treated as a multi-line #def.  
+      The same applies to #block.  There's a compiler setting to turn this off
+      if you really need empty single-line #def:'s.
+      JJ reported that this was causing great confusion with beginners.
+    - improved error message for unclosed directives, per Mike Orr's suggestion.  
+    - added optional support for passing the trans arg to methods via **KWS rather
+      than trans=None.  See the discussion on the mailing list Jan 4th (JJ's post) for
+      details.  The purpose is to avoid a positional argument clash that
+      apparently is very confusing for beginners.
+
+        Note that any existing client code that passing the trans arg in
+        positionally rather than as a keyword will break as a result.  WebKit
+        does this with the .respond method so I've kept the old style there.
+        You can also turn this new behaviour off by either manually including
+        the trans arg in your method signature (see the example below) or by
+        using the compiler setting 'useKWsDictArgForPassingTrans'=False.
+        
+        #def manualOverride(arg1, trans=None)
+          foo $arg1
+        #end def
+
+  ImportHooks: 
+    - made the ImportHook more robust against compilation errors during import [TR]
+
+  Install scripts: [TR]
+    - added optional support for pje's setuptools 
+    - added cheeseshop classifiers 
+    - removed out of date install instructions in __init__.py
+
+  Servlet Base Class For Webkit: [TR]
+    - disabled assignment of self.application (was a webware hack)   
+
+  Unit Tests: [TR]
+    - unit tests for most of the new syntax elements
+    - tidied up some old tests
+    - misc refactoring
+
+2.0b2 (Dec 30, 2005)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  Core Changes: 
+    - In previous versions of Cheetah tracebacks from exceptions that were raised
+      inside dynamically compiled Cheetah templates were opaque because
+      Python didn't have access to a python source file to use in the traceback:
+
+        File "xxxx.py", line 192, in getTextiledContent
+          content = str(template(searchList=searchList))
+        File "cheetah_yyyy.py", line 202, in __str__
+        File "cheetah_yyyy.py", line 187, in respond
+        File "cheetah_yyyy.py", line 139, in writeBody
+       ZeroDivisionError: integer division or modulo by zero
+
+      It is now possible to keep the generated source code from the python
+      classes returned by Template.compile() in a cache dir.  Having these files
+      around allows Python to include the actual source lines in tracebacks and
+      makes them much easier to understand:
+
+       File "/usr/local/unsnarl/lib/python/us/ui/views/WikiPageRenderer.py", line 192, in getTextiledContent
+         content = str(template(searchList=searchList))
+       File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 202, in __str__
+         def __str__(self): return self.respond()
+       File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 187, in respond
+         self.writeBody(trans=trans)
+       File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 139, in writeBody
+         __v = 0/0 # $(0/0)
+      ZeroDivisionError: integer division or modulo by zero
+
+      This is turned off by default. To turn it on, do this:
+
+        class NiceTracebackTemplate(Template):
+            _CHEETAH_cacheModuleFilesForTracebacks = True
+            _CHEETAH_cacheDirForModuleFiles = '/tmp/CheetahCacheDir' # change to a dirname
+        
+        templateClass = NiceTracebackTemplate.compile(src)
+
+       # or
+        templateClass = Template.compile(src,
+           cacheModuleFilesForTracebacks=True, cacheDirForModuleFiles='/tmp/CheetahCacheDir')
+       
+
+      This only works with the new Template.compile(src) usage style!
+
+      Note, Cheetah generated modules that are compiled on the command line have
+      never been affected by this issue. [TR]
+
+    - added an extra comment per $placeholder to generated python code so it is
+      easier to grok. [TR]
+
+2.0b1 (Dec 29, 2005)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  Core Changes: 
+    - enabled use of any expression in ${placeholders}. See the examples I posted to
+      the email list on Dec 12th.  All use cases of the #echo directive can now
+      be handled with ${placeholders}.  This came from a suggestion by Mike
+      Orr. [TR]
+         
+    - made it possible for templates to #extend (aka subclass) any arbitrary
+      baseclass, including Python's new style classes.  You must either compile
+      your classes on the command line or use the new classmethod
+      Template.compile() as described below.  The old Template(src) interface
+      still works, provided you don't try to use this new arbitrary baseclass
+      stuff.  See my messages to the email list for more details. [TR]
+
+    - made it possible to create template classes dynamically, rather than just
+      instances.  See the new classmethod Template.compile().  See my messages
+      to the email list for more details. [TR]
+      
+          klass = Template.compile(src)
+
+    - made it easier to work with custom compiler settings, particularly from
+      the command line tool.  You can now define a subclass of Template which
+      will compile your templates using custom compilerSettings, or even a
+      custom compiler class, without requiring you to manually pass in your
+      compilerSettings each time or define them in the template src itself via
+      the #compiler directive.  You can make the command line tool use your
+      subclass by defining the environment variable CHEETAH_TEMPLATE_CLASS.  It
+      should be in the form 'package.module:class'.  See my messages
+      to the email list for more details. [TR]
+
+    - made it possible to pass the searchList in as an argument to #def'ined
+      methods.  This makes all lookup that occur within the scope of that method
+      use the provided searchList rather than self._searchList.  This does not
+      carry over to other methods called within the top method, unless they
+      explicitly accept the searchList in their signature AND you pass it to
+      them when calling them.  This behaviour can be turned off with the
+      corresponding compilerSetting 'allowSearchListAsMethArg' [TR]
+
+    - added hooks for filtering / restricting dangerous stuff in cheetah source
+      code at compile time.  These hooks can be used to enable Cheetah template
+      authoring by untrusted users. See my messages to the email list for more
+      details. Note, it filters expressions at parse/compile time, unlike Python's
+      old rexec module which restricted the Python environment at runtime. [TR] 
+          
+          # Here are the relevant compiler settings:
+          # use lower case keys here!!
+          'disabledDirectives':[], # list of directive keys, without the start token
+          'enabledDirectives':[], # list of directive keys, without the start token
+      
+          'disabledDirectiveHooks':[], # callable(parser, directiveKey), 
+              # called when a disabled directive is found, prior to raising an exception
+      
+          'preparseDirectiveHooks':[], # callable(parser, directiveKey)
+          'postparseDirectiveHooks':[], # callable(parser, directiveKey)
+      
+          'preparsePlaceholderHooks':[], # callable(parser)
+          'postparsePlaceholderHooks':[], # callable(parser)
+      
+          'expressionFilterHooks':[], 
+          # callable(parser, expr, exprType, rawExpr=None, startPos=None)
+          # exprType is the name of the directive, 'psp', or 'placeholder'. 
+          #all lowercase
+
+    - added support for a short EOLSlurpToken to supplement the #slurp
+      directive.  It's currently re.compile('#\s*\n') (i.e # followed by
+      arbitrary whitespace and a new line), but this is not set in stone.  One
+      other suggestion was the backslash char, but I believe Python's own
+      interpretation of backslashes will lead to confusion.  The compiler
+      setting 'EOLSlurpToken' controls this.  You can turn it off completely by
+      setting 'EOLSlurpToken' to None. See the email list for more details. [TR]
+
+    - added '_CHEETAH_' prefix to all instance attribute names in compiled
+      templates. This is related to the arbitrary baseclass change. [TR] 
+
+    - shifted instance attribute setup to _initCheetahAttributes() method.  This
+      is related to the arbitrary baseclass change. [TR]
+
+    - made it possible to use full expressions in the #extends directive, rather
+      than just dotted names. This allows you to do things like this: 
+
+      #from xx.TemplateRepository import getTemplateClass
+      #extends getTemplateClass('someName')
+
+      I don't expect this to be used much. I needed it for a wiki system in
+      which the baseclasses for the templates are dynamically compiled at run
+      time and are not available via simple imports. [TR]
+
+    - added compiler setting autoImportForExtendDirective=True, so this existing
+      default behaviour can be turned off when needed. [TR]
+
+    - fixed a bug in the parsing of single-line #def's and #block's when they
+      are enclosed within #if ... #end if. Reported by Marcin Gajda [TR]
+
+    - tweak to remove needless write('') calls in generated code [TR]
+
+  The command line tool (CheetahWrapper.py): 
+    - added code to cleanup trailing slashes on path arguments (code originally
+      from Mike Orr) [TR]
+    - turned on the ImportHooks by default for the 'cheetah fill' command. See the
+      discussion on the email list [TR]
+
+  ImportHooks: 
+    - fixed a name error bug in the ImportHooks [TR]
+
+1.0 (Dec 4, 2005)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  Version bump from 1.0rc3
+
+1.0rc3 (Nov 30, 2005)
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  - added useSearchList compiler setting [TR]
+    This defaults to True, but if false, the compiler assumes the first
+    portion of a $variable (before the first dot) is a global, builtin, or local
+    var that doesn't need looking up in the searchlist.  NameMapper's unified
+    dotted notation will still be used on the rest of the lookup (provide the
+    setting useNameMapper==True):
+      $aLocalDictVar.aKey.somethingElse
+
+1.0rc2 (Nov 19, 2005)
+
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  See my email to the cheetahtemplate-discuss list on Sat. Nov. 12th for more
+  details on these changes:
+
+  - faster list-based buffering in DummyTrans, rather than StringIO (my 
+    benchmarks showed it to be significantly faster.  collections.deque wasn't 
+    any faster than a simple list.) [TR]
+  - new CompilerSettings to tweak generated code: [TR]
+    * alwaysFilterNone: filter out None immediately, before the filter is called
+    * useFilters: allows you to turn them off completely and default to str()
+    * includeRawExprInFilterArgs: allows you to disable this behaviour
+    * autoAssignDummyTransactionToSelf: off by default 
+  - and automatic $trans finding without having to pass it as an arg to methods 
+    based Jonathan Mark's suggestion.  If the template's self.transaction
+    attribute has been set, each method of the template will use it when
+    called. [TR]
+  - applied Chris Murphy's patch to fix a bug in the #shBang directive. [TR]
+
+1.0rc1 (Nov 2, 2005)
+
+  !!!THIS RELEASE REQUIRES RECOMPILATION OF ALL COMPILED CHEETAH TEMPLATES!!!
+
+  - added the compiler option "useStackFrames" for optionally turning off the
+    default lookup method which doesn't work with psyco because it uses stack
+    frame introspection.  When useStackFrames=False, an alternative psyco
+    friendly lookup method is used. [TR]
+
+  - fixed treatment of None in several filters, bug reported by Daniele Varrazzo
+    [TR]
+
+0.9.18 (Aug 22, 2005)
+  - version bump from 0.9.18rc1 [TR]
+
+0.9.18rc1 (Aug 15, 2005)
+  - applied Philippe Normand's patch for extended cache support [TR]
+  - added filter RawOrEncodedUnicode to fix a unicode issue that was discussed
+    on the email list on Aug. 9th 2005 [TR]
+
+0.9.17 (May 30, 2005)
+  - this is just 0.9.17rc1 repackaged [TR]
+
+0.9.17-rc1 (May 12, 2005)
+  This simple bug fix release resolves some issues introduced by
+  under-the-hood changes in release 0.9.16a1.
+
+  - removed the use of temp files for handling imports with dynamic
+    compilation. This removes a whole slew of issues, including a temp file
+    security issue reported on the email list by Brian Bird. [TR]
+  - fixed bug with handling of the searchList with dynamic inheritance, as
+    reported by  Brian Bird. [TR]
+
+0.9.16 (Mar 27, 2005)
+  - this is just 0.9.16b1 repackaged [TR]
+
+0.9.16b1 (Feb 28, 2005)
+  - fixed attr error in Parser.eatEncoding [TR]
+  - some tweaks to Compiler.py to prevent errors with dynamically compiled
+    templates [TR]
+  - added tests for the #encoding directive [TR]
+
+0.9.16a1 (Jan 6, 2005)
+  - fixed a unicode bug in Compiler.py [TR]
+  - added new EncodeUnicode filter that Rene Pijlman contributed (I optimized it
+    slightly) and made it the default filter for all templates. [TR]
+  - added test cases for handling unicode with the default filter [TR]
+  - fixed a caching bug related to the #include directive. Thanks to Michael
+    Engelhart for reporting it.[TR]
+  - added the new #encoding directive to handle PEP 263
+    http://www.python.org/doc/2.3/whatsnew/section-encodings.html [TR]
+  - Tools.CGITemplate: fix bug in comment.[MO]
+  - Abort with a helpful error message if user runs 'cheetah test' in a
+    directory without write permission.  (Kludge in CheetahWrapper.py; we
+    should probably move the temp files under the system tmp directory.) [MO]
+  - added better string type checking for args in Template.py, as suggested by
+    Terrel Shumway [TR]
+  - minor tweak to the class attribute lookup style in the Compiler.py
+    __getattr__ methods [TR]
+  - Fix printf format bug in "cheetah c --debug", found by Terry MacDonald. [MO]
+  - Disabled NameMapperDict test (in SyntaxAndOutput.py) because the
+    namemappers still have the dict-method bug.  (Reminder: don't use 
+    placehold names like $update that match dict method names.) [MO]
+  - #repeat now uses a local variable of the form __i$num which permits 
+    nesting [JJ]
+  - implemented a modified version of Bob and JJ's patch for better parser
+    warnings about mismatched directives and #end directives [TR]
+  - lots of little cleanups and refactoring [TR]
+  - refactored the class tree for the Parser and Compiler.  They are now
+    completely separate trees and the communication between them is one-way:
+    explicit commands from the parser to the compiler.  The parser now handles
+    all parsing tasks by itself and delegates *all* code generation to the
+    compiler.  This last bit was my original intention, but things got a bit
+    mixed up over time.  Also, all SettingsManager stuff for handling
+    compiler/parser settings is now handled by ModuleCompiler.  This should make
+    it easier to grok, maintain, and extend both. [TR]
+  - improved the parsing of singe-line #if directives. [TR]
+  - removed the old webware example of the cheetah site, as it was way out of
+    date and was more confusing than helpful. [TR]
+  - added two new lookup functions to NameMapper (valueFromFrame and
+    valueFromFrameOrSearchList) and synchronized the behaviour of the C and
+    Python versions of all functions. [TR]
+  - improved the exception handling of both versions of NameMapper. NotFound
+    exceptions now include more detail about what wasn't found. [TR]
+  - made NameMapper's searchList lookup functions work with any iterable
+    type/class rather than just with lists. [TR]
+  - added and updated a variety of test cases. [TR]
+  - checked in a patch to CheetahWrapper that improves handling of the odir
+    option when the path is absolute.  I can't remember where the patch came
+    from. [TR]
+  - checked in a patch to Template.py for better include/import support under
+    Jython.  Again, I've forgotten who contributed it. [TR]
+  - updated various bits of the user guide. [TR]
+  - made the Cheetah NameMapper calls in the generated code use the new function
+    valueFromFrameOrSearchList rather than passing locals, searchList, globals,
+    and __builtins__ into valueFromSearchList.  This is faster, less bug prone
+    and simpler to read/grok. I also removed all tracking of local
+    variable names by the compiler. [TR]
+  - other misc. refactorings [TR]
+0.9.15 (Mar 25, 2003)
+  - a minor tweak to the cleanup actions of one of the test cases [TR]
+
+0.9.15rc2 (Mar 23, 2003)
+  - Fixed a python version dependency bug related to Compiler.py's use of 
+    isinstance() [TR]
+
+0.9.15rc1 (Mar 21, 2003)
+  This is just 0.9.15b1 renamed.
+
+0.9.15b1 (Mar 17, 2003)
+  - The Cheetah version of unittest now prints a blank line after each 
+    traceback to separate them.  (MO)
+  - .webInput() now saves the cgi.FieldStorage() instance in a global
+    variable rather than recreating it each call.  That should allow the
+    method to be called multiple times with POST variables outside Webware.
+    (MO)
+  - CheetahWrapper: no verbose output on stdout with --stdout/-p. (MO)
+  - "#indent" is now undocumented.  The existing code remains intact for now.
+    See the TODO file for our future plans. (MO)
+  - Apply 2 unicode-support patches from Rodrigo B. de Oliveira (rodrigobamboo)
+    that affected the Template and ModuleCompiler classes. (TR)
+  - Template: compiling a template from a string now works if the current
+    directory doesn't have write permission.  (MO)
+  - remove temporary .pyo files in addition to .py and .pyc files (TR)
+
+0.9.15a3 (Nov 10, 2002)
+  - corrected a dictionary bug in the Python version of NameMapper (TR)
+  - Rewrote the "cheetah" command (CheetahWrapper.py) again and added test
+    cases.  New options --flat and --nobackup; --stdout is now a synonym for
+    -p.  See the "cheetah compile" section in the Users' Guide for details.
+    Deleted Utils.dualglob: merged into CheetahWrapper.  (MO)
+  - .addToSearchList() and .prependToSearchList() are gone.  Instead of 
+    adding containers to the searchList after instantiation, pass all the
+    containers you need to the constuctor, keep another reference somewhere
+    to the containers, and modify the containers directly.  Generic libraries
+    that want to add a new kind of information to the searchList (e.g.,
+    web variables) should do "self.searchList().insert(0, myContainer)". (MO)
+
+0.9.15a2 (Nov 4th, 2002)
+  - Filters now have access to the name of the placeholder they're filtering.
+    In the .filter() method, kw['rawExpr'] gives the entire placeholder name
+    including subscripts and arguments, exactly as it appears in the template
+    definition. (TR)
+  - Fix three bugs in "cheetah compile -R": (1) the generated class name 
+    contained the subdirectory, (2) the destination path did not contain the
+    subdirectory, (3) Cheetah failed to create the destination subdirectory
+    if missing.  All subdirectories created have an "__init__.py" file. 
+    "cheetah fill -R" does the same thing but does not create 
+    "__init__.py". (MO)  NOTE: this is still buggy!
+  - New directory "attic" in source contains code that has been abandoned
+    for now but may come in handy someday. (MO)
+  - Tests.CheetahWrapper: test suite for "cheetah compile" and
+    "cheetah fill".  If the module is run from the command line, the
+    option "--list PATH/CheetahWrapper.py", lists all scenarios that would be
+    tested; the argument is the path to the test module itself. (MO)
+  - made Cheetah.NameMapper.NotFound subclass the builtin LookupError (TR)
+  - added an initial implementation of single line #if directives
+    #if <condition> then <true> else <false>
+    The parsing is fairly rudimentary for now and assumes that the keywords
+    'then' and 'else' won't appear any inside a string in this directive (TR)
+
+0.9.15a1 (Oct 6th, 2002)
+  - fixed a package-relative import bug in ImportHooks.py (TR)
+  - set 'monitorSrcFile' to false as it was causing problems with the
+    ImportHooks ... This might be temporary as the problem needs more thought
+    (TR)
+  - fixed meta tag http_equiv to be http-equiv in SkeletonPage (TR)
+  - $webInput (Utils.WebInputMixin) 'source' arg can be either case. (MO)
+  - fixed code-gen bug in MethodCompiler.commitStrConst that was leading to
+    triple single quotes followed immediately by another single quote if the
+    template def contained a '$placeholder' surrounded in single quotes and
+    multiple \n newlines ... plus added new test case.(TR)
+  - undocumented the '#settings' directive.  The directive itself will be
+    removed soon.  (MO)
+  - Utils.optik: Optik 1.3 package by Gregory P Ward, for parsing
+    command-line options in 'cheetah' comamnd.  Copied unchanged into
+    Cheetah except added "Cheetah.Utils.optik." prefix to intra-package
+    imports.  Optik's copyright and license is in an appendix in the
+    Cheetah Users' Guide.  (MO)
+  - rewrite of the "cheetah" and "cheetah-compile" commands. 
+    The command-line options have changed!  Removed CheetahCompile module
+    removed and its test suite too; CheetahWrapper now takes its place. (MO)
+  - Utils.dualglob: new module to recursively generate source+destination
+    filenames from command-line filespecs. (MO)
+  - The command-line options of .py template modules have also changed
+    to conform with the "cheetah" command.  Also a --pickle bug was
+    fixed.  (MO)
+  - Utils.WebMixin: made a string type comparision backward compatible.
+    This was why the Cheetah test suite was failing on Python < 2.2! (MO)
+  - SettingsManager._createConfigFile(): bugfix in default argument. (MO)
+  - $hasVar('varName') is an alias for $varExists('varName').  (MO)
+  - $_fileDirName and $_filePath are now None rather than missing if
+    the template definition did not come from a named file.  (MO)
+  - applied patch on SourceForge for "%" in default arguments of a block (TR)
+  - removed the _underscored attribute lookup step from NameMapper NOTE THIS
+    MIGHT BREAK EXISTING TEMPLATES (TR)
+  - Install Cheetah into site-packages/Cheetah/ rather than
+    site-packages/Webware/Cheetah/. Added code to automatically remove the old
+    dir.(TR)
+  - fixed the variable name resolution order bug in $placeholders.  The new
+    implementation uses 
+     valueFromSearchList([locals()] + searchList + [globals(), __builtin__],
+     rest of the args) for all lookups. (TR)
+  - removed the #settings directive (TR)
+  - added the #del directive, for using Python's del statement (TR)
+  - I think I've fixed the problem with the searchList arg being discarded when a
+    template is generated from a .tmpl file that #extends another template. This
+    bug was reported by Edmund on Aug 30th 
+    (subject: "Bug? Was: Really basic searchList question") (TR)
+
+0.9.14 (July 14, 2002)
+  - Precompiled template Templates/SkeletonPage.py added to CVS.  This file is
+    needed for Cheetah's regression tests. (MO)
+  - removed automatic recompilation of .py template in memory if the
+    .tmpl file has changed.  (TR)
+
+0.9.14b1 (June 30, 2002)
+  - moved the Users' Guide and the Developers' Guide into a separate CVS module,
+    'CheetahDocs', so they can be distributed separately from the source distro
+    (TR,MO)
+  - added TypeType to the types that NameMapper won't do autocalling on (TR)
+  - in Template.py moved the global LegalKWs to Template._legalKWs (TR)
+  - made #set work with RVALUES that are missing the recommended $ (TR)
+  - added some new test cases for the #set directive (TR)
+  - fixed bug in the output of the #unless directive that Mike found (TR)
+  - added some module constants to clear up a missing name bug that Mike found
+    in cases where you use **KW in a Cheetah method definition (TR)
+  - fixed a bug in Parser.py:_LowLevelSemanticsParser.getExpression() that was
+    related to the default enclosures=[] argument.  This arg was unintentionally
+    being shared between calls and thus leading to parsing errors as reported by
+    Greg Czajkowski (TR)
+  - Filter: fixed an '== None' expression (should be 'is None'). (MO)
+  - TemplateMisc: new base class for Template.  This is for optional
+    convenience methods that don't require Webware. (MO)
+  - User's Guide: new sections "Non-Webware HTML Output" and "Non-HTML
+    Output". (MO)
+  - Expanded $webInput() -- renamed from $cgiImport() -- to work both with
+    Webware input and CGI scripts.  Handles GET/POST/cookie/session vars under
+    Webware, and GET/POST under CGI.  Defined in Cheetah.Utils.WebInputMixin,
+    now inherited by Template directly. (MO)
+  - Tools.CGITemplate has methods to output CGI headers: .isCgi, .cgiHeaders
+    and .cgiHeadersHook in TemplateMisc. (MO)
+  - New #indent directive allows you to indent block constructs in the
+    template definition without having that indentation in the output, and
+    allows you to set the output indentation per line independent of the
+    indentation in the template definition.  This version uses Robert
+    Kuzelj's #indent syntax exactly.  In the next few days, 
+    Cheetah.Utils.Indenter will be refactored and
+    *** THE #INDENT SYNTAX WILL CHANGE! *** (MO)
+  - added the #return directive as requested by Robert Kulezj (TR)
+  - added some test cases for the #return directive (TR)
+  - removed buggy import statement that had been left in Servlet.py after the
+    CGIInputMixin changes (TR)
+
+
+0.9.13 (May 8, 2002)
+  - changed Cheetah.Servlet.isRunningFromWebKit to isWebwareInstalled (TR)
+
+  - fixed parsing bug that would exit an expression if the directiveEndToken was a
+    valid Python token and was found inside the directive.  (TR)
+
+    E.g.:
+    #compiler-settings
+    directiveStartToken = .
+    directiveEndToken = :
+    commentStartToken = #
+    #end compiler-settings
+    
+    .for a in [1,2,3,4][2:3]:
+    blag
+    .end for
+
+  - fixed #include bug that was resulting in non-unique includeIDs (TR)
+
+0.9.13b2 (May 3, 2002)
+  - fixed the bug in Cheetah.Servlet.Servlet.serverSidePath that Jeff Johnson
+    found. (TR)
+  - changed the attribute Cheetah.Servlet.ServletisRunningFromWebKit to
+    isControlledByWebKit and set the default to False unless the .awake method
+    is called.  This makes a clear distinction between templates that are being
+    used with WebKit via the inheritance approach and the containment approach
+    (TR)
+
+0.9.13b1 (May 1, 2002)
+  - Was going to import cStringIO instead of StringIO, but it made the 
+    DummyTransaction.py unittest fail so I undid it.  Cheetah aims to provide
+    Unicode support, which cStringIO does not provide. (TR/MO)
+  - Utils.Misc.CheckKeywords(): prevent misspelled keyword arguments,
+    used by Template constructor. (MO)
+  - removed support for multiple inheritance (TR)
+  - added some bounds-checking code to _namemapper.c's getNameChunks function
+    (TR)
+  - changed the exceptions in _namemapper.c from the old string exceptions
+    to proper exception objects (TR)
+  - first portion of Developers' Guide written (MO)
+  - implemented the extended #extends directive, which does automatic importing
+    (MO,TR)
+  - added some new testcases for the extended #extends directive (TR)
+  - lots of work on the Users' Guide (MO)
+  - implemented and tested an import hook for .tmpl files (TR):
+      import MyTemplate  # will compile and import MyTemplate.tmpl
+  - made my True/False declarations friendly with Python 2.2.1, which already
+    includes True/False as builtins (TR)
+  - implemented the #compiler directive that Edmund Lian suggested (TR)
+      e.g.:
+        #compiler commentStartToken = '//'
+        // a comment
+        #compiler reset
+       // no longer a comment
+  - fixed the bug that Edmund Lian found in .addSet() when useNameMapper = 0
+  (TR)
+  - fixed bug in comment creation using lineCol that Mike found (TR)
+
+0.9.12 (April 3, 2002)
+   - no code changes from beta 2
+   - more work on the docs (MO)
+
+0.9.12b2 (Mar 28, 2002)
+   - fixed Win32 path bug in Template._makeDummyPackageForDir() (TR)
+   - prettied up the format of the debug comments in the Cheetah generated
+     Python code (TR)
+   - fixed the non-unique key error in Template._includeCheetahSource (TR)
+   - fixed the module import bug in 'cheetah compile -w' (TR)
+
+0.9.12b1 (Mar 24, 2002)
+   - $request().field(args) now works, identical to $request.field(args)
+     to implement this, the request object is now self.request() instead of
+     self.request.  This provides compatibility with Webware's servlet API.
+     (self.session already was an accessor method).  New read-only attribute
+     self.isRunningFromWebKit is boolean.  All changes are in Servlet.py. (MO)
+   - fixed nested-NotFound bug in _namemapper.c's valueFromSearchList (TR)
+   - 'cheetah' wrapper script has abbreviation -c for the 'compile' command,
+     -t for 'test', and an ASCII-art cheetah face in the help message. (MO)
+   - CheetahCompile.py: fixed to recognize --help option and to show --help/-h
+     in help message. (MO)
+   - CheetahCompile.py: 
+     changed the order of the VERBOSE mode printouts, as per Mike's request (TR)
+   - Template.py:
+     fixed the #include'd template searchList sharing problem, as reported by
+     Johannes (TR)
+   - corrected namemapper translation bug in
+     Compiler.GenUtils.genNameMapperVar() (TR)
+   - Utils.Misc.UseOrRaise(): convenience function to return a 
+     value, or raise it if it's a subclass of Exception. (MO)
+   - Utils.CGIImportMixin replaces Tools.WebwareMixin.  Servlet now
+     subclasses it.  This adds the .cgiImport() method to all servlets, allowing
+     sophisticated retrieval of form fields, cookies or session variables from
+     one line of Cheetah or Python code.  See module docstring. (MO)
+   - lots of updates to the docs (MO)
+
+0.9.11 (Mar 07, 2002)
+   - fixed a careless bug in cheetah-compile (TR)
+   - implemented the new 'cheetah' wrapper script (TR)
+   - refactored the local copy of unittest a bit (TR)
+
+0.9.10 (Mar 06, 2002): Primarily a bug fix release
+   - fixed bug in srcfile-mtime monitoring / update code (TR)
+   - fixed the parsing of single-line #defs and #blocks so they can have
+     arguments (TR)
+   - added test cases for single-line #defs and #blocks with args (TR)
+   - fixed a silly typo bug in Parser.py where a comma was left at the end of
+     regex definition, make it a tuple rather than a regex
+   - fixed the directive matching bug that Jeff Johnson reported.  It was
+     causing #else# to not match, while #else # was matching. + added a test
+     for it.(TR)
+   - fixed bug in a regex that was preventing bare $'s followed by whitespace
+     and then valid varname chars from parsing as just $ instead of as a
+     placeholder (TR)
+   - added some code to break reference cycles after the compilation is
+     complete. This helps prevent memory leaks when a process in creating then
+     discarding lots of Templates.  You also need to manually call
+     "template.shutdown()" to clear the remaining reference cycles.
+     (TR)
+   - fixed string formating bug in the autogenerated docstring code (TR)
+   - added better error message for the #attr directive (TR)
+   - removed some residual code that was causing a bug with cheetahvars that
+     started with the name of one of the imported modules, such as 'time'. (TR)
+
+0.9.9 (Dec 14, 2001)
+   - implemented one-line #def's and #block's (TR)
+        #def aTest: This is a $adj test   ---- READ THE MANUAL FOR MORE INFO.
+     NOTE: leading and trailing whitespace is stripped.  These should only be
+     used on lines by themselves as it reads to the end of the line.
+   - made cheetah-compile accept input on standard input (TR)
+   - made sure that #def and #block work with $'s on the method names (TR)
+
+0.9.9b1 (Dec 6, 2001)
+   - template constructor arg 'outputFilter' now 'filter', for consistency
+     with #filter (MO)
+   - template constructor raises TypeError if bad arguments (MO)
+   - Cheetah.Utils.VerifyType new module containing functions for verifying the
+     type of an argument (MO)
+   - Cheetah.Utils: new package for non-Cheetah-specific modules needed by
+     Cheetah (MO)
+   - Cheetah.Filters: new filter WebSafe, several bugfixes (MO)
+   - more work on the Users' Guide (MO)
+   - fixed bug with adding Python's __builtins__ to the local vars list (TR)
+   - fixed bug with #echo (TR)
+   - fixed bug that was preventing ${a, $b=1234} from working like ${a, b=1234} (TR)
+   - fixed some bugs in Template.varExists and Template.getVar() 
+     (TR - thanks to MH for spotting them)
+   - made it possible to use filenames like 'spam-eggs.txt' that have invalid
+     characters for module names with Template(): Template(file='spam-eggs.txt')
+     (TR/MH)
+   - refactored 'cheetah-compile' a little (TR)
+   - Cheetah.Filters.Strip: new filter to strip leading/trailing whitespace
+     but preserve newlines.  Suitable for #filter directive or (possible)
+     future #sed directive. (MO)
+   - Cheetah.Filters.StripSqueeze: new filter to canonicalize all whitespace
+     chunks to ' '.  Also removes all newlines (joining multi-line input into
+     one long line), and leading/trailing whitespace from the final result. (MO)
+   - Filters can now be used standalone for debugging or for use outside
+     Cheetah.  This works transparently; details are in Filters.py docstring.
+     (MO)
+   - Cheetah.Tools.MondoReport: new module for dividing a long list into
+     "pages", and for calculating statistics useful in reports. (MO)
+   - refactored Cheetah.Servlet.Servlet.Awake a little (TR)
+   - fixed an output bug in the #block generated code that turned up when you
+     tried to override a block method from Python rather than Cheetah. (TR)
+   - started preparing to shift some of the 'shared' utility classes, such as
+     SettingsManager, to the Webware core.  Cheetah 1.0 will probably require
+     Webware to be installed so it can access those shared classes. (TR)
+   - extended the template module command line interface(TR/MO)
+
+0.9.9a6 (Nov 6, 2001) 
+   - fixed bug with quotations in longer constant string chunks (TR)
+   - fixed another bug in the cheetah-compile script (TR)
+   - fixed a bug in the file-update monitoring code that was resulting in
+     infinite loops when used with Template sub-classes (TR)
+   - extended the #filter framework according to Mike's suggestions (TR)  
+   - added test modules for cheetah-compile and the file-update monitoring code (TR)
+   - extended the capabilities of cheetah-compile ... (IB)
+   - updated the docs (MO)
+
+0.9.9a5 (October 31, 2001) 
+   - fixed a bug I created yesterday (TR)
+
+0.9.9a4 (October 30, 2001) 
+   - added #repeat (TR implementing Chuck's suggestion)
+   - added #unless (TR implementing Mike's suggestion)
+   - updates to the Users' Guide (MO)
+   - fixed a small bug in the cheetah-compile script, as reported by Ian on the
+     list (TR)
+
+0.9.9a3 (October 12, 2001) 
+   - more in the Users Guide (TR)
+   - renamed #attribute as #attr  (TR)
+   - renamed #call as #silent (TR)
+   - added #echo directive (TR)
+0.9.9a2 (October 11, 2001) 
+   - updated the example site and the SkeletonPage framework (TR)
+   - fixed some small bugs (TR)
+   - corrected some typos in the docs (TR + MO)
+   - added Ian's sitehiearchy class to Cheetah.Tools (TR + IB)
+
+0.9.9a1 (October 9, 2001)  [many changes and bug-fixes]
+   - a complete reimplementation of Cheetah's core (the parser and compiler
+     classes) (TR + IB)
+
+   - implemented the #def, #implements, #import, and #from directives
+     + removed #redefine and #macros
+     + renamed #extend as #extends (TR + IB)
+
+   - replaced #data with #settings, see the docs (TR)
+
+   - restructured and updated the docs (TR + MO + IB)
+
+   - reimplemented the cheetah-compile script, without the -g option that Ian
+     had added (TR)
+
+   - changed the signature of Template.__init__.  See the docs. (TR)
+
+   - made #set distinguish between local and global vars.  See the docs. (TR)
+
+   - added hundreds of new test cases (TR)
+
+   - added the #breakpoint and #compiler-settings directives (TR)
+
+   - started restructuring the SkeletonPage framework [not complete yet] (TR)
+   - started restructuring the example sites [not complete yet] (TR)
+
+
+0.9.8 (October 9, 2001)
+   - added a few new language constructs (aka 'directives') to Cheetah (TR)
+     #while ... #end while
+     #try ... #except ... #else ... #finally ... #end try
+
+   - fixed a bug in the handling of local vars in #for loops that was preventing
+     callable local vars from being handled properly. See Chuck's post of Sept
+     10. (TR)
+
+   - fixed a pointer bug in the C version of NameMapper.valueFromSearchList()
+     that was yielding undefined values for the NotFound exception when it was
+     raised (TR)
+
+   - prefaced all internal args to Template() with underscores (TR)
+   - fixed the problem with parsing triple quoted strings in arg lists (TR)
+   - updated the docs (TR)
+
+0.9.8a4 (September 7, 2001)
+
+  - Added -g (appendGen function argument), which compiles x.tmpl to xGen.py,
+    with x.py being for non-generated Python code. Also changed option handling
+    a little and added a comment to the top of compiled files. (IB + MO)
+
+  - finalized the #include syntax after a lengthy discussion on the list 
+    This is different from in 0.9.8a3 (TR)
+        #include <ARGS> <EXPR> 
+        ... uses the value of EXPR as the path of the file to include.
+        
+        #include <ARGS> source = <EXPR> 
+        ... includes the value of the EXPR 
+
+        where <ARGS> is 'raw' or ''
+
+  - re-implemented the output mechanism to use streaming via Webware's
+    Transaction and Response objects when available and fake it with the
+    DummyTransaction DummyResponse classes when the Webware Transaction is not
+    avialable. This behaviour is roughly the same as in Webware's PSP.  Will
+    implement output buffering PHP-style later if there is any demand. (TR)
+
+  - made #include a run-time directive rather than compile-time.  This is
+    slower, but the semantics are better. (TR)
+
+  - various small optimizations to the generated code (TR)
+
+  - updated the docs (TR)
+
+
+0.9.8a3 (August 22, 2001) [includes changes for 0.9.8a1 and 0.9.8a2]
+
+  - Added package ./src/Tools/ for contributed classes/functions/packages not
+    necessary to run Cheetah.  The first such class is RecursiveNull.py by Ian
+    Bicking.  Added package Cheetah.Tools to list in ./setup.py . (MO)
+  - Template.__init__ keyword arg 'searchList': no longer has to be a tuple.  It
+    may be a list or any type that that Python's 'tuple' function accepts. (MO)
+  - Template.__init__ new keyword arg 'file': this may be a filename or file
+    object to read the Template Definition from.  If you use this, you must not
+    pass a Template Definition string also.  New instance variables 
+    ._fileName and ._fileMtime are set if a filename was passed; otherwise they
+    are None. (MO)
+  - CodeGenerator new function 'varNotFound_KeyError': raises KeyError if a
+    placeholder name is missing when filling the template.  Disabled by default.
+    (MO)  NB - this change has been superceeded by 'errorCheckers'
+  - Template.getUnknowns (new method): returns a list of Placeholder Names
+    missing in the Search List. (MO) - this change has been superceeded by 
+    'errorCheckers'
+  - made changes to Template.py, CodeGenerator.py, PlaceholderProcessor.py,
+    and TagProcessor.py to enable customization of the placeholderStartToken so 
+    it can be set to any character sequence, rather than just the default '$'.
+    This is configurable by the Template setting 'placeholderStartToken' (TR)
+  - fixed a small bug in PlaceholderProcessor.processTag() that prevented
+    static caching (i.e. $*) of a value containing ''' style quotes
+  - added #break and #continue (TR)
+  - fixed the relative path problem with #include when using Cheetah with WebKit
+    (TR)
+  - implemented the #stop directive (TR)
+  - fixed a bug in the macro processing that prevented macros defined inside
+    #includes from being visible at the top level (TR)
+  - fixed a bug in the handling of the setting 'useAutocalling' (TR)
+  - fixed some bugs in the handling of macros (TR)
+  - completed the transition to nested template #includes (TR)
+  - added direct #includes (TR)
+  - completed the transition to run-time evaluation (TR)
+  - renamed the .startServer() method of Template to .compile() (TR)
+  - renamed the 'delayedStart' setting as 'delayedCompile' (TR)
+  - added .redefineTemplateBlock as an alias to Template.defineTemplateBlock
+    (TR)
+  - got relative path includes working with Webware and Cheetah.Servlet (TR)
+  - lots of changes in the docs (TR & MO)
+  - implemented a C version of NameMapper (TR + CE)
+  - added the 'errorCheckers' framwork (TR)
+  - added the 'formatters' framework and the #formatter directive
+  - a major restructuring of the modules and internal API (TR)
+  - made sure that all the #directives with start and end tags are
+    implemented in such a way that they won't cause 'maximum recursion' limit
+    errors if their content block is long.  Simple regexes didn't cut it in these 
+    cases. (TR)
+    - #macro
+    - multiline comments
+    - #data
+    - #block
+    - #raw
+    - the parsing of the core tags (the state-dependent ones) after they have been
+      translated to the internal delimiters
+  - made a Template.shutdown() method for cleaning up reference cycles before a 
+    template object is deleted. (TR)
+  - made the parsing and processing of #macros() more robust (TR)
+  - implemented the file update checking mechanism (TR)
+    NOTE, the syntax for the #include is now:
+        #include <ARGS> file = <EXPR> 
+        ... uses the value of EXPR as the path of the file to include.
+
+        #include <ARGS> <EXPR> 
+        ... includes the value of the EXPR 
+
+        where <ARGS> is 'raw' or 'direct'
+
+0.9.7 (July 13, 2001)
+
+  - reimplemented the parsing of $placeholders using the Python tokenize module (TR)
+    - now translates into Python code instead of going through NameMapper for
+      each request
+    - supports arg lists and nested placeholders
+    - maintained support for autocalling of functions and methods,
+      will do this serially for $func.otherFunc, etc.
+  - reimplemented the #include and #raw directives using nested templates for
+    parsed includes and  string attributes of 'Template' to store raw text
+    The support for file update monitoring of includes is still not implemented (TR)
+  - moved some stuff from __init__.py into CHANGES and TODO  (TR)
+  - added a new command 'sdist_docs' to setup.py which rebuilds the docs
+    when making a source distribution (TR)
+  - changed the name of the ./Cheetah dir to ./src (TR)
+  - fixed a bug in one of the code filters that was preventing commas from 
+    being used between $placeholders (TR)
+  - generalized the line ending regex for single-line comments (TR)
+  - corrected the spelling of 'Delimiters' throughout Cheetah (TR)
+  - made insertLines in Utilities.py more robust (Chuck)
+  - added key argument to raising some NotFound exceptions in NameMapper (Chuck)
+  - fixed strange bug involving missing templateObj parameter 
+    in PlaceholderProcessor.py(Chuck)
+  - expanded on the docs (Mike)
+
+0.9.6 (June 12, 2001)
+  - fixed a bug in NameMapper that was preventing 'obj.__class__.__name__' from mapping (TR)
+
+0.9.5 (June 10, 2001)
+  - implemented the #cache directive - see the mailing list (TR)
+  - reworked the handling of cached $placeholders and set $var to mean NO_CACHE,
+    $*var to mean STATIC_CACHE, and $*15*var to mean TIMED_REFRESH_CACHE (TR)
+  - renamed Template._getValueForName as Template.mapName (TR)
+
+0.9.4 (June 9, 2001)
+  - created a SettingsManager base class to handle settings for the Template class (TR)
+  - moved the HTML docs icons into the same dir as the HTML (TR)
+
+0.9.3
+  - updated the User's Guide Makefile. Builds HTML, PDF, and PS in the ./docs dir now. (TR)
+  - changed the refs to 'Tavis Rudd' in the docs to 'The Cheetah Development Team' (TR)
+  - added a few bits to the docs (TR)
+  - did some internal renaming so 'nameMapperTags' are referred as 'placeholderTags' (TR)
+  - added the #slurp directive (TR)
+  
+0.9.2
+  - got the PSP plugin working again.  It still need test cases. (TR)
+
+0.9.1
+  - Changed the name of the package to 'Cheetah' from TemplateServer (TR)
+  - Changed the name of the Server module and its TemplateServer class to 'Template' (TR)
+  - Changed the name of the 'TScompile' script to 'cheetah-compile' (TR)
+  - updated the docs (TR)
+
+0.9.0
+  - changed the names and behaviour of the #parse and #include directives (TR)
+    see the docs for more. (TR)
+  - changed #verbatim to #raw (TR)
+  - fixed a bug in Tests.py that caused an error on the first run. (TR)
+  - more docs (TR + MO)
+  ! all tests pass with Python 2.1 and 2.0 (TR)
+  
+0.8.4
+  - changed the #directive end tags to #end if instead of #/if and #end (TR)
+    macro instead of #/macro (TR)
+  - more work on the User's Guide (TR)
+  - fixed a bug in TScompile (TR)
+  
+0.8.3
+  - fixed a problem with the regexs that caused $vars and #directives at the (TR)
+    very beginning of the template string not to match in Python 2.0 (TR)
+  - removed some Test cases that made invalid assumptions about the order (TR)
+    of items in dictionaries. (TR)
+
+0.8.2
+  - finished half of the User's Guide (TR)
+  - fixed several small bugs (TR)
+  - added the #comment directive and removed the old <# multiline comment tag #> (TR)
+  - changed the explicit directive closure to /# from ;# (TR)
+
+  
+0.7.6
+  - several small bug fixes (TR)
+  - reimplemented the #block directive to avoid maximum recursion depth errors (TR)
+    with large blocks. (TR)
+  - created many new test cases in the regression testing suite (TR)
+  - added an example site to the examples/ directory (TR)
+  - started the User's Guide (TR)
+
+0.7.5
+  - implemented the command-line compiler (TR)
+
+0.7.3-4
+  - implemented the regression testing suite (TR)
+  - fixed a number of small bugs (TR)
+
+0.7.2
+  - implemented the #longMacro directive (TR)
+
+
+================================================================================
+KEY TO INITIALS USED ABOVE:
+TR - Tavis Rudd
+MO - Mike Orr
+JJ - Shannon 'jj' Behrens
+IB - Ian Bicking
+CE - Chuck Esterbrook
+MH - Mike Halle
+
+  
diff --git a/Cheetah.egg-info/PKG-INFO b/Cheetah.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..9266cb1
--- /dev/null
@@ -0,0 +1,47 @@
+Metadata-Version: 1.0
+Name: Cheetah
+Version: 2.4.4
+Summary: Cheetah is a template engine and code generation tool.
+Home-page: http://www.cheetahtemplate.org/
+Author: R. Tyler Ballance
+Author-email: cheetahtemplate-discuss@lists.sf.net
+License: UNKNOWN
+Description: Cheetah is an open source template engine and code generation tool.
+        
+        It can be used standalone or combined with other tools and frameworks. Web
+        development is its principle use, but Cheetah is very flexible and is also being
+        used to generate C++ game code, Java, sql, form emails and even Python code.
+        
+        Documentation
+        ================================================================================
+        For a high-level introduction to Cheetah please refer to the User's Guide
+        at http://www.cheetahtemplate.org/learn.html
+        
+        Mailing list
+        ================================================================================
+        cheetahtemplate-discuss@lists.sourceforge.net
+        Subscribe at http://lists.sourceforge.net/lists/listinfo/cheetahtemplate-discuss
+        
+        Credits
+        ================================================================================
+        http://www.cheetahtemplate.org/credits.html
+        
+        Recent Changes
+        ================================================================================
+        See http://www.cheetahtemplate.org/CHANGES.txt for full details
+        
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
+Classifier: Topic :: Software Development :: Code Generators
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: User Interfaces
+Classifier: Topic :: Text Processing
diff --git a/Cheetah.egg-info/SOURCES.txt b/Cheetah.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..7514bb8
--- /dev/null
@@ -0,0 +1,80 @@
+CHANGES
+LICENSE
+MANIFEST.in
+README.markdown
+SetupConfig.py
+SetupTools.py
+TODO
+setup.py
+Cheetah.egg-info/PKG-INFO
+Cheetah.egg-info/SOURCES.txt
+Cheetah.egg-info/dependency_links.txt
+Cheetah.egg-info/requires.txt
+Cheetah.egg-info/top_level.txt
+bin/cheetah
+bin/cheetah-analyze
+bin/cheetah-compile
+cheetah/CacheRegion.py
+cheetah/CacheStore.py
+cheetah/CheetahWrapper.py
+cheetah/Compiler.py
+cheetah/DirectiveAnalyzer.py
+cheetah/Django.py
+cheetah/DummyTransaction.py
+cheetah/ErrorCatchers.py
+cheetah/FileUtils.py
+cheetah/Filters.py
+cheetah/ImportHooks.py
+cheetah/ImportManager.py
+cheetah/NameMapper.py
+cheetah/Parser.py
+cheetah/Servlet.py
+cheetah/SettingsManager.py
+cheetah/SourceReader.py
+cheetah/Template.py
+cheetah/TemplateCmdLineIface.py
+cheetah/Unspecified.py
+cheetah/Version.py
+cheetah/__init__.py
+cheetah/convertTmplPathToModuleName.py
+cheetah/Macros/I18n.py
+cheetah/Macros/__init__.py
+cheetah/Templates/SkeletonPage.py
+cheetah/Templates/SkeletonPage.tmpl
+cheetah/Templates/_SkeletonPage.py
+cheetah/Templates/__init__.py
+cheetah/Tests/Analyzer.py
+cheetah/Tests/CheetahWrapper.py
+cheetah/Tests/Cheps.py
+cheetah/Tests/Filters.py
+cheetah/Tests/Misc.py
+cheetah/Tests/NameMapper.py
+cheetah/Tests/Parser.py
+cheetah/Tests/Performance.py
+cheetah/Tests/Regressions.py
+cheetah/Tests/SyntaxAndOutput.py
+cheetah/Tests/Template.py
+cheetah/Tests/Test.py
+cheetah/Tests/Unicode.py
+cheetah/Tests/__init__.py
+cheetah/Tests/xmlrunner.py
+cheetah/Tools/CGITemplate.py
+cheetah/Tools/MondoReport.py
+cheetah/Tools/MondoReportDoc.txt
+cheetah/Tools/RecursiveNull.py
+cheetah/Tools/SiteHierarchy.py
+cheetah/Tools/__init__.py
+cheetah/Tools/turbocheetah/__init__.py
+cheetah/Tools/turbocheetah/cheetahsupport.py
+cheetah/Tools/turbocheetah/tests/__init__.py
+cheetah/Tools/turbocheetah/tests/test_template.py
+cheetah/Utils/Indenter.py
+cheetah/Utils/Misc.py
+cheetah/Utils/WebInputMixin.py
+cheetah/Utils/__init__.py
+cheetah/Utils/htmlDecode.py
+cheetah/Utils/htmlEncode.py
+cheetah/Utils/statprof.py
+cheetah/c/Cheetah.h
+cheetah/c/_namemapper.c
+cheetah/c/cheetah.h
\ No newline at end of file
diff --git a/Cheetah.egg-info/dependency_links.txt b/Cheetah.egg-info/dependency_links.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/Cheetah.egg-info/requires.txt b/Cheetah.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..302d91c
--- /dev/null
@@ -0,0 +1 @@
+Markdown >= 2.0.1
\ No newline at end of file
diff --git a/Cheetah.egg-info/top_level.txt b/Cheetah.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..471a91a
--- /dev/null
@@ -0,0 +1 @@
+Cheetah
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..af10be7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,16 @@
+Copyright 2001-2005, The Cheetah Development Team: Tavis Rudd, Mike Orr,
+Chuck Esterbrook, Ian Bicking.
+
+Permission to use, copy, modify, and distribute this software for any purpose
+and without fee is hereby granted, provided that the above copyright notice
+appear in all copies and that both that copyright notice and this permission
+notice appear in supporting documentation, and that the names of the authors not
+be used in advertising or publicity pertaining to distribution of the software
+without specific, written prior permission.
+
+THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS
+BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..c892292
--- /dev/null
@@ -0,0 +1,7 @@
+include MANIFEST.in *.py *.cfg TODO CHANGES LICENSE README.markdown examples docs bin
+recursive-include cheetah *.py *.tmpl *.txt *.h
+recursive-include bin *
+recursive-include docs * 
+recursive-include examples *
+recursive-exclude cheetah *.pyc *~ *.aux
+recursive-exclude docs *~ *.aux
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644 (file)
index 0000000..9266cb1
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,47 @@
+Metadata-Version: 1.0
+Name: Cheetah
+Version: 2.4.4
+Summary: Cheetah is a template engine and code generation tool.
+Home-page: http://www.cheetahtemplate.org/
+Author: R. Tyler Ballance
+Author-email: cheetahtemplate-discuss@lists.sf.net
+License: UNKNOWN
+Description: Cheetah is an open source template engine and code generation tool.
+        
+        It can be used standalone or combined with other tools and frameworks. Web
+        development is its principle use, but Cheetah is very flexible and is also being
+        used to generate C++ game code, Java, sql, form emails and even Python code.
+        
+        Documentation
+        ================================================================================
+        For a high-level introduction to Cheetah please refer to the User's Guide
+        at http://www.cheetahtemplate.org/learn.html
+        
+        Mailing list
+        ================================================================================
+        cheetahtemplate-discuss@lists.sourceforge.net
+        Subscribe at http://lists.sourceforge.net/lists/listinfo/cheetahtemplate-discuss
+        
+        Credits
+        ================================================================================
+        http://www.cheetahtemplate.org/credits.html
+        
+        Recent Changes
+        ================================================================================
+        See http://www.cheetahtemplate.org/CHANGES.txt for full details
+        
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
+Classifier: Topic :: Software Development :: Code Generators
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: User Interfaces
+Classifier: Topic :: Text Processing
diff --git a/README.markdown b/README.markdown
new file mode 100644 (file)
index 0000000..fe473b1
--- /dev/null
@@ -0,0 +1,51 @@
+Cheetah is an open source template engine and code generation tool.
+
+It can be used standalone or combined with other tools and frameworks. Web
+development is its principle use, but Cheetah is very flexible and is also being
+used to generate C++ game code, Java, sql, form emails and even Python code.
+
+Documentation
+================================================================================
+For a high-level introduction to Cheetah please refer to the User\'s Guide
+at http://cheetahtemplate.org/learn.html
+
+Mailing list
+================================================================================
+cheetahtemplate-discuss@lists.sourceforge.net
+Subscribe at http://lists.sourceforge.net/lists/listinfo/cheetahtemplate-discuss
+
+Credits
+================================================================================
+http://cheetahtemplate.org/credits.html
+
+Praise
+================================================================================
+"I\'m enamored with Cheetah" - Sam Ruby, senior member of IBM Emerging
+Technologies Group & director of Apache Software Foundation
+
+"Give Cheetah a try. You won\'t regret it. ... Cheetah is a truly powerful
+system. ... Cheetah is a serious contender for the 'best of breed' Python
+templating." - Alex Martelli
+
+"People with a strong PHP background absolutely love Cheetah for being Smarty,
+but much, much better." - Marek Baczynski
+
+"I am using Smarty and I know it very well, but compiled Cheetah Templates with
+its inheritance approach is much powerful and easier to use than Smarty." -
+Jaroslaw Zabiello
+
+"There is no better solution than Cheetah" - Wilk
+
+"A cheetah template can inherit from a python class, or a cheetah template, and
+a Python class can inherit from a cheetah template. This brings the full power
+of OO programming facilities to the templating system, and simply blows away
+other templating systems" - Mike Meyer
+
+"Cheetah has successfully been introduced as a replacement for the overweight
+XSL Templates for code generation. Despite the power of XSL (and notably XPath
+expressions), code generation is better suited to Cheetah as templates are much
+easier to implement and manage." - The FEAR development team
+    (http://fear.sourceforge.net/docs/latest/guide/Build.html#id2550573)
+
+"I\'ve used Cheetah quite a bit and it\'s a very good package" - Kevin Dangoor,
+lead developer of TurboGears.
diff --git a/SetupConfig.py b/SetupConfig.py
new file mode 100644 (file)
index 0000000..5620416
--- /dev/null
@@ -0,0 +1,104 @@
+#-------Main Package Settings-----------#
+import sys
+
+name = 'Cheetah'
+from cheetah.Version import Version as version
+maintainer = "R. Tyler Ballance"
+author = "Tavis Rudd"
+author_email = "cheetahtemplate-discuss@lists.sf.net"
+url = "http://www.cheetahtemplate.org/"
+packages = ['Cheetah',
+            'Cheetah.Macros',            
+            'Cheetah.Templates',
+            'Cheetah.Tests',
+            'Cheetah.Tools',
+            'Cheetah.Utils',
+            ]
+classifiers = [line.strip() for line in '''\
+  #Development Status :: 4 - Beta
+  Development Status :: 5 - Production/Stable
+  Intended Audience :: Developers
+  Intended Audience :: System Administrators
+  License :: OSI Approved :: MIT License
+  Operating System :: OS Independent
+  Programming Language :: Python
+  Topic :: Internet :: WWW/HTTP
+  Topic :: Internet :: WWW/HTTP :: Dynamic Content
+  Topic :: Internet :: WWW/HTTP :: Site Management
+  Topic :: Software Development :: Code Generators
+  Topic :: Software Development :: Libraries :: Python Modules
+  Topic :: Software Development :: User Interfaces
+  Topic :: Text Processing'''.splitlines() if not line.strip().startswith('#')]
+del line
+
+package_dir = {'Cheetah':'cheetah'}
+
+import os
+import os.path
+from distutils.core import Extension
+
+ext_modules=[
+             Extension("Cheetah._namemapper", 
+                        [os.path.join('cheetah', 'c', '_namemapper.c')]),
+           #  Extension("Cheetah._verifytype", 
+           #             [os.path.join('cheetah', 'c', '_verifytype.c')]),
+           #  Extension("Cheetah._filters", 
+           #             [os.path.join('cheetah', 'c', '_filters.c')]),
+           #  Extension('Cheetah._template',
+           #             [os.path.join('cheetah', 'c', '_template.c')]),
+             ]
+
+## Data Files and Scripts
+scripts = ('bin/cheetah-compile',
+           'bin/cheetah',
+           'bin/cheetah-analyze',
+        )
+
+data_files = ['recursive: cheetah *.tmpl *.txt LICENSE README TODO CHANGES',]
+
+if not os.getenv('CHEETAH_INSTALL_WITHOUT_SETUPTOOLS'):
+    try:
+        from setuptools import setup
+        install_requires = [
+                "Markdown >= 2.0.1",
+        ]
+        if sys.platform == 'win32':
+            # use 'entry_points' instead of 'scripts'
+            del scripts
+            entry_points = {
+                'console_scripts': [
+                    'cheetah = Cheetah.CheetahWrapper:_cheetah',
+                    'cheetah-compile = Cheetah.CheetahWrapper:_cheetah_compile',
+                ]
+        }
+    except ImportError:
+        print('Not using setuptools, so we cannot install the Markdown dependency')
+
+
+description = "Cheetah is a template engine and code generation tool."
+
+long_description = '''Cheetah is an open source template engine and code generation tool.
+
+It can be used standalone or combined with other tools and frameworks. Web
+development is its principle use, but Cheetah is very flexible and is also being
+used to generate C++ game code, Java, sql, form emails and even Python code.
+
+Documentation
+================================================================================
+For a high-level introduction to Cheetah please refer to the User\'s Guide
+at http://www.cheetahtemplate.org/learn.html
+
+Mailing list
+================================================================================
+cheetahtemplate-discuss@lists.sourceforge.net
+Subscribe at http://lists.sourceforge.net/lists/listinfo/cheetahtemplate-discuss
+
+Credits
+================================================================================
+http://www.cheetahtemplate.org/credits.html
+
+Recent Changes
+================================================================================
+See http://www.cheetahtemplate.org/CHANGES.txt for full details
+
+'''
diff --git a/SetupTools.py b/SetupTools.py
new file mode 100644 (file)
index 0000000..6608d17
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+import os
+from os import listdir
+import os.path
+from os.path import exists, isdir, isfile, join, splitext
+import sys
+import types
+import glob
+import string
+import traceback
+
+from distutils.core import setup
+if not os.getenv('CHEETAH_INSTALL_WITHOUT_SETUPTOOLS'):
+    try:
+        from setuptools import setup
+    except ImportError:   
+        from distutils.core import setup
+
+from distutils.core import Command
+from distutils.command.build_ext import build_ext
+from distutils.command.install_data import install_data
+from distutils.errors import CCompilerError, DistutilsExecError, \
+    DistutilsPlatformError
+
+#imports from Cheetah ...
+from cheetah.FileUtils import findFiles
+
+if sys.platform == 'win32' and sys.version_info > (2, 6):
+   # 2.6's distutils.msvc9compiler can raise an IOError when failing to
+   # find the compiler
+   ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError,
+                 IOError)
+else:
+   ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError)
+
+##################################################
+## CLASSES ##
+
+class BuildFailed(Exception):
+    pass
+
+class mod_build_ext(build_ext):
+    """A modified version of the distutils build_ext command that raises an
+    exception when building of the extension fails.
+    """
+
+    def run(self):
+        try:
+            build_ext.run(self)
+        except DistutilsPlatformError, x:
+            raise BuildFailed(x)
+
+    def build_extension(self, ext):
+        try:
+            build_ext.build_extension(self, ext)
+        except ext_errors, x:
+            raise BuildFailed(x)
+
+   
+class mod_install_data(install_data):
+    """A modified version of the disutils install_data command that allows data
+    files to be included directly in the installed Python package tree.
+    """
+
+    def finalize_options(self):
+
+        if self.install_dir is None:
+            installobj = self.distribution.get_command_obj('install')
+            #self.install_dir = installobj.install_platlib
+            self.install_dir = installobj.install_lib
+        install_data.finalize_options(self)
+
+    def run (self):
+
+        if not self.dry_run:
+            self.mkpath(self.install_dir)
+        data_files = self.get_inputs()
+        
+        for entry in data_files:
+            if not isinstance(entry, basestring):
+                raise ValueError('The entries in "data_files" must be strings')
+            
+            entry = string.join(string.split(entry, '/'), os.sep)
+            # entry is a filename or glob pattern
+            if entry.startswith('recursive:'):
+                entry = entry[len('recursive:'):]
+                dir = entry.split()[0]
+                globPatterns = entry.split()[1:]
+                filenames = findFiles(dir, globPatterns)
+            else:
+                filenames = glob.glob(entry)
+            
+            for filename in filenames:
+                ## generate the dstPath from the filename
+                # - deal with 'package_dir' translations
+                topDir, subPath = (filename.split(os.sep)[0],
+                                   os.sep.join( filename.split(os.sep)[1:] )
+                                   )
+
+                package_dirDict = self.distribution.package_dir
+                if package_dirDict:
+                    packageDir = topDir
+                    for key, val in package_dirDict.items():
+                        if val == topDir:
+                            packageDir = key
+                            break
+                else:
+                    packageDir = topDir
+                dstPath = os.path.join(self.install_dir, packageDir, subPath)
+
+                ## add the file to the list of outfiles
+                dstdir = os.path.split(dstPath)[0]
+                if not self.dry_run:
+                    self.mkpath(dstdir)
+                    outfile = self.copy_file(filename, dstPath)[0]
+                else:
+                    outfile = dstPath
+                self.outfiles.append(outfile)
+        
+##################################################
+## FUNCTIONS ##
+
+def run_setup(configurations):
+    """ Run distutils setup.
+
+        The parameters passed to setup() are extracted from the list of modules,
+        classes or instances given in configurations.
+
+        Names with leading underscore are removed from the parameters.
+        Parameters which are not strings, lists, tuples, or dicts are removed as
+        well.  Configurations which occur later in the configurations list
+        override settings of configurations earlier in the list.
+
+    """
+    # Build parameter dictionary
+    kws = {}
+    newkws = {}
+    for configuration in configurations:
+        kws.update(vars(configuration))
+    for name, value in kws.items():
+        if name[:1] == '_':
+            continue
+        if not isinstance(value, (basestring, list, tuple, dict, int)):
+            continue
+        newkws[name] = value
+    kws = newkws
+
+    # Add setup extensions
+    cmdclasses = {
+        'build_ext': mod_build_ext,
+        'install_data': mod_install_data,
+        }
+
+    kws['cmdclass'] = cmdclasses
+
+    # Invoke distutils setup
+    try:
+        setup(**kws)
+    except BuildFailed, x:
+        print("One or more C extensions failed to build.")
+        print("Details: %s" % x)
+        if os.environ.get('CHEETAH_C_EXTENSIONS_REQUIRED'):
+            raise x
+        print("Retrying without C extensions enabled.")
+
+        del kws['ext_modules']
+        setup(**kws)
+
+        print("One or more C extensions failed to build.")
+        print("Performance enhancements will not be available.")
+        print("Pure Python installation succeeded.")
+
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..536eb9f
--- /dev/null
+++ b/TODO
@@ -0,0 +1,253 @@
+NOTE: Please see http://bugs.cheetahtemplate.org
+       for future feature requests/bugs/TODO
+
+
+===============================================================================
+===============================================================================
+
+Desired for Cheetah 2.0
+=======================
+- Smart HTML filter that escapes all values except those individually marked as
+  preformatted, a la Kid/PTL/QPY. (MO)
+
+
+TODO Items (many are just ideas. This is not an official roadmap!)
+================================================================================
+
+- "cheetah test" problem: subcommands fail mysteriously on Windows.  Rewrite
+  to avoid using subcommands.  Instead, set sys.argv and call the appropriate
+  main() for each test.
+
+- Documentation: document #encoding.  Explain problems "cheetah test" if they
+  haven't been fixed yet.
+
+- There's a kludge in CheetahWrapper.py to abort with a helpful error message
+  if the user runs 'cheetah test' but doesn't have write permission in the
+  current directory.  The tests should instead put their temporary files
+  under the system tmp directory.
+
+- Reset the current filter to the default (or to the constructor's filter
+  if specified) at the beginning of each fill.  Currently, filter changes
+  leak from one fill to the next.
+
+- CheetahWrapper stuff: (MO)
+  * "cheetah preview [options] [FILES]"  print template-specific portion of main
+    method(s) to stdout, with line numbers based on the .py template module.  
+    Make a Template method to do the same thing, a la .generatedModuleCode().
+  * Refactor, make compile/fill/code routines callbacks using a bundle arg.
+  * If an input file ends in a dot, intelligently add the input extension if
+    not found.
+
+- Debugging tools.  See section below.
+
+- Provide a utility to list the names of all placeholders in the template.
+  Requested by Tracy Ruggles on Feb 21, 2003.
+   
+- 'errorCatcher None' to stop catching errors in the middle of a template.
+
+- Utils.WebInputMixin: factor out Cheetah-specific code so it can be used in
+  non-Cheetah applications.  Don't modify the searchList: have a Template
+  wrapper method do that.  Consider turning it into a function that does not
+  require 'self'.  Consider making Webware-specific code into plugins so that,
+  e.g., other cookie-handling methods can be grafted in.  Maybe use callback
+  classes like the planned rewrite for CheetahWrapper.  Low priority.  (MO)
+
+- Look through Zope Page Templates (TAL) for ideas to borrow.
+  http://www.zope.org/Documentation/Books/ZopeBook/current/AppendixC.stx
+  http://www.owlfish.com/software/simpleTAL/index.html
+
+Debugging Tools (Dump Tools)
+============================
+It would be nice to provide debugging tools for users who can't figure
+out why a certain placeholder value isn't found or is being overridden.  
+My idea is to define $dumpSearchList() and $dumpSearchListFlat() in
+Template, which would print a stanza in the output showing all searchList
+variables and their values.  $dumpSearchList would group by searchList
+element; $dumpSearchListFlat would combine all into a single
+alphabetical listing.
+        I made an experimental version but it printed only instance variables,
+not methods and not inherited attributes.  Also, it wouldn't print right
+using the usual pattern of write-to-custom-StringIO-object-and-return-
+the-.getvalue() and I couldn't figure out why.
+        The stanza should be set apart by a row of stars with the words
+"BEGIN/END SEARCH LIST DUMP".  Then for $dumpSearchList, precede each
+group with "*** searchList[i], type <element type>, 142 variables ***".
+        Because some elements like 'self' may have hundreds of inherited
+methods that would create a forest-through-trees situation for the user,
+we may need an option to supress the variable listing for elements with
+> 20 variables (just print the summary line instead). ?
+        The final version should be in Template so it has implicit
+access to the searchList and perhaps later to other variables (locals,
+globals, "#set global"s, builtins) too.  This is such a central
+debugging tool that you should not have to monopolize an #extends
+(the template's only #extends) to use it.  You could import it, however,
+if you pass in the searchList explicitly as an argument.  In that case,
+perhaps we can base it on a generic module for dumping variables/values.
+        Note that we cannot simply depend on str() and pprint, because
+we need to show instances as dictionaries.  Likewise, dir() and vars()
+may get us part of the distance, but only if they show methods and
+inherited attributes too.  
+        These functions should print only top-level variables, not
+the subelements of collections.  I.e, if the first searchList element
+is a dictionary, show its keys/values, but do not expand any 
+subvalues if they are dictionaries too, unless the display tool happens
+to default to that.
+
+#entry $func($arg1, $arg2="default", $**kw)
+===============================================================================
+Make a wrapper function in the .py template module that builds a searchList
+from its positional arguments, then instantiates and fills a template and
+returns the result.  The preceding example would create a function thus:
+        def func(arg1, arg2="default", searchList=None, **kw):
+                """Function docstring."""
+                sl = {'arg1': arg1, 'arg2': arg2}
+                if searchList is None:
+                        searchList = [sl]
+                elif type(searchList) == types.ListType:
+                        searchList.insert(0, sl)
+                else:
+                        raise TypeError("arg 'searchList'")
+                t = TheTemplate(searchList=searchList, **kw)
+                return str(t)
+##doc-entry: and #*doc-entry: comments are appended to the function docstring.
+        Finally, make this function accessible directly from the shell.
+If there are any non-option arguments on the command line, call the function
+instead of filling the template the normal way.  
+        This would perhaps make more sense as arguments to .respond().  But
+.respond() has that pesky 'trans' argument that mustn't be interfered with,
+and other programs may assume .respond() takes only one argument.  Also, 
+when called via str(), str() cannot take arguments.
+        
+#indent
+========================================================================
+The current indenter (which exists but is undocumented) is a kludge that has an
+indentation object, with implicit placeholder calls added at each line to
+generate the indentation, and #silent calls to adjust the object.  It should be
+reimplemented to generate code to call the indentation object directly.  Also,
+the user interface should be cleaned up, the implementation and Users' Guide
+synchronized, and test cases built.
+
+The desired implementation revolves around self._indenter, which knows the
+current indentation level (a non-negative integer), chars (the string output
+per level, default four spaces), and stack (the previous indentation levels).
+The .indent() method returns the indentation string currently appropriate.
+The desired interface for phase 1 (subject to change):
+  #indent strip    ; strip leading whitespace from input lines
+  #indent add      ; add indentation to output lines as appropriate
+  #indent on       ; do both
+  #indent off      ; do neither
+  #indent reset    ; set level to 0 and clear stack
+  #indent ++       ; increment level
+  #indent --       ; decrement level
+  #indent pop [EXPR]   ; revert to Nth previous level (default 1)
+                   ; if pop past end of stack, set level to 0 and
+                   ; clear stack.  All +/-/= operations push the old level
+                   ; onto the stack.
+  #indent debug    ; dump level, chars and stack to template output
+
+Possible extensions:
+  #indent =EXPR    ; set level to N  (likely to be added to phase 1)
+  #indent +EXPR    ; add N to level (not very necessary)
+  #indent -EXPR    ; subtract N from level (not very necessary)
+  #indent balance BOOL ; require all indent changes in a #def/#block to be
+                       ; popped before exiting the method.  (difficult to
+                      ; implement)
+  #indent implicitPop BOOL ; automatically pop indent changes within a 
+                       ; #def/block when that method exits.  (difficult to
+                      ; implement)
+  #indent ??       ; a 3-way switch that combines unbalanced, balanced and
+                   ; implicit pop.  (difficult to implement)
+  #indent ??       ; smart stripping: strip input indentation according to
+                   ; nested directive level; e.g., 
+                   ; 01: #if foo=1
+                   ; 02:     public int foo()
+                   ; 03:     {
+                   ; 04:       return FOO;
+                   ; 05:     }
+                   ; 06: #end if
+                   ; With smart stripping, line 4 would be indented and the
+                   ; others not.  With "on" or "strip" stripping, all lines
+                   ; 2-5 would be unindented.  With "off" stripping, 
+                   ; lines 2-5 would not be stripped.
+
+There should be one indentation object per Template instance, shared by
+methods and include files.
+
+
+Upload File
+========================================================================
+@@TR: This is way outside Cheetah's scope!
+
+A mixin method in Cheetah.Utils (for Template) that handles file uploads --
+these are too complicated for .webInput().  The method should do a "safe"
+file upload; e.g., http://us3.php.net/manual/en/features.file-upload.php ,
+within the limitations of Python's cgi module.  The user has the choice of
+three destinations for the file contents: (A) copied to a local
+path you specify, (B) placed in a namespace variable like .cgiImport()
+does, or (C) returned.  (B) parallels .webInput, but (A) will certainly be
+desirable situations where we just want to save the file, not read it into
+memory.  Reject files larger than a user-specified size or not in a list of
+user-approved MIME types.  Define appropriate exceptions for typical
+file-upload errors.  Method name .webUploadFileAsString?
+        One situation to support is when  form has a text(area) field
+related to a file-upload control on the same form, and the user has the choice
+of typing into the field or uploading a text file.  We need a method that
+updates the text field's value if there is an uploaded file, but not if there
+isn't.  This may be handled by the regular method(s) or may require a separate
+method.
+
+RPM Building
+============
+From: John Landahl <john@landahl.org>
+To: cheetahtemplate-discuss@lists.sourceforge.net
+Subject: [Cheetahtemplate-discuss] Building Cheetah RPMs
+Date: Wed, 05 Nov 2003 01:27:24 -0800
+
+If anyone is interested in building Cheetah RPMs, simply add the following
+lines to a file called MANIFEST.in in the Cheetah directory and you'll be
+able to use the "bdist_rpm" option to setup.py (i.e. "python setup.py
+bdist_rpm"):
+
+  include SetupTools.py
+  include SetupConfig.py
+  include bin/*
+
+Also, I've found that using /usr/lib/site-python for add-on Python
+packages is much more convenient than the default of
+/usr/lib/pythonX/site-packages, especially when jumping back and forth
+between 2.2 and 2.3.  If you'd like Cheetah in /usr/lib/site-python,
+createa a setup.cfg with the following contents:
+
+  [install]
+  install-lib = /usr/lib/site-python
+
+Of course if you do have version specific libraries they should stay in
+/usr/lib/pythonX/site-packages, but Cheetah seems happy in both 2.2 and
+2.3 and so is a good candidate for /usr/lib/site-python.
+
+
+User-defined directives
+=======================================================================
+IF we decide to support user-defined directives someday, consider Spyce's
+interface.  Spyce uses a base class which provides generic services to
+custom "active tags".
+http://spyce.sourceforge.net/doc-tag.html
+http://spyce.sourceforge.net/doc-tag_new.html
+
+
+Test Suite
+================================================================================
+- add cases that test the cheetah-compile script
+- add cases that test the integration with various webdev frameworks
+
+Examples
+================================================================================
+- create some non-html code generation examples
+  - SQL
+  - LaTeX
+  - form email
+- Template definitions in a database.  .py template modules in a 
+  database?  Caching template classes and/or instances extracted from
+  a database.
+- Pickled templates?
+
diff --git a/bin/cheetah b/bin/cheetah
new file mode 100755 (executable)
index 0000000..61b8178
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+from Cheetah.CheetahWrapper import _cheetah
+_cheetah()
diff --git a/bin/cheetah-analyze b/bin/cheetah-analyze
new file mode 100644 (file)
index 0000000..097db5f
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+from Cheetah import DirectiveAnalyzer
+
+if __name__ == '__main__':
+    DirectiveAnalyzer.main()
diff --git a/bin/cheetah-compile b/bin/cheetah-compile
new file mode 100644 (file)
index 0000000..f15528e
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env python
+from Cheetah.CheetahWrapper import _cheetah_compile
+_cheetah_compile()
diff --git a/cheetah/CacheRegion.py b/cheetah/CacheRegion.py
new file mode 100644 (file)
index 0000000..2586b72
--- /dev/null
@@ -0,0 +1,136 @@
+# $Id: CacheRegion.py,v 1.3 2006/01/28 04:19:30 tavis_rudd Exp $
+'''
+Cache holder classes for Cheetah:
+
+Cache regions are defined using the #cache Cheetah directive. Each
+cache region can be viewed as a dictionary (keyed by cacheRegionID)
+handling at least one cache item (the default one). It's possible to add
+cacheItems in a region by using the `varyBy` #cache directive parameter as
+in the following example::
+   #def getArticle
+      this is the article content.
+   #end def
+
+   #cache varyBy=$getArticleID()
+      $getArticle($getArticleID())
+   #end cache
+
+The code above will generate a CacheRegion and add new cacheItem for each value
+of $getArticleID().
+'''
+
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import md5
+
+import time
+import Cheetah.CacheStore
+
+class CacheItem(object):
+    '''
+    A CacheItem is a container storing:
+
+        - cacheID (string)
+        - refreshTime (timestamp or None) : last time the cache was refreshed
+        - data (string) : the content of the cache
+    '''
+    
+    def __init__(self, cacheItemID, cacheStore):
+        self._cacheItemID = cacheItemID
+        self._cacheStore = cacheStore
+        self._refreshTime = None
+        self._expiryTime = 0
+
+    def hasExpired(self):
+        return (self._expiryTime and time.time() > self._expiryTime)
+    
+    def setExpiryTime(self, time):
+        self._expiryTime = time
+
+    def getExpiryTime(self):
+        return self._expiryTime
+
+    def setData(self, data):
+        self._refreshTime = time.time()
+        self._cacheStore.set(self._cacheItemID, data, self._expiryTime)
+
+    def getRefreshTime(self):
+        return self._refreshTime
+
+    def getData(self):
+        assert self._refreshTime
+        return self._cacheStore.get(self._cacheItemID)
+
+    def renderOutput(self):
+        """Can be overridden to implement edge-caching"""
+        return self.getData() or ""
+
+    def clear(self):
+        self._cacheStore.delete(self._cacheItemID)
+        self._refreshTime = None
+
+class _CacheDataStoreWrapper(object):
+    def __init__(self, dataStore, keyPrefix):
+        self._dataStore = dataStore
+        self._keyPrefix = keyPrefix
+        
+    def get(self, key):
+        return self._dataStore.get(self._keyPrefix+key)
+
+    def delete(self, key):
+        self._dataStore.delete(self._keyPrefix+key)
+
+    def set(self, key, val, time=0):        
+        self._dataStore.set(self._keyPrefix+key, val, time=time)
+
+class CacheRegion(object):
+    '''
+    A `CacheRegion` stores some `CacheItem` instances.
+
+    This implementation stores the data in the memory of the current process.
+    If you need a more advanced data store, create a cacheStore class that works
+    with Cheetah's CacheStore protocol and provide it as the cacheStore argument
+    to __init__.  For example you could use
+    Cheetah.CacheStore.MemcachedCacheStore, a wrapper around the Python
+    memcached API (http://www.danga.com/memcached).
+    '''
+    _cacheItemClass = CacheItem
+    
+    def __init__(self, regionID, templateCacheIdPrefix='', cacheStore=None):
+        self._isNew = True
+        self._regionID = regionID
+        self._templateCacheIdPrefix = templateCacheIdPrefix
+        if not cacheStore:
+            cacheStore = Cheetah.CacheStore.MemoryCacheStore()
+        self._cacheStore = cacheStore
+        self._wrappedCacheDataStore = _CacheDataStoreWrapper(
+            cacheStore, keyPrefix=templateCacheIdPrefix+':'+regionID+':')
+        self._cacheItems = {}
+
+    def isNew(self):
+        return self._isNew
+        
+    def clear(self):
+        " drop all the caches stored in this cache region "
+        for cacheItemId in self._cacheItems.keys():
+            cacheItem = self._cacheItems[cacheItemId]
+            cacheItem.clear()
+            del self._cacheItems[cacheItemId]
+        
+    def getCacheItem(self, cacheItemID):
+        """ Lazy access to a cacheItem
+
+            Try to find a cache in the stored caches. If it doesn't
+            exist, it's created.
+            
+            Returns a `CacheItem` instance.
+        """
+        cacheItemID = md5(str(cacheItemID)).hexdigest()
+        
+        if cacheItemID not in self._cacheItems:
+            cacheItem = self._cacheItemClass(
+                cacheItemID=cacheItemID, cacheStore=self._wrappedCacheDataStore)
+            self._cacheItems[cacheItemID] = cacheItem
+            self._isNew = False
+        return self._cacheItems[cacheItemID]
diff --git a/cheetah/CacheStore.py b/cheetah/CacheStore.py
new file mode 100644 (file)
index 0000000..8017018
--- /dev/null
@@ -0,0 +1,106 @@
+'''
+Provides several CacheStore backends for Cheetah's caching framework.  The
+methods provided by these classes have the same semantics as those in the
+python-memcached API, except for their return values:
+
+set(key, val, time=0)
+  set the value unconditionally
+add(key, val, time=0)
+  set only if the server doesn't already have this key
+replace(key, val, time=0)
+  set only if the server already have this key
+get(key, val)
+  returns val or raises a KeyError
+delete(key)
+  deletes or raises a KeyError
+'''
+import time
+
+class Error(Exception):
+    pass
+
+class AbstractCacheStore(object):
+
+    def set(self, key, val, time=None):
+        raise NotImplementedError
+
+    def add(self, key, val, time=None):
+        raise NotImplementedError
+
+    def replace(self, key, val, time=None):
+        raise NotImplementedError
+
+    def delete(self, key):
+        raise NotImplementedError
+
+    def get(self, key):
+        raise NotImplementedError
+
+class MemoryCacheStore(AbstractCacheStore):
+    def __init__(self):
+        self._data = {}
+
+    def set(self, key, val, time=0):
+        self._data[key] = (val, time)
+
+    def add(self, key, val, time=0):
+        if key in self._data:
+            raise Error('a value for key %r is already in the cache'%key)
+        self._data[key] = (val, time)
+
+    def replace(self, key, val, time=0):
+        if key in self._data:
+            raise Error('a value for key %r is already in the cache'%key)
+        self._data[key] = (val, time)
+
+    def delete(self, key):
+        del self._data[key]
+        
+    def get(self, key):
+        (val, exptime) = self._data[key]
+        if exptime and time.time() > exptime:
+            del self._data[key]
+            raise KeyError(key)
+        else:
+            return val
+
+    def clear(self):
+        self._data.clear()        
+                  
+class MemcachedCacheStore(AbstractCacheStore):
+    servers = ('127.0.0.1:11211')
+    def __init__(self, servers=None, debug=False):
+        if servers is None:
+            servers = self.servers
+        from memcache import Client as MemcachedClient
+        self._client = MemcachedClient(servers, debug)
+
+    def set(self, key, val, time=0):
+        self._client.set(key, val, time)
+
+    def add(self, key, val, time=0):
+        res = self._client.add(key, val, time)        
+        if not res:
+            raise Error('a value for key %r is already in the cache'%key)
+        self._data[key] = (val, time)
+
+    def replace(self, key, val, time=0):
+        res = self._client.replace(key, val, time)        
+        if not res:
+            raise Error('a value for key %r is already in the cache'%key)
+        self._data[key] = (val, time)
+
+    def delete(self, key):
+        res = self._client.delete(key, time=0)        
+        if not res:
+            raise KeyError(key)
+        
+    def get(self, key):
+        val = self._client.get(key)
+        if val is None:
+            raise KeyError(key)
+        else:
+            return val
+
+    def clear(self):
+        self._client.flush_all()        
diff --git a/cheetah/CheetahWrapper.py b/cheetah/CheetahWrapper.py
new file mode 100644 (file)
index 0000000..77a5696
--- /dev/null
@@ -0,0 +1,633 @@
+# $Id: CheetahWrapper.py,v 1.26 2007/10/02 01:22:04 tavis_rudd Exp $
+"""Cheetah command-line interface.
+
+2002-09-03 MSO: Total rewrite.
+2002-09-04 MSO: Bugfix, compile command was using wrong output ext.
+2002-11-08 MSO: Another rewrite.
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com> and Mike Orr <sluggoster@gmail.com>>
+Version: $Revision: 1.26 $
+Start Date: 2001/03/30
+Last Revision Date: $Date: 2007/10/02 01:22:04 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com> and Mike Orr <sluggoster@gmail.com>"
+__revision__ = "$Revision: 1.26 $"[11:-2]
+
+import getopt, glob, os, pprint, re, shutil, sys
+import cPickle as pickle
+from optparse import OptionParser
+
+from Cheetah.Version import Version
+from Cheetah.Template import Template, DEFAULT_COMPILER_SETTINGS
+from Cheetah.Utils.Misc import mkdirsWithPyInitFiles
+
+optionDashesRE = re.compile(  R"^-{1,2}"  )
+moduleNameRE = re.compile(  R"^[a-zA-Z_][a-zA-Z_0-9]*$"  )
+   
+def fprintfMessage(stream, format, *args):
+    if format[-1:] == '^':
+        format = format[:-1]
+    else:
+        format += '\n'
+    if args:
+        message = format % args
+    else:
+        message = format
+    stream.write(message)
+
+class Error(Exception):
+    pass
+
+
+class Bundle:
+    """Wrap the source, destination and backup paths in one neat little class.
+       Used by CheetahWrapper.getBundles().
+    """
+    def __init__(self, **kw):
+        self.__dict__.update(kw)
+
+    def __repr__(self):
+        return "<Bundle %r>" % self.__dict__
+
+
+##################################################
+## USAGE FUNCTION & MESSAGES
+
+def usage(usageMessage, errorMessage="", out=sys.stderr):
+    """Write help text, an optional error message, and abort the program.
+    """
+    out.write(WRAPPER_TOP)
+    out.write(usageMessage)
+    exitStatus = 0
+    if errorMessage:
+        out.write('\n')
+        out.write("*** USAGE ERROR ***: %s\n" % errorMessage)
+        exitStatus = 1
+    sys.exit(exitStatus)
+             
+
+WRAPPER_TOP = """\
+         __  ____________  __
+         \ \/            \/ /
+          \/    *   *     \/    CHEETAH %(Version)s Command-Line Tool
+           \      |       / 
+            \  ==----==  /      by Tavis Rudd <tavis@damnsimple.com>
+             \__________/       and Mike Orr <sluggoster@gmail.com>
+              
+""" % globals()
+
+
+HELP_PAGE1 = """\
+USAGE:
+------
+  cheetah compile [options] [FILES ...]     : Compile template definitions
+  cheetah fill [options] [FILES ...]        : Fill template definitions
+  cheetah help                              : Print this help message
+  cheetah options                           : Print options help message
+  cheetah test [options]                    : Run Cheetah's regression tests
+                                            : (same as for unittest)
+  cheetah version                           : Print Cheetah version number
+
+You may abbreviate the command to the first letter; e.g., 'h' == 'help'.
+If FILES is a single "-", read standard input and write standard output.
+Run "cheetah options" for the list of valid options.
+"""
+
+##################################################
+## CheetahWrapper CLASS
+
+class CheetahWrapper(object):
+    MAKE_BACKUPS = True
+    BACKUP_SUFFIX = ".bak"
+    _templateClass = None
+    _compilerSettings = None    
+
+    def __init__(self):
+        self.progName = None
+        self.command = None
+        self.opts = None
+        self.pathArgs = None
+        self.sourceFiles = []
+        self.searchList = []
+        self.parser = None
+
+    ##################################################
+    ## MAIN ROUTINE
+
+    def main(self, argv=None):
+        """The main program controller."""
+
+        if argv is None:
+            argv = sys.argv
+
+        # Step 1: Determine the command and arguments.
+        try:
+            self.progName = progName = os.path.basename(argv[0])
+            self.command = command = optionDashesRE.sub("", argv[1])
+            if command == 'test':
+                self.testOpts = argv[2:]
+            else:
+                self.parseOpts(argv[2:])
+        except IndexError:
+            usage(HELP_PAGE1, "not enough command-line arguments")
+
+        # Step 2: Call the command
+        meths = (self.compile, self.fill, self.help, self.options,
+            self.test, self.version)
+        for meth in meths:
+            methName = meth.__name__
+            # Or meth.im_func.func_name
+            # Or meth.func_name (Python >= 2.1 only, sometimes works on 2.0)
+            methInitial = methName[0]
+            if command in (methName, methInitial):
+                sys.argv[0] += (" " + methName)
+                # @@MO: I don't necessarily agree sys.argv[0] should be 
+                # modified.
+                meth()
+                return
+        # If none of the commands matched.
+        usage(HELP_PAGE1, "unknown command '%s'" % command)
+
+    def parseOpts(self, args):
+        C, D, W = self.chatter, self.debug, self.warn
+        self.isCompile = isCompile = self.command[0] == 'c'
+        defaultOext = isCompile and ".py" or ".html"
+        self.parser = OptionParser()
+        pao = self.parser.add_option
+        pao("--idir", action="store", dest="idir", default='', help='Input directory (defaults to current directory)')
+        pao("--odir", action="store", dest="odir", default="", help='Output directory (defaults to current directory)')
+        pao("--iext", action="store", dest="iext", default=".tmpl", help='File input extension (defaults: compile: .tmpl, fill: .tmpl)')
+        pao("--oext", action="store", dest="oext", default=defaultOext, help='File output extension (defaults: compile: .py, fill: .html)')
+        pao("-R", action="store_true", dest="recurse", default=False, help='Recurse through subdirectories looking for input files')
+        pao("--stdout", "-p", action="store_true", dest="stdout", default=False, help='Send output to stdout instead of writing to a file')
+        pao("--quiet", action="store_false", dest="verbose", default=True, help='Do not print informational messages to stdout')
+        pao("--debug", action="store_true", dest="debug", default=False, help='Print diagnostic/debug information to stderr')
+        pao("--env", action="store_true", dest="env", default=False, help='Pass the environment into the search list')
+        pao("--pickle", action="store", dest="pickle", default="", help='Unpickle FILE and pass it through in the search list')
+        pao("--flat", action="store_true", dest="flat", default=False, help='Do not build destination subdirectories')
+        pao("--nobackup", action="store_true", dest="nobackup", default=False, help='Do not make backup files when generating new ones')
+        pao("--settings", action="store", dest="compilerSettingsString", default=None, help='String of compiler settings to pass through, e.g. --settings="useNameMapper=False,useFilters=False"')
+        pao('--print-settings', action='store_true', dest='print_settings', help='Print out the list of available compiler settings')
+        pao("--templateAPIClass", action="store", dest="templateClassName", default=None, help='Name of a subclass of Cheetah.Template.Template to use for compilation, e.g. MyTemplateClass')
+        pao("--parallel", action="store", type="int", dest="parallel", default=1, help='Compile/fill templates in parallel, e.g. --parallel=4')
+        pao('--shbang', dest='shbang', default='#!/usr/bin/env python', help='Specify the shbang to place at the top of compiled templates, e.g. --shbang="#!/usr/bin/python2.6"')
+
+        opts, files = self.parser.parse_args(args)
+        self.opts = opts
+        if sys.platform == "win32":
+            new_files = []
+            for spec in files:
+                file_list = glob.glob(spec)
+                if file_list:
+                    new_files.extend(file_list)
+                else:
+                    new_files.append(spec)
+            files = new_files
+        self.pathArgs = files
+
+        D("""\
+cheetah compile %s
+Options are
+%s
+Files are %s""", args, pprint.pformat(vars(opts)), files)
+
+
+        if opts.print_settings:
+            print() 
+            print('>> Available Cheetah compiler settings:')
+            from Cheetah.Compiler import _DEFAULT_COMPILER_SETTINGS
+            listing = _DEFAULT_COMPILER_SETTINGS
+            listing.sort(key=lambda l: l[0][0].lower())
+
+            for l in listing:
+                print('\t%s (default: "%s")\t%s' % l)
+            sys.exit(0)
+
+        #cleanup trailing path separators
+        seps = [sep for sep in [os.sep, os.altsep] if sep]
+        for attr in ['idir', 'odir']:
+            for sep in seps:
+                path = getattr(opts, attr, None)
+                if path and path.endswith(sep):
+                    path = path[:-len(sep)]
+                    setattr(opts, attr, path)
+                    break
+
+        self._fixExts()
+        if opts.env:
+            self.searchList.insert(0, os.environ)
+        if opts.pickle:
+            f = open(opts.pickle, 'rb')
+            unpickled = pickle.load(f)
+            f.close()
+            self.searchList.insert(0, unpickled)
+
+    ##################################################
+    ## COMMAND METHODS
+
+    def compile(self):
+        self._compileOrFill()
+
+    def fill(self):
+        from Cheetah.ImportHooks import install
+        install()
+        self._compileOrFill()
+
+    def help(self):
+        usage(HELP_PAGE1, "", sys.stdout)
+
+    def options(self):
+        return self.parser.print_help()
+
+    def test(self):
+        # @@MO: Ugly kludge.
+        TEST_WRITE_FILENAME = 'cheetah_test_file_creation_ability.tmp'
+        try:
+            f = open(TEST_WRITE_FILENAME, 'w')
+        except:
+            sys.exit("""\
+Cannot run the tests because you don't have write permission in the current
+directory.  The tests need to create temporary files.  Change to a directory
+you do have write permission to and re-run the tests.""")
+        else:
+            f.close()
+            os.remove(TEST_WRITE_FILENAME)
+        # @@MO: End ugly kludge.
+        from Cheetah.Tests import Test
+        import unittest
+        verbosity = 1
+        if '-q' in self.testOpts:
+            verbosity = 0
+        if '-v' in self.testOpts:
+            verbosity = 2
+        runner = unittest.TextTestRunner(verbosity=verbosity)
+        runner.run(unittest.TestSuite(Test.suites))
+        results = runner.run(unittest.TestSuite(Test.suites))
+        exit(int(not results.wasSuccessful()))
+
+    def version(self):
+        print(Version)
+
+    # If you add a command, also add it to the 'meths' variable in main().
+    ##################################################
+    ## LOGGING METHODS
+
+    def chatter(self, format, *args):
+        """Print a verbose message to stdout.  But don't if .opts.stdout is
+           true or .opts.verbose is false.
+        """
+        if self.opts.stdout or not self.opts.verbose:
+            return
+        fprintfMessage(sys.stdout, format, *args)
+
+
+    def debug(self, format, *args):
+        """Print a debugging message to stderr, but don't if .debug is
+           false.
+        """
+        if self.opts.debug:
+            fprintfMessage(sys.stderr, format, *args)
+    
+    def warn(self, format, *args):
+        """Always print a warning message to stderr.
+        """
+        fprintfMessage(sys.stderr, format, *args)
+
+    def error(self, format, *args):
+        """Always print a warning message to stderr and exit with an error code.        
+        """
+        fprintfMessage(sys.stderr, format, *args)
+        sys.exit(1)
+
+    ##################################################
+    ## HELPER METHODS
+
+
+    def _fixExts(self):
+        assert self.opts.oext, "oext is empty!"
+        iext, oext = self.opts.iext, self.opts.oext
+        if iext and not iext.startswith("."):
+            self.opts.iext = "." + iext
+        if oext and not oext.startswith("."):
+            self.opts.oext = "." + oext
+    
+
+
+    def _compileOrFill(self):
+        C, D, W = self.chatter, self.debug, self.warn
+        opts, files = self.opts, self.pathArgs
+        if files == ["-"]: 
+            self._compileOrFillStdin()
+            return
+        elif not files and opts.recurse:
+            which = opts.idir and "idir" or "current"
+            C("Drilling down recursively from %s directory.", which)
+            sourceFiles = []
+            dir = os.path.join(self.opts.idir, os.curdir)
+            os.path.walk(dir, self._expandSourceFilesWalk, sourceFiles)
+        elif not files:
+            usage(HELP_PAGE1, "Neither files nor -R specified!")
+        else:
+            sourceFiles = self._expandSourceFiles(files, opts.recurse, True)
+        sourceFiles = [os.path.normpath(x) for x in sourceFiles]
+        D("All source files found: %s", sourceFiles)
+        bundles = self._getBundles(sourceFiles)
+        D("All bundles: %s", pprint.pformat(bundles))
+        if self.opts.flat:
+            self._checkForCollisions(bundles)
+
+        # In parallel mode a new process is forked for each template
+        # compilation, out of a pool of size self.opts.parallel. This is not
+        # really optimal in all cases (e.g. probably wasteful for small
+        # templates), but seems to work well in real life for me.
+        #
+        # It also won't work for Windows users, but I'm not going to lose any
+        # sleep over that.
+        if self.opts.parallel > 1:
+            bad_child_exit = 0
+            pid_pool = set()
+
+            def child_wait():
+                pid, status = os.wait()
+                pid_pool.remove(pid)
+                return os.WEXITSTATUS(status)
+
+            while bundles:
+                b = bundles.pop()
+                pid = os.fork()
+                if pid:
+                    pid_pool.add(pid)
+                else:
+                    self._compileOrFillBundle(b)
+                    sys.exit(0)
+
+                if len(pid_pool) == self.opts.parallel:
+                    bad_child_exit = child_wait()
+                    if bad_child_exit:
+                        break
+
+            while pid_pool:
+                child_exit = child_wait()
+                if not bad_child_exit:
+                    bad_child_exit = child_exit
+
+            if bad_child_exit:
+                sys.exit("Child process failed, exited with code %d" % bad_child_exit)
+
+        else:
+            for b in bundles:
+                self._compileOrFillBundle(b)
+
+    def _checkForCollisions(self, bundles):
+        """Check for multiple source paths writing to the same destination
+           path.
+        """
+        C, D, W = self.chatter, self.debug, self.warn
+        isError = False
+        dstSources = {}
+        for b in bundles:
+            if b.dst in dstSources:
+                dstSources[b.dst].append(b.src)
+            else:
+                dstSources[b.dst] = [b.src]
+        keys = sorted(dstSources.keys())
+        for dst in keys:
+            sources = dstSources[dst]
+            if len(sources) > 1:
+                isError = True
+                sources.sort()
+                fmt = "Collision: multiple source files %s map to one destination file %s"
+                W(fmt, sources, dst)
+        if isError:
+            what = self.isCompile and "Compilation" or "Filling"
+            sys.exit("%s aborted due to collisions" % what)
+                
+
+    def _expandSourceFilesWalk(self, arg, dir, files):
+        """Recursion extension for .expandSourceFiles().
+           This method is a callback for os.path.walk().
+           'arg' is a list to which successful paths will be appended.
+        """
+        iext = self.opts.iext
+        for f in files:
+            path = os.path.join(dir, f)
+            if   path.endswith(iext) and os.path.isfile(path):
+                arg.append(path)
+            elif os.path.islink(path) and os.path.isdir(path):
+                os.path.walk(path, self._expandSourceFilesWalk, arg)
+            # If is directory, do nothing; 'walk' will eventually get it.
+
+
+    def _expandSourceFiles(self, files, recurse, addIextIfMissing):
+        """Calculate source paths from 'files' by applying the 
+           command-line options.
+        """
+        C, D, W = self.chatter, self.debug, self.warn
+        idir = self.opts.idir
+        iext = self.opts.iext
+        files = [] 
+        for f in self.pathArgs:
+            oldFilesLen = len(files)
+            D("Expanding %s", f)
+            path = os.path.join(idir, f)
+            pathWithExt = path + iext # May or may not be valid.
+            if os.path.isdir(path):
+                if recurse:
+                    os.path.walk(path, self._expandSourceFilesWalk, files)
+                else:
+                    raise Error("source file '%s' is a directory" % path)
+            elif os.path.isfile(path):
+                files.append(path)
+            elif (addIextIfMissing and not path.endswith(iext) and 
+                  os.path.isfile(pathWithExt)):
+                files.append(pathWithExt)
+                # Do not recurse directories discovered by iext appending.
+            elif os.path.exists(path):
+                W("Skipping source file '%s', not a plain file.", path)
+            else:
+                W("Skipping source file '%s', not found.", path)
+            if len(files) > oldFilesLen:
+                D("  ... found %s", files[oldFilesLen:])
+        return files
+
+
+    def _getBundles(self, sourceFiles):
+        flat = self.opts.flat
+        idir = self.opts.idir
+        iext = self.opts.iext
+        nobackup = self.opts.nobackup
+        odir = self.opts.odir
+        oext = self.opts.oext
+        idirSlash = idir + os.sep
+        bundles = []
+        for src in sourceFiles:
+            # 'base' is the subdirectory plus basename.
+            base = src
+            if idir and src.startswith(idirSlash):
+                base = src[len(idirSlash):]
+            if iext and base.endswith(iext):
+                base = base[:-len(iext)]
+            basename = os.path.basename(base)
+            if flat:
+                dst = os.path.join(odir, basename + oext)
+            else:
+                dbn = basename
+                if odir and base.startswith(os.sep):
+                    odd = odir
+                    while odd != '':
+                        idx = base.find(odd)
+                        if idx == 0:
+                            dbn = base[len(odd):]
+                            if dbn[0] == '/':
+                                dbn = dbn[1:]
+                            break
+                        odd = os.path.dirname(odd)
+                        if odd == '/':
+                            break
+                    dst = os.path.join(odir, dbn + oext)
+                else:
+                    dst = os.path.join(odir, base + oext)
+            bak = dst + self.BACKUP_SUFFIX
+            b = Bundle(src=src, dst=dst, bak=bak, base=base, basename=basename)
+            bundles.append(b)
+        return bundles
+
+
+    def _getTemplateClass(self):
+        C, D, W = self.chatter, self.debug, self.warn
+        modname = None
+        if self._templateClass:
+            return self._templateClass
+
+        modname = self.opts.templateClassName
+
+        if not modname:
+            return Template
+        p = modname.rfind('.')
+        if ':' not in modname:
+            self.error('The value of option --templateAPIClass is invalid\n'
+                       'It must be in the form "module:class", '
+                       'e.g. "Cheetah.Template:Template"')
+            
+        modname, classname = modname.split(':')
+
+        C('using --templateAPIClass=%s:%s'%(modname, classname))
+        
+        if p >= 0:
+            mod = getattr(__import__(modname[:p], {}, {}, [modname[p+1:]]), modname[p+1:])
+        else:
+            mod = __import__(modname, {}, {}, [])
+
+        klass = getattr(mod, classname, None)
+        if klass:
+            self._templateClass = klass
+            return klass
+        else:
+            self.error('**Template class specified in option --templateAPIClass not found\n'
+                       '**Falling back on Cheetah.Template:Template')
+
+
+    def _getCompilerSettings(self):
+        if self._compilerSettings:
+            return self._compilerSettings
+
+        def getkws(**kws):
+            return kws
+        if self.opts.compilerSettingsString:
+            try:
+                exec('settings = getkws(%s)'%self.opts.compilerSettingsString)
+            except:                
+                self.error("There's an error in your --settings option."
+                          "It must be valid Python syntax.\n"
+                          +"    --settings='%s'\n"%self.opts.compilerSettingsString
+                          +"  %s: %s"%sys.exc_info()[:2] 
+                          )
+
+            validKeys = DEFAULT_COMPILER_SETTINGS.keys()
+            if [k for k in settings.keys() if k not in validKeys]:
+                self.error(
+                    'The --setting "%s" is not a valid compiler setting name.'%k)
+            
+            self._compilerSettings = settings
+            return settings
+        else:
+            return {}
+
+    def _compileOrFillStdin(self):
+        TemplateClass = self._getTemplateClass()
+        compilerSettings = self._getCompilerSettings()
+        if self.isCompile:
+            pysrc = TemplateClass.compile(file=sys.stdin,
+                                          compilerSettings=compilerSettings,
+                                          returnAClass=False)
+            output = pysrc
+        else:
+            output = str(TemplateClass(file=sys.stdin, compilerSettings=compilerSettings))
+        sys.stdout.write(output)
+
+    def _compileOrFillBundle(self, b):
+        C, D, W = self.chatter, self.debug, self.warn
+        TemplateClass = self._getTemplateClass()
+        compilerSettings = self._getCompilerSettings()
+        src = b.src
+        dst = b.dst
+        base = b.base
+        basename = b.basename
+        dstDir = os.path.dirname(dst)
+        what = self.isCompile and "Compiling" or "Filling"
+        C("%s %s -> %s^", what, src, dst) # No trailing newline.
+        if os.path.exists(dst) and not self.opts.nobackup:
+            bak = b.bak
+            C(" (backup %s)", bak) # On same line as previous message.
+        else:
+            bak = None
+            C("")
+        if self.isCompile:
+            if not moduleNameRE.match(basename):
+                tup = basename, src
+                raise Error("""\
+%s: base name %s contains invalid characters.  It must
+be named according to the same rules as Python modules.""" % tup)
+            pysrc = TemplateClass.compile(file=src, returnAClass=False,
+                                          moduleName=basename,
+                                          className=basename,
+                                          commandlineopts=self.opts,
+                                          compilerSettings=compilerSettings)
+            output = pysrc
+        else:
+            #output = str(TemplateClass(file=src, searchList=self.searchList))
+            tclass = TemplateClass.compile(file=src, compilerSettings=compilerSettings)
+            output = str(tclass(searchList=self.searchList))
+            
+        if bak:
+            shutil.copyfile(dst, bak)
+        if dstDir and not os.path.exists(dstDir):
+            if self.isCompile:
+                mkdirsWithPyInitFiles(dstDir)
+            else:
+                os.makedirs(dstDir)
+        if self.opts.stdout:
+            sys.stdout.write(output)
+        else:
+            f = open(dst, 'w')
+            f.write(output)
+            f.close()
+            
+
+# Called when invoked as `cheetah`
+def _cheetah():
+    CheetahWrapper().main()
+
+# Called when invoked as `cheetah-compile`
+def _cheetah_compile():
+    sys.argv.insert(1, "compile")
+    CheetahWrapper().main()
+
+
+##################################################
+## if run from the command line
+if __name__ == '__main__':  CheetahWrapper().main()
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Compiler.py b/cheetah/Compiler.py
new file mode 100644 (file)
index 0000000..ee55868
--- /dev/null
@@ -0,0 +1,2002 @@
+'''
+    Compiler classes for Cheetah:
+    ModuleCompiler aka 'Compiler'
+    ClassCompiler
+    MethodCompiler
+
+    If you are trying to grok this code start with ModuleCompiler.__init__,
+    ModuleCompiler.compile, and ModuleCompiler.__getattr__.
+'''
+
+import sys
+import os
+import os.path
+from os.path import getmtime, exists
+import re
+import types
+import time
+import random
+import warnings
+import copy
+
+from Cheetah.Version import Version, VersionTuple
+from Cheetah.SettingsManager import SettingsManager
+from Cheetah.Utils.Indenter import indentize # an undocumented preprocessor
+from Cheetah import ErrorCatchers
+from Cheetah import NameMapper
+from Cheetah.Parser import Parser, ParseError, specialVarRE, \
+     STATIC_CACHE, REFRESH_CACHE, SET_LOCAL, SET_GLOBAL, SET_MODULE, \
+     unicodeDirectiveRE, encodingDirectiveRE, escapedNewlineRE
+
+from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList
+VFFSL=valueFromFrameOrSearchList
+VFSL=valueFromSearchList
+VFN=valueForName
+currentTime=time.time
+
+class Error(Exception): pass
+
+# Settings format: (key, default, docstring)
+_DEFAULT_COMPILER_SETTINGS = [
+    ('useNameMapper', True, 'Enable NameMapper for dotted notation and searchList support'),
+    ('useSearchList', True, 'Enable the searchList, requires useNameMapper=True, if disabled, first portion of the $variable is a global, builtin, or local variable that doesn\'t need looking up in the searchList'),
+    ('allowSearchListAsMethArg', True, ''),
+    ('useAutocalling', True, 'Detect and call callable objects in searchList, requires useNameMapper=True'),
+    ('useStackFrames', True, 'Used for NameMapper.valueFromFrameOrSearchList rather than NameMapper.valueFromSearchList'),
+    ('useErrorCatcher', False, 'Turn on the #errorCatcher directive for catching NameMapper errors, etc'),
+    ('alwaysFilterNone', True, 'Filter out None prior to calling the #filter'),
+    ('useFilters', True, 'If False, pass output through str()'),
+    ('includeRawExprInFilterArgs', True, ''),
+    ('useLegacyImportMode', True, 'All #import statements are relocated to the top of the generated Python module'),
+    ('prioritizeSearchListOverSelf', False, 'When iterating the searchList, look into the searchList passed into the initializer instead of Template members first'),
+
+    ('autoAssignDummyTransactionToSelf', False, ''),
+    ('useKWsDictArgForPassingTrans', True, ''),
+
+    ('commentOffset', 1, ''),
+    ('outputRowColComments', True, ''),
+    ('includeBlockMarkers', False, 'Wrap #block\'s in a comment in the template\'s output'),
+    ('blockMarkerStart', ('\n<!-- START BLOCK: ', ' -->\n'), ''),
+    ('blockMarkerEnd', ('\n<!-- END BLOCK: ', ' -->\n'), ''),
+    ('defDocStrMsg', 'Autogenerated by Cheetah: The Python-Powered Template Engine', ''),
+    ('setup__str__method', False, ''),
+    ('mainMethodName', 'respond', ''),
+    ('mainMethodNameForSubclasses', 'writeBody', ''),
+    ('indentationStep', ' ' * 4, ''),
+    ('initialMethIndentLevel', 2, ''),
+    ('monitorSrcFile', False, ''),
+    ('outputMethodsBeforeAttributes', True, ''),
+    ('addTimestampsToCompilerOutput', True, ''),
+
+    ## Customizing the #extends directive
+    ('autoImportForExtendsDirective', True, ''),
+    ('handlerForExtendsDirective', None, ''),
+
+    ('disabledDirectives', [], 'List of directive keys to disable (without starting "#")'),
+    ('enabledDirectives', [], 'List of directive keys to enable (without starting "#")'),
+    ('disabledDirectiveHooks', [], 'callable(parser, directiveKey)'),
+    ('preparseDirectiveHooks', [], 'callable(parser, directiveKey)'),
+    ('postparseDirectiveHooks', [], 'callable(parser, directiveKey)'),
+    ('preparsePlaceholderHooks', [], 'callable(parser)'),
+    ('postparsePlaceholderHooks', [], 'callable(parser)'),
+    ('expressionFilterHooks', [], '''callable(parser, expr, exprType, rawExpr=None, startPos=None), exprType is the name of the directive, "psp" or "placeholder" The filters *must* return the expr or raise an expression, they can modify the expr if needed'''),
+    ('templateMetaclass', None, 'Strictly optional, only will work with new-style basecalsses as well'),
+    ('i18NFunctionName', 'self.i18n', ''),
+
+    ('cheetahVarStartToken', '$', ''),
+    ('commentStartToken', '##', ''),
+    ('multiLineCommentStartToken', '#*', ''),
+    ('multiLineCommentEndToken', '*#', ''),
+    ('gobbleWhitespaceAroundMultiLineComments', True, ''),
+    ('directiveStartToken', '#', ''),
+    ('directiveEndToken', '#', ''),
+    ('allowWhitespaceAfterDirectiveStartToken', False, ''),
+    ('PSPStartToken', '<%', ''),
+    ('PSPEndToken', '%>', ''),
+    ('EOLSlurpToken', '#', ''),
+    ('gettextTokens', ["_", "N_", "ngettext"], ''),
+    ('allowExpressionsInExtendsDirective', False, ''),
+    ('allowEmptySingleLineMethods', False, ''),
+    ('allowNestedDefScopes', True, ''),
+    ('allowPlaceholderFilterArgs', True, ''),
+]
+
+DEFAULT_COMPILER_SETTINGS = dict([(v[0], v[1]) for v in _DEFAULT_COMPILER_SETTINGS])
+
+
+
+class GenUtils(object):
+    """An abstract baseclass for the Compiler classes that provides methods that
+    perform generic utility functions or generate pieces of output code from
+    information passed in by the Parser baseclass.  These methods don't do any
+    parsing themselves.
+    """
+
+    def genTimeInterval(self, timeString):
+        ##@@ TR: need to add some error handling here
+        if timeString[-1] == 's':
+            interval = float(timeString[:-1])
+        elif timeString[-1] == 'm':
+            interval = float(timeString[:-1])*60
+        elif timeString[-1] == 'h':
+            interval = float(timeString[:-1])*60*60
+        elif timeString[-1] == 'd':
+            interval = float(timeString[:-1])*60*60*24
+        elif timeString[-1] == 'w':
+            interval = float(timeString[:-1])*60*60*24*7
+        else:                       # default to minutes
+            interval = float(timeString)*60
+        return interval
+
+    def genCacheInfo(self, cacheTokenParts):
+        """Decipher a placeholder cachetoken
+        """
+        cacheInfo = {}
+        if cacheTokenParts['REFRESH_CACHE']:
+            cacheInfo['type'] = REFRESH_CACHE
+            cacheInfo['interval'] = self.genTimeInterval(cacheTokenParts['interval'])
+        elif cacheTokenParts['STATIC_CACHE']:
+            cacheInfo['type'] = STATIC_CACHE
+        return cacheInfo                # is empty if no cache
+
+    def genCacheInfoFromArgList(self, argList):
+        cacheInfo = {'type':REFRESH_CACHE}
+        for key, val in argList:
+            if val[0] in '"\'':
+                val = val[1:-1]
+
+            if key == 'timer':
+                key = 'interval'
+                val = self.genTimeInterval(val)
+                
+            cacheInfo[key] = val
+        return cacheInfo
+        
+    def genCheetahVar(self, nameChunks, plain=False):
+        if nameChunks[0][0] in self.setting('gettextTokens'):
+            self.addGetTextVar(nameChunks) 
+        if self.setting('useNameMapper') and not plain:
+            return self.genNameMapperVar(nameChunks)
+        else:
+            return self.genPlainVar(nameChunks)
+
+    def addGetTextVar(self, nameChunks):
+        """Output something that gettext can recognize.
+        
+        This is a harmless side effect necessary to make gettext work when it
+        is scanning compiled templates for strings marked for translation.
+
+        @@TR: another marginally more efficient approach would be to put the
+        output in a dummy method that is never called.
+        """
+        # @@TR: this should be in the compiler not here
+        self.addChunk("if False:")
+        self.indent()
+        self.addChunk(self.genPlainVar(nameChunks[:]))
+        self.dedent()
+
+    def genPlainVar(self, nameChunks):        
+        """Generate Python code for a Cheetah $var without using NameMapper
+        (Unified Dotted Notation with the SearchList).
+        """
+        nameChunks.reverse()
+        chunk = nameChunks.pop()
+        pythonCode = chunk[0] + chunk[2]
+        while nameChunks:
+            chunk = nameChunks.pop()
+            pythonCode = (pythonCode + '.' + chunk[0] + chunk[2])
+        return pythonCode
+
+    def genNameMapperVar(self, nameChunks):
+        """Generate valid Python code for a Cheetah $var, using NameMapper
+        (Unified Dotted Notation with the SearchList).
+
+        nameChunks = list of var subcomponents represented as tuples
+          [ (name,useAC,remainderOfExpr),
+          ]
+        where:
+          name = the dotted name base
+          useAC = where NameMapper should use autocalling on namemapperPart
+          remainderOfExpr = any arglist, index, or slice
+
+        If remainderOfExpr contains a call arglist (e.g. '(1234)') then useAC
+        is False, otherwise it defaults to True. It is overridden by the global
+        setting 'useAutocalling' if this setting is False.
+
+        EXAMPLE
+        ------------------------------------------------------------------------
+        if the raw Cheetah Var is
+          $a.b.c[1].d().x.y.z
+          
+        nameChunks is the list
+          [ ('a.b.c',True,'[1]'), # A
+            ('d',False,'()'),     # B
+            ('x.y.z',True,''),    # C
+          ]
+        
+        When this method is fed the list above it returns
+          VFN(VFN(VFFSL(SL, 'a.b.c',True)[1], 'd',False)(), 'x.y.z',True)
+        which can be represented as
+          VFN(B`, name=C[0], executeCallables=(useAC and C[1]))C[2]
+        where:
+          VFN = NameMapper.valueForName
+          VFFSL = NameMapper.valueFromFrameOrSearchList
+          VFSL = NameMapper.valueFromSearchList # optionally used instead of VFFSL
+          SL = self.searchList()
+          useAC = self.setting('useAutocalling') # True in this example
+          
+          A = ('a.b.c',True,'[1]')
+          B = ('d',False,'()')
+          C = ('x.y.z',True,'')
+
+          C` = VFN( VFN( VFFSL(SL, 'a.b.c',True)[1],
+                         'd',False)(),
+                    'x.y.z',True)
+             = VFN(B`, name='x.y.z', executeCallables=True)
+             
+          B` = VFN(A`, name=B[0], executeCallables=(useAC and B[1]))B[2]
+          A` = VFFSL(SL, name=A[0], executeCallables=(useAC and A[1]))A[2]
+
+
+        Note, if the compiler setting useStackFrames=False (default is true)
+        then
+          A` = VFSL([locals()]+SL+[globals(), __builtin__], name=A[0], executeCallables=(useAC and A[1]))A[2]
+        This option allows Cheetah to be used with Psyco, which doesn't support
+        stack frame introspection.
+        """
+        defaultUseAC = self.setting('useAutocalling')
+        useSearchList = self.setting('useSearchList')
+
+        nameChunks.reverse()
+        name, useAC, remainder = nameChunks.pop()
+        
+        if not useSearchList:
+            firstDotIdx = name.find('.')
+            if firstDotIdx != -1 and firstDotIdx < len(name):
+                beforeFirstDot, afterDot = name[:firstDotIdx], name[firstDotIdx+1:]
+                pythonCode = ('VFN(' + beforeFirstDot +
+                              ',"' + afterDot +
+                              '",' + repr(defaultUseAC and useAC) + ')'
+                              + remainder)
+            else:
+                pythonCode = name+remainder
+        elif self.setting('useStackFrames'):
+            pythonCode = ('VFFSL(SL,'
+                          '"'+ name + '",'
+                          + repr(defaultUseAC and useAC) + ')'
+                          + remainder)
+        else:
+            pythonCode = ('VFSL([locals()]+SL+[globals(), builtin],'
+                          '"'+ name + '",'
+                          + repr(defaultUseAC and useAC) + ')'
+                          + remainder)
+        ##    
+        while nameChunks:
+            name, useAC, remainder = nameChunks.pop()
+            pythonCode = ('VFN(' + pythonCode +
+                          ',"' + name +
+                          '",' + repr(defaultUseAC and useAC) + ')'
+                          + remainder)
+        return pythonCode
+    
+##################################################
+## METHOD COMPILERS
+
+class MethodCompiler(GenUtils):
+    def __init__(self, methodName, classCompiler,
+                 initialMethodComment=None,
+                 decorators=None):
+        self._settingsManager = classCompiler
+        self._classCompiler = classCompiler
+        self._moduleCompiler = classCompiler._moduleCompiler
+        self._methodName = methodName
+        self._initialMethodComment = initialMethodComment
+        self._setupState()
+        self._decorators = decorators or []
+
+    def setting(self, key):
+        return self._settingsManager.setting(key)
+
+    def _setupState(self):
+        self._indent = self.setting('indentationStep')
+        self._indentLev = self.setting('initialMethIndentLevel')
+        self._pendingStrConstChunks = []
+        self._methodSignature = None
+        self._methodDef = None
+        self._docStringLines = []
+        self._methodBodyChunks = []
+
+        self._cacheRegionsStack = []
+        self._callRegionsStack = []
+        self._captureRegionsStack = []
+        self._filterRegionsStack = []
+
+        self._isErrorCatcherOn = False
+
+        self._hasReturnStatement = False
+        self._isGenerator = False
+        
+        
+    def cleanupState(self):
+        """Called by the containing class compiler instance
+        """
+        pass
+
+    def methodName(self):
+        return self._methodName
+
+    def setMethodName(self, name):
+        self._methodName = name
+        
+    ## methods for managing indentation
+    
+    def indentation(self):
+        return self._indent * self._indentLev
+    
+    def indent(self):
+        self._indentLev +=1
+        
+    def dedent(self):
+        if self._indentLev:
+            self._indentLev -=1
+        else:
+            raise Error('Attempt to dedent when the indentLev is 0')
+
+    ## methods for final code wrapping
+
+    def methodDef(self):
+        if self._methodDef:
+            return self._methodDef
+        else:
+            return self.wrapCode()
+
+    __str__ = methodDef
+    __unicode__ = methodDef
+    
+    def wrapCode(self):
+        self.commitStrConst()
+        methodDefChunks = (
+            self.methodSignature(),
+            '\n',
+            self.docString(),
+            self.methodBody() )
+        methodDef = ''.join(methodDefChunks)
+        self._methodDef = methodDef
+        return methodDef
+
+    def methodSignature(self):
+        return self._indent + self._methodSignature + ':'
+
+    def setMethodSignature(self, signature):
+        self._methodSignature = signature
+
+    def methodBody(self):
+        return ''.join( self._methodBodyChunks )
+
+    def docString(self):
+        if not self._docStringLines:
+            return ''
+        
+        ind = self._indent*2        
+        docStr = (ind + '"""\n' + ind +
+                  ('\n' + ind).join([ln.replace('"""', "'''") for ln in self._docStringLines]) +
+                  '\n' + ind + '"""\n')
+        return  docStr
+
+    ## methods for adding code
+    def addMethDocString(self, line):
+        self._docStringLines.append(line.replace('%', '%%'))
+       
+    def addChunk(self, chunk):
+        self.commitStrConst()
+        chunk = "\n" + self.indentation() + chunk
+        self._methodBodyChunks.append(chunk)
+
+    def appendToPrevChunk(self, appendage):
+        self._methodBodyChunks[-1] = self._methodBodyChunks[-1] + appendage
+
+    def addWriteChunk(self, chunk):
+        self.addChunk('write(' + chunk + ')')
+
+    def addFilteredChunk(self, chunk, filterArgs=None, rawExpr=None, lineCol=None):
+        if filterArgs is None:
+            filterArgs = ''
+        if self.setting('includeRawExprInFilterArgs') and rawExpr:
+            filterArgs += ', rawExpr=%s'%repr(rawExpr)
+
+        if self.setting('alwaysFilterNone'):
+            if rawExpr and rawExpr.find('\n')==-1 and rawExpr.find('\r')==-1:
+                self.addChunk("_v = %s # %r"%(chunk, rawExpr))
+                if lineCol:
+                    self.appendToPrevChunk(' on line %s, col %s'%lineCol)
+            else:
+                self.addChunk("_v = %s"%chunk)
+                
+            if self.setting('useFilters'):
+                self.addChunk("if _v is not None: write(_filter(_v%s))"%filterArgs)
+            else:
+                self.addChunk("if _v is not None: write(str(_v))")
+        else:
+            if self.setting('useFilters'):
+                self.addChunk("write(_filter(%s%s))"%(chunk, filterArgs))
+            else:
+                self.addChunk("write(str(%s))"%chunk)
+
+    def _appendToPrevStrConst(self, strConst):
+        if self._pendingStrConstChunks:
+            self._pendingStrConstChunks.append(strConst)
+        else:
+            self._pendingStrConstChunks = [strConst]
+
+    def commitStrConst(self):
+        """Add the code for outputting the pending strConst without chopping off
+        any whitespace from it.
+        """
+        if not self._pendingStrConstChunks:
+            return
+
+        strConst = ''.join(self._pendingStrConstChunks)
+        self._pendingStrConstChunks = []
+        if not strConst:
+            return
+
+        reprstr = repr(strConst)
+        i = 0
+        out = []
+        if reprstr.startswith('u'):
+            i = 1
+            out = ['u']
+        body = escapedNewlineRE.sub('\\1\n', reprstr[i+1:-1])
+        
+        if reprstr[i]=="'":
+            out.append("'''")
+            out.append(body)
+            out.append("'''")
+        else:
+            out.append('"""')
+            out.append(body)
+            out.append('"""')
+        self.addWriteChunk(''.join(out))
+
+    def handleWSBeforeDirective(self):
+        """Truncate the pending strCont to the beginning of the current line.
+        """
+        if self._pendingStrConstChunks:
+            src = self._pendingStrConstChunks[-1]
+            BOL = max(src.rfind('\n')+1, src.rfind('\r')+1, 0)
+            if BOL < len(src):
+                self._pendingStrConstChunks[-1] = src[:BOL]
+
+
+
+    def isErrorCatcherOn(self):
+        return self._isErrorCatcherOn
+    
+    def turnErrorCatcherOn(self):
+        self._isErrorCatcherOn = True
+
+    def turnErrorCatcherOff(self):
+        self._isErrorCatcherOn = False
+            
+    # @@TR: consider merging the next two methods into one
+    def addStrConst(self, strConst):
+        self._appendToPrevStrConst(strConst)
+
+    def addRawText(self, text):
+        self.addStrConst(text)
+        
+    def addMethComment(self, comm):
+        offSet = self.setting('commentOffset')
+        self.addChunk('#' + ' '*offSet + comm)
+
+    def addPlaceholder(self, expr, filterArgs, rawPlaceholder,
+                       cacheTokenParts, lineCol,
+                       silentMode=False):
+        cacheInfo = self.genCacheInfo(cacheTokenParts)
+        if cacheInfo:
+            cacheInfo['ID'] = repr(rawPlaceholder)[1:-1]
+            self.startCacheRegion(cacheInfo, lineCol, rawPlaceholder=rawPlaceholder)
+
+        if self.isErrorCatcherOn():
+            methodName = self._classCompiler.addErrorCatcherCall(
+                expr, rawCode=rawPlaceholder, lineCol=lineCol)
+            expr = 'self.' + methodName + '(localsDict=locals())' 
+
+        if silentMode:
+            self.addChunk('try:')
+            self.indent()            
+            self.addFilteredChunk(expr, filterArgs, rawPlaceholder, lineCol=lineCol)
+            self.dedent()
+            self.addChunk('except NotFound: pass')            
+        else:
+            self.addFilteredChunk(expr, filterArgs, rawPlaceholder, lineCol=lineCol)
+
+        if self.setting('outputRowColComments'):
+            self.appendToPrevChunk(' # from line %s, col %s' % lineCol + '.')
+        if cacheInfo:
+            self.endCacheRegion()
+
+    def addSilent(self, expr):
+        self.addChunk( expr )
+
+    def addEcho(self, expr, rawExpr=None):
+        self.addFilteredChunk(expr, rawExpr=rawExpr)
+        
+    def addSet(self, expr, exprComponents, setStyle):
+        if setStyle is SET_GLOBAL:
+            (LVALUE, OP, RVALUE) = (exprComponents.LVALUE,
+                                    exprComponents.OP,
+                                    exprComponents.RVALUE)
+            # we need to split the LVALUE to deal with globalSetVars
+            splitPos1 = LVALUE.find('.')
+            splitPos2 = LVALUE.find('[')
+            if splitPos1 > 0 and splitPos2==-1:
+                splitPos = splitPos1
+            elif splitPos1 > 0 and splitPos1 < max(splitPos2, 0):
+                splitPos = splitPos1
+            else:
+                splitPos = splitPos2
+
+            if splitPos >0:
+                primary = LVALUE[:splitPos]
+                secondary = LVALUE[splitPos:]
+            else:
+                primary = LVALUE
+                secondary = ''            
+            LVALUE = 'self._CHEETAH__globalSetVars["' + primary + '"]' + secondary
+            expr = LVALUE + ' ' + OP + ' ' + RVALUE.strip()
+
+        if setStyle is SET_MODULE:
+            self._moduleCompiler.addModuleGlobal(expr)
+        else:
+            self.addChunk(expr)
+
+    def addInclude(self, sourceExpr, includeFrom, isRaw):
+        self.addChunk('self._handleCheetahInclude(' + sourceExpr +
+                           ', trans=trans, ' +
+                           'includeFrom="' + includeFrom + '", raw=' +
+                           repr(isRaw) + ')')
+
+    def addWhile(self, expr, lineCol=None):
+        self.addIndentingDirective(expr, lineCol=lineCol)
+        
+    def addFor(self, expr, lineCol=None):
+        self.addIndentingDirective(expr, lineCol=lineCol)
+
+    def addRepeat(self, expr, lineCol=None):
+        #the _repeatCount stuff here allows nesting of #repeat directives        
+        self._repeatCount = getattr(self, "_repeatCount", -1) + 1
+        self.addFor('for __i%s in range(%s)' % (self._repeatCount, expr), lineCol=lineCol)
+
+    def addIndentingDirective(self, expr, lineCol=None):
+        if expr and not expr[-1] == ':':
+            expr = expr  + ':'
+        self.addChunk( expr )
+        if lineCol:
+            self.appendToPrevChunk(' # generated from line %s, col %s'%lineCol )
+        self.indent()
+
+    def addReIndentingDirective(self, expr, dedent=True, lineCol=None):
+        self.commitStrConst()
+        if dedent:
+            self.dedent()
+        if not expr[-1] == ':':
+            expr = expr  + ':'
+            
+        self.addChunk( expr )
+        if lineCol:
+            self.appendToPrevChunk(' # generated from line %s, col %s'%lineCol )
+        self.indent()
+
+    def addIf(self, expr, lineCol=None):
+        """For a full #if ... #end if directive
+        """
+        self.addIndentingDirective(expr, lineCol=lineCol)
+
+    def addOneLineIf(self, expr, lineCol=None):
+        """For a full #if ... #end if directive
+        """
+        self.addIndentingDirective(expr, lineCol=lineCol)
+
+    def addTernaryExpr(self, conditionExpr, trueExpr, falseExpr, lineCol=None):
+        """For a single-lie #if ... then .... else ... directive
+        <condition> then <trueExpr> else <falseExpr>
+        """
+        self.addIndentingDirective(conditionExpr, lineCol=lineCol)            
+        self.addFilteredChunk(trueExpr)
+        self.dedent()
+        self.addIndentingDirective('else')            
+        self.addFilteredChunk(falseExpr)
+        self.dedent()
+
+    def addElse(self, expr, dedent=True, lineCol=None):
+        expr = re.sub(r'else[ \f\t]+if', 'elif', expr)
+        self.addReIndentingDirective(expr, dedent=dedent, lineCol=lineCol)
+
+    def addElif(self, expr, dedent=True, lineCol=None):
+        self.addElse(expr, dedent=dedent, lineCol=lineCol)
+        
+    def addUnless(self, expr, lineCol=None):
+        self.addIf('if not (' + expr + ')')
+
+    def addClosure(self, functionName, argsList, parserComment):
+        argStringChunks = []
+        for arg in argsList:
+            chunk = arg[0]
+            if not arg[1] == None:
+                chunk += '=' + arg[1]
+            argStringChunks.append(chunk)
+        signature = "def " + functionName + "(" + ','.join(argStringChunks) + "):"
+        self.addIndentingDirective(signature)
+        self.addChunk('#'+parserComment)
+
+    def addTry(self, expr, lineCol=None):
+        self.addIndentingDirective(expr, lineCol=lineCol)
+        
+    def addExcept(self, expr, dedent=True, lineCol=None):
+        self.addReIndentingDirective(expr, dedent=dedent, lineCol=lineCol)
+        
+    def addFinally(self, expr, dedent=True, lineCol=None):
+        self.addReIndentingDirective(expr, dedent=dedent, lineCol=lineCol)
+            
+    def addReturn(self, expr):
+        assert not self._isGenerator
+        self.addChunk(expr)
+        self._hasReturnStatement = True
+
+    def addYield(self, expr):
+        assert not self._hasReturnStatement
+        self._isGenerator = True
+        if expr.replace('yield', '').strip():
+            self.addChunk(expr)
+        else:
+            self.addChunk('if _dummyTrans:')
+            self.indent()
+            self.addChunk('yield trans.response().getvalue()')
+            self.addChunk('trans = DummyTransaction()')
+            self.addChunk('write = trans.response().write')
+            self.dedent()
+            self.addChunk('else:')
+            self.indent()
+            self.addChunk(
+                'raise TypeError("This method cannot be called with a trans arg")')
+            self.dedent()
+            
+
+    def addPass(self, expr):
+        self.addChunk(expr)
+
+    def addDel(self, expr):
+        self.addChunk(expr)
+
+    def addAssert(self, expr):
+        self.addChunk(expr)
+
+    def addRaise(self, expr):
+        self.addChunk(expr)
+
+    def addBreak(self, expr):
+        self.addChunk(expr)
+
+    def addContinue(self, expr):
+        self.addChunk(expr)
+
+    def addPSP(self, PSP):
+        self.commitStrConst()
+        autoIndent = False
+        if PSP[0] == '=':
+            PSP = PSP[1:]
+            if PSP:
+                self.addWriteChunk('_filter(' + PSP + ')')
+            return
+                    
+        elif PSP.lower() == 'end':
+            self.dedent()
+            return
+        elif PSP[-1] == '$':
+            autoIndent = True
+            PSP = PSP[:-1]
+        elif PSP[-1] == ':':
+            autoIndent = True
+            
+        for line in PSP.splitlines():
+            self.addChunk(line)
+            
+        if autoIndent:
+            self.indent()
+    
+    def nextCacheID(self):
+        return ('_'+str(random.randrange(100, 999)) 
+                + str(random.randrange(10000, 99999)))
+        
+    def startCacheRegion(self, cacheInfo, lineCol, rawPlaceholder=None):
+
+        # @@TR: we should add some runtime logging to this
+        
+        ID = self.nextCacheID()
+        interval = cacheInfo.get('interval', None)
+        test = cacheInfo.get('test', None)
+        customID = cacheInfo.get('id', None)
+        if customID:
+            ID = customID
+        varyBy = cacheInfo.get('varyBy', repr(ID))
+        self._cacheRegionsStack.append(ID) # attrib of current methodCompiler
+
+        # @@TR: add this to a special class var as well
+        self.addChunk('')
+
+        self.addChunk('## START CACHE REGION: ID='+ID+
+                      '. line %s, col %s'%lineCol + ' in the source.')
+        
+        self.addChunk('_RECACHE_%(ID)s = False'%locals())
+        self.addChunk('_cacheRegion_%(ID)s = self.getCacheRegion(regionID='%locals()
+                      + repr(ID)
+                      + ', cacheInfo=%r'%cacheInfo
+                      + ')')
+        self.addChunk('if _cacheRegion_%(ID)s.isNew():'%locals())
+        self.indent()
+        self.addChunk('_RECACHE_%(ID)s = True'%locals())
+        self.dedent()
+        
+        self.addChunk('_cacheItem_%(ID)s = _cacheRegion_%(ID)s.getCacheItem('%locals()
+                      +varyBy+')')
+
+        self.addChunk('if _cacheItem_%(ID)s.hasExpired():'%locals())
+        self.indent()
+        self.addChunk('_RECACHE_%(ID)s = True'%locals())
+        self.dedent()
+            
+        if test:
+            self.addChunk('if ' + test + ':')
+            self.indent()
+            self.addChunk('_RECACHE_%(ID)s = True'%locals())
+            self.dedent()
+
+        self.addChunk('if (not _RECACHE_%(ID)s) and _cacheItem_%(ID)s.getRefreshTime():'%locals())
+        self.indent()
+        #self.addChunk('print "DEBUG"+"-"*50')
+        self.addChunk('try:')
+        self.indent()
+        self.addChunk('_output = _cacheItem_%(ID)s.renderOutput()'%locals())        
+        self.dedent()                
+        self.addChunk('except KeyError:')
+        self.indent()
+        self.addChunk('_RECACHE_%(ID)s = True'%locals())
+        #self.addChunk('print "DEBUG"+"*"*50')
+        self.dedent()                
+        self.addChunk('else:')
+        self.indent()
+        self.addWriteChunk('_output')
+        self.addChunk('del _output')
+        self.dedent()                
+
+        self.dedent()                
+
+        self.addChunk('if _RECACHE_%(ID)s or not _cacheItem_%(ID)s.getRefreshTime():'%locals())
+        self.indent()
+        self.addChunk('_orig_trans%(ID)s = trans'%locals())
+        self.addChunk('trans = _cacheCollector_%(ID)s = DummyTransaction()'%locals())
+        self.addChunk('write = _cacheCollector_%(ID)s.response().write'%locals())
+        if interval:
+            self.addChunk(("_cacheItem_%(ID)s.setExpiryTime(currentTime() +"%locals())
+                          + str(interval) + ")")
+        
+    def endCacheRegion(self):
+        ID = self._cacheRegionsStack.pop()
+        self.addChunk('trans = _orig_trans%(ID)s'%locals())
+        self.addChunk('write = trans.response().write')
+        self.addChunk('_cacheData = _cacheCollector_%(ID)s.response().getvalue()'%locals())
+        self.addChunk('_cacheItem_%(ID)s.setData(_cacheData)'%locals())
+        self.addWriteChunk('_cacheData')
+        self.addChunk('del _cacheData')        
+        self.addChunk('del _cacheCollector_%(ID)s'%locals())
+        self.addChunk('del _orig_trans%(ID)s'%locals())
+        self.dedent()
+        self.addChunk('## END CACHE REGION: '+ID)
+        self.addChunk('')
+
+    def nextCallRegionID(self):
+        return self.nextCacheID()
+
+    def startCallRegion(self, functionName, args, lineCol, regionTitle='CALL'):
+        class CallDetails(object):
+            pass
+        callDetails = CallDetails()
+        callDetails.ID = ID = self.nextCallRegionID()
+        callDetails.functionName = functionName
+        callDetails.args = args
+        callDetails.lineCol = lineCol
+        callDetails.usesKeywordArgs = False
+        self._callRegionsStack.append((ID, callDetails)) # attrib of current methodCompiler
+
+        self.addChunk('## START %(regionTitle)s REGION: '%locals()
+                      +ID
+                      +' of '+functionName
+                      +' at line %s, col %s'%lineCol + ' in the source.')
+        self.addChunk('_orig_trans%(ID)s = trans'%locals())
+        self.addChunk('_wasBuffering%(ID)s = self._CHEETAH__isBuffering'%locals())
+        self.addChunk('self._CHEETAH__isBuffering = True')
+        self.addChunk('trans = _callCollector%(ID)s = DummyTransaction()'%locals())
+        self.addChunk('write = _callCollector%(ID)s.response().write'%locals())
+
+    def setCallArg(self, argName, lineCol):
+        ID, callDetails = self._callRegionsStack[-1]
+        argName = str(argName)
+        if callDetails.usesKeywordArgs:
+            self._endCallArg()
+        else:
+            callDetails.usesKeywordArgs = True
+            self.addChunk('_callKws%(ID)s = {}'%locals())
+            self.addChunk('_currentCallArgname%(ID)s = %(argName)r'%locals())
+        callDetails.currentArgname = argName
+        
+    def _endCallArg(self):
+        ID, callDetails = self._callRegionsStack[-1]
+        currCallArg = callDetails.currentArgname
+        self.addChunk(('_callKws%(ID)s[%(currCallArg)r] ='
+                       ' _callCollector%(ID)s.response().getvalue()')%locals())
+        self.addChunk('del _callCollector%(ID)s'%locals())
+        self.addChunk('trans = _callCollector%(ID)s = DummyTransaction()'%locals())
+        self.addChunk('write = _callCollector%(ID)s.response().write'%locals())
+    
+    def endCallRegion(self, regionTitle='CALL'):
+        ID, callDetails = self._callRegionsStack[-1]
+        functionName, initialKwArgs, lineCol = (
+            callDetails.functionName, callDetails.args, callDetails.lineCol)
+
+        def reset(ID=ID):
+            self.addChunk('trans = _orig_trans%(ID)s'%locals())
+            self.addChunk('write = trans.response().write')
+            self.addChunk('self._CHEETAH__isBuffering = _wasBuffering%(ID)s '%locals())
+            self.addChunk('del _wasBuffering%(ID)s'%locals())
+            self.addChunk('del _orig_trans%(ID)s'%locals())
+
+        if not callDetails.usesKeywordArgs:
+            reset()
+            self.addChunk('_callArgVal%(ID)s = _callCollector%(ID)s.response().getvalue()'%locals())
+            self.addChunk('del _callCollector%(ID)s'%locals())
+            if initialKwArgs:
+                initialKwArgs = ', '+initialKwArgs           
+            self.addFilteredChunk('%(functionName)s(_callArgVal%(ID)s%(initialKwArgs)s)'%locals())
+            self.addChunk('del _callArgVal%(ID)s'%locals())
+        else:
+            if initialKwArgs:
+                initialKwArgs = initialKwArgs+', '
+            self._endCallArg()
+            reset()
+            self.addFilteredChunk('%(functionName)s(%(initialKwArgs)s**_callKws%(ID)s)'%locals())
+            self.addChunk('del _callKws%(ID)s'%locals())
+        self.addChunk('## END %(regionTitle)s REGION: '%locals()
+                      +ID
+                      +' of '+functionName
+                      +' at line %s, col %s'%lineCol + ' in the source.')        
+        self.addChunk('')
+        self._callRegionsStack.pop() # attrib of current methodCompiler
+
+    def nextCaptureRegionID(self):
+        return self.nextCacheID()
+
+    def startCaptureRegion(self, assignTo, lineCol):
+        class CaptureDetails: pass
+        captureDetails = CaptureDetails()
+        captureDetails.ID = ID = self.nextCaptureRegionID()
+        captureDetails.assignTo = assignTo
+        captureDetails.lineCol = lineCol
+        
+        self._captureRegionsStack.append((ID, captureDetails)) # attrib of current methodCompiler
+        self.addChunk('## START CAPTURE REGION: '+ID
+                      +' '+assignTo
+                      +' at line %s, col %s'%lineCol + ' in the source.')
+        self.addChunk('_orig_trans%(ID)s = trans'%locals())
+        self.addChunk('_wasBuffering%(ID)s = self._CHEETAH__isBuffering'%locals())
+        self.addChunk('self._CHEETAH__isBuffering = True')
+        self.addChunk('trans = _captureCollector%(ID)s = DummyTransaction()'%locals())
+        self.addChunk('write = _captureCollector%(ID)s.response().write'%locals())
+
+    def endCaptureRegion(self):
+        ID, captureDetails = self._captureRegionsStack.pop()
+        assignTo, lineCol = (captureDetails.assignTo, captureDetails.lineCol)
+        self.addChunk('trans = _orig_trans%(ID)s'%locals())
+        self.addChunk('write = trans.response().write')
+        self.addChunk('self._CHEETAH__isBuffering = _wasBuffering%(ID)s '%locals())
+        self.addChunk('%(assignTo)s = _captureCollector%(ID)s.response().getvalue()'%locals())
+        self.addChunk('del _orig_trans%(ID)s'%locals())
+        self.addChunk('del _captureCollector%(ID)s'%locals())
+        self.addChunk('del _wasBuffering%(ID)s'%locals())
+        
+    def setErrorCatcher(self, errorCatcherName):
+        self.turnErrorCatcherOn()        
+
+        self.addChunk('if self._CHEETAH__errorCatchers.has_key("' + errorCatcherName + '"):')
+        self.indent()
+        self.addChunk('self._CHEETAH__errorCatcher = self._CHEETAH__errorCatchers["' +
+            errorCatcherName + '"]')
+        self.dedent()
+        self.addChunk('else:')
+        self.indent()
+        self.addChunk('self._CHEETAH__errorCatcher = self._CHEETAH__errorCatchers["'
+                      + errorCatcherName + '"] = ErrorCatchers.'
+                      + errorCatcherName + '(self)'
+                      )
+        self.dedent()
+
+    def nextFilterRegionID(self):
+        return self.nextCacheID()
+
+    def setTransform(self, transformer, isKlass):
+        self.addChunk('trans = TransformerTransaction()')
+        self.addChunk('trans._response = trans.response()')
+        self.addChunk('trans._response._filter = %s' % transformer)
+        self.addChunk('write = trans._response.write')
+        
+    def setFilter(self, theFilter, isKlass):
+        class FilterDetails: 
+            pass
+        filterDetails = FilterDetails()
+        filterDetails.ID = ID = self.nextFilterRegionID()
+        filterDetails.theFilter = theFilter
+        filterDetails.isKlass = isKlass
+        self._filterRegionsStack.append((ID, filterDetails)) # attrib of current methodCompiler
+
+        self.addChunk('_orig_filter%(ID)s = _filter'%locals())
+        if isKlass:
+            self.addChunk('_filter = self._CHEETAH__currentFilter = ' + theFilter.strip() +
+                          '(self).filter')
+        else:
+            if theFilter.lower() == 'none':
+                self.addChunk('_filter = self._CHEETAH__initialFilter')
+            else:
+                # is string representing the name of a builtin filter
+                self.addChunk('filterName = ' + repr(theFilter))
+                self.addChunk('if self._CHEETAH__filters.has_key("' + theFilter + '"):')
+                self.indent()
+                self.addChunk('_filter = self._CHEETAH__currentFilter = self._CHEETAH__filters[filterName]')
+                self.dedent()
+                self.addChunk('else:')
+                self.indent()
+                self.addChunk('_filter = self._CHEETAH__currentFilter'
+                              +' = \\\n\t\t\tself._CHEETAH__filters[filterName] = '
+                              + 'getattr(self._CHEETAH__filtersLib, filterName)(self).filter')
+                self.dedent()
+                
+    def closeFilterBlock(self):
+        ID, filterDetails = self._filterRegionsStack.pop()
+        #self.addChunk('_filter = self._CHEETAH__initialFilter')
+        #self.addChunk('_filter = _orig_filter%(ID)s'%locals())
+        self.addChunk('_filter = self._CHEETAH__currentFilter = _orig_filter%(ID)s'%locals())
+
+class AutoMethodCompiler(MethodCompiler):
+
+    def _setupState(self):
+        MethodCompiler._setupState(self)
+        self._argStringList = [ ("self", None) ]
+        self._streamingEnabled = True
+        self._isClassMethod = None
+        self._isStaticMethod = None
+
+    def _useKWsDictArgForPassingTrans(self):
+        alreadyHasTransArg = [argname for argname, defval in self._argStringList
+                              if argname=='trans']
+        return (self.methodName()!='respond'
+                and not alreadyHasTransArg
+                and self.setting('useKWsDictArgForPassingTrans'))
+
+    def isClassMethod(self):
+        if self._isClassMethod is None:
+            self._isClassMethod = '@classmethod' in self._decorators
+        return self._isClassMethod
+
+    def isStaticMethod(self):
+        if self._isStaticMethod is None:
+            self._isStaticMethod = '@staticmethod' in self._decorators
+        return self._isStaticMethod
+    
+    def cleanupState(self):
+        MethodCompiler.cleanupState(self)
+        self.commitStrConst()
+        if self._cacheRegionsStack:
+            self.endCacheRegion()
+        if self._callRegionsStack:
+            self.endCallRegion()
+            
+        if self._streamingEnabled:
+            kwargsName = None
+            positionalArgsListName = None
+            for argname, defval in self._argStringList:
+                if argname.strip().startswith('**'):
+                    kwargsName = argname.strip().replace('**', '')
+                    break
+                elif argname.strip().startswith('*'):
+                    positionalArgsListName = argname.strip().replace('*', '')
+                    
+            if not kwargsName and self._useKWsDictArgForPassingTrans():
+                kwargsName = 'KWS'
+                self.addMethArg('**KWS', None)
+            self._kwargsName = kwargsName
+
+            if not self._useKWsDictArgForPassingTrans():
+                if not kwargsName and not positionalArgsListName:
+                    self.addMethArg('trans', 'None')       
+                else:
+                    self._streamingEnabled = False
+                
+        self._indentLev = self.setting('initialMethIndentLevel')
+        mainBodyChunks = self._methodBodyChunks
+        self._methodBodyChunks = []
+        self._addAutoSetupCode()
+        self._methodBodyChunks.extend(mainBodyChunks)
+        self._addAutoCleanupCode()
+        
+    def _addAutoSetupCode(self):
+        if self._initialMethodComment:
+            self.addChunk(self._initialMethodComment)
+            
+        if self._streamingEnabled and not self.isClassMethod() and not self.isStaticMethod():
+            if self._useKWsDictArgForPassingTrans() and self._kwargsName:
+                self.addChunk('trans = %s.get("trans")'%self._kwargsName)            
+            self.addChunk('if (not trans and not self._CHEETAH__isBuffering'
+                          ' and not callable(self.transaction)):')
+            self.indent()
+            self.addChunk('trans = self.transaction'
+                          ' # is None unless self.awake() was called')
+            self.dedent()
+            self.addChunk('if not trans:')
+            self.indent()
+            self.addChunk('trans = DummyTransaction()')
+            if self.setting('autoAssignDummyTransactionToSelf'):
+                self.addChunk('self.transaction = trans')            
+            self.addChunk('_dummyTrans = True')
+            self.dedent()
+            self.addChunk('else: _dummyTrans = False')
+        else:
+            self.addChunk('trans = DummyTransaction()')
+            self.addChunk('_dummyTrans = True')
+        self.addChunk('write = trans.response().write')
+        if self.setting('useNameMapper'):
+            argNames = [arg[0] for arg in self._argStringList]
+            allowSearchListAsMethArg = self.setting('allowSearchListAsMethArg')            
+            if allowSearchListAsMethArg and 'SL' in argNames:
+                pass
+            elif allowSearchListAsMethArg and 'searchList' in argNames:
+                self.addChunk('SL = searchList')
+            elif not self.isClassMethod() and not self.isStaticMethod():
+                self.addChunk('SL = self._CHEETAH__searchList')                
+            else:
+                self.addChunk('SL = [KWS]')
+        if self.setting('useFilters'):
+            if self.isClassMethod() or self.isStaticMethod():
+                self.addChunk('_filter = lambda x, **kwargs: unicode(x)')
+            else:
+                self.addChunk('_filter = self._CHEETAH__currentFilter')
+        self.addChunk('')
+        self.addChunk("#" *40)
+        self.addChunk('## START - generated method body')
+        self.addChunk('')
+
+    def _addAutoCleanupCode(self):
+        self.addChunk('')
+        self.addChunk("#" *40)
+        self.addChunk('## END - generated method body')
+        self.addChunk('')
+
+        if not self._isGenerator:
+            self.addStop()
+        self.addChunk('')
+        
+    def addStop(self, expr=None):
+        self.addChunk('return _dummyTrans and trans.response().getvalue() or ""')
+
+    def addMethArg(self, name, defVal=None):
+        self._argStringList.append( (name, defVal) )
+        
+    def methodSignature(self):
+        argStringChunks = []
+        for arg in self._argStringList:
+            chunk = arg[0]
+            if chunk == 'self' and self.isClassMethod():
+                chunk = 'cls'
+            if chunk == 'self' and self.isStaticMethod():
+                # Skip the "self" method for @staticmethod decorators
+                continue
+            if not arg[1] == None:
+                chunk += '=' + arg[1]
+            argStringChunks.append(chunk)
+        argString = (', ').join(argStringChunks)
+
+        output = []
+        if self._decorators:
+            output.append(''.join([self._indent + decorator + '\n'
+                                   for decorator in self._decorators]))
+        output.append(self._indent + "def "
+                      + self.methodName() + "(" +
+                      argString + "):\n\n")
+        return ''.join(output)
+
+
+##################################################
+## CLASS COMPILERS
+
+_initMethod_initCheetah = """\
+if not self._CHEETAH__instanceInitialized:
+    cheetahKWArgs = {}
+    allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split()
+    for k,v in KWs.items():
+        if k in allowedKWs: cheetahKWArgs[k] = v
+    self._initCheetahInstance(**cheetahKWArgs)
+""".replace('\n', '\n'+' '*8)
+
+class ClassCompiler(GenUtils):
+    methodCompilerClass = AutoMethodCompiler
+    methodCompilerClassForInit = MethodCompiler
+        
+    def __init__(self, className, mainMethodName='respond',
+                 moduleCompiler=None,
+                 fileName=None,
+                 settingsManager=None):
+
+        self._settingsManager = settingsManager
+        self._fileName = fileName
+        self._className = className
+        self._moduleCompiler = moduleCompiler
+        self._mainMethodName = mainMethodName
+        self._setupState()
+        methodCompiler = self._spawnMethodCompiler(
+            mainMethodName,
+            initialMethodComment='## CHEETAH: main method generated for this template')
+
+        self._setActiveMethodCompiler(methodCompiler)
+        if fileName and self.setting('monitorSrcFile'):
+            self._addSourceFileMonitoring(fileName)
+
+    def setting(self, key):
+        return self._settingsManager.setting(key)
+
+    def __getattr__(self, name):
+        """Provide access to the methods and attributes of the MethodCompiler
+        at the top of the activeMethods stack: one-way namespace sharing
+
+        
+        WARNING: Use .setMethods to assign the attributes of the MethodCompiler
+        from the methods of this class!!! or you will be assigning to attributes
+        of this object instead."""
+        
+        if name in self.__dict__:
+            return self.__dict__[name]
+        elif hasattr(self.__class__, name):
+            return getattr(self.__class__, name)
+        elif self._activeMethodsList and hasattr(self._activeMethodsList[-1], name):
+            return getattr(self._activeMethodsList[-1], name)
+        else:
+            raise AttributeError(name)
+
+    def _setupState(self):
+        self._classDef = None
+        self._decoratorsForNextMethod = []
+        self._activeMethodsList = []        # stack while parsing/generating
+        self._finishedMethodsList = []      # store by order
+        self._methodsIndex = {}      # store by name
+        self._baseClass = 'Template'
+        self._classDocStringLines = []
+        # printed after methods in the gen class def:
+        self._generatedAttribs = ['_CHEETAH__instanceInitialized = False']
+        self._generatedAttribs.append('_CHEETAH_version = __CHEETAH_version__')
+        self._generatedAttribs.append(
+            '_CHEETAH_versionTuple = __CHEETAH_versionTuple__')
+
+        if self.setting('addTimestampsToCompilerOutput'):
+            self._generatedAttribs.append('_CHEETAH_genTime = __CHEETAH_genTime__')
+            self._generatedAttribs.append('_CHEETAH_genTimestamp = __CHEETAH_genTimestamp__')
+
+        self._generatedAttribs.append('_CHEETAH_src = __CHEETAH_src__')
+        self._generatedAttribs.append(
+            '_CHEETAH_srcLastModified = __CHEETAH_srcLastModified__')
+
+        if self.setting('templateMetaclass'):
+            self._generatedAttribs.append('__metaclass__ = '+self.setting('templateMetaclass'))
+        self._initMethChunks = []        
+        self._blockMetaData = {}
+        self._errorCatcherCount = 0
+        self._placeholderToErrorCatcherMap = {}
+
+    def cleanupState(self):
+        while self._activeMethodsList:
+            methCompiler = self._popActiveMethodCompiler()
+            self._swallowMethodCompiler(methCompiler)
+        self._setupInitMethod()
+        if self._mainMethodName == 'respond':
+            if self.setting('setup__str__method'):
+                self._generatedAttribs.append('def __str__(self): return self.respond()')
+        self.addAttribute('_mainCheetahMethod_for_' + self._className +
+                           '= ' + repr(self._mainMethodName) )
+
+    def _setupInitMethod(self):
+        __init__ = self._spawnMethodCompiler('__init__',
+                                             klass=self.methodCompilerClassForInit)
+        __init__.setMethodSignature("def __init__(self, *args, **KWs)")
+        __init__.addChunk('super(%s, self).__init__(*args, **KWs)' % self._className)
+        __init__.addChunk(_initMethod_initCheetah % {'className' : self._className})
+        for chunk in self._initMethChunks:
+            __init__.addChunk(chunk)
+        __init__.cleanupState()
+        self._swallowMethodCompiler(__init__, pos=0)
+
+    def _addSourceFileMonitoring(self, fileName):
+        # @@TR: this stuff needs auditing for Cheetah 2.0       
+        # the first bit is added to init
+        self.addChunkToInit('self._filePath = ' + repr(fileName))
+        self.addChunkToInit('self._fileMtime = ' + str(getmtime(fileName)) )
+
+        # the rest is added to the main output method of the class ('mainMethod')
+        self.addChunk('if exists(self._filePath) and ' +
+                      'getmtime(self._filePath) > self._fileMtime:')
+        self.indent()
+        self.addChunk('self._compile(file=self._filePath, moduleName='+self._className + ')')
+        self.addChunk(
+            'write(getattr(self, self._mainCheetahMethod_for_' + self._className +
+            ')(trans=trans))')            
+        self.addStop()
+        self.dedent()
+    
+    def setClassName(self, name):
+        self._className = name
+
+    def className(self):
+        return self._className
+
+    def setBaseClass(self, baseClassName):
+        self._baseClass = baseClassName
+               
+    def setMainMethodName(self, methodName):
+        if methodName == self._mainMethodName:
+            return
+        ## change the name in the methodCompiler and add new reference
+        mainMethod = self._methodsIndex[self._mainMethodName]
+        mainMethod.setMethodName(methodName)
+        self._methodsIndex[methodName] = mainMethod
+
+        ## make sure that fileUpdate code still works properly:
+        chunkToChange = ('write(self.' + self._mainMethodName + '(trans=trans))')
+        chunks = mainMethod._methodBodyChunks
+        if chunkToChange in chunks:
+            for i in range(len(chunks)):
+                if chunks[i] == chunkToChange:
+                    chunks[i] = ('write(self.' + methodName + '(trans=trans))')
+        ## get rid of the old reference and update self._mainMethodName                   
+        del self._methodsIndex[self._mainMethodName]
+        self._mainMethodName = methodName
+
+    def setMainMethodArgs(self, argsList):
+        mainMethodCompiler = self._methodsIndex[self._mainMethodName]
+        for argName, defVal in argsList:
+            mainMethodCompiler.addMethArg(argName, defVal)
+        
+          
+    def _spawnMethodCompiler(self, methodName, klass=None, 
+                             initialMethodComment=None):
+        if klass is None:
+            klass = self.methodCompilerClass
+
+        decorators = self._decoratorsForNextMethod or []
+        self._decoratorsForNextMethod = []
+        methodCompiler = klass(methodName, classCompiler=self,
+                               decorators=decorators,
+                               initialMethodComment=initialMethodComment)
+        self._methodsIndex[methodName] = methodCompiler
+        return methodCompiler
+
+    def _setActiveMethodCompiler(self, methodCompiler):
+        self._activeMethodsList.append(methodCompiler)
+
+    def _getActiveMethodCompiler(self):
+        return self._activeMethodsList[-1]
+
+    def _popActiveMethodCompiler(self):
+        return self._activeMethodsList.pop()
+
+    def _swallowMethodCompiler(self, methodCompiler, pos=None):
+        methodCompiler.cleanupState()
+        if pos==None:
+            self._finishedMethodsList.append( methodCompiler )
+        else:
+            self._finishedMethodsList.insert(pos, methodCompiler)
+        return methodCompiler
+
+    def startMethodDef(self, methodName, argsList, parserComment):
+        methodCompiler = self._spawnMethodCompiler(
+            methodName, initialMethodComment=parserComment)
+        self._setActiveMethodCompiler(methodCompiler)        
+        for argName, defVal in argsList:
+            methodCompiler.addMethArg(argName, defVal)
+        
+    def _finishedMethods(self):
+        return self._finishedMethodsList
+
+    def addDecorator(self, decoratorExpr):
+        """Set the decorator to be used with the next method in the source.
+
+        See _spawnMethodCompiler() and MethodCompiler for the details of how
+        this is used.
+        """
+        self._decoratorsForNextMethod.append(decoratorExpr)
+
+    def addClassDocString(self, line):
+        self._classDocStringLines.append( line.replace('%', '%%')) 
+
+    def addChunkToInit(self, chunk):
+        self._initMethChunks.append(chunk)
+
+    def addAttribute(self, attribExpr):
+        ## first test to make sure that the user hasn't used any fancy Cheetah syntax
+        #  (placeholders, directives, etc.) inside the expression 
+        if attribExpr.find('VFN(') != -1 or attribExpr.find('VFFSL(') != -1:
+            raise ParseError(self,
+                             'Invalid #attr directive.' +
+                             ' It should only contain simple Python literals.')
+        ## now add the attribute
+        self._generatedAttribs.append(attribExpr)
+
+    def addSuper(self, argsList, parserComment=None):        
+        className = self._className #self._baseClass
+        methodName = self._getActiveMethodCompiler().methodName()
+
+        argStringChunks = []
+        for arg in argsList:
+            chunk = arg[0]
+            if not arg[1] == None:
+                chunk += '=' + arg[1]
+            argStringChunks.append(chunk)
+        argString = ','.join(argStringChunks)
+
+        self.addFilteredChunk(
+            'super(%(className)s, self).%(methodName)s(%(argString)s)'%locals())
+
+    def addErrorCatcherCall(self, codeChunk, rawCode='', lineCol=''):
+        if rawCode in self._placeholderToErrorCatcherMap:
+            methodName = self._placeholderToErrorCatcherMap[rawCode]
+            if not self.setting('outputRowColComments'):
+                self._methodsIndex[methodName].addMethDocString(
+                    'plus at line %s, col %s'%lineCol)
+            return methodName
+
+        self._errorCatcherCount += 1
+        methodName = '__errorCatcher' + str(self._errorCatcherCount)
+        self._placeholderToErrorCatcherMap[rawCode] = methodName
+        
+        catcherMeth = self._spawnMethodCompiler(
+            methodName,
+            klass=MethodCompiler,
+            initialMethodComment=('## CHEETAH: Generated from ' + rawCode +
+                                  ' at line %s, col %s'%lineCol + '.')
+            )        
+        catcherMeth.setMethodSignature('def ' + methodName +
+                                       '(self, localsDict={})')
+                                        # is this use of localsDict right?
+        catcherMeth.addChunk('try:')
+        catcherMeth.indent()
+        catcherMeth.addChunk("return eval('''" + codeChunk +
+                             "''', globals(), localsDict)")
+        catcherMeth.dedent()
+        catcherMeth.addChunk('except self._CHEETAH__errorCatcher.exceptions(), e:')
+        catcherMeth.indent()        
+        catcherMeth.addChunk("return self._CHEETAH__errorCatcher.warn(exc_val=e, code= " +
+                             repr(codeChunk) + " , rawCode= " +
+                             repr(rawCode) + " , lineCol=" + str(lineCol) +")")
+        
+        catcherMeth.cleanupState()
+        
+        self._swallowMethodCompiler(catcherMeth)
+        return methodName
+
+    def closeDef(self):
+        self.commitStrConst()
+        methCompiler = self._popActiveMethodCompiler()
+        self._swallowMethodCompiler(methCompiler)
+
+    def closeBlock(self):
+        self.commitStrConst()
+        methCompiler = self._popActiveMethodCompiler()
+        methodName = methCompiler.methodName()
+        if self.setting('includeBlockMarkers'):
+            endMarker = self.setting('blockMarkerEnd')
+            methCompiler.addStrConst(endMarker[0] + methodName + endMarker[1])
+        self._swallowMethodCompiler(methCompiler)
+        
+        #metaData = self._blockMetaData[methodName] 
+        #rawDirective = metaData['raw']
+        #lineCol = metaData['lineCol']
+        
+        ## insert the code to call the block, caching if #cache directive is on
+        codeChunk = 'self.' + methodName + '(trans=trans)'
+        self.addChunk(codeChunk)
+        
+        #self.appendToPrevChunk(' # generated from ' + repr(rawDirective) )
+        #if self.setting('outputRowColComments'):
+        #    self.appendToPrevChunk(' at line %s, col %s' % lineCol + '.')
+
+
+    ## code wrapping methods
+    
+    def classDef(self):
+        if self._classDef:
+            return self._classDef
+        else:
+            return self.wrapClassDef()
+
+    __str__ = classDef
+    __unicode__ = classDef
+    
+    def wrapClassDef(self):
+        ind = self.setting('indentationStep')
+        classDefChunks = [self.classSignature(),
+                          self.classDocstring(),
+                          ]
+        def addMethods():
+            classDefChunks.extend([
+                ind + '#'*50,
+                ind + '## CHEETAH GENERATED METHODS',
+                '\n',
+                self.methodDefs(),
+                ])
+        def addAttributes():
+            classDefChunks.extend([
+                ind + '#'*50,
+                ind + '## CHEETAH GENERATED ATTRIBUTES',
+                '\n',
+                self.attributes(),
+                ])            
+        if self.setting('outputMethodsBeforeAttributes'):
+            addMethods()
+            addAttributes()
+        else:
+            addAttributes()
+            addMethods()
+            
+        classDef = '\n'.join(classDefChunks)
+        self._classDef = classDef
+        return classDef
+
+
+    def classSignature(self):
+        return "class %s(%s):" % (self.className(), self._baseClass)
+        
+    def classDocstring(self):
+        if not self._classDocStringLines:
+            return ''
+        ind = self.setting('indentationStep')
+        docStr = ('%(ind)s"""\n%(ind)s' +
+                  '\n%(ind)s'.join(self._classDocStringLines) +
+                  '\n%(ind)s"""\n'
+                  ) % {'ind':ind}
+        return  docStr
+
+    def methodDefs(self):
+        methodDefs = [methGen.methodDef() for methGen in self._finishedMethods()]
+        return '\n\n'.join(methodDefs)
+
+    def attributes(self):
+        attribs = [self.setting('indentationStep') + str(attrib)
+                      for attrib in self._generatedAttribs ]
+        return '\n\n'.join(attribs)
+  
+class AutoClassCompiler(ClassCompiler):
+    pass
+
+##################################################
+## MODULE COMPILERS
+
+class ModuleCompiler(SettingsManager, GenUtils):
+
+    parserClass = Parser
+    classCompilerClass = AutoClassCompiler
+    
+    def __init__(self, source=None, file=None,
+                 moduleName='DynamicallyCompiledCheetahTemplate',                 
+                 mainClassName=None, # string
+                 mainMethodName=None, # string
+                 baseclassName=None, # string
+                 extraImportStatements=None, # list of strings
+                 settings=None # dict
+                 ):
+        super(ModuleCompiler, self).__init__()
+        if settings:
+            self.updateSettings(settings)
+        # disable useStackFrames if the C version of NameMapper isn't compiled
+        # it's painfully slow in the Python version and bites Windows users all
+        # the time:
+        if not NameMapper.C_VERSION:
+            if not sys.platform.startswith('java'):
+                warnings.warn(
+                    "\nYou don't have the C version of NameMapper installed! "
+                    "I'm disabling Cheetah's useStackFrames option as it is "
+                    "painfully slow with the Python version of NameMapper. "
+                    "You should get a copy of Cheetah with the compiled C version of NameMapper."
+                    )
+            self.setSetting('useStackFrames', False)                    
+
+        self._compiled = False
+        self._moduleName = moduleName
+        if not mainClassName:
+            self._mainClassName = moduleName
+        else:
+            self._mainClassName = mainClassName
+        self._mainMethodNameArg = mainMethodName
+        if mainMethodName:
+            self.setSetting('mainMethodName', mainMethodName)
+        self._baseclassName = baseclassName
+        
+        self._filePath = None
+        self._fileMtime = None
+        
+        if source and file:
+            raise TypeError("Cannot compile from a source string AND file.")
+        elif isinstance(file, basestring): # it's a filename.
+            f = open(file) # Raises IOError.
+            source = f.read()
+            f.close()
+            self._filePath = file
+            self._fileMtime = os.path.getmtime(file)
+        elif hasattr(file, 'read'):
+            source = file.read()  # Can't set filename or mtime--they're not accessible.
+        elif file:
+            raise TypeError("'file' argument must be a filename string or file-like object")
+                
+        if self._filePath:
+            self._fileDirName, self._fileBaseName = os.path.split(self._filePath)
+            self._fileBaseNameRoot, self._fileBaseNameExt = os.path.splitext(self._fileBaseName)
+
+        if not isinstance(source, basestring):
+            source = unicode(source)
+            # by converting to string here we allow objects such as other Templates
+            # to be passed in
+
+        # Handle the #indent directive by converting it to other directives.
+        # (Over the long term we'll make it a real directive.)
+        if source == "":
+            warnings.warn("You supplied an empty string for the source!", )
+        
+        else:
+            unicodeMatch = unicodeDirectiveRE.search(source)
+            encodingMatch = encodingDirectiveRE.search(source)
+            if unicodeMatch:
+                if encodingMatch:
+                    raise ParseError(
+                        self, "#encoding and #unicode are mutually exclusive! "
+                        "Use one or the other.")
+                source = unicodeDirectiveRE.sub('', source)
+                if isinstance(source, str):
+                    encoding = unicodeMatch.group(1) or 'ascii'
+                    source = unicode(source, encoding)
+            elif encodingMatch:
+                encodings = encodingMatch.groups()
+                if len(encodings):
+                    encoding = encodings[0]
+                    source = source.decode(encoding)
+            else:
+                source = unicode(source)
+
+        if source.find('#indent') != -1: #@@TR: undocumented hack
+            source = indentize(source)
+
+        self._parser = self.parserClass(source, filename=self._filePath, compiler=self)
+        self._setupCompilerState()
+        
+    def __getattr__(self, name):
+        """Provide one-way access to the methods and attributes of the
+        ClassCompiler, and thereby the MethodCompilers as well.
+
+        WARNING: Use .setMethods to assign the attributes of the ClassCompiler
+        from the methods of this class!!! or you will be assigning to attributes
+        of this object instead.
+        """
+        if name in self.__dict__:
+            return self.__dict__[name]
+        elif hasattr(self.__class__, name):
+            return getattr(self.__class__, name)
+        elif self._activeClassesList and hasattr(self._activeClassesList[-1], name):
+            return getattr(self._activeClassesList[-1], name)
+        else:
+            raise AttributeError(name)
+
+    def _initializeSettings(self):
+        self.updateSettings(copy.deepcopy(DEFAULT_COMPILER_SETTINGS))
+        
+    def _setupCompilerState(self):
+        self._activeClassesList = []
+        self._finishedClassesList = []      # listed by ordered 
+        self._finishedClassIndex = {}  # listed by name
+        self._moduleDef = None
+        self._moduleShBang = '#!/usr/bin/env python'
+        self._moduleEncoding = 'ascii'
+        self._moduleEncodingStr = ''
+        self._moduleHeaderLines = []
+        self._moduleDocStringLines = []
+        self._specialVars = {}
+        self._importStatements = [
+            "import sys",
+            "import os",
+            "import os.path",
+            'try:',
+            '    import builtins as builtin',
+            'except ImportError:',
+            '    import __builtin__ as builtin',
+            "from os.path import getmtime, exists",
+            "import time",
+            "import types",
+            "from Cheetah.Version import MinCompatibleVersion as RequiredCheetahVersion",            
+            "from Cheetah.Version import MinCompatibleVersionTuple as RequiredCheetahVersionTuple",
+            "from Cheetah.Template import Template",
+            "from Cheetah.DummyTransaction import *",
+            "from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList",
+            "from Cheetah.CacheRegion import CacheRegion",
+            "import Cheetah.Filters as Filters",
+            "import Cheetah.ErrorCatchers as ErrorCatchers",
+            ]        
+
+        self._importedVarNames = ['sys',
+                                  'os',
+                                  'os.path',
+                                  'time',
+                                  'types',
+                                  'Template',
+                                  'DummyTransaction',
+                                  'NotFound',
+                                  'Filters',
+                                  'ErrorCatchers',
+                                  'CacheRegion',
+                                  ]
+        
+        self._moduleConstants = [
+            "VFFSL=valueFromFrameOrSearchList",
+            "VFSL=valueFromSearchList",
+            "VFN=valueForName",
+            "currentTime=time.time",
+            ]
+        
+    def compile(self):
+        classCompiler = self._spawnClassCompiler(self._mainClassName)            
+        if self._baseclassName:
+            classCompiler.setBaseClass(self._baseclassName)
+        self._addActiveClassCompiler(classCompiler)
+        self._parser.parse()
+        self._swallowClassCompiler(self._popActiveClassCompiler())
+        self._compiled = True
+        self._parser.cleanup()
+        
+    def _spawnClassCompiler(self, className, klass=None):
+        if klass is None:
+            klass = self.classCompilerClass
+        classCompiler = klass(className,
+                              moduleCompiler=self,
+                              mainMethodName=self.setting('mainMethodName'),
+                              fileName=self._filePath,
+                              settingsManager=self,
+                              )
+        return classCompiler
+
+    def _addActiveClassCompiler(self, classCompiler):
+        self._activeClassesList.append(classCompiler)
+
+    def _getActiveClassCompiler(self):
+        return self._activeClassesList[-1]
+
+    def _popActiveClassCompiler(self):
+        return self._activeClassesList.pop()
+
+    def _swallowClassCompiler(self, classCompiler):
+        classCompiler.cleanupState()
+        self._finishedClassesList.append( classCompiler )
+        self._finishedClassIndex[classCompiler.className()] = classCompiler
+        return classCompiler
+
+    def _finishedClasses(self):
+        return self._finishedClassesList
+
+    def importedVarNames(self):
+        return self._importedVarNames
+    
+    def addImportedVarNames(self, varNames, raw_statement=None):
+        settings = self.settings()
+        if not varNames:
+            return 
+        if not settings.get('useLegacyImportMode'):
+            if raw_statement and getattr(self, '_methodBodyChunks'):
+                self.addChunk(raw_statement)
+        else:
+            self._importedVarNames.extend(varNames)
+
+    ## methods for adding stuff to the module and class definitions
+
+    def setBaseClass(self, baseClassName):
+        if self._mainMethodNameArg:
+            self.setMainMethodName(self._mainMethodNameArg)
+        else:
+            self.setMainMethodName(self.setting('mainMethodNameForSubclasses'))
+       
+        if self.setting('handlerForExtendsDirective'):
+            handler = self.setting('handlerForExtendsDirective')
+            baseClassName = handler(compiler=self, baseClassName=baseClassName)
+            self._getActiveClassCompiler().setBaseClass(baseClassName)
+        elif (not self.setting('autoImportForExtendsDirective')
+            or baseClassName=='object' or baseClassName in self.importedVarNames()):
+            self._getActiveClassCompiler().setBaseClass(baseClassName)
+            # no need to import
+        else:
+            ##################################################
+            ## If the #extends directive contains a classname or modulename that isn't
+            #  in self.importedVarNames() already, we assume that we need to add
+            #  an implied 'from ModName import ClassName' where ModName == ClassName.
+            #  - This is the case in WebKit servlet modules.
+            #  - We also assume that the final . separates the classname from the
+            #    module name.  This might break if people do something really fancy 
+            #    with their dots and namespaces.
+            baseclasses = baseClassName.split(',')
+            for klass in baseclasses:
+                chunks = klass.split('.')
+                if len(chunks)==1:
+                    self._getActiveClassCompiler().setBaseClass(klass)
+                    if klass not in self.importedVarNames():
+                        modName = klass
+                        # we assume the class name to be the module name
+                        # and that it's not a builtin:
+                        importStatement = "from %s import %s" % (modName, klass)
+                        self.addImportStatement(importStatement)
+                        self.addImportedVarNames((klass,))
+                else:
+                    needToAddImport = True
+                    modName = chunks[0]
+                    #print chunks, ':', self.importedVarNames()
+                    for chunk in chunks[1:-1]:
+                        if modName in self.importedVarNames():
+                            needToAddImport = False
+                            finalBaseClassName = klass.replace(modName+'.', '')
+                            self._getActiveClassCompiler().setBaseClass(finalBaseClassName)
+                            break
+                        else:
+                            modName += '.'+chunk                        
+                    if needToAddImport:
+                        modName, finalClassName = '.'.join(chunks[:-1]), chunks[-1]                
+                        #if finalClassName != chunks[:-1][-1]:
+                        if finalClassName != chunks[-2]:
+                            # we assume the class name to be the module name
+                            modName = '.'.join(chunks)
+                        self._getActiveClassCompiler().setBaseClass(finalClassName)                        
+                        importStatement = "from %s import %s" % (modName, finalClassName)
+                        self.addImportStatement(importStatement)
+                        self.addImportedVarNames( [finalClassName,] ) 
+            
+    def setCompilerSetting(self, key, valueExpr):
+        self.setSetting(key, eval(valueExpr) )
+        self._parser.configureParser()
+
+    def setCompilerSettings(self, keywords, settingsStr):
+        KWs = keywords
+        merge = True
+        if 'nomerge' in KWs:
+            merge = False
+            
+        if 'reset' in KWs:
+            # @@TR: this is actually caught by the parser at the moment. 
+            # subject to change in the future
+            self._initializeSettings()
+            self._parser.configureParser()
+            return
+        elif 'python' in KWs:
+            settingsReader = self.updateSettingsFromPySrcStr
+            # this comes from SettingsManager
+        else:
+            # this comes from SettingsManager
+            settingsReader = self.updateSettingsFromConfigStr
+
+        settingsReader(settingsStr)
+        self._parser.configureParser()
+        
+    def setShBang(self, shBang):
+        self._moduleShBang = shBang
+    
+    def setModuleEncoding(self, encoding):
+        self._moduleEncoding = encoding
+
+    def getModuleEncoding(self):
+        return self._moduleEncoding
+
+    def addModuleHeader(self, line):
+        """Adds a header comment to the top of the generated module.
+        """
+        self._moduleHeaderLines.append(line)
+        
+    def addModuleDocString(self, line):        
+        """Adds a line to the generated module docstring.
+        """
+        self._moduleDocStringLines.append(line)
+
+    def addModuleGlobal(self, line):
+        """Adds a line of global module code.  It is inserted after the import
+        statements and Cheetah default module constants.
+        """
+        self._moduleConstants.append(line)
+
+    def addSpecialVar(self, basename, contents, includeUnderscores=True):
+        """Adds module __specialConstant__ to the module globals.
+        """
+        name = includeUnderscores and '__'+basename+'__' or basename
+        self._specialVars[name] = contents.strip()
+
+    def addImportStatement(self, impStatement):
+        settings = self.settings()
+        if not self._methodBodyChunks or settings.get('useLegacyImportMode'):
+            # In the case where we are importing inline in the middle of a source block
+            # we don't want to inadvertantly import the module at the top of the file either
+            self._importStatements.append(impStatement)
+
+        #@@TR 2005-01-01: there's almost certainly a cleaner way to do this!
+        importVarNames = impStatement[impStatement.find('import') + len('import'):].split(',')
+        importVarNames = [var.split()[-1] for var in importVarNames] # handles aliases
+        importVarNames = [var for var in importVarNames if not var == '*']
+        self.addImportedVarNames(importVarNames, raw_statement=impStatement) #used by #extend for auto-imports
+
+    def addAttribute(self, attribName, expr):
+        self._getActiveClassCompiler().addAttribute(attribName + ' =' + expr)
+        
+    def addComment(self, comm):
+        if re.match(r'#+$', comm):      # skip bar comments
+            return
+        
+        specialVarMatch = specialVarRE.match(comm)
+        if specialVarMatch:
+            # @@TR: this is a bit hackish and is being replaced with
+            # #set module varName = ...
+            return self.addSpecialVar(specialVarMatch.group(1),
+                                      comm[specialVarMatch.end():])
+        elif comm.startswith('doc:'):
+            addLine = self.addMethDocString
+            comm = comm[len('doc:'):].strip()
+        elif comm.startswith('doc-method:'):
+            addLine = self.addMethDocString
+            comm = comm[len('doc-method:'):].strip()
+        elif comm.startswith('doc-module:'):
+            addLine = self.addModuleDocString
+            comm = comm[len('doc-module:'):].strip()
+        elif comm.startswith('doc-class:'):
+            addLine = self.addClassDocString
+            comm = comm[len('doc-class:'):].strip()
+        elif comm.startswith('header:'):
+            addLine = self.addModuleHeader
+            comm = comm[len('header:'):].strip()
+        else:
+            addLine = self.addMethComment
+
+        for line in comm.splitlines():
+            addLine(line)
+
+    ## methods for module code wrapping
+    
+    def getModuleCode(self):
+        if not self._compiled:
+            self.compile()
+        if self._moduleDef:
+            return self._moduleDef
+        else:
+            return self.wrapModuleDef()
+        
+    __str__ = getModuleCode
+
+    def wrapModuleDef(self):
+        self.addSpecialVar('CHEETAH_docstring', self.setting('defDocStrMsg'))
+        self.addModuleGlobal('__CHEETAH_version__ = %r'%Version)
+        self.addModuleGlobal('__CHEETAH_versionTuple__ = %r'%(VersionTuple,))        
+        if self.setting('addTimestampsToCompilerOutput'):
+            self.addModuleGlobal('__CHEETAH_genTime__ = %r'%time.time())
+            self.addModuleGlobal('__CHEETAH_genTimestamp__ = %r'%self.timestamp())
+        if self._filePath:
+            timestamp = self.timestamp(self._fileMtime)
+            self.addModuleGlobal('__CHEETAH_src__ = %r'%self._filePath)
+            self.addModuleGlobal('__CHEETAH_srcLastModified__ = %r'%timestamp)
+        else:
+            self.addModuleGlobal('__CHEETAH_src__ = None')
+            self.addModuleGlobal('__CHEETAH_srcLastModified__ = None')            
+
+        moduleDef = """%(header)s
+%(docstring)s
+
+##################################################
+## DEPENDENCIES
+%(imports)s
+
+##################################################
+## MODULE CONSTANTS
+%(constants)s
+%(specialVars)s
+
+if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
+    raise AssertionError(
+      'This template was compiled with Cheetah version'
+      ' %%s. Templates compiled before version %%s must be recompiled.'%%(
+         __CHEETAH_version__, RequiredCheetahVersion))
+
+##################################################
+## CLASSES
+
+%(classes)s
+
+## END CLASS DEFINITION
+
+if not hasattr(%(mainClassName)s, '_initCheetahAttributes'):
+    templateAPIClass = getattr(%(mainClassName)s, '_CHEETAH_templateClass', Template)
+    templateAPIClass._addCheetahPlumbingCodeToClass(%(mainClassName)s)
+
+%(footer)s
+""" %   {'header': self.moduleHeader(),
+         'docstring': self.moduleDocstring(),
+         'specialVars': self.specialVars(),
+         'imports': self.importStatements(),
+         'constants': self.moduleConstants(),
+         'classes': self.classDefs(),
+         'footer': self.moduleFooter(),
+         'mainClassName': self._mainClassName,
+         }
+       
+        self._moduleDef = moduleDef
+        return moduleDef
+
+    def timestamp(self, theTime=None):
+        if not theTime:
+            theTime = time.time()
+        return time.asctime(time.localtime(theTime))
+    
+    def moduleHeader(self):
+        header = self._moduleShBang + '\n'
+        header += self._moduleEncodingStr + '\n'
+        if self._moduleHeaderLines:
+            offSet = self.setting('commentOffset')
+        
+            header += (
+                '#' + ' '*offSet + 
+                ('\n#'+ ' '*offSet).join(self._moduleHeaderLines) + '\n')
+
+        return header
+
+    def moduleDocstring(self):
+        if not self._moduleDocStringLines:
+            return ''
+        
+        return ('"""' +
+                '\n'.join(self._moduleDocStringLines) +
+                '\n"""\n')
+
+    def specialVars(self):
+        chunks = []
+        theVars = self._specialVars
+        keys = sorted(theVars.keys())
+        for key in keys:
+            chunks.append(key + ' = ' + repr(theVars[key])  )
+        return '\n'.join(chunks)
+        
+    def importStatements(self):
+        return '\n'.join(self._importStatements)
+        
+    def moduleConstants(self):
+        return '\n'.join(self._moduleConstants)
+
+    def classDefs(self):
+        classDefs = [klass.classDef() for klass in self._finishedClasses()]
+        return '\n\n'.join(classDefs)
+
+    def moduleFooter(self):
+        return """
+# CHEETAH was developed by Tavis Rudd and Mike Orr
+# with code, advice and input from many other volunteers.
+# For more information visit http://www.CheetahTemplate.org/
+
+##################################################
+## if run from command line:
+if __name__ == '__main__':
+    from Cheetah.TemplateCmdLineIface import CmdLineIface
+    CmdLineIface(templateObj=%(className)s()).run()
+
+""" % {'className':self._mainClassName}
+
+
+##################################################
+## Make Compiler an alias for ModuleCompiler
+    
+Compiler = ModuleCompiler
diff --git a/cheetah/DirectiveAnalyzer.py b/cheetah/DirectiveAnalyzer.py
new file mode 100644 (file)
index 0000000..a9f9387
--- /dev/null
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+import os
+import pprint
+
+try:
+    from functools import reduce
+except ImportError:
+    # Assume we have reduce
+    pass
+
+from Cheetah import Parser
+from Cheetah import Compiler
+from Cheetah import Template
+
+class Analyzer(Parser.Parser):
+    def __init__(self, *args, **kwargs):
+        self.calls = {}
+        super(Analyzer, self).__init__(*args, **kwargs)
+
+    def eatDirective(self):
+        directive = self.matchDirective()
+        try:
+            self.calls[directive] += 1
+        except KeyError:
+            self.calls[directive] = 1
+        super(Analyzer, self).eatDirective()
+
+class AnalysisCompiler(Compiler.ModuleCompiler):
+    parserClass = Analyzer
+
+
+def analyze(source):
+    klass = Template.Template.compile(source, compilerClass=AnalysisCompiler)
+    return klass._CHEETAH_compilerInstance._parser.calls
+
+def main_file(f):
+    fd = open(f, 'r')
+    try:
+        print u'>>> Analyzing %s' % f
+        calls = analyze(fd.read())
+        return calls
+    finally:
+        fd.close()
+
+
+def _find_templates(directory, suffix):
+    for root, dirs, files in os.walk(directory):
+        for f in files:
+            if not f.endswith(suffix):
+                continue
+            yield root + os.path.sep + f
+
+def _analyze_templates(iterable):
+    for template in iterable:
+        yield main_file(template)
+
+def main_dir(opts):
+    results = _analyze_templates(_find_templates(opts.dir, opts.suffix))
+    totals = {}
+    for series in results:
+        if not series:
+            continue
+        for k, v in series.iteritems():
+            try:
+                totals[k] += v
+            except KeyError:
+                totals[k] = v
+    return totals
+
+
+def main():
+    from optparse import OptionParser
+    op = OptionParser()
+    op.add_option('-f', '--file', dest='file', default=None,
+            help='Specify a single file to analyze')
+    op.add_option('-d', '--dir', dest='dir', default=None, 
+            help='Specify a directory of templates to analyze')
+    op.add_option('--suffix', default='tmpl', dest='suffix', 
+            help='Specify a custom template file suffix for the -d option (default: "tmpl")')
+    opts, args = op.parse_args()
+
+    if not opts.file and not opts.dir:
+        op.print_help()
+        return
+
+    results = None
+    if opts.file:
+        results = main_file(opts.file)
+    if opts.dir:
+        results = main_dir(opts)
+
+    pprint.pprint(results)
+
+
+if __name__ == '__main__':
+    main()
+
diff --git a/cheetah/Django.py b/cheetah/Django.py
new file mode 100644 (file)
index 0000000..876fbbc
--- /dev/null
@@ -0,0 +1,16 @@
+import Cheetah.Template
+
+def render(template_file, **kwargs):
+    '''
+        Cheetah.Django.render() takes the template filename 
+        (the filename should be a file in your Django 
+        TEMPLATE_DIRS)
+
+        Any additional keyword arguments are passed into the 
+        template are propogated into the template's searchList
+    '''
+    import django.http
+    import django.template.loader
+    source, loader = django.template.loader.find_template_source(template_file)
+    t = Cheetah.Template.Template(source, searchList=[kwargs])
+    return django.http.HttpResponse(t.__str__())
diff --git a/cheetah/DummyTransaction.py b/cheetah/DummyTransaction.py
new file mode 100644 (file)
index 0000000..72f8662
--- /dev/null
@@ -0,0 +1,108 @@
+
+'''
+Provides dummy Transaction and Response classes is used by Cheetah in place
+of real Webware transactions when the Template obj is not used directly as a
+Webware servlet.
+
+Warning: This may be deprecated in the future, please do not rely on any 
+specific DummyTransaction or DummyResponse behavior
+'''
+
+import logging
+import types
+
+class DummyResponseFailure(Exception):
+    pass
+
+class DummyResponse(object):
+    '''
+        A dummy Response class is used by Cheetah in place of real Webware
+        Response objects when the Template obj is not used directly as a Webware
+        servlet
+    ''' 
+    def __init__(self):
+        self._outputChunks = []
+
+    def flush(self):
+        pass
+
+    def safeConvert(self, chunk):
+        # Exceptionally gross, but the safest way
+        # I've found to ensure I get a legit unicode object
+        if not chunk:
+            return u''
+        if isinstance(chunk, unicode):
+            return chunk
+        try:
+            return chunk.decode('utf-8', 'strict')
+        except UnicodeDecodeError:
+            try:
+                return chunk.decode('latin-1', 'strict')
+            except UnicodeDecodeError:
+                return chunk.decode('ascii', 'ignore')
+        except AttributeError:
+            return unicode(chunk, errors='ignore')
+        return chunk
+
+    def write(self, value):
+        self._outputChunks.append(value)
+
+    def writeln(self, txt):
+        write(txt)
+        write('\n')
+
+    def getvalue(self, outputChunks=None):
+        chunks = outputChunks or self._outputChunks
+        try:
+            return u''.join(chunks)
+        except UnicodeDecodeError, ex:
+            logging.debug('Trying to work around a UnicodeDecodeError in getvalue()')
+            logging.debug('...perhaps you could fix "%s" while you\'re debugging')
+            return ''.join((self.safeConvert(c) for c in chunks))
+
+    def writelines(self, *lines):
+        ## not used
+        [self.writeln(ln) for ln in lines]
+        
+
+class DummyTransaction(object):
+    '''
+        A dummy Transaction class is used by Cheetah in place of real Webware
+        transactions when the Template obj is not used directly as a Webware
+        servlet.
+
+        It only provides a response object and method.  All other methods and
+        attributes make no sense in this context.
+    '''
+    def __init__(self, *args, **kwargs):
+        self._response = None
+
+    def response(self, resp=None):
+        if self._response is None:
+            self._response = resp or DummyResponse()
+        return self._response
+
+
+class TransformerResponse(DummyResponse):
+    def __init__(self, *args, **kwargs):
+        super(TransformerResponse, self).__init__(*args, **kwargs)
+        self._filter = None
+
+    def getvalue(self, **kwargs):
+        output = super(TransformerResponse, self).getvalue(**kwargs)
+        if self._filter:
+            _filter = self._filter
+            if isinstance(_filter, type):
+                _filter = _filter()
+            return _filter.filter(output)
+        return output
+
+
+class TransformerTransaction(object):
+    def __init__(self, *args, **kwargs):
+        self._response = None
+    def response(self):
+        if self._response:
+            return self._response
+        return TransformerResponse()
+
diff --git a/cheetah/ErrorCatchers.py b/cheetah/ErrorCatchers.py
new file mode 100644 (file)
index 0000000..a8b7035
--- /dev/null
@@ -0,0 +1,62 @@
+# $Id: ErrorCatchers.py,v 1.7 2005/01/03 19:59:07 tavis_rudd Exp $
+"""ErrorCatcher class for Cheetah Templates
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+Version: $Revision: 1.7 $
+Start Date: 2001/08/01
+Last Revision Date: $Date: 2005/01/03 19:59:07 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.7 $"[11:-2]
+
+import time
+from Cheetah.NameMapper import NotFound
+
+class Error(Exception):
+    pass
+
+class ErrorCatcher:
+    _exceptionsToCatch = (NotFound,)
+    
+    def __init__(self, templateObj):
+        pass
+    
+    def exceptions(self):
+        return self._exceptionsToCatch
+    
+    def warn(self, exc_val, code, rawCode, lineCol):
+        return rawCode
+## make an alias
+Echo = ErrorCatcher
+
+class BigEcho(ErrorCatcher):
+    def warn(self, exc_val, code, rawCode, lineCol):
+        return "="*15 + "&lt;" + rawCode + " could not be found&gt;" + "="*15
+
+class KeyError(ErrorCatcher):
+    def warn(self, exc_val, code, rawCode, lineCol):
+        raise KeyError("no '%s' in this Template Object's Search List" % rawCode) 
+
+class ListErrors(ErrorCatcher):
+    """Accumulate a list of errors."""
+    _timeFormat = "%c"
+    
+    def __init__(self, templateObj):
+        ErrorCatcher.__init__(self, templateObj)
+        self._errors = []
+
+    def warn(self, exc_val, code, rawCode, lineCol):
+        dict = locals().copy()
+        del dict['self']
+        dict['time'] = time.strftime(self._timeFormat,
+                                     time.localtime(time.time()))
+        self._errors.append(dict)
+        return rawCode
+    
+    def listErrors(self):
+        """Return the list of errors."""
+        return self._errors
+
+
diff --git a/cheetah/FileUtils.py b/cheetah/FileUtils.py
new file mode 100644 (file)
index 0000000..c4e65f3
--- /dev/null
@@ -0,0 +1,357 @@
+
+from glob import glob
+import os
+from os import listdir
+import os.path
+import re
+from tempfile import mktemp
+
+def _escapeRegexChars(txt,
+                     escapeRE=re.compile(r'([\$\^\*\+\.\?\{\}\[\]\(\)\|\\])')):
+    return escapeRE.sub(r'\\\1', txt)
+
+def findFiles(*args, **kw):
+    """Recursively find all the files matching a glob pattern.
+
+    This function is a wrapper around the FileFinder class.  See its docstring
+    for details about the accepted arguments, etc."""
+    
+    return FileFinder(*args, **kw).files()
+            
+def replaceStrInFiles(files, theStr, repl):
+
+    """Replace all instances of 'theStr' with 'repl' for each file in the 'files'
+    list. Returns a dictionary with data about the matches found.
+
+    This is like string.replace() on a multi-file basis.
+
+    This function is a wrapper around the FindAndReplace class. See its
+    docstring for more details."""
+    
+    pattern = _escapeRegexChars(theStr)
+    return FindAndReplace(files, pattern, repl).results()
+
+def replaceRegexInFiles(files, pattern, repl):
+
+    """Replace all instances of regex 'pattern' with 'repl' for each file in the
+    'files' list. Returns a dictionary with data about the matches found.
+
+    This is like re.sub on a multi-file basis.
+
+    This function is a wrapper around the FindAndReplace class. See its
+    docstring for more details."""
+
+    return FindAndReplace(files, pattern, repl).results()
+
+
+##################################################
+## CLASSES
+
+class FileFinder:
+    
+    """Traverses a directory tree and finds all files in it that match one of
+    the specified glob patterns."""
+    
+    def __init__(self, rootPath,
+                 globPatterns=('*',),
+                 ignoreBasenames=('CVS', '.svn'),
+                 ignoreDirs=(),
+                 ):
+        
+        self._rootPath = rootPath
+        self._globPatterns = globPatterns
+        self._ignoreBasenames = ignoreBasenames
+        self._ignoreDirs = ignoreDirs
+        self._files = []
+        
+        self.walkDirTree(rootPath)
+            
+    def walkDirTree(self, dir='.',
+                    
+                    listdir=os.listdir,
+                    isdir=os.path.isdir,
+                    join=os.path.join,
+                    ):
+
+        """Recursively walk through a directory tree and find matching files."""
+        processDir = self.processDir
+        filterDir = self.filterDir
+        
+        pendingDirs = [dir]
+        addDir = pendingDirs.append
+        getDir = pendingDirs.pop
+        
+        while pendingDirs:
+            dir = getDir()
+            ##  process this dir
+            processDir(dir)
+            
+            ## and add sub-dirs 
+            for baseName in listdir(dir):
+                fullPath = join(dir, baseName)
+                if isdir(fullPath):
+                    if filterDir(baseName, fullPath):
+                        addDir( fullPath )
+
+    def filterDir(self, baseName, fullPath):
+        
+        """A hook for filtering out certain dirs. """
+        
+        return not (baseName in self._ignoreBasenames or 
+                    fullPath in self._ignoreDirs)
+    
+    def processDir(self, dir, glob=glob):
+        extend = self._files.extend
+        for pattern in self._globPatterns:
+            extend( glob(os.path.join(dir, pattern)) )
+    
+    def files(self):
+        return self._files
+
+class _GenSubberFunc:
+
+    """Converts a 'sub' string in the form that one feeds to re.sub (backrefs,
+    groups, etc.) into a function that can be used to do the substitutions in
+    the FindAndReplace class."""
+    
+    backrefRE = re.compile(r'\\([1-9][0-9]*)')
+    groupRE = re.compile(r'\\g<([a-zA-Z_][a-zA-Z_]*)>')
+    
+    def __init__(self, replaceStr):
+        self._src = replaceStr
+        self._pos = 0
+        self._codeChunks = []
+        self.parse()
+
+    def src(self):
+        return self._src
+        
+    def pos(self):
+        return self._pos
+    
+    def setPos(self, pos):
+        self._pos = pos
+
+    def atEnd(self):
+        return self._pos >= len(self._src)
+
+    def advance(self, offset=1):
+        self._pos += offset
+
+    def readTo(self, to, start=None):
+        if start == None:
+            start = self._pos
+        self._pos = to
+        if self.atEnd():
+            return self._src[start:]
+        else:
+            return self._src[start:to]
+
+    ## match and get methods
+        
+    def matchBackref(self):
+        return self.backrefRE.match(self.src(), self.pos())
+
+    def getBackref(self):
+        m = self.matchBackref()
+        self.setPos(m.end())
+        return m.group(1)
+        
+    def matchGroup(self):
+        return self.groupRE.match(self.src(), self.pos())
+
+    def getGroup(self):
+        m = self.matchGroup()
+        self.setPos(m.end())
+        return m.group(1)
+
+    ## main parse loop and the eat methods
+    
+    def parse(self):
+        while not self.atEnd():
+            if self.matchBackref():
+                self.eatBackref()
+            elif self.matchGroup():
+                self.eatGroup()
+            else:
+                self.eatStrConst()
+                
+    def eatStrConst(self):
+        startPos = self.pos()
+        while not self.atEnd():
+            if self.matchBackref() or self.matchGroup():
+                break
+            else:
+                self.advance()
+        strConst = self.readTo(self.pos(), start=startPos)
+        self.addChunk(repr(strConst))
+    
+    def eatBackref(self):
+        self.addChunk( 'm.group(' + self.getBackref() + ')' )
+
+    def eatGroup(self):
+        self.addChunk( 'm.group("' + self.getGroup() + '")' )
+    
+    def addChunk(self, chunk):
+        self._codeChunks.append(chunk)
+
+    ## code wrapping methods
+
+    def codeBody(self):
+        return ', '.join(self._codeChunks)
+
+    def code(self):
+        return "def subber(m):\n\treturn ''.join([%s])\n" % (self.codeBody())
+    
+    def subberFunc(self):
+        exec(self.code())
+        return subber
+
+
+class FindAndReplace:
+    
+    """Find and replace all instances of 'patternOrRE' with 'replacement' for
+    each file in the 'files' list. This is a multi-file version of re.sub().
+
+    'patternOrRE' can be a raw regex pattern or
+    a regex object as generated by the re module. 'replacement' can be any
+    string that would work with patternOrRE.sub(replacement, fileContents).
+    """
+    
+    def __init__(self, files, patternOrRE, replacement,
+                 recordResults=True):
+
+        
+        if isinstance(patternOrRE, basestring):
+            self._regex = re.compile(patternOrRE)
+        else:
+            self._regex = patternOrRE
+        if isinstance(replacement, basestring):
+            self._subber = _GenSubberFunc(replacement).subberFunc()
+        else:
+            self._subber = replacement
+
+        self._pattern = pattern = self._regex.pattern
+        self._files = files
+        self._results = {}
+        self._recordResults = recordResults
+
+        ## see if we should use pgrep to do the file matching
+        self._usePgrep = False
+        if (os.popen3('pgrep')[2].read()).startswith('Usage:'):
+            ## now check to make sure pgrep understands the pattern
+            tmpFile = mktemp()
+            open(tmpFile, 'w').write('#')
+            if not (os.popen3('pgrep "' + pattern + '" ' + tmpFile)[2].read()):
+                # it didn't print an error msg so we're ok
+                self._usePgrep = True
+            os.remove(tmpFile)
+
+        self._run()
+
+    def results(self):
+        return self._results
+    
+    def _run(self):
+        regex = self._regex
+        subber = self._subDispatcher
+        usePgrep = self._usePgrep
+        pattern = self._pattern
+        for file in self._files:
+            if not os.path.isfile(file):
+                continue # skip dirs etc.
+            
+            self._currFile = file
+            found = False
+            if 'orig' in locals():
+                del orig
+            if self._usePgrep:
+                if os.popen('pgrep "' + pattern + '" ' + file ).read():
+                    found = True
+            else:
+                orig = open(file).read()
+                if regex.search(orig):
+                    found = True
+            if found:
+                if 'orig' not in locals():
+                    orig = open(file).read()
+                new = regex.sub(subber, orig)
+                open(file, 'w').write(new)
+
+    def _subDispatcher(self, match):
+        if self._recordResults:
+            if self._currFile not in self._results:
+                res = self._results[self._currFile] = {}
+                res['count'] = 0
+                res['matches'] = []
+            else:
+                res = self._results[self._currFile]
+            res['count'] += 1
+            res['matches'].append({'contents': match.group(),
+                                   'start': match.start(),
+                                   'end': match.end(),
+                                   }
+                                   )
+        return self._subber(match)
+
+
+class SourceFileStats:
+
+    """
+    """
+    
+    _fileStats = None
+    
+    def __init__(self, files):
+        self._fileStats = stats = {}
+        for file in files:
+            stats[file] = self.getFileStats(file)
+
+    def rawStats(self):
+        return self._fileStats
+
+    def summary(self):
+        codeLines = 0
+        blankLines = 0
+        commentLines = 0
+        totalLines = 0
+        for fileStats in self.rawStats().values():
+            codeLines += fileStats['codeLines']
+            blankLines += fileStats['blankLines']
+            commentLines += fileStats['commentLines']
+            totalLines += fileStats['totalLines']
+            
+        stats = {'codeLines': codeLines,
+                 'blankLines': blankLines,
+                 'commentLines': commentLines,
+                 'totalLines': totalLines,
+                 }
+        return stats
+        
+    def printStats(self):
+        pass
+
+    def getFileStats(self, fileName):
+        codeLines = 0
+        blankLines = 0
+        commentLines = 0 
+        commentLineRe = re.compile(r'\s#.*$')
+        blankLineRe = re.compile('\s$')
+        lines = open(fileName).read().splitlines()
+        totalLines = len(lines)
+        
+        for line in lines:
+            if commentLineRe.match(line):
+                commentLines += 1
+            elif blankLineRe.match(line):
+                blankLines += 1
+            else:
+                codeLines += 1
+
+        stats = {'codeLines': codeLines,
+                 'blankLines': blankLines,
+                 'commentLines': commentLines,
+                 'totalLines': totalLines,
+                 }
+        
+        return stats
diff --git a/cheetah/Filters.py b/cheetah/Filters.py
new file mode 100644 (file)
index 0000000..47858b1
--- /dev/null
@@ -0,0 +1,212 @@
+'''
+    Filters for the #filter directive as well as #transform
+    
+    #filter results in output filters Cheetah's $placeholders .
+    #transform results in a filter on the entirety of the output
+'''
+import sys
+
+# Additional entities WebSafe knows how to transform.  No need to include
+# '<', '>' or '&' since those will have been done already.
+webSafeEntities = {' ': '&nbsp;', '"': '&quot;'}
+
+class Filter(object):
+    """A baseclass for the Cheetah Filters."""
+    
+    def __init__(self, template=None):
+        """Setup a reference to the template that is using the filter instance.
+        This reference isn't used by any of the standard filters, but is
+        available to Filter subclasses, should they need it.
+        
+        Subclasses should call this method.
+        """
+        self.template = template
+        
+    def filter(self, val, encoding=None, str=str, **kw):
+        '''
+            Pass Unicode strings through unmolested, unless an encoding is specified.
+        '''
+        if val is None:
+            return u''
+        if isinstance(val, unicode):
+            # ignore the encoding and return the unicode object
+            return val
+        else:
+            try:
+                return unicode(val)
+            except UnicodeDecodeError:
+                # we could put more fallbacks here, but we'll just pass the str
+                # on and let DummyTransaction worry about it
+                return str(val)
+
+RawOrEncodedUnicode = Filter
+
+EncodeUnicode = Filter
+
+class Markdown(EncodeUnicode):
+    '''
+        Markdown will change regular strings to Markdown
+            (http://daringfireball.net/projects/markdown/)
+
+        Such that:
+            My Header
+            =========
+        Becaomes:
+            <h1>My Header</h1>
+
+        and so on.
+
+        Markdown is meant to be used with the #transform 
+        tag, as it's usefulness with #filter is marginal at
+        best
+    '''
+    def filter(self,  value, **kwargs):
+        # This is a bit of a hack to allow outright embedding of the markdown module
+        try:
+            import markdown
+        except ImportError:
+            print('>>> Exception raised importing the "markdown" module')
+            print('>>> Are you sure you have the ElementTree module installed?')
+            print('          http://effbot.org/downloads/#elementtree')
+            raise
+
+        encoded = super(Markdown, self).filter(value, **kwargs)
+        return markdown.markdown(encoded)
+
+class CodeHighlighter(EncodeUnicode):
+    '''
+        The CodeHighlighter filter depends on the "pygments" module which you can 
+        download and install from: http://pygments.org
+
+        What the CodeHighlighter assumes the string that it's receiving is source
+        code and uses pygments.lexers.guess_lexer() to try to guess which parser
+        to use when highlighting it. 
+
+        CodeHighlighter will return the HTML and CSS to render the code block, syntax 
+        highlighted, in a browser
+
+        NOTE: I had an issue installing pygments on Linux/amd64/Python 2.6 dealing with
+        importing of pygments.lexers, I was able to correct the failure by adding:
+            raise ImportError
+        to line 39 of pygments/plugin.py (since importing pkg_resources was causing issues)
+    '''
+    def filter(self, source, **kwargs):
+        encoded = super(CodeHighlighter, self).filter(source, **kwargs)
+        try:
+            from pygments import highlight
+            from pygments import lexers
+            from pygments import formatters
+        except ImportError, ex:
+            print('<%s> - Failed to import pygments! (%s)' % (self.__class__.__name__, ex))
+            print('-- You may need to install it from: http://pygments.org')
+            return encoded
+
+        lexer = None
+        try:
+            lexer = lexers.guess_lexer(source)
+        except lexers.ClassNotFound:
+            lexer = lexers.PythonLexer()
+
+        formatter = formatters.HtmlFormatter(cssclass='code_highlighter')
+        encoded = highlight(encoded, lexer, formatter)
+        css = formatter.get_style_defs('.code_highlighter')
+        return '''<style type="text/css"><!--
+                %(css)s
+            --></style>%(source)s''' % {'css' : css, 'source' : encoded}
+
+
+
+class MaxLen(Filter):
+    def filter(self, val, **kw):
+        """Replace None with '' and cut off at maxlen."""
+        
+        output = super(MaxLen, self).filter(val, **kw)
+        if 'maxlen' in kw and len(output) > kw['maxlen']:
+            return output[:kw['maxlen']]
+        return output
+
+class WebSafe(Filter):
+    """Escape HTML entities in $placeholders.
+    """
+    def filter(self, val, **kw):
+        s = super(WebSafe, self).filter(val, **kw)
+        # These substitutions are copied from cgi.escape().
+        s = s.replace("&", "&amp;") # Must be done first!
+        s = s.replace("<", "&lt;")
+        s = s.replace(">", "&gt;")
+        # Process the additional transformations if any.
+        if 'also' in kw:
+            also = kw['also']
+            entities = webSafeEntities   # Global variable.
+            for k in also:
+                if k in entities:
+                    v = entities[k]
+                else:
+                    v = "&#%s;" % ord(k)
+                s = s.replace(k, v)
+        return s
+
+
+class Strip(Filter):
+    """Strip leading/trailing whitespace but preserve newlines.
+
+    This filter goes through the value line by line, removing leading and
+    trailing whitespace on each line.  It does not strip newlines, so every
+    input line corresponds to one output line, with its trailing newline intact.
+
+    We do not use val.split('\n') because that would squeeze out consecutive
+    blank lines.  Instead, we search for each newline individually.  This
+    makes us unable to use the fast C .split method, but it makes the filter
+    much more widely useful.
+
+    This filter is intended to be usable both with the #filter directive and
+    with the proposed #sed directive (which has not been ratified yet.)
+    """
+    def filter(self, val, **kw):
+        s = super(Strip, self).filter(val, **kw)
+        result = []
+        start = 0   # The current line will be s[start:end].
+        while True: # Loop through each line.
+            end = s.find('\n', start)  # Find next newline.
+            if end == -1:  # If no more newlines.
+                break
+            chunk = s[start:end].strip()
+            result.append(chunk)
+            result.append('\n')
+            start = end + 1
+        # Write the unfinished portion after the last newline, if any.
+        chunk = s[start:].strip()
+        result.append(chunk)
+        return "".join(result)
+
+class StripSqueeze(Filter):
+    """Canonicalizes every chunk of whitespace to a single space.
+
+    Strips leading/trailing whitespace.  Removes all newlines, so multi-line
+    input is joined into one ling line with NO trailing newline.
+    """
+    def filter(self, val, **kw):
+        s = super(StripSqueeze, self).filter(val, **kw)
+        s = s.split()
+        return " ".join(s)
+    
+##################################################
+## MAIN ROUTINE -- testing
+    
+def test():
+    s1 = "abc <=> &"
+    s2 = "   asdf  \n\t  1  2    3\n"
+    print("WebSafe INPUT:", repr(s1))
+    print("      WebSafe:", repr(WebSafe().filter(s1)))
+    
+    print()
+    print(" Strip INPUT:", repr(s2))
+    print("       Strip:", repr(Strip().filter(s2)))
+    print("StripSqueeze:", repr(StripSqueeze().filter(s2)))
+
+    print("Unicode:", repr(EncodeUnicode().filter(u'aoeu12345\u1234')))
+    
+if __name__ == "__main__":  
+    test()
+    
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/ImportHooks.py b/cheetah/ImportHooks.py
new file mode 100755 (executable)
index 0000000..0ae5141
--- /dev/null
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+
+"""
+Provides some import hooks to allow Cheetah's .tmpl files to be imported
+directly like Python .py modules.
+
+To use these:
+  import Cheetah.ImportHooks
+  Cheetah.ImportHooks.install()
+"""
+
+import sys
+import os.path
+import types
+import __builtin__
+import imp
+from threading import RLock
+import string
+import traceback
+import types
+
+from Cheetah import ImportManager
+from Cheetah.ImportManager import DirOwner
+from Cheetah.Compiler import Compiler
+from Cheetah.convertTmplPathToModuleName import convertTmplPathToModuleName
+
+_installed = False
+
+##################################################
+## HELPER FUNCS
+
+_cacheDir = []
+def setCacheDir(cacheDir):
+    global _cacheDir
+    _cacheDir.append(cacheDir)
+    
+##################################################
+## CLASSES
+
+class CheetahDirOwner(DirOwner):
+    _lock = RLock()
+    _acquireLock = _lock.acquire
+    _releaseLock = _lock.release
+
+    templateFileExtensions = ('.tmpl',)
+
+    def getmod(self, name):
+        self._acquireLock()
+        try:        
+            mod = DirOwner.getmod(self, name)
+            if mod:
+                return mod
+
+            for ext in self.templateFileExtensions:
+                tmplPath =  os.path.join(self.path, name + ext)
+                if os.path.exists(tmplPath):
+                    try:
+                        return self._compile(name, tmplPath)
+                    except:
+                        # @@TR: log the error
+                        exc_txt = traceback.format_exc()
+                        exc_txt ='  '+('  \n'.join(exc_txt.splitlines()))
+                        raise ImportError(
+                            'Error while compiling Cheetah module'
+                        ' %(name)s, original traceback follows:\n%(exc_txt)s'%locals())
+            ##
+            return None
+
+        finally:
+            self._releaseLock()          
+
+    def _compile(self, name, tmplPath):
+        ## @@ consider adding an ImportError raiser here
+        code = str(Compiler(file=tmplPath, moduleName=name,
+                            mainClassName=name))
+        if _cacheDir:
+            __file__ = os.path.join(_cacheDir[0],
+                                    convertTmplPathToModuleName(tmplPath)) + '.py'
+            try:
+                open(__file__, 'w').write(code)
+            except OSError:
+                ## @@ TR: need to add some error code here
+                traceback.print_exc(file=sys.stderr)
+                __file__ = tmplPath
+        else:
+            __file__ = tmplPath
+        co = compile(code+'\n', __file__, 'exec')
+
+        mod = types.ModuleType(name)
+        mod.__file__ = co.co_filename
+        if _cacheDir:
+            mod.__orig_file__ = tmplPath # @@TR: this is used in the WebKit
+                                         # filemonitoring code
+        mod.__co__ = co
+        return mod
+        
+
+##################################################
+## FUNCTIONS
+
+def install(templateFileExtensions=('.tmpl',)):
+    """Install the Cheetah Import Hooks"""
+
+    global _installed
+    if not _installed:
+        CheetahDirOwner.templateFileExtensions = templateFileExtensions
+        import __builtin__
+        if isinstance(__builtin__.__import__, types.BuiltinFunctionType):
+            global __oldimport__
+            __oldimport__ = __builtin__.__import__
+            ImportManager._globalOwnerTypes.insert(0, CheetahDirOwner)
+            #ImportManager._globalOwnerTypes.append(CheetahDirOwner)            
+            global _manager
+            _manager=ImportManager.ImportManager()
+            _manager.setThreaded()
+            _manager.install()
+        
+def uninstall():
+    """Uninstall the Cheetah Import Hooks"""    
+    global _installed
+    if not _installed:
+        import __builtin__
+        if isinstance(__builtin__.__import__, types.MethodType):
+            __builtin__.__import__ = __oldimport__
+            global _manager
+            del _manager
+
+if __name__ == '__main__':
+    install()
diff --git a/cheetah/ImportManager.py b/cheetah/ImportManager.py
new file mode 100755 (executable)
index 0000000..a043cce
--- /dev/null
@@ -0,0 +1,541 @@
+"""
+Provides an emulator/replacement for Python's standard import system.
+
+@@TR: Be warned that Import Hooks are in the deepest, darkest corner of Python's
+jungle.  If you need to start hacking with this, be prepared to get lost for a
+while. Also note, this module predates the newstyle import hooks in Python 2.3
+http://www.python.org/peps/pep-0302.html.  
+
+
+This is a hacked/documented version of Gordon McMillan's iu.py. I have:
+
+  - made it a little less terse
+
+  - added docstrings and explanatations
+
+  - standardized the variable naming scheme
+
+  - reorganized the code layout to enhance readability
+
+""" 
+
+import sys
+import imp
+import marshal
+
+_installed = False
+
+# _globalOwnerTypes is defined at the bottom of this file
+
+_os_stat = _os_path_join = _os_getcwd = _os_path_dirname = None
+
+##################################################
+## FUNCTIONS
+
+def _os_bootstrap():
+    """Set up 'os' module replacement functions for use during import bootstrap."""
+
+    names = sys.builtin_module_names
+
+    join = dirname = None
+    if 'posix' in names:
+        sep = '/'
+        from posix import stat, getcwd
+    elif 'nt' in names:
+        sep = '\\'
+        from nt import stat, getcwd
+    elif 'dos' in names:
+        sep = '\\'
+        from dos import stat, getcwd
+    elif 'os2' in names:
+        sep = '\\'
+        from os2 import stat, getcwd
+    elif 'mac' in names:
+        from mac import stat, getcwd
+        def join(a, b):
+            if a == '':
+                return b
+            if ':' not in a:
+                a = ':' + a
+            if a[-1:] != ':':
+                a = a + ':'
+            return a + b
+    else:
+        raise ImportError('no os specific module found')
+
+    if join is None:
+        def join(a, b, sep=sep):
+            if a == '':
+                return b
+            lastchar = a[-1:]
+            if lastchar == '/' or lastchar == sep:
+                return a + b
+            return a + sep + b
+
+    if dirname is None:
+        def dirname(a, sep=sep):
+            for i in range(len(a)-1, -1, -1):
+                c = a[i]
+                if c == '/' or c == sep:
+                    return a[:i]
+            return ''
+    
+    global _os_stat
+    _os_stat = stat
+
+    global _os_path_join
+    _os_path_join = join
+
+    global _os_path_dirname
+    _os_path_dirname = dirname
+    
+    global _os_getcwd
+    _os_getcwd = getcwd
+    
+_os_bootstrap()
+
+def packageName(s):
+    for i in range(len(s)-1, -1, -1):
+        if s[i] == '.':
+            break
+    else:
+        return ''
+    return s[:i]
+
+def nameSplit(s):
+    rslt = []
+    i = j = 0
+    for j in range(len(s)):
+        if s[j] == '.':
+            rslt.append(s[i:j])
+            i = j+1
+    if i < len(s):
+        rslt.append(s[i:])
+    return rslt
+
+def getPathExt(fnm):
+    for i in range(len(fnm)-1, -1, -1):
+        if fnm[i] == '.':
+            return fnm[i:]
+    return ''
+
+def pathIsDir(pathname):
+    "Local replacement for os.path.isdir()."
+    try:
+        s = _os_stat(pathname)
+    except OSError:
+        return None
+    return (s[0] & 0170000) == 0040000
+
+def getDescr(fnm):
+    ext = getPathExt(fnm)
+    for (suffix, mode, typ) in imp.get_suffixes():
+        if suffix == ext:
+            return (suffix, mode, typ)
+
+##################################################
+## CLASSES
+
+class Owner:
+    
+    """An Owner does imports from a particular piece of turf That is, there's
+    an Owner for each thing on sys.path There are owners for directories and
+    .pyz files.  There could be owners for zip files, or even URLs.  A
+    shadowpath (a dictionary mapping the names in sys.path to their owners) is
+    used so that sys.path (or a package's __path__) is still a bunch of strings,
+    """
+    
+    def __init__(self, path):
+        self.path = path
+
+    def __str__(self):
+        return self.path
+    
+    def getmod(self, nm):
+        return None
+    
+class DirOwner(Owner):
+    
+    def __init__(self, path):
+        if path == '':
+            path = _os_getcwd()
+        if not pathIsDir(path):
+            raise ValueError("%s is not a directory" % path)
+        Owner.__init__(self, path)
+        
+    def getmod(self, nm,
+               getsuffixes=imp.get_suffixes, loadco=marshal.loads, newmod=imp.new_module):
+        
+        pth =  _os_path_join(self.path, nm)
+
+        possibles = [(pth, 0, None)]
+        if pathIsDir(pth):
+            possibles.insert(0, (_os_path_join(pth, '__init__'), 1, pth))
+        py = pyc = None
+        for pth, ispkg, pkgpth in possibles:
+            for ext, mode, typ in getsuffixes():
+                attempt = pth+ext
+                try:
+                    st = _os_stat(attempt)
+                except:
+                    pass
+                else:
+                    if typ == imp.C_EXTENSION:
+                        fp = open(attempt, 'rb')
+                        mod = imp.load_module(nm, fp, attempt, (ext, mode, typ))
+                        mod.__file__ = attempt
+                        return mod
+                    elif typ == imp.PY_SOURCE:
+                        py = (attempt, st)
+                    else:
+                        pyc = (attempt, st)
+            if py or pyc:
+                break
+        if py is None and pyc is None:
+            return None
+        while True:
+            if pyc is None or py and pyc[1][8] < py[1][8]:
+                try:
+                    co = compile(open(py[0], 'r').read()+'\n', py[0], 'exec')
+                    break
+                except SyntaxError, e:
+                    print("Invalid syntax in %s" % py[0])
+                    print(e.args)
+                    raise
+            elif pyc:
+                stuff = open(pyc[0], 'rb').read()
+                try:
+                    co = loadco(stuff[8:])
+                    break
+                except (ValueError, EOFError):
+                    pyc = None
+            else:
+                return None
+        mod = newmod(nm)
+        mod.__file__ = co.co_filename
+        if ispkg:
+            mod.__path__ = [pkgpth]
+            subimporter = PathImportDirector(mod.__path__)
+            mod.__importsub__ = subimporter.getmod
+        mod.__co__ = co
+        return mod
+
+
+class ImportDirector(Owner):
+    """ImportDirectors live on the metapath There's one for builtins, one for
+    frozen modules, and one for sys.path Windows gets one for modules gotten
+    from the Registry Mac would have them for PY_RESOURCE modules etc.  A
+    generalization of Owner - their concept of 'turf' is broader"""
+
+    pass
+
+class BuiltinImportDirector(ImportDirector):
+    """Directs imports of builtin modules"""
+    def __init__(self):
+        self.path = 'Builtins'
+
+    def getmod(self, nm, isbuiltin=imp.is_builtin):
+        if isbuiltin(nm):
+            mod = imp.load_module(nm, None, nm, ('', '', imp.C_BUILTIN))
+            return mod
+        return None
+
+class FrozenImportDirector(ImportDirector):
+    """Directs imports of frozen modules"""
+    
+    def __init__(self):
+        self.path = 'FrozenModules'
+
+    def getmod(self, nm,
+               isFrozen=imp.is_frozen, loadMod=imp.load_module):
+        if isFrozen(nm):
+            mod = loadMod(nm, None, nm, ('', '', imp.PY_FROZEN))
+            if hasattr(mod, '__path__'):
+                mod.__importsub__ = lambda name, pname=nm, owner=self: owner.getmod(pname+'.'+name)
+            return mod
+        return None
+
+
+class RegistryImportDirector(ImportDirector):
+    """Directs imports of modules stored in the Windows Registry"""
+
+    def __init__(self):
+        self.path = "WindowsRegistry"
+        self.map = {}
+        try:
+            import win32api
+            ## import win32con
+        except ImportError:
+            pass
+        else:
+            HKEY_CURRENT_USER = -2147483647
+            HKEY_LOCAL_MACHINE = -2147483646
+            KEY_ALL_ACCESS = 983103
+            subkey = r"Software\Python\PythonCore\%s\Modules" % sys.winver
+            for root in (HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE):
+                try:
+                    hkey = win32api.RegOpenKeyEx(root, subkey, 0, KEY_ALL_ACCESS)
+                except:
+                    pass
+                else:
+                    numsubkeys, numvalues, lastmodified = win32api.RegQueryInfoKey(hkey)
+                    for i in range(numsubkeys):
+                        subkeyname = win32api.RegEnumKey(hkey, i)
+                        hskey = win32api.RegOpenKeyEx(hkey, subkeyname, 0, KEY_ALL_ACCESS)
+                        val = win32api.RegQueryValueEx(hskey, '')
+                        desc = getDescr(val[0])
+                        self.map[subkeyname] = (val[0], desc)
+                        hskey.Close()
+                    hkey.Close()
+                    break
+                
+    def getmod(self, nm):
+        stuff = self.map.get(nm)
+        if stuff:
+            fnm, desc = stuff
+            fp = open(fnm, 'rb')
+            mod = imp.load_module(nm, fp, fnm, desc)
+            mod.__file__ = fnm
+            return mod
+        return None
+    
+class PathImportDirector(ImportDirector):
+    """Directs imports of modules stored on the filesystem."""
+
+    def __init__(self, pathlist=None, importers=None, ownertypes=None):
+        if pathlist is None:
+            self.path = sys.path
+        else:
+            self.path = pathlist
+        if ownertypes == None:
+            self._ownertypes = _globalOwnerTypes
+        else:
+            self._ownertypes = ownertypes
+        if importers:
+            self._shadowPath = importers
+        else:
+            self._shadowPath = {}
+        self._inMakeOwner = False
+        self._building = {}
+        
+    def getmod(self, nm):
+        mod = None
+        for thing in self.path:
+            if isinstance(thing, basestring):
+                owner = self._shadowPath.get(thing, -1)
+                if owner == -1:
+                    owner = self._shadowPath[thing] = self._makeOwner(thing)
+                if owner:
+                    mod = owner.getmod(nm)
+            else:
+                mod = thing.getmod(nm)
+            if mod:
+                break
+        return mod
+    
+    def _makeOwner(self, path):
+        if self._building.get(path):
+            return None
+        self._building[path] = 1
+        owner = None
+        for klass in self._ownertypes:
+            try:
+                # this may cause an import, which may cause recursion
+                # hence the protection
+                owner = klass(path)
+            except:
+                pass
+            else:
+                break
+        del self._building[path]
+        return owner
+
+#=================ImportManager============================#
+# The one-and-only ImportManager
+# ie, the builtin import
+
+UNTRIED = -1
+
+class ImportManager:
+    # really the equivalent of builtin import
+    def __init__(self):
+        self.metapath = [
+            BuiltinImportDirector(),
+            FrozenImportDirector(),
+            RegistryImportDirector(),
+            PathImportDirector()
+        ]
+        self.threaded = 0
+        self.rlock = None
+        self.locker = None
+        self.setThreaded()
+        
+    def setThreaded(self):
+        thread = sys.modules.get('thread', None)
+        if thread and not self.threaded:
+            self.threaded = 1
+            self.rlock = thread.allocate_lock()
+            self._get_ident = thread.get_ident
+            
+    def install(self):
+        import __builtin__
+        __builtin__.__import__ = self.importHook
+        __builtin__.reload = self.reloadHook
+        
+    def importHook(self, name, globals=None, locals=None, fromlist=None, level=-1):
+        '''
+            NOTE: Currently importHook will accept the keyword-argument "level" 
+            but it will *NOT* use it (currently). Details about the "level" keyword
+            argument can be found here: http://www.python.org/doc/2.5.2/lib/built-in-funcs.html
+        '''
+        # first see if we could be importing a relative name
+        #print "importHook(%s, %s, locals, %s)" % (name, globals['__name__'], fromlist)
+        _sys_modules_get = sys.modules.get
+        contexts = [None]
+        if globals:
+            importernm = globals.get('__name__', '')
+            if importernm:
+                if hasattr(_sys_modules_get(importernm), '__path__'):
+                    contexts.insert(0, importernm)
+                else:
+                    pkgnm = packageName(importernm)
+                    if pkgnm:
+                        contexts.insert(0, pkgnm)
+        # so contexts is [pkgnm, None] or just [None]
+        # now break the name being imported up so we get:
+        # a.b.c -> [a, b, c]
+        nmparts = nameSplit(name)
+        _self_doimport = self.doimport
+        threaded = self.threaded
+        for context in contexts:
+            ctx = context
+            for i in range(len(nmparts)):
+                nm = nmparts[i]
+                #print " importHook trying %s in %s" % (nm, ctx)
+                if ctx:
+                    fqname = ctx + '.' + nm
+                else:
+                    fqname = nm
+                if threaded:
+                    self._acquire()
+                mod = _sys_modules_get(fqname, UNTRIED)
+                if mod is UNTRIED:
+                    mod = _self_doimport(nm, ctx, fqname)
+                if threaded:
+                    self._release()
+                if mod:
+                    ctx = fqname
+                else:
+                    break
+            else:
+                # no break, point i beyond end
+                i = i + 1
+            if i:
+                break
+            
+        if i<len(nmparts):
+            if ctx and hasattr(sys.modules[ctx], nmparts[i]):
+                #print "importHook done with %s %s %s (case 1)" % (name, globals['__name__'], fromlist)
+                return sys.modules[nmparts[0]]
+            del sys.modules[fqname]
+            raise ImportError("No module named %s" % fqname)
+        if fromlist is None: 
+            #print "importHook done with %s %s %s (case 2)" % (name, globals['__name__'], fromlist)
+            if context:
+                return sys.modules[context+'.'+nmparts[0]]
+            return sys.modules[nmparts[0]]
+        bottommod = sys.modules[ctx]
+        if hasattr(bottommod, '__path__'):
+            fromlist = list(fromlist)
+            i = 0
+            while i < len(fromlist):
+                nm = fromlist[i]
+                if nm == '*':
+                    fromlist[i:i+1] = list(getattr(bottommod, '__all__', []))
+                    if i >= len(fromlist):
+                        break
+                    nm = fromlist[i]
+                i = i + 1
+                if not hasattr(bottommod, nm):
+                    if self.threaded:
+                        self._acquire()
+                    mod = self.doimport(nm, ctx, ctx+'.'+nm)
+                    if self.threaded:
+                        self._release()
+                    if not mod:
+                        raise ImportError("%s not found in %s" % (nm, ctx))
+        #print "importHook done with %s %s %s (case 3)" % (name, globals['__name__'], fromlist)
+        return bottommod
+    
+    def doimport(self, nm, parentnm, fqname):
+        # Not that nm is NEVER a dotted name at this point
+        #print "doimport(%s, %s, %s)" % (nm, parentnm, fqname)
+        if parentnm:
+            parent = sys.modules[parentnm]
+            if hasattr(parent, '__path__'):
+                importfunc = getattr(parent, '__importsub__', None)
+                if not importfunc:
+                    subimporter = PathImportDirector(parent.__path__)
+                    importfunc = parent.__importsub__ = subimporter.getmod
+                mod = importfunc(nm)
+                if mod:
+                    setattr(parent, nm, mod)
+            else:
+                #print "..parent not a package"
+                return None
+        else:
+            # now we're dealing with an absolute import
+            for director in self.metapath:
+                mod = director.getmod(nm)
+                if mod:
+                    break
+        if mod:
+            mod.__name__ = fqname
+            sys.modules[fqname] = mod
+            if hasattr(mod, '__co__'):
+                co = mod.__co__
+                del mod.__co__
+                exec(co, mod.__dict__)
+            if fqname == 'thread' and not self.threaded:
+##                print "thread detected!"
+                self.setThreaded()
+        else:
+            sys.modules[fqname] = None
+        #print "..found %s" % mod
+        return mod
+    
+    def reloadHook(self, mod):
+        fqnm = mod.__name__
+        nm = nameSplit(fqnm)[-1]
+        parentnm = packageName(fqnm)
+        newmod = self.doimport(nm, parentnm, fqnm)
+        mod.__dict__.update(newmod.__dict__)
+##        return newmod
+        
+    def _acquire(self):
+        if self.rlock.locked():
+            if self.locker == self._get_ident():
+                self.lockcount = self.lockcount + 1
+##                print "_acquire incrementing lockcount to", self.lockcount
+                return
+        self.rlock.acquire()
+        self.locker = self._get_ident()
+        self.lockcount = 0
+##        print "_acquire first time!"
+        
+    def _release(self):
+        if self.lockcount:
+            self.lockcount = self.lockcount - 1
+##            print "_release decrementing lockcount to", self.lockcount
+        else:
+            self.rlock.release()
+##            print "_release releasing lock!"
+
+
+##################################################
+## MORE CONSTANTS & GLOBALS
+
+_globalOwnerTypes = [
+    DirOwner,
+    Owner,
+]
diff --git a/cheetah/Macros/I18n.py b/cheetah/Macros/I18n.py
new file mode 100644 (file)
index 0000000..7c2b1ef
--- /dev/null
@@ -0,0 +1,67 @@
+import gettext
+_ = gettext.gettext
+class I18n(object):
+    def __init__(self, parser):
+        pass
+
+## junk I'm playing with to test the macro framework 
+#    def parseArgs(self, parser, startPos):
+#        parser.getWhiteSpace()
+#        args = parser.getExpression(useNameMapper=False,
+#                                    pyTokensToBreakAt=[':']).strip()
+#        return args
+#
+#    def convertArgStrToDict(self, args, parser=None, startPos=None):
+#        def getArgs(*pargs, **kws):
+#            return pargs, kws
+#        exec 'positionalArgs, kwArgs = getArgs(%(args)s)'%locals()
+#        return kwArgs
+
+    def __call__(self,
+                 src, # aka message,
+                 plural=None,
+                 n=None, # should be a string representing the name of the
+                          # '$var' rather than $var itself                   
+                 id=None,
+                 domain=None,
+                 source=None,
+                 target=None,
+                 comment=None,
+
+                 # args that are automatically supplied by the parser when the
+                 # macro is called:
+                 parser=None,
+                 macros=None,
+                 isShortForm=False,
+                 EOLCharsInShortForm=None,
+                 startPos=None,
+                 endPos=None,
+             ):
+        """This is just a stub at this time.
+
+          plural = the plural form of the message
+          n = a sized argument to distinguish between single and plural forms           
+
+          id = msgid in the translation catalog
+          domain = translation domain
+          source = source lang
+          target = a specific target lang
+          comment = a comment to the translation team
+
+        See the following for some ideas
+        http://www.zope.org/DevHome/Wikis/DevSite/Projects/ComponentArchitecture/ZPTInternationalizationSupport
+
+        Other notes:
+        - There is no need to replicate the i18n:name attribute from plone / PTL,
+          as cheetah placeholders serve the same purpose
+    
+    
+       """
+        
+        #print macros['i18n']
+        src = _(src)
+        if isShortForm and endPos<len(parser):
+            return src+EOLCharsInShortForm
+        else:
+            return src
+        
diff --git a/cheetah/Macros/__init__.py b/cheetah/Macros/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/cheetah/NameMapper.py b/cheetah/NameMapper.py
new file mode 100644 (file)
index 0000000..e4c773e
--- /dev/null
@@ -0,0 +1,366 @@
+#!/usr/bin/env python
+"""This module supports Cheetah's optional NameMapper syntax.
+
+Overview
+================================================================================
+
+NameMapper provides a simple syntax for accessing Python data structures,
+functions, and methods from Cheetah. It's called NameMapper because it 'maps'
+simple 'names' in Cheetah templates to possibly more complex syntax in Python.
+
+Its purpose is to make working with Cheetah easy for non-programmers.
+Specifically, non-programmers using Cheetah should NOT need to be taught (a)
+what the difference is between an object and a dictionary, (b) what functions
+and methods are, and (c) what 'self' is.  A further aim (d) is to buffer the
+code in Cheetah templates from changes in the implementation of the Python data
+structures behind them.
+
+Consider this scenario:
+
+You are building a customer information system. The designers with you want to
+use information from your system on the client's website --AND-- they want to
+understand the display code and so they can maintian it themselves.
+
+You write a UI class with a 'customers' method that returns a dictionary of all
+the customer objects.  Each customer object has an 'address' method that returns
+the a dictionary with information about the customer's address.  The designers
+want to be able to access that information.
+
+Using PSP, the display code for the website would look something like the
+following, assuming your servlet subclasses the class you created for managing
+customer information:
+
+  <%= self.customer()[ID].address()['city'] %>   (42 chars)
+
+Using Cheetah's NameMapper syntax it could be any of the following:
+
+   $self.customers()[$ID].address()['city']       (39 chars)
+   --OR--
+   $customers()[$ID].address()['city']
+   --OR--
+   $customers()[$ID].address().city
+   --OR--
+   $customers()[$ID].address.city
+   --OR--
+   $customers()[$ID].address.city
+   --OR--
+   $customers[$ID].address.city                   (27 chars)
+
+
+Which of these would you prefer to explain to the designers, who have no
+programming experience?  The last form is 15 characters shorter than the PSP
+and, conceptually, is far more accessible. With PHP or ASP, the code would be
+even messier than the PSP
+
+This is a rather extreme example and, of course, you could also just implement
+'$getCustomer($ID).city' and obey the Law of Demeter (search Google for more on that).
+But good object orientated design isn't the point here.
+
+Details
+================================================================================
+The parenthesized letters below correspond to the aims in the second paragraph.
+
+DICTIONARY ACCESS (a)
+---------------------
+
+NameMapper allows access to items in a dictionary using the same dotted notation
+used to access object attributes in Python.  This aspect of NameMapper is known
+as 'Unified Dotted Notation'.
+
+For example, with Cheetah it is possible to write:
+   $customers()['kerr'].address()  --OR--  $customers().kerr.address()
+where the second form is in NameMapper syntax.
+
+This only works with dictionary keys that are also valid python identifiers:
+  regex = '[a-zA-Z_][a-zA-Z_0-9]*'
+
+
+AUTOCALLING (b,d)
+-----------------
+
+NameMapper automatically detects functions and methods in Cheetah $vars and calls
+them if the parentheses have been left off.
+
+For example if 'a' is an object, 'b' is a method
+  $a.b
+is equivalent to
+  $a.b()
+
+If b returns a dictionary, then following variations are possible
+  $a.b.c  --OR--  $a.b().c  --OR--  $a.b()['c']
+where 'c' is a key in the dictionary that a.b() returns.
+
+Further notes:
+* NameMapper autocalls the function or method without any arguments.  Thus
+autocalling can only be used with functions or methods that either have no
+arguments or have default values for all arguments.
+
+* NameMapper only autocalls functions and methods.  Classes and callable object instances
+will not be autocalled.
+
+* Autocalling can be disabled using Cheetah's 'useAutocalling' setting.
+
+LEAVING OUT 'self' (c,d)
+------------------------
+
+NameMapper makes it possible to access the attributes of a servlet in Cheetah
+without needing to include 'self' in the variable names.  See the NAMESPACE
+CASCADING section below for details.
+
+NAMESPACE CASCADING (d)
+--------------------
+...
+
+Implementation details
+================================================================================
+
+* NameMapper's search order is dictionary keys then object attributes
+
+* NameMapper.NotFound is raised if a value can't be found for a name.
+
+Performance and the C version
+================================================================================
+
+Cheetah comes with both a C version and a Python version of NameMapper.  The C
+version is significantly faster and the exception tracebacks are much easier to
+read.  It's still slower than standard Python syntax, but you won't notice the
+difference in realistic usage scenarios.
+
+Cheetah uses the optimized C version (_namemapper.c) if it has
+been compiled or falls back to the Python version if not.
+"""
+
+__author__ = "Tavis Rudd <tavis@damnsimple.com>," +\
+             "\nChuck Esterbrook <echuck@mindspring.com>"
+from pprint import pformat
+import inspect
+
+_INCLUDE_NAMESPACE_REPR_IN_NOTFOUND_EXCEPTIONS = False
+_ALLOW_WRAPPING_OF_NOTFOUND_EXCEPTIONS = True
+__all__ = ['NotFound',
+           'hasKey',
+           'valueForKey',
+           'valueForName',
+           'valueFromSearchList',
+           'valueFromFrameOrSearchList',
+           'valueFromFrame',
+           ]
+
+if not hasattr(inspect.imp, 'get_suffixes'):
+    # This is to fix broken behavior of the inspect module under the
+    # Google App Engine, see the following issue:
+    # http://bugs.communitycheetah.org/view.php?id=10
+    setattr(inspect.imp, 'get_suffixes', lambda: [('.py', 'U', 1)])
+
+## N.B. An attempt is made at the end of this module to import C versions of
+## these functions.  If _namemapper.c has been compiled succesfully and the
+## import goes smoothly, the Python versions defined here will be replaced with
+## the C versions.
+
+class NotFound(LookupError):
+    pass
+
+def _raiseNotFoundException(key, namespace):
+    excString = "cannot find '%s'"%key
+    if _INCLUDE_NAMESPACE_REPR_IN_NOTFOUND_EXCEPTIONS:
+        excString += ' in the namespace %s'%pformat(namespace)
+    raise NotFound(excString)
+
+def _wrapNotFoundException(exc, fullName, namespace):
+    if not _ALLOW_WRAPPING_OF_NOTFOUND_EXCEPTIONS:
+        raise
+    else:
+        excStr = exc.args[0]
+        if excStr.find('while searching')==-1: # only wrap once!
+            excStr +=" while searching for '%s'"%fullName
+            if _INCLUDE_NAMESPACE_REPR_IN_NOTFOUND_EXCEPTIONS:
+                excStr += ' in the namespace %s'%pformat(namespace)
+            exc.args = (excStr,)
+        raise
+
+def _isInstanceOrClass(obj):
+    if isinstance(obj, type):
+        # oldstyle
+        return True
+
+    if hasattr(obj, "__class__"):
+        # newstyle
+        if hasattr(obj, 'mro'):
+            # type/class
+            return True
+        elif (hasattr(obj, 'im_func') or hasattr(obj, 'func_code') or hasattr(obj, '__self__')):
+            # method, func, or builtin func
+            return False
+        elif hasattr(obj, '__init__'):
+            # instance
+            return True
+    return False
+
+def hasKey(obj, key):
+    """Determine if 'obj' has 'key' """
+    if hasattr(obj, 'has_key') and key in obj:
+        return True
+    elif hasattr(obj, key):
+        return True
+    else:
+        return False
+
+def valueForKey(obj, key):
+    if hasattr(obj, 'has_key') and key in obj:
+        return obj[key]
+    elif hasattr(obj, key):
+        return getattr(obj, key)
+    else:
+        _raiseNotFoundException(key, obj)
+
+def _valueForName(obj, name, executeCallables=False):
+    nameChunks=name.split('.')
+    for i in range(len(nameChunks)):
+        key = nameChunks[i]
+        if hasattr(obj, 'has_key') and key in obj:
+            nextObj = obj[key]
+        else:
+            try:
+                nextObj = getattr(obj, key)
+            except AttributeError:
+                _raiseNotFoundException(key, obj)
+
+        if executeCallables and hasattr(nextObj, '__call__') and not _isInstanceOrClass(nextObj):
+            obj = nextObj()
+        else:
+            obj = nextObj
+    return obj
+
+def valueForName(obj, name, executeCallables=False):
+    try:
+        return _valueForName(obj, name, executeCallables)
+    except NotFound, e:
+        _wrapNotFoundException(e, fullName=name, namespace=obj)
+
+def valueFromSearchList(searchList, name, executeCallables=False):
+    key = name.split('.')[0]
+    for namespace in searchList:
+        if hasKey(namespace, key):
+            return _valueForName(namespace, name,
+                                executeCallables=executeCallables)
+    _raiseNotFoundException(key, searchList)
+
+def _namespaces(callerFrame, searchList=None):
+    yield callerFrame.f_locals
+    if searchList:
+        for namespace in searchList:
+            yield namespace
+    yield callerFrame.f_globals
+    yield __builtins__
+
+def valueFromFrameOrSearchList(searchList, name, executeCallables=False,
+                               frame=None):
+    def __valueForName():
+        try:
+            return _valueForName(namespace, name, executeCallables=executeCallables)
+        except NotFound, e:
+            _wrapNotFoundException(e, fullName=name, namespace=searchList)
+    try:
+        if not frame:
+            frame = inspect.stack()[1][0]
+        key = name.split('.')[0]
+        for namespace in _namespaces(frame, searchList):
+            if hasKey(namespace, key):
+                return __valueForName()
+        _raiseNotFoundException(key, searchList)
+    finally:
+        del frame
+
+def valueFromFrame(name, executeCallables=False, frame=None):
+    # @@TR consider implementing the C version the same way
+    # at the moment it provides a seperate but mirror implementation
+    # to valueFromFrameOrSearchList
+    try:
+        if not frame:
+            frame = inspect.stack()[1][0]
+        return valueFromFrameOrSearchList(searchList=None,
+                                          name=name,
+                                          executeCallables=executeCallables,
+                                          frame=frame)
+    finally:
+        del frame
+
+def hasName(obj, name):
+    #Not in the C version
+    """Determine if 'obj' has the 'name' """
+    key = name.split('.')[0]
+    if not hasKey(obj, key):
+        return False
+    try:
+        valueForName(obj, name)
+        return True
+    except NotFound:
+        return False
+try:
+    from Cheetah._namemapper import NotFound, valueForKey, valueForName, \
+         valueFromSearchList, valueFromFrameOrSearchList, valueFromFrame
+    # it is possible with Jython or Windows, for example, that _namemapper.c hasn't been compiled
+    C_VERSION = True
+except:
+    C_VERSION = False
+
+##################################################
+## CLASSES
+
+class Mixin:
+    """@@ document me"""
+    def valueForName(self, name):
+        return valueForName(self, name)
+
+    def valueForKey(self, key):
+        return valueForKey(self, key)
+
+##################################################
+## if run from the command line ##
+
+def example():
+    class A(Mixin):
+        classVar = 'classVar val'
+        def method(self,arg='method 1 default arg'):
+            return arg
+
+        def method2(self, arg='meth 2 default arg'):
+            return {'item1':arg}
+
+        def method3(self, arg='meth 3 default'):
+            return arg
+
+    class B(A):
+        classBvar = 'classBvar val'
+
+    a = A()
+    a.one = 'valueForOne'
+    def function(whichOne='default'):
+        values = {
+            'default': 'default output',
+            'one': 'output option one',
+            'two': 'output option two'
+            }
+        return values[whichOne]
+
+    a.dic = {
+        'func': function,
+        'method': a.method3,
+        'item': 'itemval',
+        'subDict': {'nestedMethod':a.method3}
+        }
+    b = 'this is local b'
+
+    print(valueForKey(a.dic, 'subDict'))
+    print(valueForName(a, 'dic.item'))
+    print(valueForName(vars(), 'b'))
+    print(valueForName(__builtins__, 'dir')())
+    print(valueForName(vars(), 'a.classVar'))
+    print(valueForName(vars(), 'a.dic.func', executeCallables=True))
+    print(valueForName(vars(), 'a.method2.item1', executeCallables=True))
+
+if __name__ == '__main__':
+    example()
+
+
+
diff --git a/cheetah/Parser.py b/cheetah/Parser.py
new file mode 100644 (file)
index 0000000..98bceda
--- /dev/null
@@ -0,0 +1,2661 @@
+#!/usr/bin/env python
+"""
+Parser classes for Cheetah's Compiler
+
+Classes:
+  ParseError( Exception )
+  _LowLevelParser( Cheetah.SourceReader.SourceReader ), basically a lexer
+  _HighLevelParser( _LowLevelParser )
+  Parser === _HighLevelParser (an alias)
+"""
+
+import os
+import sys
+import re
+from re import DOTALL, MULTILINE
+import types
+import time
+from tokenize import pseudoprog
+import inspect
+import traceback
+
+from Cheetah.SourceReader import SourceReader
+from Cheetah import Filters
+from Cheetah import ErrorCatchers
+from Cheetah.Unspecified import Unspecified
+from Cheetah.Macros.I18n import I18n
+
+# re tools
+_regexCache = {}
+def cachedRegex(pattern):
+    if pattern not in _regexCache:
+        _regexCache[pattern] = re.compile(pattern)
+    return _regexCache[pattern]
+
+def escapeRegexChars(txt,
+                     escapeRE=re.compile(r'([\$\^\*\+\.\?\{\}\[\]\(\)\|\\])')):
+    
+    """Return a txt with all special regular expressions chars escaped."""
+    
+    return escapeRE.sub(r'\\\1', txt)
+
+def group(*choices): return '(' + '|'.join(choices) + ')'
+def nongroup(*choices): return '(?:' + '|'.join(choices) + ')'
+def namedGroup(name, *choices): return '(P:<' + name +'>' + '|'.join(choices) + ')'
+def any(*choices): return group(*choices) + '*'
+def maybe(*choices): return group(*choices) + '?'
+
+##################################################
+## CONSTANTS & GLOBALS ##
+
+NO_CACHE = 0
+STATIC_CACHE = 1
+REFRESH_CACHE = 2
+
+SET_LOCAL = 0
+SET_GLOBAL = 1
+SET_MODULE = 2
+
+##################################################
+## Tokens for the parser ##
+
+#generic
+identchars = "abcdefghijklmnopqrstuvwxyz" \
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZ_"
+namechars = identchars + "0123456789"
+
+#operators
+powerOp = '**'
+unaryArithOps = ('+', '-', '~')
+binaryArithOps = ('+', '-', '/', '//', '%')
+shiftOps = ('>>', '<<')
+bitwiseOps = ('&', '|', '^')
+assignOp = '='
+augAssignOps = ('+=', '-=', '/=', '*=', '**=', '^=', '%=',
+          '>>=', '<<=', '&=', '|=', )
+assignmentOps = (assignOp,) + augAssignOps
+
+compOps = ('<', '>', '==', '!=', '<=', '>=', '<>', 'is', 'in',)
+booleanOps = ('and', 'or', 'not')
+operators = (powerOp,) + unaryArithOps + binaryArithOps \
+            + shiftOps + bitwiseOps + assignmentOps \
+            + compOps + booleanOps
+
+delimeters = ('(', ')', '{', '}', '[', ']',
+              ',', '.', ':', ';', '=', '`') + augAssignOps
+
+
+keywords = ('and',       'del',       'for',       'is',        'raise',
+            'assert',    'elif',      'from',      'lambda',    'return',
+            'break',     'else',      'global',    'not',       'try',   
+            'class',     'except',    'if',        'or',        'while',
+            'continue',  'exec',      'import',    'pass',
+            'def',       'finally',   'in',        'print',
+            )
+
+single3 = "'''"
+double3 = '"""'
+
+tripleQuotedStringStarts =  ("'''", '"""', 
+                             "r'''", 'r"""', "R'''", 'R"""',
+                             "u'''", 'u"""', "U'''", 'U"""',
+                             "ur'''", 'ur"""', "Ur'''", 'Ur"""',
+                             "uR'''", 'uR"""', "UR'''", 'UR"""')
+
+tripleQuotedStringPairs = {"'''": single3, '"""': double3,
+                           "r'''": single3, 'r"""': double3,
+                           "u'''": single3, 'u"""': double3,
+                           "ur'''": single3, 'ur"""': double3,
+                           "R'''": single3, 'R"""': double3,
+                           "U'''": single3, 'U"""': double3,
+                           "uR'''": single3, 'uR"""': double3,
+                           "Ur'''": single3, 'Ur"""': double3,
+                           "UR'''": single3, 'UR"""': double3,
+                           }
+
+closurePairs= {')':'(',']':'[','}':'{'}
+closurePairsRev= {'(':')','[':']','{':'}'}
+
+##################################################
+## Regex chunks for the parser ##
+
+tripleQuotedStringREs = {}
+def makeTripleQuoteRe(start, end):
+    start = escapeRegexChars(start)
+    end = escapeRegexChars(end)
+    return re.compile(r'(?:' + start + r').*?' + r'(?:' + end + r')', re.DOTALL)
+
+for start, end in tripleQuotedStringPairs.items():
+    tripleQuotedStringREs[start] = makeTripleQuoteRe(start, end)
+
+WS = r'[ \f\t]*'  
+EOL = r'\r\n|\n|\r'
+EOLZ = EOL + r'|\Z'
+escCharLookBehind = nongroup(r'(?<=\A)', r'(?<!\\)')
+nameCharLookAhead = r'(?=[A-Za-z_])'
+identRE=re.compile(r'[a-zA-Z_][a-zA-Z_0-9]*')
+EOLre=re.compile(r'(?:\r\n|\r|\n)')
+
+specialVarRE=re.compile(r'([a-zA-z_]+)@') # for matching specialVar comments
+# e.g. ##author@ Tavis Rudd
+
+unicodeDirectiveRE = re.compile(
+    r'(?:^|\r\n|\r|\n)\s*#\s{0,5}unicode[:\s]*([-\w.]*)\s*(?:\r\n|\r|\n)', re.MULTILINE)
+encodingDirectiveRE = re.compile(
+    r'(?:^|\r\n|\r|\n)\s*#\s{0,5}encoding[:\s]*([-\w.]*)\s*(?:\r\n|\r|\n)', re.MULTILINE)
+
+escapedNewlineRE = re.compile(r'(?<!\\)((\\\\)*)\\(n|012)')
+
+directiveNamesAndParsers = {
+    # importing and inheritance
+    'import': None,
+    'from': None,
+    'extends': 'eatExtends',
+    'implements': 'eatImplements',
+    'super': 'eatSuper',
+
+    # output, filtering, and caching
+    'slurp': 'eatSlurp',
+    'raw': 'eatRaw',
+    'include': 'eatInclude',
+    'cache': 'eatCache',
+    'filter': 'eatFilter',
+    'echo': None,
+    'silent': None,
+    'transform': 'eatTransform',
+    
+    'call': 'eatCall',
+    'arg': 'eatCallArg',
+    
+    'capture': 'eatCapture',
+    
+    # declaration, assignment, and deletion
+    'attr': 'eatAttr',
+    'def': 'eatDef',
+    'block': 'eatBlock',
+    '@': 'eatDecorator',
+    'defmacro': 'eatDefMacro',
+    
+    'closure': 'eatClosure',
+    
+    'set': 'eatSet',
+    'del': None,
+    
+    # flow control
+    'if': 'eatIf',
+    'while': None,
+    'for': None,
+    'else': None,
+    'elif': None,
+    'pass': None,
+    'break': None,
+    'continue': None,
+    'stop': None,
+    'return': None,
+    'yield': None,
+    
+    # little wrappers
+    'repeat': None,
+    'unless': None,
+    
+    # error handling
+    'assert': None,
+    'raise': None,
+    'try': None,
+    'except': None,
+    'finally': None,
+    'errorCatcher': 'eatErrorCatcher',
+    
+    # intructions to the parser and compiler
+    'breakpoint': 'eatBreakPoint',
+    'compiler': 'eatCompiler',
+    'compiler-settings': 'eatCompilerSettings',
+    
+    # misc
+    'shBang': 'eatShbang',
+    'encoding': 'eatEncoding',
+    
+    'end': 'eatEndDirective',
+    }
+
+endDirectiveNamesAndHandlers = {
+    'def': 'handleEndDef',      # has short-form
+    'block': None,              # has short-form
+    'closure': None,            # has short-form
+    'cache': None,              # has short-form
+    'call': None,               # has short-form
+    'capture': None,            # has short-form
+    'filter': None,
+    'errorCatcher': None,            
+    'while': None,              # has short-form
+    'for': None,                # has short-form
+    'if': None,                 # has short-form
+    'try': None,                # has short-form
+    'repeat': None,             # has short-form
+    'unless': None,             # has short-form
+    }
+
+##################################################
+## CLASSES ##
+
+# @@TR: SyntaxError doesn't call exception.__str__ for some reason!
+#class ParseError(SyntaxError):
+class ParseError(ValueError):
+    def __init__(self, stream, msg='Invalid Syntax', extMsg='', lineno=None, col=None):
+        self.stream = stream
+        if stream.pos() >= len(stream):
+            stream.setPos(len(stream) -1)
+        self.msg = msg
+        self.extMsg = extMsg
+        self.lineno = lineno
+        self.col = col
+        
+    def __str__(self):
+        return self.report()
+
+    def report(self):
+        stream = self.stream
+        if stream.filename():
+            f = " in file %s" % stream.filename()
+        else:
+            f = ''
+        report = ''
+        if self.lineno:
+            lineno = self.lineno
+            row, col, line = (lineno, (self.col or 0),
+                              self.stream.splitlines()[lineno-1])
+        else:
+            row, col, line = self.stream.getRowColLine()
+
+        ## get the surrounding lines
+        lines = stream.splitlines()
+        prevLines = []                  # (rowNum, content)
+        for i in range(1, 4):
+            if row-1-i <=0:
+                break
+            prevLines.append( (row-i, lines[row-1-i]) )
+
+        nextLines = []                  # (rowNum, content)
+        for i in range(1, 4):
+            if not row-1+i < len(lines):
+                break
+            nextLines.append( (row+i, lines[row-1+i]) )
+        nextLines.reverse()
+        
+        ## print the main message
+        report += "\n\n%s\n" %self.msg
+        report += "Line %i, column %i%s\n\n" % (row, col, f)
+        report += 'Line|Cheetah Code\n'
+        report += '----|-------------------------------------------------------------\n'
+        while prevLines:
+            lineInfo = prevLines.pop()
+            report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
+        report += "%(row)-4d|%(line)s\n"% {'row':row, 'line':line}
+        report += ' '*5 +' '*(col-1) + "^\n"
+        
+        while nextLines:
+            lineInfo = nextLines.pop()
+            report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
+        ## add the extra msg
+        if self.extMsg:
+            report += self.extMsg + '\n'
+            
+        return report
+
+class ForbiddenSyntax(ParseError):
+    pass
+class ForbiddenExpression(ForbiddenSyntax):
+    pass
+class ForbiddenDirective(ForbiddenSyntax):
+    pass
+
+class CheetahVariable(object):
+    def __init__(self, nameChunks, useNameMapper=True, cacheToken=None,
+                 rawSource=None):
+        self.nameChunks = nameChunks
+        self.useNameMapper = useNameMapper
+        self.cacheToken = cacheToken
+        self.rawSource = rawSource
+        
+class Placeholder(CheetahVariable):
+    pass
+
+class ArgList(object):
+    """Used by _LowLevelParser.getArgList()"""
+
+    def __init__(self):
+        self.arguments = []
+        self.defaults = []
+        self.count = 0
+
+    def add_argument(self, name):
+        self.arguments.append(name)
+        self.defaults.append(None)
+
+    def next(self):
+        self.count += 1
+
+    def add_default(self, token):
+        count = self.count
+        if self.defaults[count] is None:
+            self.defaults[count] = ''
+        self.defaults[count] += token
+    
+    def merge(self):
+        defaults = (isinstance(d, basestring) and d.strip() or None for d in self.defaults)
+        return list(map(None, (a.strip() for a in self.arguments), defaults))
+    
+    def __str__(self):
+        return str(self.merge())
+    
+class _LowLevelParser(SourceReader):
+    """This class implements the methods to match or extract ('get*') the basic
+    elements of Cheetah's grammar.  It does NOT handle any code generation or
+    state management.
+    """
+
+    _settingsManager = None
+
+    def setSettingsManager(self, settingsManager):
+        self._settingsManager = settingsManager
+        
+    def setting(self, key, default=Unspecified):
+        if default is Unspecified:
+            return self._settingsManager.setting(key)
+        else:
+            return self._settingsManager.setting(key, default=default)
+        
+    def setSetting(self, key, val):
+        self._settingsManager.setSetting(key, val)
+
+    def settings(self):
+        return self._settingsManager.settings()
+        
+    def updateSettings(self, settings):
+        self._settingsManager.updateSettings(settings)
+
+    def _initializeSettings(self): 
+        self._settingsManager._initializeSettings()
+    
+    def configureParser(self):
+        """Is called by the Compiler instance after the parser has had a
+        settingsManager assigned with self.setSettingsManager() 
+        """
+        self._makeCheetahVarREs()
+        self._makeCommentREs()
+        self._makeDirectiveREs()
+        self._makePspREs()
+        self._possibleNonStrConstantChars = (
+            self.setting('commentStartToken')[0] +
+            self.setting('multiLineCommentStartToken')[0] + 
+            self.setting('cheetahVarStartToken')[0] +
+            self.setting('directiveStartToken')[0] +
+            self.setting('PSPStartToken')[0])
+        self._nonStrConstMatchers = [
+            self.matchCommentStartToken,
+            self.matchMultiLineCommentStartToken,
+            self.matchVariablePlaceholderStart,
+            self.matchExpressionPlaceholderStart,
+            self.matchDirective,
+            self.matchPSPStartToken,
+            self.matchEOLSlurpToken,
+            ]
+
+    ## regex setup ##
+
+    def _makeCheetahVarREs(self):
+        
+        """Setup the regexs for Cheetah $var parsing."""
+
+        num = r'[0-9\.]+'
+        interval =   (r'(?P<interval>' + 
+                      num + r's|' +
+                      num + r'm|' +
+                      num + r'h|' +
+                      num + r'd|' +
+                      num + r'w|' +
+                      num + ')' 
+                      )
+    
+        cacheToken = (r'(?:' +
+                      r'(?P<REFRESH_CACHE>\*' + interval + '\*)'+
+                      '|' +
+                      r'(?P<STATIC_CACHE>\*)' +
+                      '|' +                      
+                      r'(?P<NO_CACHE>)' +
+                      ')')
+        self.cacheTokenRE = cachedRegex(cacheToken)
+
+        silentPlaceholderToken = (r'(?:' +
+                                  r'(?P<SILENT>' +escapeRegexChars('!')+')'+
+                                  '|' +
+                                  r'(?P<NOT_SILENT>)' +
+                                  ')')
+        self.silentPlaceholderTokenRE = cachedRegex(silentPlaceholderToken)
+        
+        self.cheetahVarStartRE = cachedRegex(
+            escCharLookBehind +
+            r'(?P<startToken>'+escapeRegexChars(self.setting('cheetahVarStartToken'))+')'+
+            r'(?P<silenceToken>'+silentPlaceholderToken+')'+
+            r'(?P<cacheToken>'+cacheToken+')'+
+            r'(?P<enclosure>|(?:(?:\{|\(|\[)[ \t\f]*))' + # allow WS after enclosure
+            r'(?=[A-Za-z_])')
+        validCharsLookAhead = r'(?=[A-Za-z_\*!\{\(\[])'
+        self.cheetahVarStartToken = self.setting('cheetahVarStartToken')
+        self.cheetahVarStartTokenRE = cachedRegex(
+            escCharLookBehind +
+            escapeRegexChars(self.setting('cheetahVarStartToken'))
+            +validCharsLookAhead
+            )
+
+        self.cheetahVarInExpressionStartTokenRE = cachedRegex(
+            escapeRegexChars(self.setting('cheetahVarStartToken'))
+            +r'(?=[A-Za-z_])'
+            )
+
+        self.expressionPlaceholderStartRE = cachedRegex(
+            escCharLookBehind +
+            r'(?P<startToken>' + escapeRegexChars(self.setting('cheetahVarStartToken')) + ')' +
+            r'(?P<cacheToken>' + cacheToken + ')' +
+            #r'\[[ \t\f]*'
+            r'(?:\{|\(|\[)[ \t\f]*'
+            + r'(?=[^\)\}\]])'
+            )
+
+        if self.setting('EOLSlurpToken'):
+            self.EOLSlurpRE = cachedRegex(
+                escapeRegexChars(self.setting('EOLSlurpToken'))
+                + r'[ \t\f]*'
+                + r'(?:'+EOL+')'
+                )
+        else:
+            self.EOLSlurpRE = None
+
+
+    def _makeCommentREs(self):
+        """Construct the regex bits that are used in comment parsing."""
+        startTokenEsc = escapeRegexChars(self.setting('commentStartToken'))
+        self.commentStartTokenRE = cachedRegex(escCharLookBehind + startTokenEsc)
+        del startTokenEsc
+        
+        startTokenEsc = escapeRegexChars(
+            self.setting('multiLineCommentStartToken'))
+        endTokenEsc = escapeRegexChars(
+            self.setting('multiLineCommentEndToken'))
+        self.multiLineCommentTokenStartRE = cachedRegex(escCharLookBehind +
+                                                       startTokenEsc)
+        self.multiLineCommentEndTokenRE = cachedRegex(escCharLookBehind +
+                                                     endTokenEsc)
+        
+    def _makeDirectiveREs(self):
+        """Construct the regexs that are used in directive parsing."""
+        startToken = self.setting('directiveStartToken')
+        endToken = self.setting('directiveEndToken')
+        startTokenEsc = escapeRegexChars(startToken)
+        endTokenEsc = escapeRegexChars(endToken)
+        validSecondCharsLookAhead = r'(?=[A-Za-z_@])'
+        reParts = [escCharLookBehind, startTokenEsc]
+        if self.setting('allowWhitespaceAfterDirectiveStartToken'):
+            reParts.append('[ \t]*')
+        reParts.append(validSecondCharsLookAhead)
+        self.directiveStartTokenRE = cachedRegex(''.join(reParts))
+        self.directiveEndTokenRE = cachedRegex(escCharLookBehind + endTokenEsc)
+
+    def _makePspREs(self):
+        """Setup the regexs for PSP parsing."""
+        startToken = self.setting('PSPStartToken')
+        startTokenEsc = escapeRegexChars(startToken)
+        self.PSPStartTokenRE = cachedRegex(escCharLookBehind + startTokenEsc)
+        endToken = self.setting('PSPEndToken')
+        endTokenEsc = escapeRegexChars(endToken)
+        self.PSPEndTokenRE = cachedRegex(escCharLookBehind + endTokenEsc)
+
+    def _unescapeCheetahVars(self, theString):
+        """Unescape any escaped Cheetah \$vars in the string.
+        """
+        
+        token = self.setting('cheetahVarStartToken')
+        return theString.replace('\\' + token, token)
+
+    def _unescapeDirectives(self, theString):
+        """Unescape any escaped Cheetah directives in the string.
+        """
+        
+        token = self.setting('directiveStartToken')
+        return theString.replace('\\' + token, token)
+
+    def isLineClearToStartToken(self, pos=None):
+        return self.isLineClearToPos(pos)
+
+    def matchTopLevelToken(self):
+        """Returns the first match found from the following methods:
+            self.matchCommentStartToken
+            self.matchMultiLineCommentStartToken
+            self.matchVariablePlaceholderStart
+            self.matchExpressionPlaceholderStart
+            self.matchDirective
+            self.matchPSPStartToken
+            self.matchEOLSlurpToken
+
+        Returns None if no match.
+        """
+        match = None
+        if self.peek() in self._possibleNonStrConstantChars:
+            for matcher in self._nonStrConstMatchers:
+                match = matcher()
+                if match:
+                    break
+        return match
+
+    def matchPyToken(self):
+        match = pseudoprog.match(self.src(), self.pos())
+        
+        if match and match.group() in tripleQuotedStringStarts:
+            TQSmatch = tripleQuotedStringREs[match.group()].match(self.src(), self.pos())
+            if TQSmatch:
+                return TQSmatch
+        return match
+        
+    def getPyToken(self):
+        match = self.matchPyToken()
+        if match is None:
+            raise ParseError(self)
+        elif match.group() in tripleQuotedStringStarts:
+            raise ParseError(self, msg='Malformed triple-quoted string')
+        return self.readTo(match.end())
+
+    def matchEOLSlurpToken(self):
+        if self.EOLSlurpRE:
+            return self.EOLSlurpRE.match(self.src(), self.pos())
+
+    def getEOLSlurpToken(self):
+        match = self.matchEOLSlurpToken()
+        if not match:
+            raise ParseError(self, msg='Invalid EOL slurp token')
+        return self.readTo(match.end())
+
+    def matchCommentStartToken(self):
+        return self.commentStartTokenRE.match(self.src(), self.pos())
+    
+    def getCommentStartToken(self):
+        match = self.matchCommentStartToken()
+        if not match:
+            raise ParseError(self, msg='Invalid single-line comment start token')
+        return self.readTo(match.end())
+
+    def matchMultiLineCommentStartToken(self):
+        return self.multiLineCommentTokenStartRE.match(self.src(), self.pos())
+    
+    def getMultiLineCommentStartToken(self):
+        match = self.matchMultiLineCommentStartToken()
+        if not match:
+            raise ParseError(self, msg='Invalid multi-line comment start token')
+        return self.readTo(match.end())
+
+    def matchMultiLineCommentEndToken(self):
+        return self.multiLineCommentEndTokenRE.match(self.src(), self.pos())
+    
+    def getMultiLineCommentEndToken(self):
+        match = self.matchMultiLineCommentEndToken()
+        if not match:
+            raise ParseError(self, msg='Invalid multi-line comment end token')
+        return self.readTo(match.end())
+
+    def getCommaSeparatedSymbols(self):
+        """
+            Loosely based on getDottedName to pull out comma separated
+            named chunks
+        """
+        srcLen = len(self)
+        pieces = []
+        nameChunks = []
+
+        if not self.peek() in identchars:
+            raise ParseError(self)
+    
+        while self.pos() < srcLen:
+            c = self.peek()
+            if c in namechars:
+                nameChunk = self.getIdentifier()
+                nameChunks.append(nameChunk)
+            elif c == '.':
+                if self.pos()+1 <srcLen and self.peek(1) in identchars:
+                    nameChunks.append(self.getc())
+                else:
+                    break
+            elif c == ',':
+                self.getc()
+                pieces.append(''.join(nameChunks))
+                nameChunks = []
+            elif c in (' ', '\t'):
+                self.getc()
+            else:
+                break
+
+        if nameChunks:
+            pieces.append(''.join(nameChunks))
+
+        return pieces
+    
+    def getDottedName(self):
+        srcLen = len(self)
+        nameChunks = []
+        
+        if not self.peek() in identchars:
+            raise ParseError(self)
+    
+        while self.pos() < srcLen:
+            c = self.peek()
+            if c in namechars:
+                nameChunk = self.getIdentifier()
+                nameChunks.append(nameChunk)
+            elif c == '.':
+                if self.pos()+1 <srcLen and self.peek(1) in identchars:
+                    nameChunks.append(self.getc())
+                else:
+                    break
+            else:
+                break
+
+        return ''.join(nameChunks)
+
+    def matchIdentifier(self):
+        return identRE.match(self.src(), self.pos())
+    
+    def getIdentifier(self):
+        match = self.matchIdentifier()
+        if not match:
+            raise ParseError(self, msg='Invalid identifier')
+        return self.readTo(match.end())
+
+    def matchOperator(self):
+        match = self.matchPyToken()
+        if match and match.group() not in operators:
+            match = None
+        return match
+
+    def getOperator(self):
+        match = self.matchOperator()
+        if not match:
+            raise ParseError(self, msg='Expected operator')
+        return self.readTo( match.end() )
+
+    def matchAssignmentOperator(self):
+        match = self.matchPyToken()
+        if match and match.group() not in assignmentOps:
+            match = None
+        return match
+        
+    def getAssignmentOperator(self):
+        match = self.matchAssignmentOperator()
+        if not match:
+            raise ParseError(self, msg='Expected assignment operator')
+        return self.readTo( match.end() )
+
+    def matchDirective(self):
+        """Returns False or the name of the directive matched.
+        """
+        startPos = self.pos()
+        if not self.matchDirectiveStartToken():
+            return False
+        self.getDirectiveStartToken()
+        directiveName = self.matchDirectiveName()
+        self.setPos(startPos)
+        return directiveName
+
+    def matchDirectiveName(self, directiveNameChars=identchars+'0123456789-@'):
+        startPos = self.pos()
+        possibleMatches = self._directiveNamesAndParsers.keys()
+        name = ''
+        match = None
+
+        while not self.atEnd():
+            c = self.getc()
+            if not c in directiveNameChars:
+                break
+            name += c
+            if name == '@':
+                if not self.atEnd() and self.peek() in identchars:
+                    match = '@'
+                break
+            possibleMatches = [dn for dn in possibleMatches if dn.startswith(name)]
+            if not possibleMatches:
+                break
+            elif (name in possibleMatches and (self.atEnd() or self.peek() not in directiveNameChars)):
+                match = name
+                break
+
+        self.setPos(startPos)
+        return match
+        
+    def matchDirectiveStartToken(self):
+        return self.directiveStartTokenRE.match(self.src(), self.pos())
+    
+    def getDirectiveStartToken(self):
+        match = self.matchDirectiveStartToken()
+        if not match:
+            raise ParseError(self, msg='Invalid directive start token')
+        return self.readTo(match.end())
+
+    def matchDirectiveEndToken(self):
+        return self.directiveEndTokenRE.match(self.src(), self.pos())
+    
+    def getDirectiveEndToken(self):
+        match = self.matchDirectiveEndToken()
+        if not match:
+            raise ParseError(self, msg='Invalid directive end token')
+        return self.readTo(match.end())
+
+        
+    def matchColonForSingleLineShortFormDirective(self):
+        if not self.atEnd() and self.peek()==':':
+            restOfLine = self[self.pos()+1:self.findEOL()]
+            restOfLine = restOfLine.strip()
+            if not restOfLine:
+                return False
+            elif self.commentStartTokenRE.match(restOfLine):
+                return False
+            else: # non-whitespace, non-commment chars found
+                return True
+        return False        
+
+    def matchPSPStartToken(self):
+        return self.PSPStartTokenRE.match(self.src(), self.pos())
+
+    def matchPSPEndToken(self):
+        return self.PSPEndTokenRE.match(self.src(), self.pos())
+
+    def getPSPStartToken(self):
+        match = self.matchPSPStartToken()
+        if not match:
+            raise ParseError(self, msg='Invalid psp start token')
+        return self.readTo(match.end())
+
+    def getPSPEndToken(self):
+        match = self.matchPSPEndToken()
+        if not match:
+            raise ParseError(self, msg='Invalid psp end token')
+        return self.readTo(match.end())
+
+    def matchCheetahVarStart(self):
+        """includes the enclosure and cache token"""
+        return self.cheetahVarStartRE.match(self.src(), self.pos())
+
+    def matchCheetahVarStartToken(self):
+        """includes the enclosure and cache token"""
+        return self.cheetahVarStartTokenRE.match(self.src(), self.pos())
+
+    def matchCheetahVarInExpressionStartToken(self):
+        """no enclosures or cache tokens allowed"""
+        return self.cheetahVarInExpressionStartTokenRE.match(self.src(), self.pos())
+
+    def matchVariablePlaceholderStart(self):
+        """includes the enclosure and cache token"""
+        return self.cheetahVarStartRE.match(self.src(), self.pos())
+
+    def matchExpressionPlaceholderStart(self):
+        """includes the enclosure and cache token"""
+        return self.expressionPlaceholderStartRE.match(self.src(), self.pos())        
+
+    def getCheetahVarStartToken(self):
+        """just the start token, not the enclosure or cache token"""
+        match = self.matchCheetahVarStartToken()
+        if not match:
+            raise ParseError(self, msg='Expected Cheetah $var start token')            
+        return self.readTo( match.end() )
+
+
+    def getCacheToken(self):
+        try:
+            token = self.cacheTokenRE.match(self.src(), self.pos())
+            self.setPos( token.end() )
+            return token.group()
+        except:
+            raise ParseError(self, msg='Expected cache token')
+
+    def getSilentPlaceholderToken(self):
+        try:
+            token = self.silentPlaceholderTokenRE.match(self.src(), self.pos())
+            self.setPos( token.end() )
+            return token.group()
+        except:
+            raise ParseError(self, msg='Expected silent placeholder token')
+
+
+
+    def getTargetVarsList(self):
+        varnames = []
+        while not self.atEnd():
+            if self.peek() in ' \t\f':
+                self.getWhiteSpace()
+            elif self.peek() in '\r\n':
+                break
+            elif self.startswith(','):
+                self.advance()
+            elif self.startswith('in ') or self.startswith('in\t'):
+                break
+            #elif self.matchCheetahVarStart():
+            elif self.matchCheetahVarInExpressionStartToken():
+                self.getCheetahVarStartToken()
+                self.getSilentPlaceholderToken()
+                self.getCacheToken()
+                varnames.append( self.getDottedName() )
+            elif self.matchIdentifier():
+                varnames.append( self.getDottedName() )
+            else:
+                break
+        return varnames
+        
+    def getCheetahVar(self, plain=False, skipStartToken=False):
+        """This is called when parsing inside expressions. Cache tokens are only
+        valid in placeholders so this method discards any cache tokens found.
+        """
+        if not skipStartToken:
+            self.getCheetahVarStartToken()
+        self.getSilentPlaceholderToken()
+        self.getCacheToken()
+        return self.getCheetahVarBody(plain=plain)
+            
+    def getCheetahVarBody(self, plain=False):
+        # @@TR: this should be in the compiler
+        return self._compiler.genCheetahVar(self.getCheetahVarNameChunks(), plain=plain)
+        
+    def getCheetahVarNameChunks(self):
+        
+        """
+        nameChunks = list of Cheetah $var subcomponents represented as tuples
+          [ (namemapperPart,autoCall,restOfName),
+          ]
+        where:
+          namemapperPart = the dottedName base
+          autocall = where NameMapper should use autocalling on namemapperPart
+          restOfName = any arglist, index, or slice
+
+        If restOfName contains a call arglist (e.g. '(1234)') then autocall is
+        False, otherwise it defaults to True.
+
+        EXAMPLE
+        ------------------------------------------------------------------------
+
+        if the raw CheetahVar is
+          $a.b.c[1].d().x.y.z
+          
+        nameChunks is the list
+          [ ('a.b.c',True,'[1]'),
+            ('d',False,'()'),     
+            ('x.y.z',True,''),   
+          ]
+
+        """
+
+        chunks = []
+        while self.pos() < len(self):
+            rest = ''
+            autoCall = True
+            if not self.peek() in identchars + '.':
+                break
+            elif self.peek() == '.':
+                
+                if self.pos()+1 < len(self) and self.peek(1) in identchars:
+                    self.advance()  # discard the period as it isn't needed with NameMapper
+                else:
+                    break
+                
+            dottedName = self.getDottedName()
+            if not self.atEnd() and self.peek() in '([':
+                if self.peek() == '(':
+                    rest = self.getCallArgString()
+                else:
+                    rest = self.getExpression(enclosed=True)
+                
+                period = max(dottedName.rfind('.'), 0)
+                if period:
+                    chunks.append( (dottedName[:period], autoCall, '') )
+                    dottedName = dottedName[period+1:]
+                if rest and rest[0]=='(':
+                    autoCall = False
+            chunks.append( (dottedName, autoCall, rest) )
+
+        return chunks
+    
+
+    def getCallArgString(self,
+                         enclosures=[],  # list of tuples (char, pos), where char is ({ or [ 
+                         useNameMapper=Unspecified):
+
+        """ Get a method/function call argument string. 
+
+        This method understands *arg, and **kw
+        """
+
+        # @@TR: this settings mangling should be removed
+        if useNameMapper is not Unspecified:
+            useNameMapper_orig = self.setting('useNameMapper')
+            self.setSetting('useNameMapper', useNameMapper)
+        
+        if enclosures:
+            pass
+        else:
+            if not self.peek() == '(':
+                raise ParseError(self, msg="Expected '('")
+            startPos = self.pos()
+            self.getc()
+            enclosures = [('(', startPos),
+                          ]
+        
+        argStringBits = ['(']
+        addBit = argStringBits.append
+
+        while True:
+            if self.atEnd():
+                open = enclosures[-1][0]
+                close = closurePairsRev[open]
+                self.setPos(enclosures[-1][1])
+                raise ParseError(
+                    self, msg="EOF was reached before a matching '" + close +
+                    "' was found for the '" + open + "'")
+
+            c = self.peek()
+            if c in ")}]": # get the ending enclosure and break                
+                if not enclosures:
+                    raise ParseError(self)
+                c = self.getc()
+                open = closurePairs[c]
+                if enclosures[-1][0] == open:
+                    enclosures.pop()
+                    addBit(')')  
+                    break
+                else:
+                    raise ParseError(self)
+            elif c in " \t\f\r\n":
+                addBit(self.getc())
+            elif self.matchCheetahVarInExpressionStartToken():
+                startPos = self.pos()
+                codeFor1stToken = self.getCheetahVar()
+                WS = self.getWhiteSpace()
+                if not self.atEnd() and self.peek() == '=':
+                    nextToken = self.getPyToken()
+                    if nextToken == '=':
+                        endPos = self.pos()
+                        self.setPos(startPos)
+                        codeFor1stToken = self.getCheetahVar(plain=True)
+                        self.setPos(endPos)
+                        
+                    ## finally
+                    addBit( codeFor1stToken + WS + nextToken )
+                else:
+                    addBit( codeFor1stToken + WS)
+            elif self.matchCheetahVarStart():
+                # it has syntax that is only valid at the top level
+                self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()
+            else:
+                beforeTokenPos = self.pos()
+                token = self.getPyToken()
+                if token in ('{', '(', '['):
+                    self.rev()
+                    token = self.getExpression(enclosed=True)
+                token = self.transformToken(token, beforeTokenPos)
+                addBit(token)
+
+        if useNameMapper is not Unspecified:
+            self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above
+
+        return ''.join(argStringBits)
+    
+    def getDefArgList(self, exitPos=None, useNameMapper=False):
+
+        """ Get an argument list. Can be used for method/function definition
+        argument lists or for #directive argument lists. Returns a list of
+        tuples in the form (argName, defVal=None) with one tuple for each arg
+        name.
+
+        These defVals are always strings, so (argName, defVal=None) is safe even
+        with a case like (arg1, arg2=None, arg3=1234*2), which would be returned as
+        [('arg1', None),
+         ('arg2', 'None'),
+         ('arg3', '1234*2'),         
+        ]
+
+        This method understands *arg, and **kw
+
+        """
+
+        if self.peek() == '(':
+            self.advance()
+        else:
+            exitPos = self.findEOL()  # it's a directive so break at the EOL
+        argList = ArgList()
+        onDefVal = False
+
+        # @@TR: this settings mangling should be removed
+        useNameMapper_orig = self.setting('useNameMapper')
+        self.setSetting('useNameMapper', useNameMapper)
+
+        while True:
+            if self.atEnd():
+                raise ParseError(
+                    self, msg="EOF was reached before a matching ')'"+
+                    " was found for the '('")
+
+            if self.pos() == exitPos:
+                break
+
+            c = self.peek()
+            if c == ")" or self.matchDirectiveEndToken():
+                break
+            elif c == ":":
+                break            
+            elif c in " \t\f\r\n":
+                if onDefVal:
+                    argList.add_default(c)
+                self.advance()
+            elif c == '=':
+                onDefVal = True
+                self.advance()
+            elif c == ",":
+                argList.next()
+                onDefVal = False
+                self.advance()
+            elif self.startswith(self.cheetahVarStartToken) and not onDefVal:
+                self.advance(len(self.cheetahVarStartToken))
+            elif self.matchIdentifier() and not onDefVal:
+                argList.add_argument( self.getIdentifier() )
+            elif onDefVal:
+                if self.matchCheetahVarInExpressionStartToken():
+                    token = self.getCheetahVar()
+                elif self.matchCheetahVarStart():
+                    # it has syntax that is only valid at the top level                    
+                    self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()
+                else:
+                    beforeTokenPos = self.pos()
+                    token = self.getPyToken()
+                    if token in ('{', '(', '['):
+                        self.rev()
+                        token = self.getExpression(enclosed=True)
+                    token = self.transformToken(token, beforeTokenPos)
+                argList.add_default(token)
+            elif c == '*' and not onDefVal:
+                varName = self.getc()
+                if self.peek() == '*':
+                    varName += self.getc()
+                if not self.matchIdentifier():
+                    raise ParseError(self)
+                varName += self.getIdentifier()
+                argList.add_argument(varName)
+            else:
+                raise ParseError(self)
+
+                
+        self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above
+        return argList.merge()
+    
+    def getExpressionParts(self,
+                           enclosed=False, 
+                           enclosures=None, # list of tuples (char, pos), where char is ({ or [ 
+                           pyTokensToBreakAt=None, # only works if not enclosed
+                           useNameMapper=Unspecified,
+                           ):
+
+        """ Get a Cheetah expression that includes $CheetahVars and break at
+        directive end tokens, the end of an enclosure, or at a specified
+        pyToken.
+        """
+
+        if useNameMapper is not Unspecified:
+            useNameMapper_orig = self.setting('useNameMapper')
+            self.setSetting('useNameMapper', useNameMapper)
+
+        if enclosures is None:
+            enclosures = []
+        
+        srcLen = len(self)
+        exprBits = []
+        while True:
+            if self.atEnd():
+                if enclosures:
+                    open = enclosures[-1][0]
+                    close = closurePairsRev[open]
+                    self.setPos(enclosures[-1][1])
+                    raise ParseError(
+                        self, msg="EOF was reached before a matching '" + close +
+                        "' was found for the '" + open + "'")
+                else:
+                    break
+
+            c = self.peek()
+            if c in "{([":
+                exprBits.append(c)
+                enclosures.append( (c, self.pos()) )
+                self.advance()                
+            elif enclosed and not enclosures:
+                break                
+            elif c in "])}":
+                if not enclosures:
+                    raise ParseError(self)
+                open = closurePairs[c]
+                if enclosures[-1][0] == open:
+                    enclosures.pop()
+                    exprBits.append(c)
+                else:
+                    open = enclosures[-1][0]
+                    close = closurePairsRev[open]
+                    row, col = self.getRowCol()
+                    self.setPos(enclosures[-1][1])
+                    raise ParseError(
+                        self, msg= "A '" + c + "' was found at line " + str(row) +
+                        ", col " + str(col) +
+                        " before a matching '" + close +
+                        "' was found\nfor the '" + open + "'")
+                self.advance()
+                                
+            elif c in " \f\t":
+                exprBits.append(self.getWhiteSpace())            
+            elif self.matchDirectiveEndToken() and not enclosures:
+                break            
+            elif c == "\\" and self.pos()+1 < srcLen:
+                eolMatch = EOLre.match(self.src(), self.pos()+1)
+                if not eolMatch:
+                    self.advance()
+                    raise ParseError(self, msg='Line ending expected')
+                self.setPos( eolMatch.end() )
+            elif c in '\r\n':
+                if enclosures:
+                    self.advance()                    
+                else:
+                    break                    
+            elif self.matchCheetahVarInExpressionStartToken():
+                expr = self.getCheetahVar()
+                exprBits.append(expr)
+            elif self.matchCheetahVarStart():
+                # it has syntax that is only valid at the top level                
+                self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()                    
+            else:                
+                beforeTokenPos = self.pos()
+                token = self.getPyToken()
+                if (not enclosures 
+                    and pyTokensToBreakAt
+                    and token in pyTokensToBreakAt):
+                    
+                    self.setPos(beforeTokenPos)
+                    break
+
+                token = self.transformToken(token, beforeTokenPos)
+                        
+                exprBits.append(token)                    
+                if identRE.match(token):
+                    if token == 'for':
+                        expr = self.getExpression(useNameMapper=False, pyTokensToBreakAt=['in'])
+                        exprBits.append(expr)
+                    else:
+                        exprBits.append(self.getWhiteSpace())
+                        if not self.atEnd() and self.peek() == '(':
+                            exprBits.append(self.getCallArgString())                    
+        ##
+        if useNameMapper is not Unspecified:                            
+            self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above
+        return exprBits
+
+    def getExpression(self,
+                      enclosed=False, 
+                      enclosures=None, # list of tuples (char, pos), where # char is ({ or [
+                      pyTokensToBreakAt=None,
+                      useNameMapper=Unspecified,
+                      ):
+        """Returns the output of self.getExpressionParts() as a concatenated
+        string rather than as a list.
+        """
+        return ''.join(self.getExpressionParts(
+            enclosed=enclosed, enclosures=enclosures,
+            pyTokensToBreakAt=pyTokensToBreakAt,
+            useNameMapper=useNameMapper))
+
+
+    def transformToken(self, token, beforeTokenPos):
+        """Takes a token from the expression being parsed and performs and
+        special transformations required by Cheetah.
+
+        At the moment only Cheetah's c'$placeholder strings' are transformed.
+        """
+        if token=='c' and not self.atEnd() and self.peek() in '\'"':
+            nextToken = self.getPyToken()
+            token = nextToken.upper()
+            theStr = eval(token)
+            endPos = self.pos()
+            if not theStr:
+                return
+            
+            if token.startswith(single3) or token.startswith(double3):
+                startPosIdx = 3
+            else:
+                startPosIdx = 1
+            self.setPos(beforeTokenPos+startPosIdx+1)
+            outputExprs = []
+            strConst = ''
+            while self.pos() < (endPos-startPosIdx):
+                if self.matchCheetahVarStart() or self.matchExpressionPlaceholderStart():
+                    if strConst:
+                        outputExprs.append(repr(strConst))
+                        strConst = ''
+                    placeholderExpr = self.getPlaceholder()
+                    outputExprs.append('str('+placeholderExpr+')')
+                else:
+                    strConst += self.getc()
+            self.setPos(endPos)
+            if strConst:
+                outputExprs.append(repr(strConst))
+            token = "''.join(["+','.join(outputExprs)+"])"
+        return token
+
+    def _raiseErrorAboutInvalidCheetahVarSyntaxInExpr(self):
+        match = self.matchCheetahVarStart()
+        groupdict = match.groupdict()
+        if groupdict.get('cacheToken'):
+            raise ParseError(
+                self,
+                msg='Cache tokens are not valid inside expressions. '
+                'Use them in top-level $placeholders only.')                    
+        elif groupdict.get('enclosure'):                    
+            raise ParseError(
+                self,
+                msg='Long-form placeholders - ${}, $(), $[], etc. are not valid inside expressions. '
+                'Use them in top-level $placeholders only.')
+        else:
+            raise ParseError(
+                self,
+                msg='This form of $placeholder syntax is not valid here.')
+        
+
+    def getPlaceholder(self, allowCacheTokens=False, plain=False, returnEverything=False):
+        # filtered 
+        for callback in self.setting('preparsePlaceholderHooks'):
+            callback(parser=self)
+
+        startPos = self.pos()
+        lineCol = self.getRowCol(startPos)
+        startToken = self.getCheetahVarStartToken()
+        silentPlaceholderToken = self.getSilentPlaceholderToken()
+        if silentPlaceholderToken:
+            isSilentPlaceholder = True
+        else:
+            isSilentPlaceholder = False
+            
+        
+        if allowCacheTokens:
+            cacheToken = self.getCacheToken()
+            cacheTokenParts = self.cacheTokenRE.match(cacheToken).groupdict()        
+        else:
+            cacheTokenParts = {}
+
+        if self.peek() in '({[':         
+            pos = self.pos()
+            enclosureOpenChar = self.getc()
+            enclosures = [ (enclosureOpenChar, pos) ]
+            self.getWhiteSpace()
+        else:
+            enclosures = []
+
+        filterArgs = None
+        if self.matchIdentifier(): 
+            nameChunks = self.getCheetahVarNameChunks()
+            expr = self._compiler.genCheetahVar(nameChunks[:], plain=plain)
+            restOfExpr = None
+            if enclosures:
+                WS = self.getWhiteSpace()
+                expr += WS
+                if self.setting('allowPlaceholderFilterArgs') and self.peek()==',':
+                    filterArgs = self.getCallArgString(enclosures=enclosures)[1:-1]
+                else:
+                    if self.peek()==closurePairsRev[enclosureOpenChar]:
+                        self.getc()
+                    else:
+                        restOfExpr = self.getExpression(enclosed=True, enclosures=enclosures)
+                        if restOfExpr[-1] == closurePairsRev[enclosureOpenChar]:
+                            restOfExpr = restOfExpr[:-1]
+                        expr += restOfExpr
+            rawPlaceholder = self[startPos: self.pos()]
+        else:
+            expr = self.getExpression(enclosed=True, enclosures=enclosures)
+            if expr[-1] == closurePairsRev[enclosureOpenChar]:
+                expr = expr[:-1]
+            rawPlaceholder=self[startPos: self.pos()]
+            
+        expr = self._applyExpressionFilters(expr, 'placeholder',
+                                            rawExpr=rawPlaceholder, startPos=startPos)
+        for callback in self.setting('postparsePlaceholderHooks'):
+            callback(parser=self)
+
+        if returnEverything:
+            return (expr, rawPlaceholder, lineCol, cacheTokenParts,
+                    filterArgs, isSilentPlaceholder)
+        else:
+            return expr
+        
+
+class _HighLevelParser(_LowLevelParser):
+    """This class is a StateMachine for parsing Cheetah source and
+    sending state dependent code generation commands to
+    Cheetah.Compiler.Compiler.
+    """
+    def __init__(self, src, filename=None, breakPoint=None, compiler=None):
+        super(_HighLevelParser, self).__init__(src, filename=filename, breakPoint=breakPoint)
+        self.setSettingsManager(compiler)
+        self._compiler = compiler
+        self.setupState()
+        self.configureParser()
+
+    def setupState(self):
+        self._macros = {}        
+        self._macroDetails = {}
+        self._openDirectivesStack = []
+
+    def cleanup(self):
+        """Cleanup to remove any possible reference cycles
+        """
+        self._macros.clear()
+        for macroname, macroDetails in self._macroDetails.items():
+            macroDetails.template.shutdown()
+            del macroDetails.template
+        self._macroDetails.clear()
+
+    def configureParser(self):
+        super(_HighLevelParser, self).configureParser()
+        self._initDirectives()
+    
+    def _initDirectives(self):
+        def normalizeParserVal(val):
+            if isinstance(val, (str, unicode)):
+                handler = getattr(self, val)
+            elif isinstance(val, type):
+                handler = val(self)
+            elif hasattr(val, '__call__'):
+                handler = val
+            elif val is None:
+                handler = val
+            else:
+                raise Exception('Invalid parser/handler value %r for %s'%(val, name))
+            return handler
+        
+        normalizeHandlerVal = normalizeParserVal
+
+        _directiveNamesAndParsers = directiveNamesAndParsers.copy()
+        customNamesAndParsers = self.setting('directiveNamesAndParsers', {})
+        _directiveNamesAndParsers.update(customNamesAndParsers)
+
+        _endDirectiveNamesAndHandlers = endDirectiveNamesAndHandlers.copy()
+        customNamesAndHandlers = self.setting('endDirectiveNamesAndHandlers', {})
+        _endDirectiveNamesAndHandlers.update(customNamesAndHandlers)        
+        
+        self._directiveNamesAndParsers = {}
+        for name, val in _directiveNamesAndParsers.items():
+            if val in (False, 0):
+                continue
+            self._directiveNamesAndParsers[name] = normalizeParserVal(val)
+
+        self._endDirectiveNamesAndHandlers = {}        
+        for name, val in _endDirectiveNamesAndHandlers.items():
+            if val in (False, 0):
+                continue
+            self._endDirectiveNamesAndHandlers[name] = normalizeHandlerVal(val)
+        
+        self._closeableDirectives = ['def', 'block', 'closure', 'defmacro',
+                                     'call',
+                                     'capture',
+                                     'cache',
+                                     'filter',
+                                     'if', 'unless',
+                                     'for', 'while', 'repeat',
+                                     'try',
+                                     ]
+        for directiveName in self.setting('closeableDirectives', []):
+            self._closeableDirectives.append(directiveName)
+
+
+
+        macroDirectives = self.setting('macroDirectives', {})
+        macroDirectives['i18n'] = I18n
+
+
+        for macroName, callback in macroDirectives.items():
+            if isinstance(callback, type):
+                callback = callback(parser=self)
+            assert callback                
+            self._macros[macroName] = callback
+            self._directiveNamesAndParsers[macroName] = self.eatMacroCall
+            
+    def _applyExpressionFilters(self, expr, exprType, rawExpr=None, startPos=None):
+        """Pipes cheetah expressions through a set of optional filter hooks.
+
+        The filters are functions which may modify the expressions or raise
+        a ForbiddenExpression exception if the expression is not allowed.  They
+        are defined in the compiler setting 'expressionFilterHooks'.
+
+        Some intended use cases:
+
+         - to implement 'restricted execution' safeguards in cases where you
+           can't trust the author of the template.
+
+         - to enforce style guidelines  
+           
+        filter call signature:  (parser, expr, exprType, rawExpr=None, startPos=None)
+         - parser is the Cheetah parser  
+         - expr is the expression to filter.  In some cases the parser will have
+           already modified it from the original source code form.  For example,
+           placeholders will have been translated into namemapper calls.  If you
+           need to work with the original source, see rawExpr.        
+         - exprType is the name of the directive, 'psp', or 'placeholder'. All
+           lowercase.  @@TR: These will eventually be replaced with a set of
+           constants.
+         - rawExpr is the original source string that Cheetah parsed.  This
+           might be None in some cases.
+         - startPos is the character position in the source string/file
+           where the parser started parsing the current expression.
+
+        @@TR: I realize this use of the term 'expression' is a bit wonky as many
+         of the 'expressions' are actually statements, but I haven't thought of
+         a better name yet.  Suggestions?
+        """
+        for callback in self.setting('expressionFilterHooks'):
+            expr = callback(parser=self, expr=expr,  exprType=exprType,
+                            rawExpr=rawExpr, startPos=startPos)
+        return expr
+
+    def _filterDisabledDirectives(self, directiveName):
+        directiveName = directiveName.lower()
+        if (directiveName in self.setting('disabledDirectives')
+            or (self.setting('enabledDirectives')
+                and directiveName not in self.setting('enabledDirectives'))):
+            for callback in self.setting('disabledDirectiveHooks'):
+                callback(parser=self, directiveName=directiveName)
+            raise ForbiddenDirective(self, msg='This %r directive is disabled'%directiveName)
+        
+    ## main parse loop
+
+    def parse(self, breakPoint=None, assertEmptyStack=True):
+        if breakPoint:
+            origBP = self.breakPoint()
+            self.setBreakPoint(breakPoint)
+            assertEmptyStack = False
+
+        while not self.atEnd():
+            if self.matchCommentStartToken():
+                self.eatComment()
+            elif self.matchMultiLineCommentStartToken():
+                self.eatMultiLineComment()
+            elif self.matchVariablePlaceholderStart():
+                self.eatPlaceholder()
+            elif self.matchExpressionPlaceholderStart():
+                self.eatPlaceholder()
+            elif self.matchDirective():
+                self.eatDirective()
+            elif self.matchPSPStartToken():
+                self.eatPSP()
+            elif self.matchEOLSlurpToken():
+                self.eatEOLSlurpToken()
+            else:
+                self.eatPlainText()
+        if assertEmptyStack:
+            self.assertEmptyOpenDirectivesStack()
+        if breakPoint:
+            self.setBreakPoint(origBP)
+            
+    ## non-directive eat methods    
+                
+    def eatPlainText(self):
+        startPos = self.pos()
+        match = None
+        while not self.atEnd():
+            match = self.matchTopLevelToken()
+            if match:
+                break
+            else:
+                self.advance()
+        strConst = self.readTo(self.pos(), start=startPos)
+        strConst = self._unescapeCheetahVars(strConst)
+        strConst = self._unescapeDirectives(strConst)
+        self._compiler.addStrConst(strConst)
+        return match
+
+    def eatComment(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        if isLineClearToStartToken:
+            self._compiler.handleWSBeforeDirective()
+        self.getCommentStartToken()            
+        comm = self.readToEOL(gobble=isLineClearToStartToken)
+        self._compiler.addComment(comm)
+
+    def eatMultiLineComment(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+
+        self.getMultiLineCommentStartToken()
+        endPos = startPos = self.pos()
+        level = 1
+        while True:
+            endPos = self.pos()
+            if self.atEnd():
+                break
+            if self.matchMultiLineCommentStartToken():
+                self.getMultiLineCommentStartToken()
+                level += 1
+            elif self.matchMultiLineCommentEndToken():
+                self.getMultiLineCommentEndToken()
+                level -= 1
+            if not level:
+                break
+            self.advance()
+        comm = self.readTo(endPos, start=startPos)
+
+        if not self.atEnd():
+            self.getMultiLineCommentEndToken()
+
+        if (not self.atEnd()) and self.setting('gobbleWhitespaceAroundMultiLineComments'):
+            restOfLine = self[self.pos():self.findEOL()]
+            if not restOfLine.strip(): # WS only to EOL
+                self.readToEOL(gobble=isLineClearToStartToken)
+
+            if isLineClearToStartToken and (self.atEnd() or self.pos() > endOfFirstLine):
+                self._compiler.handleWSBeforeDirective()
+        
+        self._compiler.addComment(comm)
+
+    def eatPlaceholder(self):
+        (expr, rawPlaceholder,
+         lineCol, cacheTokenParts,
+         filterArgs, isSilentPlaceholder) = self.getPlaceholder(
+            allowCacheTokens=True, returnEverything=True)
+        
+        self._compiler.addPlaceholder(
+            expr,
+            filterArgs=filterArgs,
+            rawPlaceholder=rawPlaceholder,
+            cacheTokenParts=cacheTokenParts,
+            lineCol=lineCol,
+            silentMode=isSilentPlaceholder)
+        return
+        
+    def eatPSP(self):
+        # filtered
+        self._filterDisabledDirectives(directiveName='psp')
+        self.getPSPStartToken()
+        endToken = self.setting('PSPEndToken')
+        startPos = self.pos()            
+        while not self.atEnd():
+            if self.peek() == endToken[0]:
+                if self.matchPSPEndToken():
+                    break
+            self.advance()
+        pspString = self.readTo(self.pos(), start=startPos).strip()
+        pspString = self._applyExpressionFilters(pspString, 'psp', startPos=startPos)
+        self._compiler.addPSP(pspString)
+        self.getPSPEndToken()
+
+    ## generic directive eat methods
+    _simpleIndentingDirectives = '''
+    else elif for while repeat unless try except finally'''.split()
+    _simpleExprDirectives = '''
+    pass continue stop return yield break
+    del assert raise
+    silent echo    
+    import from'''.split()
+    _directiveHandlerNames = {'import': 'addImportStatement',
+                              'from': 'addImportStatement', }
+    def eatDirective(self):
+        directiveName = self.matchDirective()
+        self._filterDisabledDirectives(directiveName)
+
+        for callback in self.setting('preparseDirectiveHooks'):
+            callback(parser=self, directiveName=directiveName)
+
+        # subclasses can override the default behaviours here by providing an
+        # eater method in self._directiveNamesAndParsers[directiveName]
+        directiveParser = self._directiveNamesAndParsers.get(directiveName)
+        if directiveParser:
+            directiveParser()
+        elif directiveName in self._simpleIndentingDirectives:
+            handlerName = self._directiveHandlerNames.get(directiveName)
+            if not handlerName:
+                handlerName = 'add'+directiveName.capitalize()
+            handler = getattr(self._compiler, handlerName)
+            self.eatSimpleIndentingDirective(directiveName, callback=handler)
+        elif directiveName in self._simpleExprDirectives:
+            handlerName = self._directiveHandlerNames.get(directiveName)
+            if not handlerName:
+                handlerName = 'add'+directiveName.capitalize()
+            handler = getattr(self._compiler, handlerName)
+            if directiveName in ('silent', 'echo'):
+                includeDirectiveNameInExpr = False
+            else:
+                includeDirectiveNameInExpr = True
+            expr = self.eatSimpleExprDirective(
+                directiveName,
+                includeDirectiveNameInExpr=includeDirectiveNameInExpr)
+            handler(expr)
+        ##    
+        for callback in self.setting('postparseDirectiveHooks'):
+            callback(parser=self, directiveName=directiveName)
+
+    def _eatRestOfDirectiveTag(self, isLineClearToStartToken, endOfFirstLinePos):
+        foundComment = False
+        if self.matchCommentStartToken():
+            pos = self.pos()
+            self.advance()
+            if not self.matchDirective():
+                self.setPos(pos)
+                foundComment = True
+                self.eatComment() # this won't gobble the EOL
+            else:
+                self.setPos(pos)
+            
+        if not foundComment and self.matchDirectiveEndToken():
+                self.getDirectiveEndToken()
+        elif isLineClearToStartToken and (not self.atEnd()) and self.peek() in '\r\n':
+            # still gobble the EOL if a comment was found. 
+            self.readToEOL(gobble=True)
+            
+        if isLineClearToStartToken and (self.atEnd() or self.pos() > endOfFirstLinePos):
+            self._compiler.handleWSBeforeDirective()
+
+    def _eatToThisEndDirective(self, directiveName):
+        finalPos = endRawPos = startPos = self.pos()
+        directiveChar = self.setting('directiveStartToken')[0]
+        isLineClearToStartToken = False
+        while not self.atEnd():
+            if self.peek() == directiveChar:
+                if self.matchDirective() == 'end':
+                    endRawPos = self.pos()
+                    self.getDirectiveStartToken()
+                    self.advance(len('end'))
+                    self.getWhiteSpace()
+                    if self.startswith(directiveName):
+                        if self.isLineClearToStartToken(endRawPos):
+                            isLineClearToStartToken = True
+                            endRawPos = self.findBOL(endRawPos)
+                        self.advance(len(directiveName)) # to end of directiveName
+                        self.getWhiteSpace()
+                        finalPos = self.pos()
+                        break
+            self.advance()
+            finalPos = endRawPos = self.pos()
+
+        textEaten = self.readTo(endRawPos, start=startPos)
+        self.setPos(finalPos)
+        
+        endOfFirstLinePos = self.findEOL()
+        
+        if self.matchDirectiveEndToken():
+            self.getDirectiveEndToken()
+        elif isLineClearToStartToken and (not self.atEnd()) and self.peek() in '\r\n':
+            self.readToEOL(gobble=True)
+            
+        if isLineClearToStartToken and self.pos() > endOfFirstLinePos:
+            self._compiler.handleWSBeforeDirective()
+        return textEaten
+
+
+    def eatSimpleExprDirective(self, directiveName, includeDirectiveNameInExpr=True):
+        # filtered 
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        if not includeDirectiveNameInExpr:
+            self.advance(len(directiveName))
+        startPos = self.pos()
+        expr = self.getExpression().strip()
+        directiveName = expr.split()[0]
+        expr = self._applyExpressionFilters(expr, directiveName, startPos=startPos)
+        if directiveName in self._closeableDirectives:
+            self.pushToOpenDirectivesStack(directiveName)
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+        return expr
+
+    def eatSimpleIndentingDirective(self, directiveName, callback,
+                                    includeDirectiveNameInExpr=False):
+        # filtered 
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        lineCol = self.getRowCol()
+        self.getDirectiveStartToken()
+        if directiveName not in 'else elif for while try except finally'.split():
+            self.advance(len(directiveName))
+        startPos = self.pos()
+
+        self.getWhiteSpace()
+
+        expr = self.getExpression(pyTokensToBreakAt=[':'])
+        expr = self._applyExpressionFilters(expr, directiveName, startPos=startPos)
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            if directiveName in 'else elif except finally'.split():
+                callback(expr, dedent=False, lineCol=lineCol)
+            else:
+                callback(expr, lineCol=lineCol)
+                
+            self.getWhiteSpace(max=1)
+            self.parse(breakPoint=self.findEOL(gobble=True))
+            self._compiler.commitStrConst()
+            self._compiler.dedent()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            if directiveName in self._closeableDirectives:
+                self.pushToOpenDirectivesStack(directiveName)
+            callback(expr, lineCol=lineCol)
+
+    def eatEndDirective(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        self.getDirectiveStartToken()
+        self.advance(3)                 # to end of 'end'
+        self.getWhiteSpace()
+        pos = self.pos()
+        directiveName = False
+        for key in self._endDirectiveNamesAndHandlers.keys():
+            if self.find(key, pos) == pos:
+                directiveName = key
+                break
+        if not directiveName:
+            raise ParseError(self, msg='Invalid end directive')
+        
+        endOfFirstLinePos = self.findEOL()
+        self.getExpression() # eat in any extra comment-like crap
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)            
+        if directiveName in self._closeableDirectives:
+            self.popFromOpenDirectivesStack(directiveName)
+
+        # subclasses can override the default behaviours here by providing an
+        # end-directive handler in self._endDirectiveNamesAndHandlers[directiveName]
+        if self._endDirectiveNamesAndHandlers.get(directiveName):
+            handler = self._endDirectiveNamesAndHandlers[directiveName]
+            handler()
+        elif directiveName in 'block capture cache call filter errorCatcher'.split():
+            if key == 'block':
+                self._compiler.closeBlock()
+            elif key == 'capture':
+                self._compiler.endCaptureRegion()
+            elif key == 'cache':
+                self._compiler.endCacheRegion()
+            elif key == 'call':
+                self._compiler.endCallRegion()
+            elif key == 'filter':
+                self._compiler.closeFilterBlock()
+            elif key == 'errorCatcher':
+                self._compiler.turnErrorCatcherOff()
+        elif directiveName in 'while for if try repeat unless'.split():
+            self._compiler.commitStrConst()
+            self._compiler.dedent()
+        elif directiveName=='closure':
+            self._compiler.commitStrConst()
+            self._compiler.dedent()
+            # @@TR: temporary hack of useSearchList
+            self.setSetting('useSearchList', self._useSearchList_orig)            
+
+    ## specific directive eat methods
+    
+    def eatBreakPoint(self):
+        """Tells the parser to stop parsing at this point and completely ignore
+        everything else.
+
+        This is a debugging tool.
+        """
+        self.setBreakPoint(self.pos())
+
+    def eatShbang(self):
+        # filtered 
+        self.getDirectiveStartToken()
+        self.advance(len('shBang'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        shBang = self.readToEOL()
+        shBang = self._applyExpressionFilters(shBang, 'shbang', startPos=startPos)
+        self._compiler.setShBang(shBang.strip())
+
+    def eatEncoding(self):
+        # filtered 
+        self.getDirectiveStartToken()
+        self.advance(len('encoding'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        encoding = self.readToEOL()
+        encoding = self._applyExpressionFilters(encoding, 'encoding', startPos=startPos)               
+        self._compiler.setModuleEncoding(encoding.strip())
+        
+    def eatCompiler(self):
+        # filtered 
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        startPos = self.pos()
+        self.getDirectiveStartToken()
+        self.advance(len('compiler'))   # to end of 'compiler'
+        self.getWhiteSpace()
+
+        startPos = self.pos()
+        settingName = self.getIdentifier()
+
+        if settingName.lower() == 'reset':
+            self.getExpression() # gobble whitespace & junk
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+            self._initializeSettings()
+            self.configureParser()
+            return
+        
+        self.getWhiteSpace()
+        if self.peek() == '=':
+            self.advance()
+        else:
+            raise ParseError(self)
+        valueExpr = self.getExpression()
+        endPos = self.pos()
+
+        # @@TR: it's unlikely that anyone apply filters would have left this
+        # directive enabled:
+        # @@TR: fix up filtering, regardless
+        self._applyExpressionFilters('%s=%r'%(settingName, valueExpr),
+                                     'compiler', startPos=startPos)
+        
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+        try:
+            self._compiler.setCompilerSetting(settingName, valueExpr)
+        except:
+            sys.stderr.write('An error occurred while processing the following #compiler directive.\n')
+            sys.stderr.write('----------------------------------------------------------------------\n')
+            sys.stderr.write('%s\n' % self[startPos:endPos])
+            sys.stderr.write('----------------------------------------------------------------------\n')
+            sys.stderr.write('Please check the syntax of these settings.\n\n')
+            raise
+
+
+    def eatCompilerSettings(self):
+        # filtered         
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('compiler-settings'))   # to end of 'settings'
+        
+        keywords = self.getTargetVarsList()
+        self.getExpression()            # gobble any garbage
+            
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+
+        if 'reset' in keywords:
+            self._compiler._initializeSettings()
+            self.configureParser()
+            # @@TR: this implies a single-line #compiler-settings directive, and
+            # thus we should parse forward for an end directive.
+            # Subject to change in the future
+            return 
+        startPos = self.pos()
+        settingsStr = self._eatToThisEndDirective('compiler-settings')            
+        settingsStr = self._applyExpressionFilters(settingsStr, 'compilerSettings', 
+                                                   startPos=startPos)
+        try:
+            self._compiler.setCompilerSettings(keywords=keywords, settingsStr=settingsStr)
+        except:
+            sys.stderr.write('An error occurred while processing the following compiler settings.\n')
+            sys.stderr.write('----------------------------------------------------------------------\n')
+            sys.stderr.write('%s\n' % settingsStr.strip())
+            sys.stderr.write('----------------------------------------------------------------------\n')
+            sys.stderr.write('Please check the syntax of these settings.\n\n')
+            raise
+
+    def eatAttr(self):
+        # filtered         
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        startPos = self.pos()
+        self.getDirectiveStartToken()
+        self.advance(len('attr'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        if self.matchCheetahVarStart():
+            self.getCheetahVarStartToken()
+        attribName = self.getIdentifier()
+        self.getWhiteSpace()
+        self.getAssignmentOperator()
+        expr = self.getExpression()
+        expr = self._applyExpressionFilters(expr, 'attr', startPos=startPos)
+        self._compiler.addAttribute(attribName, expr)
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+
+    def eatDecorator(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        startPos = self.pos()
+        self.getDirectiveStartToken()
+        #self.advance() # eat @
+        startPos = self.pos()
+        decoratorExpr = self.getExpression()
+        decoratorExpr = self._applyExpressionFilters(decoratorExpr, 'decorator', startPos=startPos)
+        self._compiler.addDecorator(decoratorExpr)
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+        self.getWhiteSpace()
+
+        directiveName = self.matchDirective()
+        if not directiveName or directiveName not in ('def', 'block', 'closure', '@'):
+            raise ParseError(
+                self, msg='Expected #def, #block, #closure or another @decorator')
+        self.eatDirective()
+        
+    def eatDef(self):
+        # filtered         
+        self._eatDefOrBlock('def')
+
+    def eatBlock(self):
+        # filtered
+        startPos = self.pos()
+        methodName, rawSignature = self._eatDefOrBlock('block')
+        self._compiler._blockMetaData[methodName] = {
+            'raw': rawSignature,
+            'lineCol': self.getRowCol(startPos),
+            }
+
+    def eatClosure(self):
+        # filtered         
+        self._eatDefOrBlock('closure')
+        
+    def _eatDefOrBlock(self, directiveName):
+        # filtered 
+        assert directiveName in ('def', 'block', 'closure')
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        startPos = self.pos()
+        self.getDirectiveStartToken()
+        self.advance(len(directiveName))
+        self.getWhiteSpace()
+        if self.matchCheetahVarStart():
+            self.getCheetahVarStartToken()
+        methodName = self.getIdentifier()
+        self.getWhiteSpace()
+        if self.peek() == '(':
+            argsList = self.getDefArgList()
+            self.advance()              # past the closing ')'
+            if argsList and argsList[0][0] == 'self':
+                del argsList[0]
+        else:
+            argsList=[]
+
+        def includeBlockMarkers():
+            if self.setting('includeBlockMarkers'):
+                startMarker = self.setting('blockMarkerStart')
+                self._compiler.addStrConst(startMarker[0] + methodName + startMarker[1])
+
+        # @@TR: fix up filtering
+        self._applyExpressionFilters(self[startPos:self.pos()], 'def', startPos=startPos)
+
+        if self.matchColonForSingleLineShortFormDirective():
+            isNestedDef = (self.setting('allowNestedDefScopes')
+                           and [name for name in self._openDirectivesStack if name=='def'])
+            self.getc()
+            rawSignature = self[startPos:endOfFirstLinePos]
+            self._eatSingleLineDef(directiveName=directiveName,
+                                   methodName=methodName,
+                                   argsList=argsList,
+                                   startPos=startPos,
+                                   endPos=endOfFirstLinePos)
+            if directiveName == 'def' and not isNestedDef:
+                #@@TR: must come before _eatRestOfDirectiveTag ... for some reason
+                self._compiler.closeDef()
+            elif directiveName == 'block':
+                includeBlockMarkers()
+                self._compiler.closeBlock()
+            elif directiveName == 'closure' or isNestedDef:
+                self._compiler.dedent()
+                
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+        else:
+            if self.peek()==':':
+                self.getc()            
+            self.pushToOpenDirectivesStack(directiveName)
+            rawSignature = self[startPos:self.pos()]
+            self._eatMultiLineDef(directiveName=directiveName,
+                                  methodName=methodName,
+                                  argsList=argsList,
+                                  startPos=startPos,
+                                  isLineClearToStartToken=isLineClearToStartToken)
+            if directiveName == 'block':
+                includeBlockMarkers()
+
+        return methodName, rawSignature
+
+    def _eatMultiLineDef(self, directiveName, methodName, argsList, startPos,
+                         isLineClearToStartToken=False):
+        # filtered in calling method
+        self.getExpression()            # slurp up any garbage left at the end
+        signature = self[startPos:self.pos()]
+        endOfFirstLinePos = self.findEOL()
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+        signature = ' '.join([line.strip() for line in signature.splitlines()]) 
+        parserComment = ('## CHEETAH: generated from ' + signature + 
+                         ' at line %s, col %s' % self.getRowCol(startPos)
+                         + '.')
+
+        isNestedDef = (self.setting('allowNestedDefScopes')
+                       and len([name for name in self._openDirectivesStack if name=='def'])>1)
+        if directiveName=='block' or (directiveName=='def' and not isNestedDef):
+            self._compiler.startMethodDef(methodName, argsList, parserComment)
+        else: #closure
+            self._useSearchList_orig = self.setting('useSearchList')
+            self.setSetting('useSearchList', False)
+            self._compiler.addClosure(methodName, argsList, parserComment)
+
+        return methodName
+
+    def _eatSingleLineDef(self, directiveName, methodName, argsList, startPos, endPos):
+        # filtered in calling method        
+        fullSignature = self[startPos:endPos]
+        parserComment = ('## Generated from ' + fullSignature + 
+                         ' at line %s, col %s' % self.getRowCol(startPos)
+                         + '.')
+        isNestedDef = (self.setting('allowNestedDefScopes')
+                       and [name for name in self._openDirectivesStack if name=='def'])
+        if directiveName=='block' or (directiveName=='def' and not isNestedDef):
+            self._compiler.startMethodDef(methodName, argsList, parserComment)
+        else: #closure
+            # @@TR: temporary hack of useSearchList
+            useSearchList_orig = self.setting('useSearchList')
+            self.setSetting('useSearchList', False)            
+            self._compiler.addClosure(methodName, argsList, parserComment)            
+
+        self.getWhiteSpace(max=1)
+        self.parse(breakPoint=endPos)        
+        if directiveName=='closure' or isNestedDef: # @@TR: temporary hack of useSearchList
+            self.setSetting('useSearchList', useSearchList_orig)
+    
+    def eatExtends(self):
+        # filtered
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('extends'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        if self.setting('allowExpressionsInExtendsDirective'):
+            baseName = self.getExpression()
+        else:
+            baseName = self.getCommaSeparatedSymbols()
+            baseName = ', '.join(baseName)
+                       
+        baseName = self._applyExpressionFilters(baseName, 'extends', startPos=startPos)
+        self._compiler.setBaseClass(baseName) # in compiler
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+            
+    def eatImplements(self):
+        # filtered
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('implements'))
+        self.getWhiteSpace()       
+        startPos = self.pos()
+        methodName = self.getIdentifier()
+        if not self.atEnd() and self.peek() == '(':
+            argsList = self.getDefArgList()
+            self.advance()              # past the closing ')'
+            if argsList and argsList[0][0] == 'self':
+                del argsList[0]
+        else:
+            argsList=[]
+
+        # @@TR: need to split up filtering of the methodname and the args
+        #methodName = self._applyExpressionFilters(methodName, 'implements', startPos=startPos)
+        self._applyExpressionFilters(self[startPos:self.pos()], 'implements', startPos=startPos)
+
+        self._compiler.setMainMethodName(methodName)
+        self._compiler.setMainMethodArgs(argsList)
+            
+        self.getExpression()  # throw away and unwanted crap that got added in
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+
+    def eatSuper(self):
+        # filtered
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('super'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        if not self.atEnd() and self.peek() == '(':
+            argsList = self.getDefArgList()
+            self.advance()              # past the closing ')'
+            if argsList and argsList[0][0] == 'self':
+                del argsList[0]
+        else:
+            argsList=[]
+
+        self._applyExpressionFilters(self[startPos:self.pos()], 'super', startPos=startPos)
+
+        #parserComment = ('## CHEETAH: generated from ' + signature + 
+        #                 ' at line %s, col %s' % self.getRowCol(startPos)
+        #                 + '.')
+
+        self.getExpression()  # throw away and unwanted crap that got added in
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+        self._compiler.addSuper(argsList)
+
+    def eatSet(self):
+        # filtered
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(3)
+        self.getWhiteSpace()
+        style = SET_LOCAL
+        if self.startswith('local'):
+            self.getIdentifier()
+            self.getWhiteSpace()
+        elif self.startswith('global'):
+            self.getIdentifier()
+            self.getWhiteSpace()
+            style = SET_GLOBAL
+        elif self.startswith('module'):
+            self.getIdentifier()
+            self.getWhiteSpace()
+            style = SET_MODULE
+
+        startsWithDollar = self.matchCheetahVarStart()
+        startPos = self.pos()
+        LVALUE = self.getExpression(pyTokensToBreakAt=assignmentOps, useNameMapper=False).strip()
+        OP = self.getAssignmentOperator()
+        RVALUE = self.getExpression()        
+        expr = LVALUE + ' ' + OP + ' ' + RVALUE.strip()
+        
+        expr = self._applyExpressionFilters(expr, 'set', startPos=startPos)
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
+
+        class Components: pass # used for 'set global'
+        exprComponents = Components()
+        exprComponents.LVALUE = LVALUE
+        exprComponents.OP = OP
+        exprComponents.RVALUE = RVALUE
+        self._compiler.addSet(expr, exprComponents, style)
+    
+    def eatSlurp(self):
+        if self.isLineClearToStartToken():
+            self._compiler.handleWSBeforeDirective()
+        self._compiler.commitStrConst()
+        self.readToEOL(gobble=True)
+
+    def eatEOLSlurpToken(self):
+        if self.isLineClearToStartToken():
+            self._compiler.handleWSBeforeDirective()
+        self._compiler.commitStrConst()
+        self.readToEOL(gobble=True)        
+
+    def eatRaw(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('raw'))
+        self.getWhiteSpace()
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self.getWhiteSpace(max=1)
+            rawBlock = self.readToEOL(gobble=False)
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            rawBlock = self._eatToThisEndDirective('raw')
+        self._compiler.addRawText(rawBlock)
+    
+    def eatInclude(self):
+        # filtered
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('include'))
+
+        self.getWhiteSpace()
+        includeFrom = 'file'
+        isRaw = False
+        if self.startswith('raw'):
+            self.advance(3)
+            isRaw=True
+            
+        self.getWhiteSpace()            
+        if self.startswith('source'):
+            self.advance(len('source'))
+            includeFrom = 'str'
+            self.getWhiteSpace()
+            if not self.peek() == '=':
+                raise ParseError(self)
+            self.advance()
+        startPos = self.pos()
+        sourceExpr = self.getExpression()
+        sourceExpr = self._applyExpressionFilters(sourceExpr, 'include', startPos=startPos)        
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+        self._compiler.addInclude(sourceExpr, includeFrom, isRaw)
+
+    
+    def eatDefMacro(self):
+        # @@TR: not filtered yet
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('defmacro'))
+
+        self.getWhiteSpace()
+        if self.matchCheetahVarStart():
+            self.getCheetahVarStartToken()
+        macroName = self.getIdentifier()
+        self.getWhiteSpace()
+        if self.peek() == '(':
+            argsList = self.getDefArgList(useNameMapper=False)
+            self.advance()              # past the closing ')'
+            if argsList and argsList[0][0] == 'self':
+                del argsList[0]
+        else:
+            argsList=[]
+
+        assert macroName not in self._directiveNamesAndParsers
+        argsList.insert(0, ('src', None))
+        argsList.append(('parser', 'None'))
+        argsList.append(('macros', 'None'))
+        argsList.append(('compilerSettings', 'None'))
+        argsList.append(('isShortForm', 'None'))
+        argsList.append(('EOLCharsInShortForm', 'None'))        
+        argsList.append(('startPos', 'None'))
+        argsList.append(('endPos', 'None'))
+        
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self.getWhiteSpace(max=1)
+            macroSrc = self.readToEOL(gobble=False)
+            self.readToEOL(gobble=True)
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            macroSrc = self._eatToThisEndDirective('defmacro')
+
+        #print argsList
+        normalizedMacroSrc = ''.join(
+            ['%def callMacro('+','.join([defv and '%s=%s'%(n, defv) or n
+                                         for n, defv in argsList])
+             +')\n',
+             macroSrc,
+             '%end def'])
+
+        
+        from Cheetah.Template import Template
+        templateAPIClass = self.setting('templateAPIClassForDefMacro', default=Template)
+        compilerSettings = self.setting('compilerSettingsForDefMacro', default={})
+        searchListForMacros = self.setting('searchListForDefMacro', default=[])
+        searchListForMacros = list(searchListForMacros) # copy to avoid mutation bugs
+        searchListForMacros.append({'macros': self._macros,
+                                    'parser': self,
+                                    'compilerSettings': self.settings(),                                    
+                                    })
+        
+        templateAPIClass._updateSettingsWithPreprocessTokens(
+            compilerSettings, placeholderToken='@', directiveToken='%')
+        macroTemplateClass = templateAPIClass.compile(source=normalizedMacroSrc,
+                                                      compilerSettings=compilerSettings)
+        #print normalizedMacroSrc
+        #t = macroTemplateClass()
+        #print t.callMacro('src')
+        #print t.generatedClassCode()
+        
+        class MacroDetails: pass
+        macroDetails = MacroDetails()
+        macroDetails.macroSrc = macroSrc
+        macroDetails.argsList = argsList
+        macroDetails.template = macroTemplateClass(searchList=searchListForMacros)
+
+        self._macroDetails[macroName] = macroDetails
+        self._macros[macroName] = macroDetails.template.callMacro
+        self._directiveNamesAndParsers[macroName] = self.eatMacroCall
+
+    def eatMacroCall(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        startPos = self.pos()
+        self.getDirectiveStartToken()
+        macroName = self.getIdentifier()
+        macro = self._macros[macroName]
+        if hasattr(macro, 'parse'):
+            return macro.parse(parser=self, startPos=startPos)
+        
+        if hasattr(macro, 'parseArgs'):
+            args = macro.parseArgs(parser=self, startPos=startPos)
+        else:
+            self.getWhiteSpace()
+            args = self.getExpression(useNameMapper=False,
+                                      pyTokensToBreakAt=[':']).strip()
+
+        if self.matchColonForSingleLineShortFormDirective():
+            isShortForm = True
+            self.advance() # skip over :
+            self.getWhiteSpace(max=1)
+            srcBlock = self.readToEOL(gobble=False)
+            EOLCharsInShortForm = self.readToEOL(gobble=True)
+            #self.readToEOL(gobble=False)
+        else:
+            isShortForm = False
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            srcBlock = self._eatToThisEndDirective(macroName)
+
+
+        if hasattr(macro, 'convertArgStrToDict'):
+            kwArgs = macro.convertArgStrToDict(args, parser=self, startPos=startPos)
+        else:
+            def getArgs(*pargs, **kws):
+                return pargs, kws
+            exec('positionalArgs, kwArgs = getArgs(%(args)s)'%locals())
+
+        assert 'src' not in kwArgs
+        kwArgs['src'] = srcBlock
+
+        if isinstance(macro, types.MethodType):
+            co = macro.im_func.func_code
+        elif (hasattr(macro, '__call__')
+              and hasattr(macro.__call__, 'im_func')):
+            co = macro.__call__.im_func.func_code
+        else:
+            co = macro.func_code
+        availableKwArgs = inspect.getargs(co)[0]
+        
+        if 'parser' in availableKwArgs:
+            kwArgs['parser'] = self
+        if 'macros' in availableKwArgs:
+            kwArgs['macros'] = self._macros
+        if 'compilerSettings' in availableKwArgs:
+            kwArgs['compilerSettings'] = self.settings()
+        if 'isShortForm' in availableKwArgs:
+            kwArgs['isShortForm'] = isShortForm
+        if isShortForm and 'EOLCharsInShortForm' in availableKwArgs:
+            kwArgs['EOLCharsInShortForm'] = EOLCharsInShortForm
+
+        if 'startPos' in availableKwArgs:
+            kwArgs['startPos'] = startPos
+        if 'endPos' in availableKwArgs:
+            kwArgs['endPos'] = self.pos()
+
+        srcFromMacroOutput = macro(**kwArgs)
+
+        origParseSrc = self._src
+        origBreakPoint = self.breakPoint()
+        origPos = self.pos()
+        # add a comment to the output about the macro src that is being parsed
+        # or add a comment prefix to all the comments added by the compiler
+        self._src = srcFromMacroOutput
+        self.setPos(0)
+        self.setBreakPoint(len(srcFromMacroOutput))
+        
+        self.parse(assertEmptyStack=False)
+
+        self._src = origParseSrc
+        self.setBreakPoint(origBreakPoint)
+        self.setPos(origPos)                
+
+
+        #self._compiler.addRawText('end')
+        
+    def eatCache(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        lineCol = self.getRowCol()
+        self.getDirectiveStartToken()
+        self.advance(len('cache'))
+
+        startPos = self.pos()
+        argList = self.getDefArgList(useNameMapper=True)
+        argList = self._applyExpressionFilters(argList, 'cache', startPos=startPos)
+
+        def startCache():
+            cacheInfo = self._compiler.genCacheInfoFromArgList(argList)
+            self._compiler.startCacheRegion(cacheInfo, lineCol)
+
+        if self.matchColonForSingleLineShortFormDirective():            
+            self.advance() # skip over :
+            self.getWhiteSpace(max=1)
+            startCache()
+            self.parse(breakPoint=self.findEOL(gobble=True))
+            self._compiler.endCacheRegion()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()            
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            self.pushToOpenDirectivesStack('cache')
+            startCache()        
+
+    def eatCall(self):
+        # @@TR: need to enable single line version of this
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        lineCol = self.getRowCol()
+        self.getDirectiveStartToken()
+        self.advance(len('call'))
+        startPos = self.pos()
+        
+        useAutocallingOrig = self.setting('useAutocalling')
+        self.setSetting('useAutocalling', False)
+        self.getWhiteSpace()
+        if self.matchCheetahVarStart():
+            functionName = self.getCheetahVar()
+        else:
+            functionName = self.getCheetahVar(plain=True, skipStartToken=True)
+        self.setSetting('useAutocalling', useAutocallingOrig)
+        # @@TR: fix up filtering
+        self._applyExpressionFilters(self[startPos:self.pos()], 'call', startPos=startPos)
+
+        self.getWhiteSpace()
+        args = self.getExpression(pyTokensToBreakAt=[':']).strip()
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self._compiler.startCallRegion(functionName, args, lineCol)
+            self.getWhiteSpace(max=1)
+            self.parse(breakPoint=self.findEOL(gobble=False))
+            self._compiler.endCallRegion()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self.pushToOpenDirectivesStack("call")            
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            self._compiler.startCallRegion(functionName, args, lineCol)
+
+    def eatCallArg(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        lineCol = self.getRowCol()
+        self.getDirectiveStartToken()
+
+        self.advance(len('arg'))
+        startPos = self.pos()
+        self.getWhiteSpace()
+        argName = self.getIdentifier()
+        self.getWhiteSpace()
+        argName = self._applyExpressionFilters(argName, 'arg', startPos=startPos)
+        self._compiler.setCallArg(argName, lineCol)
+        if self.peek() == ':':
+            self.getc()
+        else:        
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+
+    def eatFilter(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+
+        self.getDirectiveStartToken()
+        self.advance(len('filter'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        if self.matchCheetahVarStart():
+            isKlass = True
+            theFilter = self.getExpression(pyTokensToBreakAt=[':'])
+        else:
+            isKlass = False
+            theFilter = self.getIdentifier()
+            self.getWhiteSpace()
+        theFilter = self._applyExpressionFilters(theFilter, 'filter', startPos=startPos)            
+
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self.getWhiteSpace(max=1)            
+            self._compiler.setFilter(theFilter, isKlass)
+            self.parse(breakPoint=self.findEOL(gobble=False))
+            self._compiler.closeFilterBlock()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self.pushToOpenDirectivesStack("filter")
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            self._compiler.setFilter(theFilter, isKlass)        
+
+    def eatTransform(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+
+        self.getDirectiveStartToken()
+        self.advance(len('transform'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        if self.matchCheetahVarStart():
+            isKlass = True
+            transformer = self.getExpression(pyTokensToBreakAt=[':'])
+        else:
+            isKlass = False
+            transformer = self.getIdentifier()
+            self.getWhiteSpace()
+        transformer = self._applyExpressionFilters(transformer, 'transform', startPos=startPos)
+
+        if self.peek()==':':
+            self.advance()
+        self.getWhiteSpace()
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+        self._compiler.setTransform(transformer, isKlass)
+
+        
+    def eatErrorCatcher(self):
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        self.getDirectiveStartToken()
+        self.advance(len('errorCatcher'))
+        self.getWhiteSpace()
+        startPos = self.pos()
+        errorCatcherName = self.getIdentifier()
+        errorCatcherName = self._applyExpressionFilters(
+            errorCatcherName, 'errorcatcher', startPos=startPos)
+        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)        
+        self._compiler.setErrorCatcher(errorCatcherName)
+
+    def eatCapture(self):
+        # @@TR:  this could be refactored to use the code in eatSimpleIndentingDirective
+        # filtered 
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLinePos = self.findEOL()
+        lineCol = self.getRowCol()
+
+        self.getDirectiveStartToken()
+        self.advance(len('capture'))        
+        startPos = self.pos()
+        self.getWhiteSpace()
+
+        expr = self.getExpression(pyTokensToBreakAt=[':'])
+        expr = self._applyExpressionFilters(expr, 'capture', startPos=startPos)
+        if self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self._compiler.startCaptureRegion(assignTo=expr, lineCol=lineCol)
+            self.getWhiteSpace(max=1)
+            self.parse(breakPoint=self.findEOL(gobble=False))
+            self._compiler.endCaptureRegion()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
+            self.pushToOpenDirectivesStack("capture")
+            self._compiler.startCaptureRegion(assignTo=expr, lineCol=lineCol)
+        
+
+    def eatIf(self):
+        # filtered 
+        isLineClearToStartToken = self.isLineClearToStartToken()
+        endOfFirstLine = self.findEOL()
+        lineCol = self.getRowCol()
+        self.getDirectiveStartToken()
+        startPos = self.pos()
+        
+        expressionParts = self.getExpressionParts(pyTokensToBreakAt=[':'])
+        expr = ''.join(expressionParts).strip()
+        expr = self._applyExpressionFilters(expr, 'if', startPos=startPos)
+
+        isTernaryExpr = ('then' in expressionParts and 'else' in expressionParts)
+        if isTernaryExpr:
+            conditionExpr = []
+            trueExpr = []
+            falseExpr = []
+            currentExpr = conditionExpr
+            for part in expressionParts:
+                if part.strip()=='then':
+                    currentExpr = trueExpr
+                elif part.strip()=='else':
+                    currentExpr = falseExpr
+                else:
+                    currentExpr.append(part)
+                    
+            conditionExpr = ''.join(conditionExpr)
+            trueExpr = ''.join(trueExpr)
+            falseExpr = ''.join(falseExpr)
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)            
+            self._compiler.addTernaryExpr(conditionExpr, trueExpr, falseExpr, lineCol=lineCol)
+        elif self.matchColonForSingleLineShortFormDirective():
+            self.advance() # skip over :
+            self._compiler.addIf(expr, lineCol=lineCol)
+            self.getWhiteSpace(max=1)
+            self.parse(breakPoint=self.findEOL(gobble=True))            
+            self._compiler.commitStrConst()            
+            self._compiler.dedent()
+        else:
+            if self.peek()==':':
+                self.advance()
+            self.getWhiteSpace()                
+            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)            
+            self.pushToOpenDirectivesStack('if')
+            self._compiler.addIf(expr, lineCol=lineCol)
+
+    ## end directive handlers
+    def handleEndDef(self):
+        isNestedDef = (self.setting('allowNestedDefScopes')
+                       and [name for name in self._openDirectivesStack if name=='def'])
+        if not isNestedDef:
+            self._compiler.closeDef()
+        else:
+            # @@TR: temporary hack of useSearchList
+            self.setSetting('useSearchList', self._useSearchList_orig)                    
+            self._compiler.commitStrConst()
+            self._compiler.dedent()
+    ###
+
+    def pushToOpenDirectivesStack(self, directiveName):
+        assert directiveName in self._closeableDirectives
+        self._openDirectivesStack.append(directiveName)
+
+    def popFromOpenDirectivesStack(self, directiveName):
+        if not self._openDirectivesStack:
+            raise ParseError(self, msg="#end found, but nothing to end")
+        
+        if self._openDirectivesStack[-1] == directiveName:
+            del self._openDirectivesStack[-1]
+        else:
+            raise ParseError(self, msg="#end %s found, expected #end %s" %(
+                directiveName, self._openDirectivesStack[-1]))
+
+    def assertEmptyOpenDirectivesStack(self):
+        if self._openDirectivesStack:
+            errorMsg = (
+                "Some #directives are missing their corresponding #end ___ tag: %s" %(
+                ', '.join(self._openDirectivesStack)))
+            raise ParseError(self, msg=errorMsg)
+
+##################################################
+## Make an alias to export   
+Parser = _HighLevelParser
diff --git a/cheetah/Servlet.py b/cheetah/Servlet.py
new file mode 100644 (file)
index 0000000..70e8315
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+'''
+Provides an abstract Servlet baseclass for Cheetah's Template class
+'''
+
+import sys
+import os.path
+
+class Servlet(object):
+    """
+        This class is an abstract baseclass for Cheetah.Template.Template.
+    """
+
+    transaction = None
+    application = None
+    request = None
+    session = None
+
+    def respond(self, trans=None):
+        raise NotImplementedError("""\
+couldn't find the template's main method.  If you are using #extends
+without #implements, try adding '#implements respond' to your template
+definition.""")
+
+    def sleep(self, transaction):
+        super(Servlet, self).sleep(transaction)
+        self.session = None
+        self.request  = None
+        self._request  = None
+        self.response = None
+        self.transaction = None
+
+    def shutdown(self):
+        pass
+
+    def serverSidePath(self, path=None,
+                       normpath=os.path.normpath,
+                       abspath=os.path.abspath
+                       ):
+
+        if path:
+            return normpath(abspath(path.replace("\\", '/')))
+        elif hasattr(self, '_filePath') and self._filePath:
+            return normpath(abspath(self._filePath))
+        else:
+            return None
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/SettingsManager.py b/cheetah/SettingsManager.py
new file mode 100644 (file)
index 0000000..437954b
--- /dev/null
@@ -0,0 +1,284 @@
+import sys
+import os.path
+import copy as copyModule
+from ConfigParser import ConfigParser 
+import re
+from tokenize import Intnumber, Floatnumber, Number
+import types
+import time
+from StringIO import StringIO # not cStringIO because of unicode support
+import imp                 # used by SettingsManager.updateSettingsFromPySrcFile()
+
+
+numberRE = re.compile(Number)
+complexNumberRE = re.compile('[\(]*' +Number + r'[ \t]*\+[ \t]*' + Number + '[\)]*')
+
+##################################################
+## FUNCTIONS ##
+
+def mergeNestedDictionaries(dict1, dict2, copy=False, deepcopy=False):
+    """Recursively merge the values of dict2 into dict1.
+
+    This little function is very handy for selectively overriding settings in a
+    settings dictionary that has a nested structure.
+    """
+
+    if copy:
+        dict1 = copyModule.copy(dict1)
+    elif deepcopy:
+        dict1 = copyModule.deepcopy(dict1)
+        
+    for key, val in dict2.iteritems():
+        if key in dict1 and isinstance(val, dict) and isinstance(dict1[key], dict):
+            dict1[key] = mergeNestedDictionaries(dict1[key], val)
+        else:
+            dict1[key] = val
+    return dict1
+    
+def stringIsNumber(S):
+    """Return True if theString represents a Python number, False otherwise.
+    This also works for complex numbers and numbers with +/- in front."""
+
+    S = S.strip()
+    
+    if S[0] in '-+' and len(S) > 1:
+        S = S[1:].strip()
+    
+    match = complexNumberRE.match(S)
+    if not match:
+        match = numberRE.match(S)
+    if not match or (match.end() != len(S)):
+        return False
+    else:
+        return True
+        
+def convStringToNum(theString):
+    """Convert a string representation of a Python number to the Python version"""
+    
+    if not stringIsNumber(theString):
+        raise Error(theString + ' cannot be converted to a Python number')
+    return eval(theString, {}, {})
+
+
+class Error(Exception):
+    pass
+
+class NoDefault(object):
+    pass
+
+class ConfigParserCaseSensitive(ConfigParser):
+    """A case sensitive version of the standard Python ConfigParser."""
+    
+    def optionxform(self, optionstr):
+        """Don't change the case as is done in the default implemenation."""
+        return optionstr
+
+class _SettingsCollector(object):
+    """An abstract base class that provides the methods SettingsManager uses to
+    collect settings from config files and strings.
+
+    This class only collects settings it doesn't modify the _settings dictionary
+    of SettingsManager instances in any way.
+    """
+
+    _ConfigParserClass = ConfigParserCaseSensitive 
+
+    def readSettingsFromModule(self, mod, ignoreUnderscored=True):
+        """Returns all settings from a Python module.
+        """
+        S = {}
+        attrs = vars(mod)
+        for k, v in attrs.iteritems():
+            if (ignoreUnderscored and k.startswith('_')):
+                continue
+            else:
+                S[k] = v
+        return S
+        
+    def readSettingsFromPySrcStr(self, theString):
+        """Return a dictionary of the settings in a Python src string."""
+
+        globalsDict = {'True': (1==1),
+                       'False': (0==1),
+                       }
+        newSettings = {'self':self}
+        exec((theString+os.linesep), globalsDict, newSettings)        
+        del newSettings['self']
+        module = types.ModuleType('temp_settings_module')
+        module.__dict__.update(newSettings)
+        return self.readSettingsFromModule(module)
+
+    def readSettingsFromConfigFileObj(self, inFile, convert=True):
+        """Return the settings from a config file that uses the syntax accepted by
+        Python's standard ConfigParser module (like Windows .ini files).
+
+        NOTE:
+        this method maintains case unlike the ConfigParser module, unless this
+        class was initialized with the 'caseSensitive' keyword set to False.
+
+        All setting values are initially parsed as strings. However, If the
+        'convert' arg is True this method will do the following value
+        conversions:
+        
+        * all Python numeric literals will be coverted from string to number
+        
+        * The string 'None' will be converted to the Python value None
+        
+        * The string 'True' will be converted to a Python truth value
+        
+        * The string 'False' will be converted to a Python false value
+        
+        * Any string starting with 'python:' will be treated as a Python literal
+          or expression that needs to be eval'd. This approach is useful for
+          declaring lists and dictionaries.
+
+        If a config section titled 'Globals' is present the options defined
+        under it will be treated as top-level settings.        
+        """
+        
+        p = self._ConfigParserClass()
+        p.readfp(inFile)
+        sects = p.sections()
+        newSettings = {}
+
+        sects = p.sections()
+        newSettings = {}
+        
+        for s in sects:
+            newSettings[s] = {}
+            for o in p.options(s):
+                if o != '__name__':
+                    newSettings[s][o] = p.get(s, o)
+
+        ## loop through new settings -> deal with global settings, numbers,
+        ## booleans and None ++ also deal with 'importSettings' commands
+
+        for sect, subDict in newSettings.items():
+            for key, val in subDict.items():
+                if convert:
+                    if val.lower().startswith('python:'):
+                        subDict[key] = eval(val[7:], {}, {})
+                    if val.lower() == 'none':
+                        subDict[key] = None
+                    if val.lower() == 'true':
+                        subDict[key] = True
+                    if val.lower() == 'false':
+                        subDict[key] = False
+                    if stringIsNumber(val):
+                        subDict[key] = convStringToNum(val)
+                        
+                ## now deal with any 'importSettings' commands
+                if key.lower() == 'importsettings':
+                    if val.find(';') < 0:
+                        importedSettings = self.readSettingsFromPySrcFile(val)
+                    else:
+                        path = val.split(';')[0]
+                        rest = ''.join(val.split(';')[1:]).strip()
+                        parentDict = self.readSettingsFromPySrcFile(path)
+                        importedSettings = eval('parentDict["' + rest + '"]')
+                        
+                    subDict.update(mergeNestedDictionaries(subDict,
+                                                           importedSettings))
+                        
+            if sect.lower() == 'globals':
+                newSettings.update(newSettings[sect])
+                del newSettings[sect]
+                
+        return newSettings
+
+
+class SettingsManager(_SettingsCollector):
+    """A mixin class that provides facilities for managing application settings.
+    
+    SettingsManager is designed to work well with nested settings dictionaries
+    of any depth.
+    """
+
+    def __init__(self):
+        super(SettingsManager, self).__init__()
+        self._settings = {}
+        self._initializeSettings()
+
+    def _defaultSettings(self):
+        return {}
+    
+    def _initializeSettings(self):
+        """A hook that allows for complex setting initialization sequences that
+        involve references to 'self' or other settings.  For example:
+              self._settings['myCalcVal'] = self._settings['someVal'] * 15        
+        This method should be called by the class' __init__() method when needed.       
+        The dummy implementation should be reimplemented by subclasses.
+        """
+        
+        pass 
+
+    ## core post startup methods
+
+    def setting(self, name, default=NoDefault):
+        """Get a setting from self._settings, with or without a default value."""
+        
+        if default is NoDefault:
+            return self._settings[name]
+        else:
+            return self._settings.get(name, default)
+
+
+    def hasSetting(self, key):
+        """True/False"""
+        return key in self._settings
+
+    def setSetting(self, name, value):
+        """Set a setting in self._settings."""
+        self._settings[name] = value
+
+    def settings(self):
+        """Return a reference to the settings dictionary"""
+        return self._settings
+        
+    def copySettings(self):
+        """Returns a shallow copy of the settings dictionary"""
+        return copyModule.copy(self._settings)
+
+    def deepcopySettings(self):
+        """Returns a deep copy of the settings dictionary"""
+        return copyModule.deepcopy(self._settings)
+    
+    def updateSettings(self, newSettings, merge=True):
+        """Update the settings with a selective merge or a complete overwrite."""
+        
+        if merge:
+            mergeNestedDictionaries(self._settings, newSettings)
+        else:
+            self._settings.update(newSettings)
+
+
+    ## source specific update methods
+
+    def updateSettingsFromPySrcStr(self, theString, merge=True):
+        """Update the settings from a code in a Python src string."""
+        
+        newSettings = self.readSettingsFromPySrcStr(theString)
+        self.updateSettings(newSettings,
+                            merge=newSettings.get('mergeSettings', merge) )
+        
+    
+    def updateSettingsFromConfigFileObj(self, inFile, convert=True, merge=True):
+        """See the docstring for .updateSettingsFromConfigFile()
+
+        The caller of this method is responsible for closing the inFile file
+        object."""
+
+        newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
+        self.updateSettings(newSettings,
+                            merge=newSettings.get('mergeSettings', merge))
+
+    def updateSettingsFromConfigStr(self, configStr, convert=True, merge=True):
+        """See the docstring for .updateSettingsFromConfigFile()
+        """
+
+        configStr = '[globals]\n' + configStr
+        inFile = StringIO(configStr)
+        newSettings = self.readSettingsFromConfigFileObj(inFile, convert=convert)
+        self.updateSettings(newSettings,
+                            merge=newSettings.get('mergeSettings', merge))
+
diff --git a/cheetah/SourceReader.py b/cheetah/SourceReader.py
new file mode 100644 (file)
index 0000000..0a44ed0
--- /dev/null
@@ -0,0 +1,267 @@
+"""SourceReader class for Cheetah's Parser and CodeGenerator
+"""
+import re
+import sys
+
+EOLre = re.compile(r'[ \f\t]*(?:\r\n|\r|\n)')
+EOLZre = re.compile(r'(?:\r\n|\r|\n|\Z)')
+ENCODINGsearch = re.compile("coding[=:]\s*([-\w.]+)").search
+
+class Error(Exception):
+    pass
+                                
+class SourceReader(object):
+    def __init__(self, src, filename=None, breakPoint=None, encoding=None):
+        self._src = src
+        self._filename = filename
+        self._srcLen = len(src)
+        if breakPoint == None:
+            self._breakPoint = self._srcLen
+        else:
+            self.setBreakPoint(breakPoint)
+        self._pos = 0
+        self._bookmarks = {}
+        self._posTobookmarkMap = {}
+
+        ## collect some meta-information
+        self._EOLs = []
+        pos = 0
+        while pos < len(self):
+            EOLmatch = EOLZre.search(src, pos)
+            self._EOLs.append(EOLmatch.start())
+            pos = EOLmatch.end()
+            
+        self._BOLs = []
+        for pos in self._EOLs:
+            BOLpos = self.findBOL(pos)
+            self._BOLs.append(BOLpos)
+        
+    def src(self):
+        return self._src
+
+    def filename(self):
+        return self._filename
+
+    def __len__(self):
+        return self._breakPoint
+    
+    def __getitem__(self, i):
+        if not isinstance(i, int):
+            self.checkPos(i.stop)
+        else:
+            self.checkPos(i)
+        return self._src[i]
+    
+    def __getslice__(self, i, j):
+        i = max(i, 0); j = max(j, 0)
+        return self._src[i:j]
+
+    def splitlines(self):
+        if not hasattr(self, '_srcLines'):
+            self._srcLines = self._src.splitlines()
+        return self._srcLines
+
+    def lineNum(self, pos=None):
+        if pos == None:
+            pos = self._pos
+            
+        for i in range(len(self._BOLs)):
+            if pos >= self._BOLs[i] and pos <= self._EOLs[i]:
+                return i
+            
+    def getRowCol(self, pos=None):
+        if pos == None:
+            pos = self._pos
+        lineNum = self.lineNum(pos)
+        BOL, EOL = self._BOLs[lineNum], self._EOLs[lineNum]
+        return lineNum+1, pos-BOL+1
+            
+    def getRowColLine(self, pos=None):
+        if pos == None:
+            pos = self._pos
+        row, col = self.getRowCol(pos)    
+        return row, col, self.splitlines()[row-1]
+
+    def getLine(self, pos):
+        if pos == None:
+            pos = self._pos
+        lineNum = self.lineNum(pos)
+        return self.splitlines()[lineNum]
+        
+    def pos(self):
+        return self._pos
+    
+    def setPos(self, pos):
+        self.checkPos(pos)
+        self._pos = pos
+
+
+    def validPos(self, pos):
+        return pos <= self._breakPoint and pos >=0 
+                    
+    def checkPos(self, pos):
+        if not pos <= self._breakPoint:
+            raise Error("pos (" + str(pos) + ") is invalid: beyond the stream's end (" +
+                        str(self._breakPoint-1) + ")" )
+        elif not pos >=0:
+            raise Error("pos (" + str(pos) + ") is invalid: less than 0" )
+
+    def breakPoint(self):
+        return self._breakPoint
+    
+    def setBreakPoint(self, pos):
+        if pos > self._srcLen:
+            raise Error("New breakpoint (" + str(pos) +
+                        ") is invalid: beyond the end of stream's source string (" +
+                        str(self._srcLen) + ")" )
+        elif not pos >= 0:
+            raise Error("New breakpoint (" + str(pos) + ") is invalid: less than 0" )        
+        
+        self._breakPoint = pos
+
+    def setBookmark(self, name):
+        self._bookmarks[name] = self._pos
+        self._posTobookmarkMap[self._pos] = name
+
+    def hasBookmark(self, name):
+        return name in self._bookmarks
+    
+    def gotoBookmark(self, name):
+        if not self.hasBookmark(name):
+            raise Error("Invalid bookmark (" + name + ") is invalid: does not exist")
+        pos = self._bookmarks[name]
+        if not self.validPos(pos):
+            raise Error("Invalid bookmark (" + name + ', '+
+                        str(pos) + ") is invalid: pos is out of range" )        
+        self._pos = pos
+
+    def atEnd(self):
+        return self._pos >= self._breakPoint
+
+    def atStart(self):
+        return self._pos == 0
+                          
+    def peek(self, offset=0):
+        self.checkPos(self._pos+offset)
+        pos = self._pos + offset
+        return self._src[pos]
+
+    def getc(self):
+        pos = self._pos
+        if self.validPos(pos+1):
+            self._pos += 1
+        return self._src[pos]
+
+    def ungetc(self, c=None):
+        if not self.atStart():
+            raise Error('Already at beginning of stream')
+
+        self._pos -= 1
+        if not c==None:
+            self._src[self._pos] = c
+
+    def advance(self, offset=1):
+        self.checkPos(self._pos + offset)
+        self._pos += offset
+
+    def rev(self, offset=1):
+        self.checkPos(self._pos - offset)
+        self._pos -= offset
+               
+    def read(self, offset):
+        self.checkPos(self._pos + offset)
+        start = self._pos
+        self._pos += offset
+        return self._src[start:self._pos]
+
+    def readTo(self, to, start=None):
+        self.checkPos(to)
+        if start == None:
+            start = self._pos
+        self._pos = to
+        return self._src[start:to]
+
+        
+    def readToEOL(self, start=None, gobble=True):
+        EOLmatch = EOLZre.search(self.src(), self.pos())
+        if gobble:
+            pos = EOLmatch.end()
+        else:
+            pos = EOLmatch.start()
+        return self.readTo(to=pos, start=start)
+    
+
+    def find(self, it, pos=None):
+        if pos == None:
+            pos = self._pos
+        return self._src.find(it, pos )
+
+    def startswith(self, it, pos=None):
+        if self.find(it, pos) == self.pos():
+            return True
+        else:
+            return False
+                    
+    def rfind(self, it, pos):
+        if pos == None:
+            pos = self._pos
+        return self._src.rfind(it, pos)
+        
+    def findBOL(self, pos=None):
+        if pos == None:
+            pos = self._pos
+        src = self.src()
+        return max(src.rfind('\n', 0, pos)+1, src.rfind('\r', 0, pos)+1, 0)
+        
+    def findEOL(self, pos=None, gobble=False):
+        if pos == None:
+            pos = self._pos
+
+        match = EOLZre.search(self.src(), pos)
+        if gobble:
+            return match.end()
+        else:
+            return match.start()
+    
+    def isLineClearToPos(self, pos=None):
+        if pos == None:
+            pos = self.pos()
+        self.checkPos(pos)            
+        src = self.src()
+        BOL = self.findBOL()
+        return BOL == pos or src[BOL:pos].isspace()
+
+    def matches(self, strOrRE):
+        if isinstance(strOrRE, (str, unicode)):
+            return self.startswith(strOrRE, pos=self.pos())
+        else: # assume an re object
+            return strOrRE.match(self.src(), self.pos())
+
+    def matchWhiteSpace(self, WSchars=' \f\t'):
+        return (not self.atEnd()) and  self.peek() in WSchars
+
+    def getWhiteSpace(self, max=None, WSchars=' \f\t'):
+        if not self.matchWhiteSpace(WSchars):
+            return ''
+        start = self.pos()
+        breakPoint = self.breakPoint()
+        if max is not None:
+            breakPoint = min(breakPoint, self.pos()+max)
+        while self.pos() < breakPoint:
+            self.advance()
+            if not self.matchWhiteSpace(WSchars):
+                break
+        return self.src()[start:self.pos()]
+
+    def matchNonWhiteSpace(self, WSchars=' \f\t\n\r'):
+        return self.atEnd() or not self.peek() in WSchars
+
+    def getNonWhiteSpace(self, WSchars=' \f\t\n\r'):
+        if not self.matchNonWhiteSpace(WSchars):
+            return ''
+        start = self.pos()
+        while self.pos() < self.breakPoint():
+            self.advance()
+            if not self.matchNonWhiteSpace(WSchars):
+                break
+        return self.src()[start:self.pos()]
diff --git a/cheetah/Template.py b/cheetah/Template.py
new file mode 100644 (file)
index 0000000..7a9d0a7
--- /dev/null
@@ -0,0 +1,1941 @@
+'''
+Provides the core API for Cheetah.
+
+See the docstring in the Template class and the Users' Guide for more information
+'''
+
+################################################################################
+## DEPENDENCIES
+import sys                        # used in the error handling code
+import re                         # used to define the internal delims regex
+import logging
+import string
+import os.path
+import time                       # used in the cache refresh code
+from random import randrange
+import imp
+import inspect
+import StringIO
+import traceback
+import pprint
+import cgi                # Used by .webInput() if the template is a CGI script.
+import types 
+    
+try:
+    from threading import Lock
+except ImportError:
+    class Lock:
+        def acquire(self):
+            pass
+        def release(self): 
+            pass
+
+filetype = None
+
+if isinstance(sys.version_info[:], tuple):
+    # Python 2.xx
+    filetype = types.FileType
+    def createMethod(func, cls):
+        return types.MethodType(func, None, cls)
+else:
+    import io
+    filetype = io.IOBase
+    def createMethod(func, cls):
+        return types.MethodType(func, cls)
+
+
+
+from Cheetah.Version import convertVersionStringToTuple, MinCompatibleVersionTuple
+from Cheetah.Version import MinCompatibleVersion
+# Base classes for Template
+from Cheetah.Servlet import Servlet                 
+# More intra-package imports ...
+from Cheetah.Parser import ParseError, SourceReader
+from Cheetah.Compiler import Compiler, DEFAULT_COMPILER_SETTINGS
+from Cheetah import ErrorCatchers              # for placeholder tags
+from Cheetah import Filters                    # the output filters
+from Cheetah.convertTmplPathToModuleName import convertTmplPathToModuleName
+
+from Cheetah.Utils.Misc import checkKeywords     # Used in Template.__init__
+from Cheetah.Utils.Indenter import Indenter      # Used in Template.__init__ and for
+                                                 # placeholders
+from Cheetah.NameMapper import NotFound, valueFromSearchList
+from Cheetah.CacheStore import MemoryCacheStore, MemcachedCacheStore
+from Cheetah.CacheRegion import CacheRegion
+from Cheetah.Utils.WebInputMixin import _Converter, _lookup, NonNumericInputError
+
+from Cheetah.Unspecified import Unspecified
+
+# Decide whether to use the file modification time in file's cache key 
+__checkFileMtime = True
+def checkFileMtime(value):
+    globals()['__checkFileMtime'] = value
+
+class Error(Exception):
+    pass
+class PreprocessError(Error):
+    pass
+
+def hashList(l):
+    hashedList = []
+    for v in l:
+        if isinstance(v, dict):
+            v = hashDict(v)
+        elif isinstance(v, list):
+            v = hashList(v)
+        hashedList.append(v)
+    return hash(tuple(hashedList))
+
+def hashDict(d):
+    items = sorted(d.items())
+    hashedList = []
+    for k, v in items:
+        if isinstance(v, dict):
+            v = hashDict(v)
+        elif isinstance(v, list):
+            v = hashList(v)
+        hashedList.append((k, v))
+    return hash(tuple(hashedList))
+
+
+################################################################################
+## MODULE GLOBALS AND CONSTANTS
+
+def _genUniqueModuleName(baseModuleName):
+    """The calling code is responsible for concurrency locking.
+    """
+    if baseModuleName not in sys.modules:
+        finalName = baseModuleName
+    else:
+        finalName = ('cheetah_%s_%s_%s'%(baseModuleName,
+                                         str(time.time()).replace('.', '_'),
+                                         str(randrange(10000, 99999))))
+    return finalName
+
+# Cache of a cgi.FieldStorage() instance, maintained by .webInput().
+# This is only relavent to templates used as CGI scripts.
+_formUsedByWebInput = None
+
+def updateLinecache(filename, src):
+    import linecache
+    size = len(src)
+    mtime = time.time()
+    lines = src.splitlines()
+    fullname = filename
+    linecache.cache[filename] = size, mtime, lines, fullname
+
+class CompileCacheItem(object):
+    pass
+
+class TemplatePreprocessor(object):
+    '''
+    This is used with the preprocessors argument to Template.compile().
+
+    See the docstring for Template.compile
+
+    ** Preprocessors are an advanced topic **
+    '''
+    
+    def __init__(self, settings):
+        self._settings = settings
+
+    def preprocess(self, source, file):
+        """Create an intermediate template and return the source code
+        it outputs                
+        """
+        settings = self._settings
+        if not source: # @@TR: this needs improving
+            if isinstance(file, (str, unicode)): # it's a filename.
+                f = open(file)
+                source = f.read()
+                f.close()
+            elif hasattr(file, 'read'):
+                source = file.read()
+            file = None        
+
+        templateAPIClass = settings.templateAPIClass
+        possibleKwArgs = [
+            arg for arg in
+            inspect.getargs(templateAPIClass.compile.im_func.func_code)[0]
+            if arg not in ('klass', 'source', 'file',)]
+
+        compileKwArgs = {}
+        for arg in possibleKwArgs:
+            if hasattr(settings, arg):
+                compileKwArgs[arg] = getattr(settings, arg)
+
+        tmplClass = templateAPIClass.compile(source=source, file=file, **compileKwArgs)
+        tmplInstance = tmplClass(**settings.templateInitArgs)
+        outputSource = settings.outputTransformer(tmplInstance)
+        outputFile = None
+        return outputSource, outputFile
+        
+class Template(Servlet):
+    '''
+    This class provides a) methods used by templates at runtime and b)
+    methods for compiling Cheetah source code into template classes.
+
+    This documentation assumes you already know Python and the basics of object
+    oriented programming.  If you don't know Python, see the sections of the
+    Cheetah Users' Guide for non-programmers.  It also assumes you have read
+    about Cheetah's syntax in the Users' Guide.
+
+    The following explains how to use Cheetah from within Python programs or via
+    the interpreter. If you statically compile your templates on the command
+    line using the 'cheetah' script, this is not relevant to you. Statically
+    compiled Cheetah template modules/classes (e.g. myTemplate.py:
+    MyTemplateClasss) are just like any other Python module or class. Also note,
+    most Python web frameworks (Webware, Aquarium, mod_python, Turbogears,
+    CherryPy, Quixote, etc.) provide plugins that handle Cheetah compilation for
+    you.
+
+    There are several possible usage patterns:          
+       1) tclass = Template.compile(src)
+          t1 = tclass() # or tclass(namespaces=[namespace,...])
+          t2 = tclass() # or tclass(namespaces=[namespace2,...])
+          outputStr = str(t1) # or outputStr = t1.aMethodYouDefined()
+
+          Template.compile provides a rich and very flexible API via its
+          optional arguments so there are many possible variations of this
+          pattern.  One example is:
+            tclass = Template.compile('hello $name from $caller', baseclass=dict)
+            print tclass(name='world', caller='me')
+          See the Template.compile() docstring for more details.  
+
+       2) tmplInstance = Template(src)
+             # or Template(src, namespaces=[namespace,...])
+          outputStr = str(tmplInstance) # or outputStr = tmplInstance.aMethodYouDefined(...args...)
+
+    Notes on the usage patterns:
+    
+       usage pattern 1)       
+          This is the most flexible, but it is slightly more verbose unless you
+          write a wrapper function to hide the plumbing.  Under the hood, all
+          other usage patterns are based on this approach.  Templates compiled
+          this way can #extend (subclass) any Python baseclass: old-style or
+          new-style (based on object or a builtin type).
+
+       usage pattern 2)
+          This was Cheetah's original usage pattern.  It returns an instance,
+          but you can still access the generated class via
+          tmplInstance.__class__.  If you want to use several different
+          namespace 'searchLists' with a single template source definition,
+          you're better off with Template.compile (1).
+
+          Limitations (use pattern 1 instead):
+           - Templates compiled this way can only #extend subclasses of the
+             new-style 'object' baseclass.  Cheetah.Template is a subclass of
+             'object'.  You also can not #extend dict, list, or other builtin
+             types.  
+           - If your template baseclass' __init__ constructor expects args there
+             is currently no way to pass them in.
+
+    If you need to subclass a dynamically compiled Cheetah class, do something like this:
+        from Cheetah.Template import Template
+        T1 = Template.compile('$meth1 #def meth1: this is meth1 in T1')
+        T2 = Template.compile('#implements meth1\nthis is meth1 redefined in T2', baseclass=T1)
+        print T1, T1()
+        print T2, T2()
+
+
+    Note about class and instance attribute names:
+      Attributes used by Cheetah have a special prefix to avoid confusion with
+      the attributes of the templates themselves or those of template
+      baseclasses.
+      
+      Class attributes which are used in class methods look like this:
+          klass._CHEETAH_useCompilationCache (_CHEETAH_xxx)
+
+      Instance attributes look like this:
+          klass._CHEETAH__globalSetVars (_CHEETAH__xxx with 2 underscores)
+    '''
+
+    # this is used by ._addCheetahPlumbingCodeToClass()
+    _CHEETAH_requiredCheetahMethods = (
+         '_initCheetahInstance',
+         'searchList',
+         'errorCatcher',
+         'getVar',
+         'varExists',
+         'getFileContents',
+         'i18n',
+         'runAsMainProgram',
+         'respond',
+         'shutdown',
+         'webInput',
+         'serverSidePath',
+         'generatedClassCode',
+         'generatedModuleCode',
+
+         '_getCacheStore',
+         '_getCacheStoreIdPrefix',
+         '_createCacheRegion',
+         'getCacheRegion',
+         'getCacheRegions',
+         'refreshCache',
+         
+         '_handleCheetahInclude',
+         '_getTemplateAPIClassForIncludeDirectiveCompilation',
+         )
+    _CHEETAH_requiredCheetahClassMethods = ('subclass',) 
+    _CHEETAH_requiredCheetahClassAttributes = ('cacheRegionClass', 'cacheStore',
+                                               'cacheStoreIdPrefix', 'cacheStoreClass')
+
+    ## the following are used by .compile(). Most are documented in its docstring.
+    _CHEETAH_cacheModuleFilesForTracebacks = False
+    _CHEETAH_cacheDirForModuleFiles = None # change to a dirname
+
+    _CHEETAH_compileCache = dict() # cache store for compiled code and classes
+    # To do something other than simple in-memory caching you can create an
+    # alternative cache store. It just needs to support the basics of Python's
+    # mapping/dict protocol. E.g.:
+    #   class AdvCachingTemplate(Template):
+    #       _CHEETAH_compileCache = MemoryOrFileCache()
+    _CHEETAH_compileLock = Lock() # used to prevent race conditions
+    _CHEETAH_defaultMainMethodName = None
+    _CHEETAH_compilerSettings = None
+    _CHEETAH_compilerClass = Compiler
+    _CHEETAH_compilerInstance = None
+    _CHEETAH_cacheCompilationResults = True
+    _CHEETAH_useCompilationCache = True
+    _CHEETAH_keepRefToGeneratedCode = True
+    _CHEETAH_defaultBaseclassForTemplates = None
+    _CHEETAH_defaultClassNameForTemplates = None
+    # defaults to DEFAULT_COMPILER_SETTINGS['mainMethodName']:
+    _CHEETAH_defaultMainMethodNameForTemplates = None 
+    _CHEETAH_defaultModuleNameForTemplates = 'DynamicallyCompiledCheetahTemplate'
+    _CHEETAH_defaultModuleGlobalsForTemplates = None
+    _CHEETAH_preprocessors = None
+    _CHEETAH_defaultPreprocessorClass = TemplatePreprocessor    
+    
+    ## The following attributes are used by instance methods:
+    _CHEETAH_generatedModuleCode = None
+    NonNumericInputError = NonNumericInputError
+    _CHEETAH_cacheRegionClass = CacheRegion
+    _CHEETAH_cacheStoreClass = MemoryCacheStore
+    #_CHEETAH_cacheStoreClass = MemcachedCacheStore
+    _CHEETAH_cacheStore = None  
+    _CHEETAH_cacheStoreIdPrefix = None  
+
+    @classmethod
+    def _getCompilerClass(klass, source=None, file=None):
+        return klass._CHEETAH_compilerClass
+
+    @classmethod
+    def _getCompilerSettings(klass, source=None, file=None):
+        return klass._CHEETAH_compilerSettings
+    
+    @classmethod
+    def compile(klass, source=None, file=None,
+                returnAClass=True,
+                
+                compilerSettings=Unspecified,
+                compilerClass=Unspecified,
+                moduleName=None,
+                className=Unspecified,
+                mainMethodName=Unspecified,
+                baseclass=Unspecified,
+                moduleGlobals=Unspecified,
+                cacheCompilationResults=Unspecified,
+                useCache=Unspecified,
+                preprocessors=Unspecified,
+                cacheModuleFilesForTracebacks=Unspecified,
+                cacheDirForModuleFiles=Unspecified,
+                commandlineopts=None,
+                keepRefToGeneratedCode=Unspecified,                
+                ):
+        
+        """
+        The core API for compiling Cheetah source code into template classes.
+
+        This class method compiles Cheetah source code and returns a python
+        class.  You then create template instances using that class.  All
+        Cheetah's other compilation API's use this method under the hood.
+
+        Internally, this method a) parses the Cheetah source code and generates
+        Python code defining a module with a single class in it, b) dynamically
+        creates a module object with a unique name, c) execs the generated code
+        in that module's namespace then inserts the module into sys.modules, and
+        d) returns a reference to the generated class.  If you want to get the
+        generated python source code instead, pass the argument
+        returnAClass=False.
+
+        It caches generated code and classes.  See the descriptions of the
+        arguments'cacheCompilationResults' and 'useCache' for details. This
+        doesn't mean that templates will automatically recompile themselves when
+        the source file changes. Rather, if you call Template.compile(src) or
+        Template.compile(file=path) repeatedly it will attempt to return a
+        cached class definition instead of recompiling.
+
+        Hooks are provided template source preprocessing.  See the notes on the
+        'preprocessors' arg.
+
+        If you are an advanced user and need to customize the way Cheetah parses
+        source code or outputs Python code, you should check out the
+        compilerSettings argument.
+
+        Arguments:
+          You must provide either a 'source' or 'file' arg, but not both:
+            - source (string or None)
+            - file (string path, file-like object, or None)
+
+          The rest of the arguments are strictly optional. All but the first
+          have defaults in attributes of the Template class which can be
+          overridden in subclasses of this class.  Working with most of these is
+          an advanced topic.
+          
+            - returnAClass=True            
+              If false, return the generated module code rather than a class.
+
+            - compilerSettings (a dict)
+              Default: Template._CHEETAH_compilerSettings=None
+            
+              a dictionary of settings to override those defined in
+              DEFAULT_COMPILER_SETTINGS. These can also be overridden in your
+              template source code with the #compiler or #compiler-settings
+              directives.
+                  
+            - compilerClass (a class)
+              Default: Template._CHEETAH_compilerClass=Cheetah.Compiler.Compiler
+            
+              a subclass of Cheetah.Compiler.Compiler. Mucking with this is a
+              very advanced topic.
+                  
+            - moduleName (a string)
+              Default:
+                  Template._CHEETAH_defaultModuleNameForTemplates
+                  ='DynamicallyCompiledCheetahTemplate'
+            
+              What to name the generated Python module.  If the provided value is
+              None and a file arg was given, the moduleName is created from the
+              file path.  In all cases if the moduleName provided is already in
+              sys.modules it is passed through a filter that generates a unique
+              variant of the name.
+
+
+            - className (a string)
+              Default: Template._CHEETAH_defaultClassNameForTemplates=None
+              
+              What to name the generated Python class.  If the provided value is
+              None, the moduleName is use as the class name.
+
+            - mainMethodName (a string)
+              Default:
+                  Template._CHEETAH_defaultMainMethodNameForTemplates
+                  =None (and thus DEFAULT_COMPILER_SETTINGS['mainMethodName']) 
+            
+              What to name the main output generating method in the compiled
+              template class.  
+
+            - baseclass (a string or a class)
+              Default: Template._CHEETAH_defaultBaseclassForTemplates=None
+
+              Specifies the baseclass for the template without manually
+              including an #extends directive in the source. The #extends
+              directive trumps this arg.
+
+              If the provided value is a string you must make sure that a class
+              reference by that name is available to your template, either by
+              using an #import directive or by providing it in the arg
+              'moduleGlobals'.  
+
+              If the provided value is a class, Cheetah will handle all the
+              details for you.
+
+            - moduleGlobals (a dict)
+              Default: Template._CHEETAH_defaultModuleGlobalsForTemplates=None
+
+              A dict of vars that will be added to the global namespace of the
+              module the generated code is executed in, prior to the execution
+              of that code.  This should be Python values, not code strings!
+              
+            - cacheCompilationResults (True/False)
+              Default: Template._CHEETAH_cacheCompilationResults=True
+
+              Tells Cheetah to cache the generated code and classes so that they
+              can be reused if Template.compile() is called multiple times with
+              the same source and options.
+                           
+            - useCache (True/False)
+              Default: Template._CHEETAH_useCompilationCache=True
+
+              Should the compilation cache be used?  If True and a previous
+              compilation created a cached template class with the same source
+              code, compiler settings and other options, the cached template
+              class will be returned.
+
+            - cacheModuleFilesForTracebacks (True/False)
+              Default: Template._CHEETAH_cacheModuleFilesForTracebacks=False
+
+              In earlier versions of Cheetah tracebacks from exceptions that
+              were raised inside dynamically compiled Cheetah templates were
+              opaque because Python didn't have access to a python source file
+              to use in the traceback:
+        
+                File "xxxx.py", line 192, in getTextiledContent
+                  content = str(template(searchList=searchList))
+                File "cheetah_yyyy.py", line 202, in __str__
+                File "cheetah_yyyy.py", line 187, in respond
+                File "cheetah_yyyy.py", line 139, in writeBody
+               ZeroDivisionError: integer division or modulo by zero
+        
+              It is now possible to keep those files in a cache dir and allow
+              Python to include the actual source lines in tracebacks and makes
+              them much easier to understand:
+        
+               File "xxxx.py", line 192, in getTextiledContent
+                 content = str(template(searchList=searchList))
+               File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 202, in __str__
+                 def __str__(self): return self.respond()
+               File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 187, in respond
+                 self.writeBody(trans=trans)
+               File "/tmp/CheetahCacheDir/cheetah_yyyy.py", line 139, in writeBody
+                 __v = 0/0 # $(0/0)
+              ZeroDivisionError: integer division or modulo by zero
+            
+            - cacheDirForModuleFiles (a string representing a dir path)
+              Default: Template._CHEETAH_cacheDirForModuleFiles=None
+
+              See notes on cacheModuleFilesForTracebacks.
+
+            - preprocessors
+              Default: Template._CHEETAH_preprocessors=None
+
+              ** THIS IS A VERY ADVANCED TOPIC **
+              
+              These are used to transform the source code prior to compilation.
+              They provide a way to use Cheetah as a code generator for Cheetah
+              code. In other words, you use one Cheetah template to output the
+              source code for another Cheetah template.
+              
+              The major expected use cases are:
+                  
+                a) 'compile-time caching' aka 'partial template binding',
+                   wherein an intermediate Cheetah template is used to output
+                   the source for the final Cheetah template. The intermediate
+                   template is a mix of a modified Cheetah syntax (the
+                   'preprocess syntax') and standard Cheetah syntax.  The
+                   preprocessor syntax is executed at compile time and outputs
+                   Cheetah code which is then compiled in turn. This approach
+                   allows one to completely soft-code all the elements in the
+                   template which are subject to change yet have it compile to
+                   extremely efficient Python code with everything but the
+                   elements that must be variable at runtime (per browser
+                   request, etc.) compiled as static strings.  Examples of this
+                   usage pattern will be added to the Cheetah Users' Guide.
+
+                   The'preprocess syntax' is just Cheetah's standard one with
+                   alternatives for the $ and # tokens:
+                    
+                    e.g. '@' and '%' for code like this
+                     @aPreprocessVar $aRuntimeVar
+                     %if aCompileTimeCondition then yyy else zzz
+                     %% preprocessor comment
+                     
+                     #if aRunTimeCondition then aaa else bbb
+                     ## normal comment
+                     $aRuntimeVar
+                                     
+                b) adding #import and #extends directives dynamically based on
+                   the source
+                
+              If preprocessors are provided, Cheetah pipes the source code
+              through each one in the order provided.  Each preprocessor should
+              accept the args (source, file) and should return a tuple (source,
+              file).
+
+              The argument value should be a list, but a single non-list value
+              is acceptable and will automatically be converted into a list.
+              Each item in the list will be passed through
+              Template._normalizePreprocessor().  The items should either match
+              one of the following forms:
+              
+                - an object with a .preprocess(source, file) method
+                - a callable with the following signature:
+                    source, file = f(source, file)
+                
+                or one of the forms below:
+                
+                - a single string denoting the 2 'tokens' for the preprocess
+                  syntax.  The tokens should be in the order (placeholderToken,
+                  directiveToken) and should separated with a space:
+                     e.g. '@ %'
+                     klass = Template.compile(src, preprocessors='@ %')
+                     # or 
+                     klass = Template.compile(src, preprocessors=['@ %'])
+                     
+                - a dict with the following keys or an object with the
+                  following attributes (all are optional, but nothing will
+                  happen if you don't provide at least one):
+                   - tokens: same as the single string described above. You can
+                     also provide a tuple of 2 strings.
+                   - searchList: the searchList used for preprocess $placeholders
+                   - compilerSettings: used in the compilation of the intermediate
+                     template                
+                   - templateAPIClass: an optional subclass of `Template`
+                   - outputTransformer: a simple hook for passing in a callable
+                     which can do further transformations of the preprocessor
+                     output, or do something else like debug logging. The
+                     default is str().
+                   + any keyword arguments to Template.compile which you want to
+                     provide for the compilation of the intermediate template.
+                     
+                   klass = Template.compile(src,
+                          preprocessors=[ dict(tokens='@ %', searchList=[...]) ] )
+            
+        """
+        errmsg = "arg '%s' must be %s"
+
+        if not isinstance(source, (types.NoneType, basestring)):
+            raise TypeError(errmsg % ('source', 'string or None'))
+
+        if not isinstance(file, (types.NoneType, basestring, filetype)):
+            raise TypeError(errmsg %
+                            ('file', 'string, file-like object, or None'))
+
+        if baseclass is Unspecified:
+            baseclass = klass._CHEETAH_defaultBaseclassForTemplates
+        if isinstance(baseclass, Template):
+            baseclass = baseclass.__class__
+
+        if not isinstance(baseclass, (types.NoneType, basestring, type)):
+            raise TypeError(errmsg % ('baseclass', 'string, class or None'))
+
+        if cacheCompilationResults is Unspecified:
+            cacheCompilationResults = klass._CHEETAH_cacheCompilationResults
+
+        if not isinstance(cacheCompilationResults, (int, bool)):
+            raise TypeError(errmsg % ('cacheCompilationResults', 'boolean'))
+
+        if useCache is Unspecified:
+            useCache = klass._CHEETAH_useCompilationCache
+
+        if not isinstance(useCache, (int, bool)):
+            raise TypeError(errmsg % ('useCache', 'boolean'))
+
+        if compilerSettings is Unspecified:
+            compilerSettings = klass._getCompilerSettings(source, file) or {}
+        if not isinstance(compilerSettings, dict):
+            raise TypeError(errmsg % ('compilerSettings', 'dictionary'))
+
+        if compilerClass is Unspecified:
+            compilerClass = klass._getCompilerClass(source, file)
+        if preprocessors is Unspecified:
+            preprocessors = klass._CHEETAH_preprocessors
+
+        if keepRefToGeneratedCode is Unspecified:
+            keepRefToGeneratedCode = klass._CHEETAH_keepRefToGeneratedCode
+
+        if not isinstance(keepRefToGeneratedCode, (int, bool)):
+            raise TypeError(errmsg % ('keepReftoGeneratedCode', 'boolean'))
+
+        if not isinstance(moduleName, (types.NoneType, basestring)):
+            raise TypeError(errmsg % ('moduleName', 'string or None'))
+        __orig_file__ = None
+        if not moduleName:
+            if file and isinstance(file, basestring):
+                moduleName = convertTmplPathToModuleName(file)
+                __orig_file__ = file
+            else:
+                moduleName = klass._CHEETAH_defaultModuleNameForTemplates
+
+        if className is Unspecified:
+            className = klass._CHEETAH_defaultClassNameForTemplates
+
+        if not isinstance(className, (types.NoneType, basestring)):
+            raise TypeError(errmsg % ('className', 'string or None'))
+        className = re.sub(r'^_+','', className or moduleName)
+
+        if mainMethodName is Unspecified:
+            mainMethodName = klass._CHEETAH_defaultMainMethodNameForTemplates
+
+        if not isinstance(mainMethodName, (types.NoneType, basestring)):
+            raise TypeError(errmsg % ('mainMethodName', 'string or None'))
+
+        if moduleGlobals is Unspecified:
+            moduleGlobals = klass._CHEETAH_defaultModuleGlobalsForTemplates
+
+        if cacheModuleFilesForTracebacks is Unspecified:
+            cacheModuleFilesForTracebacks = klass._CHEETAH_cacheModuleFilesForTracebacks
+
+        if not isinstance(cacheModuleFilesForTracebacks, (int, bool)):
+            raise TypeError(errmsg %
+                            ('cacheModuleFilesForTracebacks', 'boolean'))
+
+        if cacheDirForModuleFiles is Unspecified:
+            cacheDirForModuleFiles = klass._CHEETAH_cacheDirForModuleFiles
+
+        if not isinstance(cacheDirForModuleFiles, (types.NoneType, basestring)):
+            raise TypeError(errmsg %
+                            ('cacheDirForModuleFiles', 'string or None'))
+
+        ##################################################           
+        ## handle any preprocessors
+        if preprocessors:
+            origSrc = source
+            source, file = klass._preprocessSource(source, file, preprocessors)
+
+        ##################################################                       
+        ## compilation, using cache if requested/possible
+        baseclassValue = None
+        baseclassName = None
+        if baseclass:
+            if isinstance(baseclass, basestring):
+                baseclassName = baseclass
+            elif isinstance(baseclass, type):
+                # @@TR: should soft-code this
+                baseclassName = 'CHEETAH_dynamicallyAssignedBaseClass_'+baseclass.__name__
+                baseclassValue = baseclass
+
+
+        cacheHash = None
+        cacheItem = None
+        if source or isinstance(file, basestring):
+            compilerSettingsHash = None
+            if compilerSettings:
+                compilerSettingsHash = hashDict(compilerSettings)
+
+            moduleGlobalsHash = None
+            if moduleGlobals:
+                moduleGlobalsHash = hashDict(moduleGlobals)
+
+            fileHash = None
+            if file:
+                fileHash = str(hash(file))
+                if globals()['__checkFileMtime']:
+                    fileHash += str(os.path.getmtime(file))
+                
+            try:
+                # @@TR: find some way to create a cacheHash that is consistent
+                # between process restarts.  It would allow for caching the
+                # compiled module on disk and thereby reduce the startup time
+                # for applications that use a lot of dynamically compiled
+                # templates.                
+                cacheHash = ''.join([str(v) for v in
+                                     [hash(source),
+                                      fileHash,
+                                      className,
+                                      moduleName,
+                                      mainMethodName,
+                                      hash(compilerClass),
+                                      hash(baseclass),
+                                      compilerSettingsHash,
+                                      moduleGlobalsHash,
+                                      hash(cacheDirForModuleFiles),
+                                      ]])
+            except:
+                #@@TR: should add some logging to this
+                pass
+        outputEncoding = 'ascii'
+        compiler = None
+        if useCache and cacheHash and cacheHash in klass._CHEETAH_compileCache:
+            cacheItem = klass._CHEETAH_compileCache[cacheHash]
+            generatedModuleCode = cacheItem.code
+        else:
+            compiler = compilerClass(source, file,
+                                     moduleName=moduleName,
+                                     mainClassName=className,
+                                     baseclassName=baseclassName,
+                                     mainMethodName=mainMethodName,
+                                     settings=(compilerSettings or {}))
+            if commandlineopts:
+                compiler.setShBang(commandlineopts.shbang)
+            compiler.compile()
+            generatedModuleCode = compiler.getModuleCode()
+            outputEncoding = compiler.getModuleEncoding()
+
+        if not returnAClass:
+            # This is a bit of a hackish solution to make sure we're setting the proper 
+            # encoding on generated code that is destined to be written to a file
+            if not outputEncoding == 'ascii':
+                generatedModuleCode = generatedModuleCode.split('\n')
+                generatedModuleCode.insert(1, '# -*- coding: %s -*-' % outputEncoding)
+                generatedModuleCode = '\n'.join(generatedModuleCode)
+            return generatedModuleCode.encode(outputEncoding)
+        else:
+            if cacheItem:
+                cacheItem.lastCheckoutTime = time.time()
+                return cacheItem.klass
+
+            try:
+                klass._CHEETAH_compileLock.acquire()
+                uniqueModuleName = _genUniqueModuleName(moduleName)
+                __file__ = uniqueModuleName+'.py' # relative file path with no dir part
+
+                if cacheModuleFilesForTracebacks:
+                    if not os.path.exists(cacheDirForModuleFiles):
+                        raise Exception('%s does not exist'%cacheDirForModuleFiles)
+
+                    __file__ = os.path.join(cacheDirForModuleFiles, __file__)
+                    # @@TR: might want to assert that it doesn't already exist
+                    open(__file__, 'w').write(generatedModuleCode)
+                    # @@TR: should probably restrict the perms, etc.
+
+                mod = types.ModuleType(str(uniqueModuleName))
+                if moduleGlobals:
+                    for k, v in moduleGlobals.items():
+                        setattr(mod, k, v)
+                mod.__file__ = __file__
+                if __orig_file__ and os.path.exists(__orig_file__):
+                    # this is used in the WebKit filemonitoring code
+                    mod.__orig_file__ = __orig_file__
+
+                if baseclass and baseclassValue:
+                    setattr(mod, baseclassName, baseclassValue)
+                ##
+                try:
+                    co = compile(generatedModuleCode, __file__, 'exec')
+                    exec(co, mod.__dict__)
+                except SyntaxError, e:
+                    try:
+                        parseError = genParserErrorFromPythonException(
+                            source, file, generatedModuleCode, exception=e)
+                    except:
+                        updateLinecache(__file__, generatedModuleCode)
+                        e.generatedModuleCode = generatedModuleCode
+                        raise e
+                    else:
+                        raise parseError
+                except Exception, e:
+                    updateLinecache(__file__, generatedModuleCode)
+                    e.generatedModuleCode = generatedModuleCode
+                    raise
+                ##
+                sys.modules[uniqueModuleName] = mod
+            finally:
+                klass._CHEETAH_compileLock.release()
+
+            templateClass = getattr(mod, className)
+
+            if (cacheCompilationResults
+                and cacheHash
+                and cacheHash not in klass._CHEETAH_compileCache):
+                
+                cacheItem = CompileCacheItem()
+                cacheItem.cacheTime = cacheItem.lastCheckoutTime = time.time()
+                cacheItem.code = generatedModuleCode
+                cacheItem.klass = templateClass
+                templateClass._CHEETAH_isInCompilationCache = True
+                klass._CHEETAH_compileCache[cacheHash] = cacheItem
+            else:
+                templateClass._CHEETAH_isInCompilationCache = False
+
+            if keepRefToGeneratedCode or cacheCompilationResults:
+                templateClass._CHEETAH_generatedModuleCode = generatedModuleCode                
+     
+            # If we have a compiler object, let's set it to the compiler class
+            # to help the directive analyzer code
+            if compiler:
+                templateClass._CHEETAH_compilerInstance = compiler
+            return templateClass
+
+    @classmethod
+    def subclass(klass, *args, **kws):
+        """Takes the same args as the .compile() classmethod and returns a
+        template that is a subclass of the template this method is called from.
+
+          T1 = Template.compile(' foo - $meth1 - bar\n#def meth1: this is T1.meth1')
+          T2 = T1.subclass('#implements meth1\n this is T2.meth1')
+        """
+        kws['baseclass'] = klass
+        if isinstance(klass, Template):
+            templateAPIClass = klass
+        else:
+            templateAPIClass = Template
+        return templateAPIClass.compile(*args, **kws)
+
+    @classmethod
+    def _preprocessSource(klass, source, file, preprocessors):
+        """Iterates through the .compile() classmethod's preprocessors argument
+        and pipes the source code through each each preprocessor.
+
+        It returns the tuple (source, file) which is then used by
+        Template.compile to finish the compilation.
+        """
+        if not isinstance(preprocessors, (list, tuple)):
+            preprocessors = [preprocessors]
+        for preprocessor in preprocessors:
+            preprocessor = klass._normalizePreprocessorArg(preprocessor)
+            source, file = preprocessor.preprocess(source, file)
+        return source, file
+
+    @classmethod
+    def _normalizePreprocessorArg(klass, arg):
+        """Used to convert the items in the .compile() classmethod's
+        preprocessors argument into real source preprocessors.  This permits the
+        use of several shortcut forms for defining preprocessors.
+        """
+        
+        if hasattr(arg, 'preprocess'):
+            return arg
+        elif hasattr(arg, '__call__'):
+            class WrapperPreprocessor:
+                def preprocess(self, source, file):
+                    return arg(source, file)
+            return WrapperPreprocessor()
+        else:
+            class Settings(object):
+                placeholderToken = None
+                directiveToken = None
+            settings = Settings()
+            if isinstance(arg, str) or isinstance(arg, (list, tuple)):
+                settings.tokens = arg
+            elif isinstance(arg, dict):
+                for k, v in arg.items():
+                    setattr(settings, k, v)   
+            else:
+                settings = arg
+
+            settings = klass._normalizePreprocessorSettings(settings)
+            return klass._CHEETAH_defaultPreprocessorClass(settings)
+
+        
+    @classmethod
+    def _normalizePreprocessorSettings(klass, settings):
+        settings.keepRefToGeneratedCode = True
+
+        def normalizeSearchList(searchList):
+            if not isinstance(searchList, (list, tuple)):
+                searchList = [searchList]
+            return searchList            
+
+        def normalizeTokens(tokens):
+            if isinstance(tokens, str):
+                return tokens.split() # space delimited string e.g.'@ %'
+            elif isinstance(tokens, (list, tuple)):
+                return tokens
+            else:
+                raise PreprocessError('invalid tokens argument: %r'%tokens)
+
+        if hasattr(settings, 'tokens'):
+            (settings.placeholderToken,
+             settings.directiveToken) = normalizeTokens(settings.tokens)
+            
+        if (not getattr(settings, 'compilerSettings', None)
+            and not getattr(settings, 'placeholderToken', None) ):
+            
+            raise TypeError(
+                'Preprocessor requires either a "tokens" or a "compilerSettings" arg.'
+                ' Neither was provided.')
+
+        if not hasattr(settings, 'templateInitArgs'):
+            settings.templateInitArgs = {}
+        if 'searchList' not in settings.templateInitArgs:
+            if not hasattr(settings, 'searchList') and hasattr(settings, 'namespaces'):
+                settings.searchList = settings.namespaces
+            elif not hasattr(settings, 'searchList'):
+                settings.searchList = []
+            settings.templateInitArgs['searchList'] = settings.searchList
+        settings.templateInitArgs['searchList'] = (
+            normalizeSearchList(settings.templateInitArgs['searchList']))
+            
+        if not hasattr(settings, 'outputTransformer'):
+            settings.outputTransformer = unicode
+
+        if not hasattr(settings, 'templateAPIClass'):
+            class PreprocessTemplateAPIClass(klass): pass
+            settings.templateAPIClass = PreprocessTemplateAPIClass
+
+        if not hasattr(settings, 'compilerSettings'):
+            settings.compilerSettings = {}
+
+        klass._updateSettingsWithPreprocessTokens(
+            compilerSettings=settings.compilerSettings,
+            placeholderToken=settings.placeholderToken,
+            directiveToken=settings.directiveToken
+            )                            
+        return settings
+
+    @classmethod
+    def _updateSettingsWithPreprocessTokens(
+        klass, compilerSettings, placeholderToken, directiveToken):
+        
+        if (placeholderToken and 'cheetahVarStartToken' not in compilerSettings):
+            compilerSettings['cheetahVarStartToken'] = placeholderToken
+        if directiveToken:
+            if 'directiveStartToken' not in compilerSettings:
+                compilerSettings['directiveStartToken'] = directiveToken
+            if 'directiveEndToken' not in compilerSettings:
+                compilerSettings['directiveEndToken'] = directiveToken
+            if 'commentStartToken' not in compilerSettings:
+                compilerSettings['commentStartToken'] = directiveToken*2
+            if 'multiLineCommentStartToken' not in compilerSettings:
+                compilerSettings['multiLineCommentStartToken'] = (
+                    directiveToken+'*')
+            if 'multiLineCommentEndToken' not in compilerSettings:
+                compilerSettings['multiLineCommentEndToken'] = (
+                    '*'+directiveToken)
+            if 'EOLSlurpToken' not in compilerSettings:
+                compilerSettings['EOLSlurpToken'] = directiveToken
+
+    @classmethod
+    def _addCheetahPlumbingCodeToClass(klass, concreteTemplateClass):
+        """If concreteTemplateClass is not a subclass of Cheetah.Template, add
+        the required cheetah methods and attributes to it.
+
+        This is called on each new template class after it has been compiled.
+        If concreteTemplateClass is not a subclass of Cheetah.Template but
+        already has method with the same name as one of the required cheetah
+        methods, this will skip that method.
+        """
+        for methodname in klass._CHEETAH_requiredCheetahMethods:
+            if not hasattr(concreteTemplateClass, methodname):
+                method = getattr(Template, methodname)
+                newMethod = createMethod(method.im_func, concreteTemplateClass)
+                setattr(concreteTemplateClass, methodname, newMethod)
+
+        for classMethName in klass._CHEETAH_requiredCheetahClassMethods:
+            if not hasattr(concreteTemplateClass, classMethName):
+                meth = getattr(klass, classMethName)
+                setattr(concreteTemplateClass, classMethName, classmethod(meth.im_func))
+            
+        for attrname in klass._CHEETAH_requiredCheetahClassAttributes:
+            attrname = '_CHEETAH_'+attrname
+            if not hasattr(concreteTemplateClass, attrname):
+                attrVal = getattr(klass, attrname)
+                setattr(concreteTemplateClass, attrname, attrVal)
+
+        if (not hasattr(concreteTemplateClass, '__str__')
+            or concreteTemplateClass.__str__ is object.__str__):
+            
+            mainMethNameAttr = '_mainCheetahMethod_for_'+concreteTemplateClass.__name__
+            mainMethName = getattr(concreteTemplateClass, mainMethNameAttr, None)
+            if mainMethName:
+                def __str__(self): 
+                    rc = getattr(self, mainMethName)()
+                    if isinstance(rc, unicode):
+                        return rc.encode('utf-8')
+                    return rc
+                def __unicode__(self):
+                    return getattr(self, mainMethName)()
+            elif (hasattr(concreteTemplateClass, 'respond')
+                  and concreteTemplateClass.respond!=Servlet.respond):
+                def __str__(self):
+                    rc = self.respond()
+                    if isinstance(rc, unicode):
+                        return rc.encode('utf-8')
+                    return rc
+                def __unicode__(self):
+                    return self.respond()
+            else:
+                def __str__(self):
+                    rc = None
+                    if hasattr(self, mainMethNameAttr):
+                        rc = getattr(self, mainMethNameAttr)()
+                    elif hasattr(self, 'respond'):
+                        rc = self.respond()
+                    else:
+                        rc = super(self.__class__, self).__str__()
+                    if isinstance(rc, unicode):
+                        return rc.encode('utf-8')
+                    return rc
+                def __unicode__(self):
+                    if hasattr(self, mainMethNameAttr):
+                        return getattr(self, mainMethNameAttr)()
+                    elif hasattr(self, 'respond'):
+                        return self.respond()
+                    else:
+                        return super(self.__class__, self).__unicode__()
+                    
+            __str__ = createMethod(__str__, concreteTemplateClass)
+            __unicode__ = createMethod(__unicode__, concreteTemplateClass)
+            setattr(concreteTemplateClass, '__str__', __str__)
+            setattr(concreteTemplateClass, '__unicode__', __unicode__)
+
+
+    def __init__(self, source=None,
+
+                 namespaces=None, searchList=None,
+                 # use either or.  They are aliases for the same thing.
+                 
+                 file=None,
+                 filter='RawOrEncodedUnicode', # which filter from Cheetah.Filters
+                 filtersLib=Filters,
+                 errorCatcher=None,
+                 
+                 compilerSettings=Unspecified, # control the behaviour of the compiler
+                 _globalSetVars=None, # used internally for #include'd templates
+                 _preBuiltSearchList=None # used internally for #include'd templates
+                 ):        
+        """a) compiles a new template OR b) instantiates an existing template.
+
+        Read this docstring carefully as there are two distinct usage patterns.
+        You should also read this class' main docstring.
+        
+        a) to compile a new template:
+             t = Template(source=aSourceString)
+                 # or 
+             t = Template(file='some/path')
+                 # or 
+             t = Template(file=someFileObject)
+                 # or
+             namespaces = [{'foo':'bar'}]               
+             t = Template(source=aSourceString, namespaces=namespaces)
+                 # or 
+             t = Template(file='some/path', namespaces=namespaces)
+  
+             print t
+             
+        b) to create an instance of an existing, precompiled template class:
+             ## i) first you need a reference to a compiled template class:
+             tclass = Template.compile(source=src) # or just Template.compile(src)
+                 # or 
+             tclass = Template.compile(file='some/path')
+                 # or 
+             tclass = Template.compile(file=someFileObject)
+                 # or 
+             # if you used the command line compiler or have Cheetah's ImportHooks
+             # installed your template class is also available via Python's
+             # standard import mechanism:
+             from ACompileTemplate import AcompiledTemplate as tclass
+             
+             ## ii) then you create an instance
+             t = tclass(namespaces=namespaces)
+                 # or 
+             t = tclass(namespaces=namespaces, filter='RawOrEncodedUnicode')
+             print t
+
+        Arguments:
+          for usage pattern a)           
+            If you are compiling a new template, you must provide either a
+            'source' or 'file' arg, but not both:          
+              - source (string or None)
+              - file (string path, file-like object, or None)
+
+            Optional args (see below for more) :
+              - compilerSettings
+               Default: Template._CHEETAH_compilerSettings=None
+               
+               a dictionary of settings to override those defined in
+               DEFAULT_COMPILER_SETTINGS.  See
+               Cheetah.Template.DEFAULT_COMPILER_SETTINGS and the Users' Guide
+               for details.
+
+            You can pass the source arg in as a positional arg with this usage
+            pattern.  Use keywords for all other args.           
+
+          for usage pattern b)
+            Do not use positional args with this usage pattern, unless your
+            template subclasses something other than Cheetah.Template and you
+            want to pass positional args to that baseclass.  E.g.:
+              dictTemplate = Template.compile('hello $name from $caller', baseclass=dict)
+              tmplvars = dict(name='world', caller='me')
+              print dictTemplate(tmplvars)
+            This usage requires all Cheetah args to be passed in as keyword args.
+
+          optional args for both usage patterns:
+
+            - namespaces (aka 'searchList')
+              Default: None
+              
+              an optional list of namespaces (dictionaries, objects, modules,
+              etc.) which Cheetah will search through to find the variables
+              referenced in $placeholders.
+
+              If you provide a single namespace instead of a list, Cheetah will
+              automatically convert it into a list.
+                
+              NOTE: Cheetah does NOT force you to use the namespaces search list
+              and related features.  It's on by default, but you can turn if off
+              using the compiler settings useSearchList=False or
+              useNameMapper=False.
+                
+             - filter
+               Default: 'EncodeUnicode'
+               
+               Which filter should be used for output filtering. This should
+               either be a string which is the name of a filter in the
+               'filtersLib' or a subclass of Cheetah.Filters.Filter. . See the
+               Users' Guide for more details.
+
+             - filtersLib
+               Default: Cheetah.Filters
+               
+               A module containing subclasses of Cheetah.Filters.Filter. See the
+               Users' Guide for more details. 
+
+             - errorCatcher
+               Default: None
+
+               This is a debugging tool. See the Users' Guide for more details.
+               Do not use this or the #errorCatcher diretive with live
+               production systems.
+
+          Do NOT mess with the args _globalSetVars or _preBuiltSearchList!
+
+
+        """
+        errmsg = "arg '%s' must be %s"
+        errmsgextra = errmsg + "\n%s"
+
+        if not isinstance(source, (types.NoneType, basestring)):
+            raise TypeError(errmsg % ('source', 'string or None'))
+
+        if not isinstance(source, (types.NoneType, basestring, filetype)):
+            raise TypeError(errmsg %
+                            ('file', 'string, file open for reading, or None'))
+
+        if not isinstance(filter, (basestring, types.TypeType)) and not \
+                (isinstance(filter, type) and issubclass(filter, Filters.Filter)):
+            raise TypeError(errmsgextra %
+                            ('filter', 'string or class',
+                             '(if class, must be subclass of Cheetah.Filters.Filter)'))
+        if not isinstance(filtersLib, (basestring, types.ModuleType)):
+            raise TypeError(errmsgextra %
+                            ('filtersLib', 'string or module',
+                             '(if module, must contain subclasses of Cheetah.Filters.Filter)'))
+
+        if not errorCatcher is None:
+            err = True
+            if isinstance(errorCatcher, (basestring, types.TypeType)):
+                err = False
+            if isinstance(errorCatcher, type) and \
+                    issubclass(errorCatcher, ErrorCatchers.ErrorCatcher): 
+                err = False
+            if err:
+                raise TypeError(errmsgextra %
+                            ('errorCatcher', 'string, class or None',
+                             '(if class, must be subclass of Cheetah.ErrorCatchers.ErrorCatcher)'))
+        if compilerSettings is not Unspecified:
+            if not isinstance(compilerSettings, types.DictType):
+                raise TypeError(errmsg %
+                                ('compilerSettings', 'dictionary'))
+        
+        if source is not None and file is not None:
+            raise TypeError("you must supply either a source string or the" + 
+                            " 'file' keyword argument, but not both")
+                    
+        ##################################################           
+        ## Do superclass initialization.
+        super(Template, self).__init__()
+
+        ##################################################           
+        ## Do required version check
+        if not hasattr(self, '_CHEETAH_versionTuple'):
+            try:
+                mod = sys.modules[self.__class__.__module__]
+                compiledVersion = mod.__CHEETAH_version__
+                compiledVersionTuple = convertVersionStringToTuple(compiledVersion)
+                if compiledVersionTuple < MinCompatibleVersionTuple:
+                    raise AssertionError(
+                     'This template was compiled with Cheetah version'
+                     ' %s. Templates compiled before version %s must be recompiled.'%(
+                        compiledVersion, MinCompatibleVersion))                    
+            except AssertionError:
+                raise 
+            except:
+                pass
+        
+        ##################################################           
+        ## Setup instance state attributes used during the life of template
+        ## post-compile
+        if searchList:
+            for namespace in searchList:
+                if isinstance(namespace, dict):
+                    intersection = self.Reserved_SearchList & set(namespace.keys())
+                    warn = False
+                    if intersection:
+                        warn = True
+                    if isinstance(compilerSettings, dict) and compilerSettings.get('prioritizeSearchListOverSelf'):
+                        warn = False
+                    if warn:
+                        logging.info(''' The following keys are members of the Template class and will result in NameMapper collisions! ''')
+                        logging.info('''  > %s ''' % ', '.join(list(intersection)))
+                        logging.info(''' Please change the key's name or use the compiler setting "prioritizeSearchListOverSelf=True" to prevent the NameMapper from using ''')
+                        logging.info(''' the Template member in place of your searchList variable ''')
+                        
+
+        self._initCheetahInstance(
+            searchList=searchList, namespaces=namespaces,
+            filter=filter, filtersLib=filtersLib,
+            errorCatcher=errorCatcher,
+            _globalSetVars=_globalSetVars,
+            compilerSettings=compilerSettings,
+            _preBuiltSearchList=_preBuiltSearchList)
+        
+        ##################################################
+        ## Now, compile if we're meant to
+        if (source is not None) or (file is not None):
+            self._compile(source, file, compilerSettings=compilerSettings)
+
+    def generatedModuleCode(self):
+        """Return the module code the compiler generated, or None if no
+        compilation took place.
+        """
+        
+        return self._CHEETAH_generatedModuleCode
+    
+    def generatedClassCode(self):        
+        """Return the class code the compiler generated, or None if no
+        compilation took place.
+        """
+        
+        return self._CHEETAH_generatedModuleCode[
+                    self._CHEETAH_generatedModuleCode.find('\nclass '):
+                    self._CHEETAH_generatedModuleCode.find('\n## END CLASS DEFINITION')]
+    
+    def searchList(self):
+        """Return a reference to the searchlist
+        """
+        return self._CHEETAH__searchList
+
+    def errorCatcher(self):
+        """Return a reference to the current errorCatcher
+        """
+        return self._CHEETAH__errorCatcher
+
+    ## cache methods ##
+    def _getCacheStore(self):
+        if not self._CHEETAH__cacheStore:
+            if self._CHEETAH_cacheStore is not None:
+                self._CHEETAH__cacheStore = self._CHEETAH_cacheStore
+            else:
+                # @@TR: might want to provide a way to provide init args
+                self._CHEETAH__cacheStore = self._CHEETAH_cacheStoreClass()
+
+        return self._CHEETAH__cacheStore
+
+    def _getCacheStoreIdPrefix(self):
+        if self._CHEETAH_cacheStoreIdPrefix is not None:            
+            return self._CHEETAH_cacheStoreIdPrefix
+        else:
+            return str(id(self))
+    
+    def _createCacheRegion(self, regionID):
+        return self._CHEETAH_cacheRegionClass(
+            regionID=regionID,
+            templateCacheIdPrefix=self._getCacheStoreIdPrefix(),
+            cacheStore=self._getCacheStore())
+
+    def getCacheRegion(self, regionID, cacheInfo=None, create=True):
+        cacheRegion = self._CHEETAH__cacheRegions.get(regionID)
+        if not cacheRegion and create:
+            cacheRegion = self._createCacheRegion(regionID)
+            self._CHEETAH__cacheRegions[regionID] = cacheRegion
+        return cacheRegion        
+    
+    def getCacheRegions(self):
+        """Returns a dictionary of the 'cache regions' initialized in a
+        template.
+
+        Each #cache directive block or $*cachedPlaceholder is a separate 'cache
+        region'.        
+        """
+        # returns a copy to prevent users mucking it up
+        return self._CHEETAH__cacheRegions.copy()
+
+    def refreshCache(self, cacheRegionId=None, cacheItemId=None):        
+        """Refresh a cache region or a specific cache item within a region.
+        """
+        
+        if not cacheRegionId:
+            for cacheRegion in self.getCacheRegions().itervalues():
+                cacheRegion.clear()
+        else:
+            cregion = self._CHEETAH__cacheRegions.get(cacheRegionId)
+            if not cregion:
+                return
+            if not cacheItemId: # clear the desired region and all its cacheItems
+                cregion.clear()
+            else: # clear one specific cache of a specific region
+                cache = cregion.getCacheItem(cacheItemId)
+                if cache:
+                    cache.clear()
+                    
+    ## end cache methods ##
+                    
+    def shutdown(self):
+        """Break reference cycles before discarding a servlet.
+        """
+        try:
+            Servlet.shutdown(self)
+        except:
+            pass
+        self._CHEETAH__searchList = None
+        self.__dict__ = {}
+            
+    ## utility functions ##   
+
+    def getVar(self, varName, default=Unspecified, autoCall=True):        
+        """Get a variable from the searchList.  If the variable can't be found
+        in the searchList, it returns the default value if one was given, or
+        raises NameMapper.NotFound.
+        """
+        
+        try:
+            return valueFromSearchList(self.searchList(), varName.replace('$', ''), autoCall)
+        except NotFound:
+            if default is not Unspecified:
+                return default
+            else:
+                raise
+    
+    def varExists(self, varName, autoCall=True):
+        """Test if a variable name exists in the searchList.
+        """
+        try:
+            valueFromSearchList(self.searchList(), varName.replace('$', ''), autoCall)
+            return True
+        except NotFound:
+            return False
+
+
+    hasVar = varExists
+
+
+    def i18n(self, message,
+             plural=None,
+             n=None,
+                   
+             id=None,
+             domain=None,
+             source=None,
+             target=None,
+             comment=None
+             ):
+        """This is just a stub at this time.
+
+          plural = the plural form of the message
+          n = a sized argument to distinguish between single and plural forms           
+
+          id = msgid in the translation catalog
+          domain = translation domain
+          source = source lang
+          target = a specific target lang
+          comment = a comment to the translation team
+
+        See the following for some ideas
+        http://www.zope.org/DevHome/Wikis/DevSite/Projects/ComponentArchitecture/ZPTInternationalizationSupport
+
+        Other notes:
+        - There is no need to replicate the i18n:name attribute from plone / PTL,
+          as cheetah placeholders serve the same purpose
+    
+    
+       """
+
+        return message
+
+    def getFileContents(self, path):
+        """A hook for getting the contents of a file.  The default
+        implementation just uses the Python open() function to load local files.
+        This method could be reimplemented to allow reading of remote files via
+        various protocols, as PHP allows with its 'URL fopen wrapper'
+        """
+        
+        fp = open(path, 'r')
+        output = fp.read()
+        fp.close()
+        return output
+    
+    def runAsMainProgram(self):        
+        """Allows the Template to function as a standalone command-line program
+        for static page generation.
+
+        Type 'python yourtemplate.py --help to see what it's capabable of.
+        """
+
+        from TemplateCmdLineIface import CmdLineIface
+        CmdLineIface(templateObj=self).run()
+        
+    ##################################################
+    ## internal methods -- not to be called by end-users
+
+    def _initCheetahInstance(self,
+                             searchList=None,
+                             namespaces=None,
+                             filter='RawOrEncodedUnicode', # which filter from Cheetah.Filters
+                             filtersLib=Filters,
+                             errorCatcher=None,
+                             _globalSetVars=None,
+                             compilerSettings=None,
+                             _preBuiltSearchList=None):
+        """Sets up the instance attributes that cheetah templates use at
+        run-time.
+
+        This is automatically called by the __init__ method of compiled
+        templates.
+
+        Note that the names of instance attributes used by Cheetah are prefixed
+        with '_CHEETAH__' (2 underscores), where class attributes are prefixed
+        with '_CHEETAH_' (1 underscore).
+        """
+        if getattr(self, '_CHEETAH__instanceInitialized', False):
+            return
+
+        if namespaces is not None: 
+            assert searchList is None, (
+                'Provide "namespaces" or "searchList", not both!')
+            searchList = namespaces
+        if searchList is not None and not isinstance(searchList, (list, tuple)):
+            searchList = [searchList]
+
+        self._CHEETAH__globalSetVars = {}
+        if _globalSetVars is not None:
+            # this is intended to be used internally by Nested Templates in #include's
+            self._CHEETAH__globalSetVars = _globalSetVars
+            
+        if _preBuiltSearchList is not None:
+            # happens with nested Template obj creation from #include's
+            self._CHEETAH__searchList = list(_preBuiltSearchList)
+            self._CHEETAH__searchList.append(self)
+        else:
+            # create our own searchList
+            self._CHEETAH__searchList = [self._CHEETAH__globalSetVars, self]
+            if searchList is not None:
+                if isinstance(compilerSettings, dict) and compilerSettings.get('prioritizeSearchListOverSelf'):
+                    self._CHEETAH__searchList = searchList + self._CHEETAH__searchList
+                else:
+                    self._CHEETAH__searchList.extend(list(searchList))
+        self._CHEETAH__cheetahIncludes = {}
+        self._CHEETAH__cacheRegions = {}
+        self._CHEETAH__indenter = Indenter()
+
+        # @@TR: consider allowing simple callables as the filter argument
+        self._CHEETAH__filtersLib = filtersLib
+        self._CHEETAH__filters = {}
+        if isinstance(filter, basestring):
+            filterName = filter
+            klass = getattr(self._CHEETAH__filtersLib, filterName)
+        else:
+            klass = filter
+            filterName = klass.__name__            
+        self._CHEETAH__currentFilter = self._CHEETAH__filters[filterName] = klass(self).filter
+        self._CHEETAH__initialFilter = self._CHEETAH__currentFilter
+
+        self._CHEETAH__errorCatchers = {}
+        if errorCatcher:
+            if isinstance(errorCatcher, basestring):
+                errorCatcherClass = getattr(ErrorCatchers, errorCatcher)
+            elif isinstance(errorCatcher, type):
+                errorCatcherClass = errorCatcher
+
+            self._CHEETAH__errorCatcher = ec = errorCatcherClass(self)
+            self._CHEETAH__errorCatchers[errorCatcher.__class__.__name__] = ec
+                                 
+        else:
+            self._CHEETAH__errorCatcher = None
+        self._CHEETAH__initErrorCatcher = self._CHEETAH__errorCatcher        
+
+        if not hasattr(self, 'transaction'):
+            self.transaction = None
+        self._CHEETAH__instanceInitialized = True
+        self._CHEETAH__isBuffering = False
+        self._CHEETAH__isControlledByWebKit = False 
+
+        self._CHEETAH__cacheStore = None
+        if self._CHEETAH_cacheStore is not None:
+            self._CHEETAH__cacheStore = self._CHEETAH_cacheStore
+        
+    def _compile(self, source=None, file=None, compilerSettings=Unspecified,
+                 moduleName=None, mainMethodName=None):
+        """Compile the template. This method is automatically called by
+        Template.__init__ it is provided with 'file' or 'source' args.
+
+        USERS SHOULD *NEVER* CALL THIS METHOD THEMSELVES.  Use Template.compile
+        instead.
+        """
+        if compilerSettings is Unspecified:
+            compilerSettings = self._getCompilerSettings(source, file) or {}        
+        mainMethodName = mainMethodName or self._CHEETAH_defaultMainMethodName
+        self._fileMtime = None
+        self._fileDirName = None
+        self._fileBaseName = None
+        if file and isinstance(file, basestring):
+            file = self.serverSidePath(file)
+            self._fileMtime = os.path.getmtime(file)
+            self._fileDirName, self._fileBaseName = os.path.split(file)
+        self._filePath = file
+        templateClass = self.compile(source, file,
+                                      moduleName=moduleName,
+                                      mainMethodName=mainMethodName,
+                                      compilerSettings=compilerSettings,
+                                      keepRefToGeneratedCode=True)
+
+        if not self.__class__ == Template:
+            # Only propogate attributes if we're in a subclass of 
+            # Template
+            for k, v in self.__class__.__dict__.iteritems():
+                if not v or k.startswith('__'):
+                    continue
+                ## Propogate the class attributes to the instance 
+                ## since we're about to obliterate self.__class__
+                ## (see: cheetah.Tests.Tepmlate.SubclassSearchListTest)
+                setattr(self, k, v)
+
+        self.__class__ = templateClass
+        # must initialize it so instance attributes are accessible
+        templateClass.__init__(self,
+                               #_globalSetVars=self._CHEETAH__globalSetVars,
+                               #_preBuiltSearchList=self._CHEETAH__searchList
+                               )                               
+        if not hasattr(self, 'transaction'):
+            self.transaction = None
+
+    def _handleCheetahInclude(self, srcArg, trans=None, includeFrom='file', raw=False):        
+        """Called at runtime to handle #include directives.
+        """
+        _includeID = srcArg            
+        if _includeID not in self._CHEETAH__cheetahIncludes:
+            if not raw:
+                if includeFrom == 'file':
+                    source = None
+                    if isinstance(srcArg, basestring):
+                        if hasattr(self, 'serverSidePath'):
+                            file = path = self.serverSidePath(srcArg)
+                        else:
+                            file = path = os.path.normpath(srcArg)
+                    else:
+                        file = srcArg ## a file-like object
+                else:
+                    source = srcArg
+                    file = None
+                # @@TR: might want to provide some syntax for specifying the
+                # Template class to be used for compilation so compilerSettings
+                # can be changed.
+                compiler = self._getTemplateAPIClassForIncludeDirectiveCompilation(source, file)
+                nestedTemplateClass = compiler.compile(source=source, file=file)
+                nestedTemplate = nestedTemplateClass(_preBuiltSearchList=self.searchList(),
+                                                     _globalSetVars=self._CHEETAH__globalSetVars)
+                # Set the inner template filters to the initial filter of the
+                # outer template:
+                # this is the only really safe way to use
+                # filter='WebSafe'.
+                nestedTemplate._CHEETAH__initialFilter = self._CHEETAH__initialFilter
+                nestedTemplate._CHEETAH__currentFilter = self._CHEETAH__initialFilter   
+                self._CHEETAH__cheetahIncludes[_includeID] = nestedTemplate
+            else:
+                if includeFrom == 'file':
+                    path = self.serverSidePath(srcArg)
+                    self._CHEETAH__cheetahIncludes[_includeID] = self.getFileContents(path)
+                else:
+                    self._CHEETAH__cheetahIncludes[_includeID] = srcArg
+        ##
+        if not raw:
+            self._CHEETAH__cheetahIncludes[_includeID].respond(trans)
+        else:
+            trans.response().write(self._CHEETAH__cheetahIncludes[_includeID])
+
+    def _getTemplateAPIClassForIncludeDirectiveCompilation(self, source, file):
+        """Returns the subclass of Template which should be used to compile
+        #include directives.
+
+        This abstraction allows different compiler settings to be used in the
+        included template than were used in the parent.
+        """
+        if issubclass(self.__class__, Template):
+            return self.__class__
+        else:
+            return Template
+
+    ## functions for using templates as CGI scripts
+    def webInput(self, names, namesMulti=(), default='', src='f',
+        defaultInt=0, defaultFloat=0.00, badInt=0, badFloat=0.00, debug=False):
+        """Method for importing web transaction variables in bulk.
+
+        This works for GET/POST fields both in Webware servlets and in CGI
+        scripts, and for cookies and session variables in Webware servlets.  If
+        you try to read a cookie or session variable in a CGI script, you'll get
+        a RuntimeError.  'In a CGI script' here means 'not running as a Webware
+        servlet'.  If the CGI environment is not properly set up, Cheetah will
+        act like there's no input.
+
+        The public method provided is:
+
+          def webInput(self, names, namesMulti=(), default='', src='f',
+            defaultInt=0, defaultFloat=0.00, badInt=0, badFloat=0.00, debug=False):
+
+        This method places the specified GET/POST fields, cookies or session
+        variables into a dictionary, which is both returned and put at the
+        beginning of the searchList.  It handles:
+            
+            * single vs multiple values
+            * conversion to integer or float for specified names
+            * default values/exceptions for missing or bad values
+            * printing a snapshot of all values retrieved for debugging        
+
+        All the 'default*' and 'bad*' arguments have 'use or raise' behavior,
+        meaning that if they're a subclass of Exception, they're raised.  If
+        they're anything else, that value is substituted for the missing/bad
+        value.
+
+
+        The simplest usage is:
+
+            #silent $webInput(['choice'])
+            $choice
+
+            dic = self.webInput(['choice'])
+            write(dic['choice'])
+
+        Both these examples retrieves the GET/POST field 'choice' and print it.
+        If you leave off the'#silent', all the values would be printed too.  But
+        a better way to preview the values is
+
+            #silent $webInput(['name'], $debug=1)
+
+        because this pretty-prints all the values inside HTML <PRE> tags.
+
+        ** KLUDGE: 'debug' is supposed to insert into the template output, but it
+        wasn't working so I changed it to a'print' statement.  So the debugging
+        output will appear wherever standard output is pointed, whether at the
+        terminal, in a Webware log file, or whatever. ***
+
+        Since we didn't specify any coversions, the value is a string.  It's a
+        'single' value because we specified it in 'names' rather than
+        'namesMulti'. Single values work like this:
+        
+            * If one value is found, take it.
+            * If several values are found, choose one arbitrarily and ignore the rest.
+            * If no values are found, use or raise the appropriate 'default*' value.
+
+        Multi values work like this:
+            * If one value is found, put it in a list.
+            * If several values are found, leave them in a list.
+            * If no values are found, use the empty list ([]).  The 'default*' 
+              arguments are *not* consulted in this case.
+
+        Example: assume 'days' came from a set of checkboxes or a multiple combo
+        box on a form, and the user  chose'Monday', 'Tuesday' and 'Thursday'.
+
+            #silent $webInput([], ['days'])
+            The days you chose are: #slurp
+            #for $day in $days
+            $day #slurp
+            #end for
+
+            dic = self.webInput([], ['days'])
+            write('The days you chose are: ')
+            for day in dic['days']:
+                write(day + ' ')
+
+        Both these examples print:  'The days you chose are: Monday Tuesday Thursday'.
+
+        By default, missing strings are replaced by '' and missing/bad numbers
+        by zero.  (A'bad number' means the converter raised an exception for
+        it, usually because of non-numeric characters in the value.)  This
+        mimics Perl/PHP behavior, and simplifies coding for many applications
+        where missing/bad values *should* be blank/zero.  In those relatively
+        few cases where you must distinguish between empty-string/zero on the
+        one hand and missing/bad on the other, change the appropriate
+        'default*' and 'bad*' arguments to something like: 
+
+            * None
+            * another constant value
+            * $NonNumericInputError/self.NonNumericInputError
+            * $ValueError/ValueError
+            
+        (NonNumericInputError is defined in this class and is useful for
+        distinguishing between bad input vs a TypeError/ValueError thrown for
+        some other rason.)
+
+        Here's an example using multiple values to schedule newspaper
+        deliveries.  'checkboxes' comes from a form with checkboxes for all the
+        days of the week.  The days the user previously chose are preselected.
+        The user checks/unchecks boxes as desired and presses Submit.  The value
+        of 'checkboxes' is a list of checkboxes that were checked when Submit
+        was pressed.  Our task now is to turn on the days the user checked, turn
+        off the days he unchecked, and leave on or off the days he didn't
+        change.
+
+            dic = self.webInput([], ['dayCheckboxes'])
+            wantedDays = dic['dayCheckboxes'] # The days the user checked.
+            for day, on in self.getAllValues():
+                if   not on and wantedDays.has_key(day):
+                    self.TurnOn(day)
+                    # ... Set a flag or insert a database record ...
+                elif on and not wantedDays.has_key(day):
+                    self.TurnOff(day)
+                    # ... Unset a flag or delete a database record ...
+
+        'source' allows you to look up the variables from a number of different
+        sources:
+            'f'   fields (CGI GET/POST parameters)
+            'c'   cookies
+            's'   session variables
+            'v'   'values', meaning fields or cookies
+
+        In many forms, you're dealing only with strings, which is why the
+        'default' argument is third and the numeric arguments are banished to
+        the end.  But sometimes you want automatic number conversion, so that
+        you can do numeric comparisions in your templates without having to
+        write a bunch of conversion/exception handling code.  Example:
+
+            #silent $webInput(['name', 'height:int'])
+            $name is $height cm tall.
+            #if $height >= 300
+            Wow, you're tall!
+            #else
+            Pshaw, you're short.
+            #end if
+
+            dic = self.webInput(['name', 'height:int'])
+            name = dic[name]
+            height = dic[height]
+            write('%s is %s cm tall.' % (name, height))
+            if height > 300:
+                write('Wow, you're tall!')
+            else:
+                write('Pshaw, you're short.')
+
+        To convert a value to a number, suffix ':int' or ':float' to the name.
+        The method will search first for a 'height:int' variable and then for a
+        'height' variable.  (It will be called 'height' in the final
+        dictionary.)  If a numeric conversion fails, use or raise 'badInt' or
+        'badFloat'.  Missing values work the same way as for strings, except the
+        default is 'defaultInt' or 'defaultFloat' instead of 'default'.
+
+        If a name represents an uploaded file, the entire file will be read into
+        memory.  For more sophistocated file-upload handling, leave that name
+        out of the list and do your own handling, or wait for
+        Cheetah.Utils.UploadFileMixin.
+
+        This only in a subclass that also inherits from Webware's Servlet or
+        HTTPServlet.  Otherwise you'll get an AttributeError on 'self.request'.
+
+        EXCEPTIONS: ValueError if 'source' is not one of the stated characters.
+        TypeError if a conversion suffix is not ':int' or ':float'.
+
+        FUTURE EXPANSION: a future version of this method may allow source
+        cascading; e.g., 'vs' would look first in 'values' and then in session
+        variables.
+
+        Meta-Data
+        ================================================================================
+        Author: Mike Orr <iron@mso.oz.net>
+        License: This software is released for unlimited distribution under the
+                 terms of the MIT license.  See the LICENSE file.
+        Version: $Revision: 1.186 $
+        Start Date: 2002/03/17
+        Last Revision Date: $Date: 2008/03/10 04:48:11 $
+        """ 
+        src = src.lower()
+        isCgi = not self._CHEETAH__isControlledByWebKit
+        if   isCgi and src in ('f', 'v'):
+            global _formUsedByWebInput
+            if _formUsedByWebInput is None:
+                _formUsedByWebInput = cgi.FieldStorage()
+            source, func = 'field',   _formUsedByWebInput.getvalue
+        elif isCgi and src == 'c':
+            raise RuntimeError("can't get cookies from a CGI script")
+        elif isCgi and src == 's':
+            raise RuntimeError("can't get session variables from a CGI script")
+        elif isCgi and src == 'v':
+            source, func = 'value',   self.request().value
+        elif isCgi and src == 's':
+            source, func = 'session', self.request().session().value
+        elif src == 'f':
+            source, func = 'field',   self.request().field
+        elif src == 'c':
+            source, func = 'cookie',  self.request().cookie
+        elif src == 'v':
+            source, func = 'value',   self.request().value
+        elif src == 's':
+            source, func = 'session', self.request().session().value
+        else:
+            raise TypeError("arg 'src' invalid")
+        sources = source + 's'
+        converters = {
+            '': _Converter('string', None, default,      default ),
+            'int': _Converter('int',     int, defaultInt,   badInt  ),
+            'float': _Converter('float', float, defaultFloat, badFloat),  }
+        #pprint.pprint(locals());  return {}
+        dic = {} # Destination.
+        for name in names:
+            k, v = _lookup(name, func, False, converters)
+            dic[k] = v
+        for name in namesMulti:
+            k, v = _lookup(name, func, True, converters)
+            dic[k] = v
+        # At this point, 'dic' contains all the keys/values we want to keep.
+        # We could split the method into a superclass
+        # method for Webware/WebwareExperimental and a subclass for Cheetah.
+        # The superclass would merely 'return dic'.  The subclass would
+        # 'dic = super(ThisClass, self).webInput(names, namesMulti, ...)'
+        # and then the code below.
+        if debug:
+           print("<PRE>\n" + pprint.pformat(dic) + "\n</PRE>\n\n")
+        self.searchList().insert(0, dic)
+        return dic
+
+T = Template   # Short and sweet for debugging at the >>> prompt.
+Template.Reserved_SearchList = set(dir(Template))
+
+def genParserErrorFromPythonException(source, file, generatedPyCode, exception):
+
+    #print dir(exception)
+    
+    filename = isinstance(file, (str, unicode)) and file or None
+
+    sio = StringIO.StringIO()
+    traceback.print_exc(1, sio)
+    formatedExc = sio.getvalue()
+    
+    if hasattr(exception, 'lineno'):
+        pyLineno = exception.lineno
+    else:
+        pyLineno = int(re.search('[ \t]*File.*line (\d+)', formatedExc).group(1))
+       
+    lines = generatedPyCode.splitlines()
+    
+    prevLines = []                  # (i, content)
+    for i in range(1, 4):
+        if pyLineno-i <=0:
+            break
+        prevLines.append( (pyLineno+1-i, lines[pyLineno-i]) )
+    
+    nextLines = []                  # (i, content)
+    for i in range(1, 4):
+        if not pyLineno+i < len(lines):
+            break
+        nextLines.append( (pyLineno+i, lines[pyLineno+i]) )
+    nextLines.reverse()
+    report = 'Line|Python Code\n'
+    report += '----|-------------------------------------------------------------\n'
+    while prevLines:
+        lineInfo = prevLines.pop()
+        report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
+
+    if hasattr(exception, 'offset'):
+        report += ' '*(3+(exception.offset or 0)) + '^\n'
+    
+    while nextLines:
+        lineInfo = nextLines.pop()
+        report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
+    
+    
+    message = [
+        "Error in the Python code which Cheetah generated for this template:",
+        '='*80,
+        '',
+        str(exception),
+        '',                            
+        report,
+        '='*80,
+        ]
+    cheetahPosMatch = re.search('line (\d+), col (\d+)', formatedExc)
+    if cheetahPosMatch:
+        lineno = int(cheetahPosMatch.group(1))
+        col = int(cheetahPosMatch.group(2))
+        #if hasattr(exception, 'offset'):
+        #    col = exception.offset
+        message.append('\nHere is the corresponding Cheetah code:\n')
+    else:
+        lineno = None
+        col = None
+        cheetahPosMatch = re.search('line (\d+), col (\d+)',
+                                    '\n'.join(lines[max(pyLineno-2, 0):]))
+        if cheetahPosMatch:
+            lineno = int(cheetahPosMatch.group(1))
+            col = int(cheetahPosMatch.group(2))
+            message.append('\nHere is the corresponding Cheetah code.')
+            message.append('** I had to guess the line & column numbers,'
+                           ' so they are probably incorrect:\n')
+
+    
+    message = '\n'.join(message)
+    reader = SourceReader(source, filename=filename)
+    return ParseError(reader, message, lineno=lineno, col=col)
+    
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/TemplateCmdLineIface.py b/cheetah/TemplateCmdLineIface.py
new file mode 100644 (file)
index 0000000..9787577
--- /dev/null
@@ -0,0 +1,107 @@
+# $Id: TemplateCmdLineIface.py,v 1.13 2006/01/10 20:34:35 tavis_rudd Exp $
+
+"""Provides a command line interface to compiled Cheetah template modules.
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+Version: $Revision: 1.13 $
+Start Date: 2001/12/06
+Last Revision Date: $Date: 2006/01/10 20:34:35 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.13 $"[11:-2]
+
+import sys
+import os
+import getopt
+import os.path
+try:
+    from cPickle import load
+except ImportError:
+    from pickle import load
+
+from Cheetah.Version import Version
+
+class Error(Exception):
+    pass
+
+class CmdLineIface:
+    """A command line interface to compiled Cheetah template modules."""
+
+    def __init__(self, templateObj,
+                 scriptName=os.path.basename(sys.argv[0]),
+                 cmdLineArgs=sys.argv[1:]):
+
+        self._template = templateObj
+        self._scriptName = scriptName
+        self._cmdLineArgs = cmdLineArgs
+
+    def run(self):
+        """The main program controller."""
+        
+        self._processCmdLineArgs()
+        print(self._template)
+        
+    def _processCmdLineArgs(self):
+        try:
+            self._opts, self._args = getopt.getopt(
+                self._cmdLineArgs, 'h', ['help',
+                                            'env',
+                                            'pickle=',
+                                            ])
+
+        except getopt.GetoptError, v:
+            # print help information and exit:
+            print(v)
+            print(self.usage())
+            sys.exit(2)
+        
+        for o, a in self._opts:
+            if o in ('-h', '--help'):
+                print(self.usage())
+                sys.exit()
+            if o == '--env':
+                self._template.searchList().insert(0, os.environ)
+            if o == '--pickle':
+                if a == '-':
+                    unpickled = load(sys.stdin)
+                    self._template.searchList().insert(0, unpickled)
+                else:
+                    f = open(a)
+                    unpickled = load(f)
+                    f.close()
+                    self._template.searchList().insert(0, unpickled)
+
+    def usage(self):
+        return """Cheetah %(Version)s template module command-line interface
+
+Usage
+-----
+  %(scriptName)s [OPTION]
+
+Options
+-------
+  -h, --help                 Print this help information
+  
+  --env                      Use shell ENVIRONMENT variables to fill the
+                             $placeholders in the template.
+                             
+  --pickle <file>            Use a variables from a dictionary stored in Python
+                             pickle file to fill $placeholders in the template.
+                             If <file> is - stdin is used: 
+                             '%(scriptName)s --pickle -'
+
+Description
+-----------
+
+This interface allows you to execute a Cheetah template from the command line
+and collect the output.  It can prepend the shell ENVIRONMENT or a pickled
+Python dictionary to the template's $placeholder searchList, overriding the
+defaults for the $placeholders.
+
+""" % {'scriptName': self._scriptName,
+       'Version': Version,
+       }
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Templates/SkeletonPage.py b/cheetah/Templates/SkeletonPage.py
new file mode 100644 (file)
index 0000000..928ae2b
--- /dev/null
@@ -0,0 +1,272 @@
+
+
+"""A Skeleton HTML page template, that provides basic structure and utility methods.
+"""
+
+
+##################################################
+## DEPENDENCIES
+import sys
+import os
+import os.path
+from os.path import getmtime, exists
+import time
+import types
+import __builtin__
+from Cheetah.Version import MinCompatibleVersion as RequiredCheetahVersion
+from Cheetah.Version import MinCompatibleVersionTuple as RequiredCheetahVersionTuple
+from Cheetah.Template import Template
+from Cheetah.DummyTransaction import DummyTransaction
+from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList
+from Cheetah.CacheRegion import CacheRegion
+import Cheetah.Filters as Filters
+import Cheetah.ErrorCatchers as ErrorCatchers
+from Cheetah.Templates._SkeletonPage import _SkeletonPage
+
+##################################################
+## MODULE CONSTANTS
+try:
+    True, False
+except NameError:
+    True, False = (1==1), (1==0)
+VFFSL=valueFromFrameOrSearchList
+VFSL=valueFromSearchList
+VFN=valueForName
+currentTime=time.time
+__CHEETAH_version__ = '2.0rc6'
+__CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 6)
+__CHEETAH_genTime__ = 1139107954.3640411
+__CHEETAH_genTimestamp__ = 'Sat Feb  4 18:52:34 2006'
+__CHEETAH_src__ = 'src/Templates/SkeletonPage.tmpl'
+__CHEETAH_srcLastModified__ = 'Mon Oct  7 11:37:30 2002'
+__CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
+
+if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
+    raise AssertionError(
+      'This template was compiled with Cheetah version'
+      ' %s. Templates compiled before version %s must be recompiled.'%(
+         __CHEETAH_version__, RequiredCheetahVersion))
+
+##################################################
+## CLASSES
+
+class SkeletonPage(_SkeletonPage):
+
+    ##################################################
+    ## CHEETAH GENERATED METHODS
+
+
+    def __init__(self, *args, **KWs):
+
+        _SkeletonPage.__init__(self, *args, **KWs)
+        if not self._CHEETAH__instanceInitialized:
+            cheetahKWArgs = {}
+            allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split()
+            for k, v in KWs.items():
+                if k in allowedKWs: cheetahKWArgs[k] = v
+            self._initCheetahInstance(**cheetahKWArgs)
+        
+
+    def writeHeadTag(self, **KWS):
+
+
+
+        ## CHEETAH: generated from #block writeHeadTag at line 22, col 1.
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not hasattr(self.transaction, '__call__')):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        write('<head>\n<title>')
+        _v = VFFSL(SL, "title", True) # '$title' on line 24, col 8
+        if _v is not None: write(_filter(_v, rawExpr='$title')) # from line 24, col 8.
+        write('</title>\n')
+        _v = VFFSL(SL, "metaTags", True) # '$metaTags' on line 25, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$metaTags')) # from line 25, col 1.
+        write(' \n')
+        _v = VFFSL(SL, "stylesheetTags", True) # '$stylesheetTags' on line 26, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$stylesheetTags')) # from line 26, col 1.
+        write(' \n')
+        _v = VFFSL(SL, "javascriptTags", True) # '$javascriptTags' on line 27, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$javascriptTags')) # from line 27, col 1.
+        write('\n</head>\n')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+
+    def writeBody(self, **KWS):
+
+
+
+        ## CHEETAH: generated from #block writeBody at line 36, col 1.
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not hasattr(self.transaction, '__call__')):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        write('This skeleton page has no flesh. Its body needs to be implemented.\n')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+
+    def respond(self, trans=None):
+
+
+
+        ## CHEETAH: main method generated for this template
+        if (not trans and not self._CHEETAH__isBuffering and not hasattr(self.transaction, '__call__')):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        
+        ## START CACHE REGION: ID=header. line 6, col 1 in the source.
+        _RECACHE_header = False
+        _cacheRegion_header = self.getCacheRegion(regionID='header', cacheInfo={'type': 2, 'id': 'header'})
+        if _cacheRegion_header.isNew():
+            _RECACHE_header = True
+        _cacheItem_header = _cacheRegion_header.getCacheItem('header')
+        if _cacheItem_header.hasExpired():
+            _RECACHE_header = True
+        if (not _RECACHE_header) and _cacheItem_header.getRefreshTime():
+            try:
+                _output = _cacheItem_header.renderOutput()
+            except KeyError:
+                _RECACHE_header = True
+            else:
+                write(_output)
+                del _output
+        if _RECACHE_header or not _cacheItem_header.getRefreshTime():
+            _orig_transheader = trans
+            trans = _cacheCollector_header = DummyTransaction()
+            write = _cacheCollector_header.response().write
+            _v = VFFSL(SL, "docType", True) # '$docType' on line 7, col 1
+            if _v is not None: write(_filter(_v, rawExpr='$docType')) # from line 7, col 1.
+            write('\n')
+            _v = VFFSL(SL, "htmlTag", True) # '$htmlTag' on line 8, col 1
+            if _v is not None: write(_filter(_v, rawExpr='$htmlTag')) # from line 8, col 1.
+            write('''
+<!-- This document was autogenerated by Cheetah(http://CheetahTemplate.org). 
+Do not edit it directly!
+
+Copyright ''')
+            _v = VFFSL(SL, "currentYr", True) # '$currentYr' on line 12, col 11
+            if _v is not None: write(_filter(_v, rawExpr='$currentYr')) # from line 12, col 11.
+            write(' - ')
+            _v = VFFSL(SL, "siteCopyrightName", True) # '$siteCopyrightName' on line 12, col 24
+            if _v is not None: write(_filter(_v, rawExpr='$siteCopyrightName')) # from line 12, col 24.
+            write(' - All Rights Reserved.\nFeel free to copy any javascript or html you like on this site,\nprovided you remove all links and/or references to ')
+            _v = VFFSL(SL, "siteDomainName", True) # '$siteDomainName' on line 14, col 52
+            if _v is not None: write(_filter(_v, rawExpr='$siteDomainName')) # from line 14, col 52.
+            write('''
+However, please do not copy any content or images without permission.
+
+''')
+            _v = VFFSL(SL, "siteCredits", True) # '$siteCredits' on line 17, col 1
+            if _v is not None: write(_filter(_v, rawExpr='$siteCredits')) # from line 17, col 1.
+            write('''
+
+-->
+
+
+''')
+            self.writeHeadTag(trans=trans)
+            write('\n')
+            trans = _orig_transheader
+            write = trans.response().write
+            _cacheData = _cacheCollector_header.response().getvalue()
+            _cacheItem_header.setData(_cacheData)
+            write(_cacheData)
+            del _cacheData
+            del _cacheCollector_header
+            del _orig_transheader
+        ## END CACHE REGION: header
+        
+        write('\n')
+        _v = VFFSL(SL, "bodyTag", True) # '$bodyTag' on line 34, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$bodyTag')) # from line 34, col 1.
+        write('\n\n')
+        self.writeBody(trans=trans)
+        write('''
+</body>
+</html>
+
+
+
+''')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+    ##################################################
+    ## CHEETAH GENERATED ATTRIBUTES
+
+
+    _CHEETAH__instanceInitialized = False
+
+    _CHEETAH_version = __CHEETAH_version__
+
+    _CHEETAH_versionTuple = __CHEETAH_versionTuple__
+
+    _CHEETAH_genTime = __CHEETAH_genTime__
+
+    _CHEETAH_genTimestamp = __CHEETAH_genTimestamp__
+
+    _CHEETAH_src = __CHEETAH_src__
+
+    _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__
+
+    _mainCheetahMethod_for_SkeletonPage= 'respond'
+
+## END CLASS DEFINITION
+
+if not hasattr(SkeletonPage, '_initCheetahAttributes'):
+    templateAPIClass = getattr(SkeletonPage, '_CHEETAH_templateClass', Template)
+    templateAPIClass._addCheetahPlumbingCodeToClass(SkeletonPage)
+
+
+# CHEETAH was developed by Tavis Rudd and Mike Orr
+# with code, advice and input from many other volunteers.
+# For more information visit http://www.CheetahTemplate.org/
+
+##################################################
+## if run from command line:
+if __name__ == '__main__':
+    from Cheetah.TemplateCmdLineIface import CmdLineIface
+    CmdLineIface(templateObj=SkeletonPage()).run()
+
+
diff --git a/cheetah/Templates/SkeletonPage.tmpl b/cheetah/Templates/SkeletonPage.tmpl
new file mode 100644 (file)
index 0000000..43c5ecd
--- /dev/null
@@ -0,0 +1,44 @@
+##doc-module: A Skeleton HTML page template, that provides basic structure and utility methods.
+################################################################################
+#extends Cheetah.Templates._SkeletonPage
+#implements respond
+################################################################################
+#cache id='header'
+$docType
+$htmlTag
+<!-- This document was autogenerated by Cheetah(http://CheetahTemplate.org). 
+Do not edit it directly!
+
+Copyright $currentYr - $siteCopyrightName - All Rights Reserved.
+Feel free to copy any javascript or html you like on this site,
+provided you remove all links and/or references to $siteDomainName
+However, please do not copy any content or images without permission.
+
+$siteCredits
+
+-->
+
+
+#block writeHeadTag
+<head>
+<title>$title</title>
+$metaTags 
+$stylesheetTags 
+$javascriptTags
+</head>
+#end block writeHeadTag
+
+#end cache header
+#################
+
+$bodyTag
+
+#block writeBody
+This skeleton page has no flesh. Its body needs to be implemented.
+#end block writeBody
+
+</body>
+</html>
+
+
+
diff --git a/cheetah/Templates/_SkeletonPage.py b/cheetah/Templates/_SkeletonPage.py
new file mode 100644 (file)
index 0000000..13f9db3
--- /dev/null
@@ -0,0 +1,215 @@
+# $Id: _SkeletonPage.py,v 1.13 2002/10/01 17:52:02 tavis_rudd Exp $
+"""A baseclass for the SkeletonPage template
+
+Meta-Data
+==========
+Author: Tavis Rudd <tavis@damnsimple.com>,
+Version: $Revision: 1.13 $
+Start Date: 2001/04/05
+Last Revision Date: $Date: 2002/10/01 17:52:02 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.13 $"[11:-2]
+
+##################################################
+## DEPENDENCIES ##
+
+import time, types, os, sys
+
+# intra-package imports ...
+from Cheetah.Template import Template
+
+
+##################################################
+## GLOBALS AND CONSTANTS ##
+
+True = (1==1)
+False = (0==1)
+
+##################################################
+## CLASSES ##
+        
+class _SkeletonPage(Template):
+    """A baseclass for the SkeletonPage template"""
+
+    docType = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" ' + \
+              '"http://www.w3.org/TR/html4/loose.dtd">'
+    
+    # docType = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' + \
+    #'"http://www.w3.org/TR/xhtml1l/DTD/transitional.dtd">'
+        
+    title = ''
+    siteDomainName = 'www.example.com'
+    siteCredits = 'Designed & Implemented by Tavis Rudd'
+    siteCopyrightName = "Tavis Rudd"
+    htmlTag = '<html>'
+    
+    def __init__(self, *args, **KWs):
+        Template.__init__(self, *args, **KWs)
+        self._metaTags = {'HTTP-EQUIV':{'keywords': 'Cheetah',
+                                        'Content-Type': 'text/html; charset=iso-8859-1',
+                                        }, 
+                    'NAME':{'generator':'Cheetah: The Python-Powered Template Engine'}
+                    }
+        # metaTags = {'HTTP_EQUIV':{'test':1234}, 'NAME':{'test':1234,'test2':1234} }
+        self._stylesheets = {}
+        # stylesheets = {'.cssClassName':'stylesheetCode'}
+        self._stylesheetsOrder = []
+        # stylesheetsOrder = ['.cssClassName',]
+        self._stylesheetLibs = {}
+        # stylesheetLibs = {'libName':'libSrcPath'}
+        self._javascriptLibs = {}
+        self._javascriptTags = {}
+        # self._javascriptLibs = {'libName':'libSrcPath'}
+        self._bodyTagAttribs = {}
+
+    def metaTags(self):
+        """Return a formatted vesion of the self._metaTags dictionary, using the
+        formatMetaTags function from Cheetah.Macros.HTML"""
+        
+        return self.formatMetaTags(self._metaTags)
+    
+    def stylesheetTags(self):
+        """Return a formatted version of the self._stylesheetLibs and
+        self._stylesheets dictionaries.  The keys in self._stylesheets must
+        be listed in the order that they should appear in the list
+        self._stylesheetsOrder, to ensure that the style rules are defined in
+        the correct order."""
+        
+        stylesheetTagsTxt = ''
+        for title, src in self._stylesheetLibs.items():
+            stylesheetTagsTxt += '<link rel="stylesheet" type="text/css" href="' + str(src) + '" />\n'
+
+        if not self._stylesheetsOrder:
+            return stylesheetTagsTxt
+        
+        stylesheetTagsTxt += '<style type="text/css"><!--\n'
+        for identifier in self._stylesheetsOrder:
+            if identifier not in self._stylesheets:
+                warning = '# the identifier ' + identifier + \
+                          'was in stylesheetsOrder, but not in stylesheets'
+                print(warning)
+                stylesheetTagsTxt += warning
+                continue
+                    
+            attribsDict = self._stylesheets[identifier]
+            cssCode = ''
+            attribCode = ''
+            for k, v in attribsDict.items():
+                attribCode += str(k) + ': ' + str(v) + '; '
+            attribCode = attribCode[:-2] # get rid of the last semicolon
+                
+            cssCode = '\n' + identifier + ' {' +  attribCode + '}'
+            stylesheetTagsTxt += cssCode
+            
+        stylesheetTagsTxt += '\n//--></style>\n'
+
+        return stylesheetTagsTxt
+
+    def javascriptTags(self):
+        """Return a formatted version of the javascriptTags and
+        javascriptLibs dictionaries.  Each value in javascriptTags
+        should be a either a code string to include, or a list containing the
+        JavaScript version number and the code string. The keys can be anything.
+        The same applies for javascriptLibs, but the string should be the
+        SRC filename rather than a code string."""
+        
+        javascriptTagsTxt = []
+        for key, details in self._javascriptTags.iteritems():
+            if not isinstance(details, (list, tuple)):
+                details = ['', details]
+                
+            javascriptTagsTxt += ['<script language="JavaScript', str(details[0]),
+                                  '" type="text/javascript"><!--\n',
+                                  str(details[0]), '\n//--></script>\n']
+
+
+        for key, details in self._javascriptLibs.iteritems():
+            if not isinstance(details, (list, tuple)):
+                details = ['', details]
+
+            javascriptTagsTxt += ['<script language="JavaScript', str(details[0]),
+                                  '" type="text/javascript" src="',
+                                  str(details[1]), '" />\n']
+        return ''.join(javascriptTagsTxt)
+    
+    def bodyTag(self):
+        """Create a body tag from the entries in the dict bodyTagAttribs."""
+        return self.formHTMLTag('body', self._bodyTagAttribs)
+
+
+    def imgTag(self, src, alt='', width=None, height=None, border=0):
+        
+        """Dynamically generate an image tag.  Cheetah will try to convert the
+        src argument to a WebKit serverSidePath relative to the servlet's
+        location. If width and height aren't specified they are calculated using
+        PIL or ImageMagick if available."""
+        
+        src = self.normalizePath(src)
+        
+
+        if not width or not height:
+            try:                    # see if the dimensions can be calc'd with PIL
+                import Image
+                im = Image.open(src)
+                calcWidth, calcHeight = im.size
+                del im
+                if not width: width = calcWidth
+                if not height: height = calcHeight
+
+            except:
+                try:                # try imageMagick instead
+                    calcWidth, calcHeight = os.popen(
+                        'identify -format "%w,%h" ' + src).read().split(',')
+                    if not width: width = calcWidth
+                    if not height: height = calcHeight
+        
+                except:
+                    pass
+                
+        if width and height:
+            return ''.join(['<img src="', src, '" width="', str(width), '" height="', str(height),
+                           '" alt="', alt, '" border="', str(border), '" />'])
+        elif width:
+            return ''.join(['<img src="', src, '" width="', str(width),
+                           '" alt="', alt, '" border="', str(border), '" />'])
+        elif height:
+            return ''.join(['<img src="', src, '" height="', str(height),
+                           '" alt="', alt, '" border="', str(border), '" />'])
+        else:
+            return ''.join(['<img src="', src, '" alt="', alt, '" border="', str(border), '" />'])
+
+
+    def currentYr(self):
+        """Return a string representing the current yr."""
+        return time.strftime("%Y", time.localtime(time.time()))
+    
+    def currentDate(self, formatString="%b %d, %Y"):
+        """Return a string representing the current localtime."""
+        return time.strftime(formatString, time.localtime(time.time()))
+    
+    def spacer(self, width=1,height=1):
+        return '<img src="spacer.gif" width="%s" height="%s" alt="" />'% (str(width), str(height))
+    
+    def formHTMLTag(self, tagName, attributes={}):
+        """returns a string containing an HTML <tag> """
+        tagTxt = ['<', tagName.lower()]
+        for name, val in attributes.items():
+            tagTxt += [' ', name.lower(), '="', str(val), '"']
+        tagTxt.append('>')
+        return ''.join(tagTxt)
+    
+    def formatMetaTags(self, metaTags):
+        """format a dict of metaTag definitions into an HTML version"""
+        metaTagsTxt = []
+        if 'HTTP-EQUIV' in metaTags:
+            for http_equiv, contents in metaTags['HTTP-EQUIV'].items():
+                metaTagsTxt += ['<meta http-equiv="', str(http_equiv), '" content="',
+                                str(contents), '" />\n']
+                
+        if 'NAME' in metaTags:
+            for name, contents in metaTags['NAME'].items():
+                metaTagsTxt += ['<meta name="', str(name), '" content="', str(contents),
+                                '" />\n']
+        return ''.join(metaTagsTxt)
+    
diff --git a/cheetah/Templates/__init__.py b/cheetah/Templates/__init__.py
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/cheetah/Tests/Analyzer.py b/cheetah/Tests/Analyzer.py
new file mode 100644 (file)
index 0000000..59f6c1c
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+import unittest
+
+from Cheetah import DirectiveAnalyzer
+
+
+class AnalyzerTests(unittest.TestCase):
+    def test_set(self):
+        template = '''
+        #set $foo = "bar"
+        Hello ${foo}!
+        '''
+        calls = DirectiveAnalyzer.analyze(template)
+        self.assertEquals(1, calls.get('set'))
+
+    def test_compilersettings(self):
+        template = '''
+#compiler-settings
+useNameMapper = False
+#end compiler-settings
+        '''
+        calls = DirectiveAnalyzer.analyze(template)
+        self.assertEquals(1, calls.get('compiler-settings'))
+
+
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/cheetah/Tests/CheetahWrapper.py b/cheetah/Tests/CheetahWrapper.py
new file mode 100644 (file)
index 0000000..cabe651
--- /dev/null
@@ -0,0 +1,579 @@
+#!/usr/bin/env python
+'''
+Tests for the 'cheetah' command.
+
+Besides unittest usage, recognizes the following command-line options:
+    --list CheetahWrapper.py
+        List all scenarios that are tested.  The argument is the path
+        of this script.
+     --nodelete
+        Don't delete scratch directory at end.
+     --output
+        Show the output of each subcommand.  (Normally suppressed.)
+'''
+import os
+import os.path
+import pdb
+import re                                     # Used by listTests.
+import shutil
+import sys
+import tempfile
+import unittest
+
+from optparse import OptionParser
+from Cheetah.CheetahWrapper import CheetahWrapper  # Used by NoBackup.
+
+try:
+    from subprocess import Popen, PIPE, STDOUT
+    class Popen4(Popen):
+        def __init__(self, cmd, bufsize=-1, shell=True, close_fds=True,
+                        stdin=PIPE, stdout=PIPE, stderr=STDOUT, **kwargs):
+
+            super(Popen4, self).__init__(cmd, bufsize=bufsize, shell=shell,
+                            close_fds=close_fds, stdin=stdin, stdout=stdout,
+                            stderr=stderr, **kwargs)
+
+            self.tochild = self.stdin
+            self.fromchild = self.stdout
+            self.childerr = self.stderr
+except ImportError:
+    from popen2 import Popen4
+
+DELETE = True # True to clean up after ourselves, False for debugging.
+OUTPUT = False # Normally False, True for debugging.
+
+BACKUP_SUFFIX = CheetahWrapper.BACKUP_SUFFIX
+
+def warn(msg):
+    sys.stderr.write(msg + '\n')
+
+class CFBase(unittest.TestCase):
+    """Base class for "cheetah compile" and "cheetah fill" unit tests.
+    """
+    srcDir = '' # Nonblank to create source directory.
+    subdirs = ('child', 'child/grandkid') # Delete in reverse order.
+    srcFiles = ('a.tmpl', 'child/a.tmpl', 'child/grandkid/a.tmpl')
+    expectError = False # Used by --list option.
+
+    def inform(self, message):
+        if self.verbose:
+            print(message)
+
+    def setUp(self):
+        """Create the top-level directories, subdirectories and .tmpl
+           files.
+        """
+        self.cmd = self.locate_cheetah('cheetah')
+        pythonPath = os.getcwd()
+        if not os.environ.get('PYTHONPATH'):
+            os.environ['PYTHONPATH'] = pythonPath
+        else:
+            os.environ['PYTHONPATH'] = '%s:%s' % (os.environ['PYTHONPATH'], pythonPath)
+        I = self.inform
+        # Step 1: Create the scratch directory and chdir into it.
+        self.scratchDir = scratchDir = tempfile.mktemp() 
+        os.mkdir(scratchDir)
+        self.origCwd = os.getcwd()
+        os.chdir(scratchDir)
+        if self.srcDir:
+            os.mkdir(self.srcDir)
+        # Step 2: Create source subdirectories.
+        for dir in self.subdirs:
+            os.mkdir(dir)
+        # Step 3: Create the .tmpl files, each in its proper directory.
+        for fil in self.srcFiles:
+            f = open(fil, 'w')
+            f.write("Hello, world!\n")
+            f.close()
+
+    def tearDown(self):
+        os.chdir(self.origCwd)
+        if DELETE:
+            shutil.rmtree(self.scratchDir, True) # Ignore errors.
+            if os.path.exists(self.scratchDir):
+                warn("Warning: unable to delete scratch directory %s")
+        else:
+            warn("Warning: not deleting scratch directory %s" % self.scratchDir)
+
+
+    def _checkDestFileHelper(self, path, expected, 
+        allowSurroundingText, errmsg):
+        """Low-level helper to check a destination file.
+
+           in : path, string, the destination path.
+                expected, string, the expected contents.
+                allowSurroundingtext, bool, allow the result to contain
+                  additional text around the 'expected' substring?
+                errmsg, string, the error message.  It may contain the
+                  following "%"-operator keys: path, expected, result.
+           out: None
+        """
+        path = os.path.abspath(path)
+        exists = os.path.exists(path)
+        msg = "destination file missing: %s" % path
+        self.failUnless(exists, msg)
+        f = open(path, 'r')
+        result = f.read()
+        f.close()
+        if allowSurroundingText:
+            success = result.find(expected) != -1
+        else:
+            success = result == expected
+        msg = errmsg % locals()
+        self.failUnless(success, msg)
+
+
+    def checkCompile(self, path):
+        # Raw string to prevent "\n" from being converted to a newline.
+        #expected = R"write('Hello, world!\n')"
+        expected = "Hello, world!" # might output a u'' string
+        errmsg = """\
+destination file %(path)s doesn't contain expected substring:
+%(expected)r"""
+        self._checkDestFileHelper(path, expected, True, errmsg)
+
+
+    def checkFill(self, path):
+        expected = "Hello, world!\n"
+        errmsg = """\
+destination file %(path)s contains wrong result.
+Expected %(expected)r
+Found %(result)r"""
+        self._checkDestFileHelper(path, expected, False, errmsg)
+
+
+    def checkSubdirPyInit(self, path):
+        """Verify a destination subdirectory exists and contains an
+           __init__.py file.
+        """
+        exists = os.path.exists(path)
+        msg = "destination subdirectory %s misssing" % path
+        self.failUnless(exists, msg)
+        initPath = os.path.join(path, "__init__.py")
+        exists = os.path.exists(initPath)
+        msg = "destination init file missing: %s" % initPath
+        self.failUnless(exists, msg)
+
+
+    def checkNoBackup(self, path):
+        """Verify 'path' does not exist.  (To check --nobackup.)
+        """
+        exists = os.path.exists(path)
+        msg = "backup file exists in spite of --nobackup: %s" % path
+        self.failIf(exists, msg)
+
+    def locate_cheetah(self, cmd):
+        paths = os.getenv('PATH')
+        if not paths:
+            return cmd
+        parts = cmd.split(' ')
+        paths = paths.split(':')
+        for p in paths:
+            p = os.path.join(p, cmd)
+            p = os.path.abspath(p)
+            if os.path.isfile(p):
+                return p
+        return cmd
+
+    def assertWin32Subprocess(self, cmd):
+        _in, _out = os.popen4(cmd)
+        _in.close()
+        output = _out.read()
+        rc = _out.close()
+        if rc is None:
+            rc = 0
+        return rc, output
+
+    def assertPosixSubprocess(self, cmd):
+        cmd = cmd.replace('cheetah', self.cmd)
+        process = Popen4(cmd, env=os.environ)
+        process.tochild.close()
+        output = process.fromchild.read()
+        status = process.wait()
+        process.fromchild.close()
+        return status, output
+
+    def assertSubprocess(self, cmd, nonzero=False):
+        status, output = None, None
+        if sys.platform == 'win32':
+            status, output = self.assertWin32Subprocess(cmd)
+        else:
+            status, output = self.assertPosixSubprocess(cmd)
+
+        if not nonzero:
+            self.failUnlessEqual(status, 0, '''Subprocess exited with a non-zero status (%d)
+                            %s''' % (status, output))
+        else:
+            self.failIfEqual(status, 0, '''Subprocess exited with a zero status (%d)
+                            %s''' % (status, output))
+        return output 
+    
+    def go(self, cmd, expectedStatus=0, expectedOutputSubstring=None):
+        """Run a "cheetah compile" or "cheetah fill" subcommand.
+
+           in : cmd, string, the command to run.
+                expectedStatus, int, subcommand's expected output status.
+                  0 if the subcommand is expected to succeed, 1-255 otherwise.
+                expectedOutputSubstring, string, substring which much appear
+                  in the standard output or standard error.  None to skip this
+                  test.
+           out: None.
+        """
+        output = self.assertSubprocess(cmd)
+        if expectedOutputSubstring is not None:
+            msg = "substring %r not found in subcommand output: %s" % \
+                (expectedOutputSubstring, cmd)
+            substringTest = output.find(expectedOutputSubstring) != -1
+            self.failUnless(substringTest, msg)
+
+
+class CFIdirBase(CFBase):
+    """Subclass for tests with --idir.
+    """
+    srcDir = 'SRC'
+    subdirs = ('SRC/child', 'SRC/child/grandkid') # Delete in reverse order.
+    srcFiles = ('SRC/a.tmpl', 'SRC/child/a.tmpl', 'SRC/child/grandkid/a.tmpl')
+
+
+
+##################################################
+## TEST CASE CLASSES
+
+class OneFile(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile a.tmpl")
+        self.checkCompile("a.py")
+
+    def testFill(self):
+        self.go("cheetah fill a.tmpl")
+        self.checkFill("a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt a.tmpl")
+        self.checkFill("a.txt")
+
+
+class OneFileNoExtension(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile a")
+        self.checkCompile("a.py")
+
+    def testFill(self):
+        self.go("cheetah fill a")
+        self.checkFill("a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt a")
+        self.checkFill("a.txt")
+
+
+class SplatTmpl(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile *.tmpl")
+        self.checkCompile("a.py")
+
+    def testFill(self):
+        self.go("cheetah fill *.tmpl")
+        self.checkFill("a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt *.tmpl")
+        self.checkFill("a.txt")
+
+class ThreeFilesWithSubdirectories(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile a.tmpl child/a.tmpl child/grandkid/a.tmpl")
+        self.checkCompile("a.py")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill a.tmpl child/a.tmpl child/grandkid/a.tmpl")
+        self.checkFill("a.html")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt a.tmpl child/a.tmpl child/grandkid/a.tmpl")
+        self.checkFill("a.txt")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class ThreeFilesWithSubdirectoriesNoExtension(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile a child/a child/grandkid/a")
+        self.checkCompile("a.py")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill a child/a child/grandkid/a")
+        self.checkFill("a.html")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt a child/a child/grandkid/a")
+        self.checkFill("a.txt")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class SplatTmplWithSubdirectories(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile *.tmpl child/*.tmpl child/grandkid/*.tmpl")
+        self.checkCompile("a.py")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill *.tmpl child/*.tmpl child/grandkid/*.tmpl")
+        self.checkFill("a.html")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill --oext txt *.tmpl child/*.tmpl child/grandkid/*.tmpl")
+        self.checkFill("a.txt")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class OneFileWithOdir(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile --odir DEST a.tmpl")
+        self.checkSubdirPyInit("DEST")
+        self.checkCompile("DEST/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill --odir DEST a.tmpl")
+        self.checkFill("DEST/a.html")
+
+    def testText(self):
+        self.go("cheetah fill --odir DEST --oext txt a.tmpl")
+        self.checkFill("DEST/a.txt")
+
+
+class VarietyWithOdir(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile --odir DEST a.tmpl child/a child/grandkid/*.tmpl")
+        self.checkSubdirPyInit("DEST")
+        self.checkSubdirPyInit("DEST/child")
+        self.checkSubdirPyInit("DEST/child/grandkid")
+        self.checkCompile("DEST/a.py")
+        self.checkCompile("DEST/child/a.py")
+        self.checkCompile("DEST/child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill --odir DEST a.tmpl child/a child/grandkid/*.tmpl")
+        self.checkFill("DEST/a.html")
+        self.checkFill("DEST/child/a.html")
+        self.checkFill("DEST/child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill --odir DEST --oext txt a.tmpl child/a child/grandkid/*.tmpl")
+        self.checkFill("DEST/a.txt")
+        self.checkFill("DEST/child/a.txt")
+        self.checkFill("DEST/child/grandkid/a.txt")
+
+
+class RecurseExplicit(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile -R child")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill -R child")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill -R --oext txt child")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class RecurseImplicit(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile -R")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill -R")
+        self.checkFill("a.html")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill -R --oext txt")
+        self.checkFill("a.txt")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class RecurseExplicitWIthOdir(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile -R --odir DEST child")
+        self.checkSubdirPyInit("DEST/child")
+        self.checkSubdirPyInit("DEST/child/grandkid")
+        self.checkCompile("DEST/child/a.py")
+        self.checkCompile("DEST/child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill -R --odir DEST child")
+        self.checkFill("DEST/child/a.html")
+        self.checkFill("DEST/child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill -R --odir DEST --oext txt child")
+        self.checkFill("DEST/child/a.txt")
+        self.checkFill("DEST/child/grandkid/a.txt")
+
+
+class Flat(CFBase):
+    def testCompile(self):
+        self.go("cheetah compile --flat child/a.tmpl")
+        self.checkCompile("a.py")
+
+    def testFill(self):
+        self.go("cheetah fill --flat child/a.tmpl")
+        self.checkFill("a.html")
+
+    def testText(self):
+        self.go("cheetah fill --flat --oext txt child/a.tmpl")
+        self.checkFill("a.txt")
+
+
+class FlatRecurseCollision(CFBase):
+    expectError = True
+
+    def testCompile(self):
+        self.assertSubprocess("cheetah compile -R --flat", nonzero=True)
+
+    def testFill(self):
+        self.assertSubprocess("cheetah fill -R --flat", nonzero=True)
+
+    def testText(self):
+        self.assertSubprocess("cheetah fill -R --flat", nonzero=True)
+
+
+class IdirRecurse(CFIdirBase):
+    def testCompile(self):
+        self.go("cheetah compile -R --idir SRC child")
+        self.checkSubdirPyInit("child")
+        self.checkSubdirPyInit("child/grandkid")
+        self.checkCompile("child/a.py")
+        self.checkCompile("child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill -R --idir SRC child")
+        self.checkFill("child/a.html")
+        self.checkFill("child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill -R --idir SRC --oext txt child")
+        self.checkFill("child/a.txt")
+        self.checkFill("child/grandkid/a.txt")
+
+
+class IdirOdirRecurse(CFIdirBase):
+    def testCompile(self):
+        self.go("cheetah compile -R --idir SRC --odir DEST child")
+        self.checkSubdirPyInit("DEST/child")
+        self.checkSubdirPyInit("DEST/child/grandkid")
+        self.checkCompile("DEST/child/a.py")
+        self.checkCompile("DEST/child/grandkid/a.py")
+
+    def testFill(self):
+        self.go("cheetah fill -R --idir SRC --odir DEST child")
+        self.checkFill("DEST/child/a.html")
+        self.checkFill("DEST/child/grandkid/a.html")
+
+    def testText(self):
+        self.go("cheetah fill -R --idir SRC --odir DEST --oext txt child")
+        self.checkFill("DEST/child/a.txt")
+        self.checkFill("DEST/child/grandkid/a.txt")
+
+
+class IdirFlatRecurseCollision(CFIdirBase):
+    expectError = True
+
+    def testCompile(self):
+        self.assertSubprocess("cheetah compile -R --flat --idir SRC", nonzero=True)
+
+    def testFill(self):
+        self.assertSubprocess("cheetah fill -R --flat --idir SRC", nonzero=True)
+
+    def testText(self):
+        self.assertSubprocess("cheetah fill -R --flat --idir SRC --oext txt", nonzero=True)
+
+
+class NoBackup(CFBase):
+    """Run the command twice each time and verify a backup file is 
+       *not* created.
+    """
+    def testCompile(self):
+        self.go("cheetah compile --nobackup a.tmpl")
+        self.go("cheetah compile --nobackup a.tmpl")
+        self.checkNoBackup("a.py" + BACKUP_SUFFIX)
+
+    def testFill(self):
+        self.go("cheetah fill --nobackup a.tmpl")
+        self.go("cheetah fill --nobackup a.tmpl")
+        self.checkNoBackup("a.html" + BACKUP_SUFFIX)
+
+    def testText(self):
+        self.go("cheetah fill --nobackup --oext txt a.tmpl")
+        self.go("cheetah fill --nobackup --oext txt a.tmpl")
+        self.checkNoBackup("a.txt" + BACKUP_SUFFIX)
+
+def listTests(cheetahWrapperFile):
+    """cheetahWrapperFile, string, path of this script.
+
+       XXX TODO: don't print test where expectError is true.
+    """
+    rx = re.compile( R'self\.go\("(.*?)"\)' )
+    f = open(cheetahWrapperFile)
+    while True:
+        lin = f.readline()
+        if not lin:
+            break
+        m = rx.search(lin)
+        if m:
+            print(m.group(1))
+    f.close()
+
+def main():
+    global DELETE, OUTPUT
+    parser = OptionParser()
+    parser.add_option("--list", action="store", dest="listTests")
+    parser.add_option("--nodelete", action="store_true")
+    parser.add_option("--output", action="store_true")
+    # The following options are passed to unittest.
+    parser.add_option("-e", "--explain", action="store_true")
+    parser.add_option("-v", "--verbose", action="store_true")
+    parser.add_option("-q", "--quiet", action="store_true")
+    opts, files = parser.parse_args()
+    if opts.nodelete:
+        DELETE = False
+    if opts.output:
+        OUTPUT = True
+    if opts.listTests:
+        listTests(opts.listTests)
+    else:
+        # Eliminate script-specific command-line arguments to prevent
+        # errors in unittest.
+        del sys.argv[1:]
+        for opt in ("explain", "verbose", "quiet"):
+            if getattr(opts, opt):
+                sys.argv.append("--" + opt)
+        sys.argv.extend(files)
+        unittest.main()
+        
+if __name__ == '__main__':
+    main()
+
+# vim: sw=4 ts=4 expandtab
diff --git a/cheetah/Tests/Cheps.py b/cheetah/Tests/Cheps.py
new file mode 100644 (file)
index 0000000..7918435
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+import unittest
+
+import Cheetah
+import Cheetah.Parser
+import Cheetah.Template
+
+class Chep_2_Conditionalized_Import_Behavior(unittest.TestCase):
+    def test_ModuleLevelImport(self):
+        ''' Verify module level (traditional) import behavior '''
+        pass
+
+    def test_InlineImport(self):
+        ''' Verify (new) inline import behavior works '''
+        template = '''
+            #def funky($s)
+                #try
+                    #import urllib
+                #except ImportError
+                    #pass
+                #end try
+                #return urllib.quote($s)
+            #end def
+        '''
+        try:
+            template = Cheetah.Template.Template.compile(template)
+        except Cheetah.Parser.ParseError, ex:
+            self.fail('Failed to properly generate code %s' % ex)
+        template = template()
+        rc = tepmlate.funky('abc def')
+        assert rc == 'abc+def'
+
+    def test_LegacyMode(self):
+        ''' Verify disabling of CHEP #2 works '''
+        pass
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/Filters.py b/cheetah/Tests/Filters.py
new file mode 100644 (file)
index 0000000..65b3d93
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+import sys
+import unittest
+
+import Cheetah.Template
+import Cheetah.Filters
+
+majorVer, minorVer = sys.version_info[0], sys.version_info[1]
+versionTuple = (majorVer, minorVer)
+
+class BasicMarkdownFilterTest(unittest.TestCase):
+    '''
+        Test that our markdown filter works
+    '''
+    def test_BasicHeader(self):
+        template = '''  
+#from Cheetah.Filters import Markdown
+#transform Markdown
+$foo
+
+Header
+======
+        '''
+        expected = '''<p>bar</p>
+<h1>Header</h1>'''
+        try:
+            template = Cheetah.Template.Template(template, searchList=[{'foo' : 'bar'}])
+            template = str(template)
+            assert template == expected
+        except ImportError, ex:
+            print('>>> We probably failed to import markdown, bummer %s' % ex)
+            return
+        except Exception, ex:
+            if ex.__class__.__name__ == 'MarkdownException' and majorVer == 2 and minorVer < 5:
+                print('>>> NOTE: Support for the Markdown filter will be broken for you. Markdown says: %s' % ex)
+                return
+            raise
+
+
+class BasicCodeHighlighterFilterTest(unittest.TestCase):
+    '''
+        Test that our code highlighter filter works
+    '''
+    def test_Python(self):
+        template = '''  
+#from Cheetah.Filters import CodeHighlighter
+#transform CodeHighlighter
+
+def foo(self):
+    return '$foo'
+        '''
+        template = Cheetah.Template.Template(template, searchList=[{'foo' : 'bar'}])
+        template = str(template)
+        assert template, (template, 'We should have some content here...')
+
+    def test_Html(self):
+        template = '''  
+#from Cheetah.Filters import CodeHighlighter
+#transform CodeHighlighter
+
+<html><head></head><body>$foo</body></html>
+        '''
+        template = Cheetah.Template.Template(template, searchList=[{'foo' : 'bar'}])
+        template = str(template)
+        assert template, (template, 'We should have some content here...')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/Misc.py b/cheetah/Tests/Misc.py
new file mode 100644 (file)
index 0000000..9ea66f0
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import unittest
+
+from Cheetah import SettingsManager
+
+
+class SettingsManagerTests(unittest.TestCase):
+    def test_mergeDictionaries(self):
+        left = {'foo' : 'bar', 'abc' : {'a' : 1, 'b' : 2, 'c' : (3,)}}
+        right = {'xyz' : (10, 9)}
+        expect = {'xyz': (10, 9), 'foo': 'bar', 'abc': {'a': 1, 'c': (3,), 'b': 2}}
+
+        result = SettingsManager.mergeNestedDictionaries(left, right)
+        self.assertEquals(result, expect)
+
+
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/cheetah/Tests/NameMapper.py b/cheetah/Tests/NameMapper.py
new file mode 100644 (file)
index 0000000..c44fb4c
--- /dev/null
@@ -0,0 +1,548 @@
+#!/usr/bin/env python
+
+import sys
+import types
+import os
+import os.path
+
+import unittest
+from Cheetah.NameMapper import NotFound, valueForKey, \
+     valueForName, valueFromSearchList, valueFromFrame, valueFromFrameOrSearchList
+
+
+class DummyClass(object):
+    classVar1 = 123
+
+    def __init__(self):
+        self.instanceVar1 = 123
+        
+    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"):
+        raise ValueError
+
+    def meth3(self):
+        """Tests a bug that Jeff Johnson reported on Oct 1, 2001"""
+        
+        x = 'A string'
+        try:
+            for i in [1, 2, 3, 4]:
+                if x == 2:     
+                    pass
+                
+                if x == 'xx':
+                    pass
+            return x
+        except:
+            raise
+
+class DummyClassGetAttrRaises(object):
+    def __getattr__(self, name):
+        raise ValueError
+
+
+def dummyFunc(arg="Scooby"):
+    return arg
+
+def funcThatRaises():
+    raise ValueError
+
+                 
+testNamespace = {
+    'aStr': 'blarg',
+    'anInt': 1,
+    'aFloat': 1.5,
+    'aDict': {'one': 'item1',
+              'two': 'item2',
+              'nestedDict': {'one': 'nestedItem1',
+                            'two': 'nestedItem2',
+                            'funcThatRaises': funcThatRaises,
+                            'aClass': DummyClass,
+                            },
+              'nestedFunc': dummyFunc,
+              },
+    'aClass': DummyClass,    
+    'aFunc': dummyFunc,
+    'anObj': DummyClass(),
+    'anObjThatRaises': DummyClassGetAttrRaises(),
+    'aMeth': DummyClass().meth1,
+    'none': None,  
+    'emptyString': '',
+    'funcThatRaises': funcThatRaises,
+    }
+    
+autoCallResults = {'aFunc': 'Scooby',
+                   'aMeth': 'doo',
+                   }
+
+results = testNamespace.copy()
+results.update({'anObj.meth1': 'doo',
+                'aDict.one': 'item1',
+                'aDict.nestedDict': testNamespace['aDict']['nestedDict'],
+                'aDict.nestedDict.one': 'nestedItem1',
+                'aDict.nestedDict.aClass': DummyClass,
+                'aDict.nestedFunc': 'Scooby',
+                'aClass.classVar1': 123,
+                'anObj.instanceVar1': 123,
+                'anObj.meth3': 'A string',
+                })
+
+for k in testNamespace.keys():
+    # put them in the globals for the valueFromFrame tests
+    exec('%s = testNamespace[k]'%k)
+
+##################################################
+## TEST BASE CLASSES
+
+class NameMapperTest(unittest.TestCase):
+    failureException = NotFound
+    _testNamespace = testNamespace
+    _results = results
+    
+    def namespace(self):
+        return self._testNamespace
+
+    def VFN(self, name, autocall=True):
+        return valueForName(self.namespace(), name, autocall)
+
+    def VFS(self, searchList, name, autocall=True):
+        return valueFromSearchList(searchList, name, autocall)
+
+    
+    # alias to be overriden later
+    get = VFN
+
+    def check(self, name):
+        got = self.get(name)
+        if name in autoCallResults:
+            expected = autoCallResults[name]
+        else:
+            expected = self._results[name]
+        assert got == expected
+        
+
+##################################################
+## TEST CASE CLASSES
+
+class VFN(NameMapperTest):
+
+    def test1(self):
+        """string in dict lookup"""
+        self.check('aStr')
+
+    def test2(self):
+        """string in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aStr')
+            
+    def test3(self):
+        """int in dict lookup"""
+        self.check('anInt')
+
+    def test4(self):
+        """int in dict lookup in a loop"""
+        for i in range(10):
+            self.check('anInt')
+
+    def test5(self):
+        """float in dict lookup"""
+        self.check('aFloat')
+
+    def test6(self):
+        """float in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aFloat')
+          
+    def test7(self):
+        """class in dict lookup"""
+        self.check('aClass')
+
+    def test8(self):
+        """class in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aClass')
+            
+    def test9(self):
+        """aFunc in dict lookup"""
+        self.check('aFunc')
+
+    def test10(self):
+        """aFunc in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aFunc')
+
+    def test11(self):
+        """aMeth in dict lookup"""
+        self.check('aMeth')
+
+    def test12(self):
+        """aMeth in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aMeth')
+
+    def test13(self):
+        """aMeth in dict lookup"""
+        self.check('aMeth')
+
+    def test14(self):
+        """aMeth in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aMeth')
+
+    def test15(self):
+        """anObj in dict lookup"""
+        self.check('anObj')
+
+    def test16(self):
+        """anObj in dict lookup in a loop"""
+        for i in range(10):
+            self.check('anObj')
+
+    def test17(self):
+        """aDict in dict lookup"""
+        self.check('aDict')
+
+    def test18(self):
+        """aDict in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict')
+
+    def test17(self):
+        """aDict in dict lookup"""
+        self.check('aDict')
+
+    def test18(self):
+        """aDict in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict')
+
+    def test19(self):
+        """aClass.classVar1 in dict lookup"""
+        self.check('aClass.classVar1')
+
+    def test20(self):
+        """aClass.classVar1 in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aClass.classVar1')
+
+
+    def test23(self):
+        """anObj.instanceVar1 in dict lookup"""
+        self.check('anObj.instanceVar1')
+
+    def test24(self):
+        """anObj.instanceVar1 in dict lookup in a loop"""
+        for i in range(10):
+            self.check('anObj.instanceVar1')
+
+    ## tests 22, 25, and 26 removed when the underscored lookup was removed
+
+    def test27(self):
+        """anObj.meth1 in dict lookup"""
+        self.check('anObj.meth1')
+
+    def test28(self):
+        """anObj.meth1 in dict lookup in a loop"""
+        for i in range(10):
+            self.check('anObj.meth1')
+
+    def test29(self):
+        """aDict.one in dict lookup"""
+        self.check('aDict.one')
+
+    def test30(self):
+        """aDict.one in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict.one')
+
+    def test31(self):
+        """aDict.nestedDict in dict lookup"""
+        self.check('aDict.nestedDict')
+
+    def test32(self):
+        """aDict.nestedDict in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict.nestedDict')
+            
+    def test33(self):
+        """aDict.nestedDict.one in dict lookup"""
+        self.check('aDict.nestedDict.one')
+
+    def test34(self):
+        """aDict.nestedDict.one in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict.nestedDict.one')
+            
+    def test35(self):
+        """aDict.nestedFunc in dict lookup"""
+        self.check('aDict.nestedFunc')
+
+    def test36(self):
+        """aDict.nestedFunc in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict.nestedFunc')
+
+    def test37(self):
+        """aDict.nestedFunc in dict lookup - without autocalling"""
+        assert self.get('aDict.nestedFunc', False) == dummyFunc
+
+    def test38(self):
+        """aDict.nestedFunc in dict lookup in a loop - without autocalling"""
+        for i in range(10):
+            assert self.get('aDict.nestedFunc', False) == dummyFunc
+
+    def test39(self):
+        """aMeth in dict lookup - without autocalling"""
+        assert self.get('aMeth', False) == self.namespace()['aMeth']
+
+    def test40(self):
+        """aMeth in dict lookup in a loop - without autocalling"""
+        for i in range(10):
+            assert self.get('aMeth', False) == self.namespace()['aMeth']
+
+    def test41(self):
+        """anObj.meth3 in dict lookup"""
+        self.check('anObj.meth3')
+
+    def test42(self):
+        """aMeth in dict lookup in a loop"""
+        for i in range(10):
+            self.check('anObj.meth3')
+
+    def test43(self):
+        """NotFound test"""
+
+        def test(self=self):
+            self.get('anObj.methX')    
+        self.assertRaises(NotFound, test)
+        
+    def test44(self):
+        """NotFound test in a loop"""
+        def test(self=self):
+            self.get('anObj.methX')    
+
+        for i in range(10):
+            self.assertRaises(NotFound, test)
+            
+    def test45(self):
+        """Other exception from meth test"""
+
+        def test(self=self):
+            self.get('anObj.meth2')    
+        self.assertRaises(ValueError, test)
+        
+    def test46(self):
+        """Other exception from meth test in a loop"""
+        def test(self=self):
+            self.get('anObj.meth2')    
+
+        for i in range(10):
+            self.assertRaises(ValueError, test)
+
+    def test47(self):
+        """None in dict lookup"""
+        self.check('none')
+
+    def test48(self):
+        """None in dict lookup in a loop"""
+        for i in range(10):
+            self.check('none')
+            
+    def test49(self):
+        """EmptyString in dict lookup"""
+        self.check('emptyString')
+
+    def test50(self):
+        """EmptyString in dict lookup in a loop"""
+        for i in range(10):
+            self.check('emptyString')
+
+    def test51(self):
+        """Other exception from func test"""
+
+        def test(self=self):
+            self.get('funcThatRaises')    
+        self.assertRaises(ValueError, test)
+        
+    def test52(self):
+        """Other exception from func test in a loop"""
+        def test(self=self):
+            self.get('funcThatRaises')    
+
+        for i in range(10):
+            self.assertRaises(ValueError, test)
+
+
+    def test53(self):
+        """Other exception from func test"""
+
+        def test(self=self):
+            self.get('aDict.nestedDict.funcThatRaises')    
+        self.assertRaises(ValueError, test)
+        
+    def test54(self):
+        """Other exception from func test in a loop"""
+        def test(self=self):
+            self.get('aDict.nestedDict.funcThatRaises')    
+
+        for i in range(10):
+            self.assertRaises(ValueError, test)
+
+    def test55(self):
+        """aDict.nestedDict.aClass in dict lookup"""
+        self.check('aDict.nestedDict.aClass')
+
+    def test56(self):
+        """aDict.nestedDict.aClass in dict lookup in a loop"""
+        for i in range(10):
+            self.check('aDict.nestedDict.aClass')
+
+    def test57(self):
+        """aDict.nestedDict.aClass in dict lookup - without autocalling"""
+        assert self.get('aDict.nestedDict.aClass', False) == DummyClass
+
+    def test58(self):
+        """aDict.nestedDict.aClass in dict lookup in a loop - without autocalling"""
+        for i in range(10):
+            assert self.get('aDict.nestedDict.aClass', False) == DummyClass
+
+    def test59(self):
+        """Other exception from func test -- but without autocalling shouldn't raise"""
+
+        self.get('aDict.nestedDict.funcThatRaises', False)    
+        
+    def test60(self):
+        """Other exception from func test in a loop -- but without autocalling shouldn't raise"""
+
+        for i in range(10):
+            self.get('aDict.nestedDict.funcThatRaises', False)    
+
+    def test61(self):
+        """Accessing attribute where __getattr__ raises shouldn't segfault if something follows it"""
+
+        def test(self=self):
+            self.get('anObjThatRaises.willraise.anything')
+        self.assertRaises(ValueError, test)
+
+
+class VFS(VFN):
+    _searchListLength = 1
+    
+    def searchList(self):
+        lng = self._searchListLength
+        if lng == 1:
+            return [self.namespace()]
+        elif lng == 2:
+            return [self.namespace(), {'dummy':1234}]
+        elif lng == 3:
+            # a tuple for kicks
+            return ({'dummy':1234}, self.namespace(), {'dummy':1234})
+        elif lng == 4:
+            # a generator for more kicks
+            return self.searchListGenerator()
+
+    def searchListGenerator(self):
+        class Test:
+            pass
+        for i in [Test(), {'dummy':1234}, self.namespace(), {'dummy':1234}]:
+            yield i
+  
+    def get(self, name, autocall=True):
+        return self.VFS(self.searchList(), name, autocall)
+        
+class VFS_2namespaces(VFS):
+    _searchListLength = 2
+    
+class VFS_3namespaces(VFS):
+    _searchListLength = 3
+
+class VFS_4namespaces(VFS):
+    _searchListLength = 4
+    
+class VFF(VFN): 
+    def get(self, name, autocall=True):
+        ns = self._testNamespace
+        aStr = ns['aStr'] 
+        aFloat = ns['aFloat']
+        none = 'some'
+        return valueFromFrame(name, autocall)
+
+    def setUp(self):
+        """Mod some of the data
+        """
+        self._testNamespace = ns = self._testNamespace.copy()
+        self._results = res = self._results.copy()
+        ns['aStr'] = res['aStr'] = 'BLARG'
+        ns['aFloat'] = res['aFloat'] = 0.1234
+        res['none'] = 'some'
+        res['True'] = True
+        res['False'] = False
+        res['None'] = None
+        res['eval'] = eval
+
+    def test_VFF_1(self):
+        """Builtins"""
+        self.check('True')
+        self.check('None')
+        self.check('False')
+        assert self.get('eval', False)==eval
+        assert self.get('range', False)==range
+
+class VFFSL(VFS):
+    _searchListLength = 1
+
+    def setUp(self):
+        """Mod some of the data
+        """
+        self._testNamespace = ns = self._testNamespace.copy()
+        self._results = res = self._results.copy()
+        ns['aStr'] = res['aStr'] = 'BLARG'
+        ns['aFloat'] = res['aFloat'] = 0.1234
+        res['none'] = 'some'
+        
+        del ns['anInt'] # will be picked up by globals
+        
+    def VFFSL(self, searchList, name, autocall=True):
+        anInt = 1
+        none = 'some'
+        return valueFromFrameOrSearchList(searchList, name, autocall)
+    
+    def get(self, name, autocall=True):
+        return self.VFFSL(self.searchList(), name, autocall)
+
+class VFFSL_2(VFFSL):
+    _searchListLength = 2
+
+class VFFSL_3(VFFSL):
+    _searchListLength = 3
+
+class VFFSL_4(VFFSL):
+    _searchListLength = 4
+
+if sys.platform.startswith('java'):
+    del VFF, VFFSL, VFFSL_2, VFFSL_3, VFFSL_4
+
+
+class MapBuiltins(unittest.TestCase):
+    def test_int(self):
+        from Cheetah.Template import Template
+        t = Template('''
+            #def intify(val)
+                #return $int(val)
+            #end def''', compilerSettings={'useStackFrames' : False})
+        self.assertEquals(5, t.intify('5'))
+
+
+
+##################################################
+## if run from the command line ##
+        
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/Parser.py b/cheetah/Tests/Parser.py
new file mode 100644 (file)
index 0000000..050b613
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import unittest
+
+from Cheetah import Parser
+
+class ArgListTest(unittest.TestCase):
+    def setUp(self):
+        super(ArgListTest, self).setUp()
+        self.al = Parser.ArgList()
+
+    def test_merge1(self):
+        ''' 
+            Testing the ArgList case results from Template.Preprocessors.test_complexUsage 
+        '''
+        self.al.add_argument('arg')
+        expect = [('arg', None)]
+
+        self.assertEquals(expect, self.al.merge())
+
+    def test_merge2(self):
+        '''
+            Testing the ArgList case results from SyntaxAndOutput.BlockDirective.test4
+        '''
+        self.al.add_argument('a')
+        self.al.add_default('999')
+        self.al.next()
+        self.al.add_argument('b')
+        self.al.add_default('444')
+
+        expect = [(u'a', u'999'), (u'b', u'444')]
+
+        self.assertEquals(expect, self.al.merge())
+
+
+
+    def test_merge3(self):
+        '''
+            Testing the ArgList case results from SyntaxAndOutput.BlockDirective.test13
+        '''
+        self.al.add_argument('arg')
+        self.al.add_default("'This is my block'")
+        expect = [('arg', "'This is my block'")]
+
+        self.assertEquals(expect, self.al.merge())
+
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/cheetah/Tests/Performance.py b/cheetah/Tests/Performance.py
new file mode 100644 (file)
index 0000000..d76cc00
--- /dev/null
@@ -0,0 +1,243 @@
+#!/usr/bin/env python
+
+import hotshot
+import hotshot.stats
+import os
+import sys
+import unittest
+
+from test import pystone
+import time
+
+import Cheetah.NameMapper 
+import Cheetah.Template
+
+# This can be turned on with the `--debug` flag when running the test
+# and will cause the tests to all just dump out how long they took
+# insteasd of asserting on duration
+DEBUG = False
+
+# TOLERANCE in Pystones
+kPS = 1000
+TOLERANCE = 0.5*kPS 
+
+class DurationError(AssertionError):
+    pass
+
+_pystone_calibration_mark = None
+def _pystone_calibration():
+    global _pystone_calibration_mark
+    if not _pystone_calibration_mark:
+        _pystone_calibration_mark = pystone.pystones(loops=pystone.LOOPS)
+    return _pystone_calibration_mark
+
+def perftest(max_num_pystones, current_pystone=None):
+    '''
+        Performance test decorator based off the 'timedtest' 
+        decorator found in this Active State recipe:
+            http://code.activestate.com/recipes/440700/
+    '''
+    if not isinstance(max_num_pystones, float):
+        max_num_pystones = float(max_num_pystones)
+
+    if not current_pystone:
+        current_pystone = _pystone_calibration()
+
+    def _test(function):
+        def wrapper(*args, **kw):
+            start_time = time.time()
+            try:
+                return function(*args, **kw)
+            finally:
+                total_time = time.time() - start_time
+                if total_time == 0:
+                    pystone_total_time = 0
+                else:
+                    pystone_rate = current_pystone[0] / current_pystone[1]
+                    pystone_total_time = total_time / pystone_rate
+                global DEBUG
+                if DEBUG:
+                    print('The test "%s" took: %s pystones' % (function.func_name,
+                        pystone_total_time))
+                else:
+                    if pystone_total_time > (max_num_pystones + TOLERANCE):
+                        raise DurationError((('Test too long (%.2f Ps, '
+                                        'need at most %.2f Ps)')
+                                        % (pystone_total_time,
+                                            max_num_pystones)))
+        return wrapper
+    return _test
+
+
+class DynamicTemplatePerformanceTest(unittest.TestCase):
+    loops = 10
+    #@perftest(1200)
+    def test_BasicDynamic(self):
+        template = '''
+            #def foo(arg1, arg2)
+                #pass
+            #end def
+        '''
+        for i in range(self.loops):
+            klass = Cheetah.Template.Template.compile(template)
+            assert klass
+    test_BasicDynamic = perftest(1200)(test_BasicDynamic)
+
+class PerformanceTest(unittest.TestCase):
+    iterations = 100000
+    display = False
+    save = False
+
+    def runTest(self):
+        self.prof = hotshot.Profile('%s.prof' % self.__class__.__name__)
+        self.prof.start()
+        for i in range(self.iterations):
+            if hasattr(self, 'performanceSample'):
+                self.display = True
+                self.performanceSample()
+        self.prof.stop()
+        self.prof.close()
+        if self.display:
+            print('>>> %s (%d iterations) ' % (self.__class__.__name__,
+                    self.iterations))
+            stats = hotshot.stats.load('%s.prof' % self.__class__.__name__)
+            #stats.strip_dirs()
+            stats.sort_stats('time', 'calls')
+            stats.print_stats(50)
+
+        if not self.save:
+            os.unlink('%s.prof' % self.__class__.__name__)
+
+class DynamicMethodCompilationTest(PerformanceTest):
+    def performanceSample(self):
+        template = '''
+            #import sys
+            #import os
+            #def testMethod()
+                #set foo = [1, 2, 3, 4]
+                #return $foo[0]
+            #end def
+        '''
+        template = Cheetah.Template.Template.compile(template, 
+            keepRefToGeneratedCode=False)
+        template = template()
+        value = template.testMethod()
+
+
+class BunchOfWriteCalls(PerformanceTest):
+    iterations = 1000
+    def performanceSample(self):
+        template = '''
+            #import sys
+            #import os
+            #for i in range(1000)
+                $i
+            #end for
+        '''
+        template = Cheetah.Template.Template.compile(template, 
+            keepRefToGeneratedCode=False)
+        template = template()
+        value = template.respond()
+        del value
+
+class DynamicSimpleCompilationTest(PerformanceTest):
+    def performanceSample(self):
+        template = '''
+            #import sys
+            #import os
+            #set foo = [1,2,3,4]
+
+            Well hello there! This is basic.
+
+            Here's an array too: $foo
+        '''
+        template = Cheetah.Template.Template.compile(template, 
+            keepRefToGeneratedCode=False)
+        template = template()
+        template = unicode(template)
+
+
+class FilterTest(PerformanceTest):
+    template = None
+    def setUp(self):
+        super(FilterTest, self).setUp()
+        template = '''
+            #import sys
+            #import os
+            #set foo = [1, 2, 3, 4]
+
+            $foo, $foo, $foo
+        '''
+        template = Cheetah.Template.Template.compile(template, 
+            keepRefToGeneratedCode=False)
+        self.template = template()
+
+    def performanceSample(self):
+        value = unicode(self.template)
+
+
+class LongCompileTest(PerformanceTest):
+    ''' Test the compilation on a sufficiently large template '''
+    def compile(self, template):
+        return Cheetah.Template.Template.compile(template, keepRefToGeneratedCode=False)
+
+    def performanceSample(self):
+        template = '''
+            #import sys
+            #import Cheetah.Template
+
+            #extends Cheetah.Template.Template
+
+            #def header()
+                <center><h2>This is my header</h2></center>
+            #end def
+            
+            #def footer()
+                #return "Huzzah"
+            #end def
+
+            #def scripts()
+                #pass
+            #end def
+
+            #def respond()
+                <html>
+                    <head>
+                        <title>${title}</title>
+                        
+                        $scripts()
+                    </head>
+                    <body>
+                        $header()
+
+                        #for $i in $range(10)
+                            This is just some stupid page!
+                            <br/>
+                        #end for
+
+                        <br/>
+                        $footer()
+                    </body>
+                    </html>
+            #end def
+            
+        '''
+        return self.compile(template)
+
+class LongCompile_CompilerSettingsTest(LongCompileTest):
+    def compile(self, template):
+        return Cheetah.Template.Template.compile(template, keepRefToGeneratedCode=False,
+            compilerSettings={'useStackFrames' : True, 'useAutocalling' : True})
+
+class LongCompileAndRun(LongCompileTest):
+    def performanceSample(self):
+        template = super(LongCompileAndRun, self).performanceSample()
+        template = template(searchList=[{'title' : 'foo'}])
+        template = template.respond()
+            
+
+if __name__ == '__main__':
+    if '--debug' in sys.argv:
+        DEBUG = True
+        sys.argv = [arg for arg in sys.argv if not arg == '--debug']
+    unittest.main()
diff --git a/cheetah/Tests/Regressions.py b/cheetah/Tests/Regressions.py
new file mode 100644 (file)
index 0000000..67a736a
--- /dev/null
@@ -0,0 +1,247 @@
+#!/usr/bin/env python
+
+import Cheetah.NameMapper 
+import Cheetah.Template
+
+import sys
+import unittest
+
+
+majorVer, minorVer = sys.version_info[0], sys.version_info[1]
+versionTuple = (majorVer, minorVer)
+
+def isPython23():
+    ''' Python 2.3 is still supported by Cheetah, but doesn't support decorators '''
+    return majorVer == 2 and minorVer < 4
+
+class GetAttrException(Exception):
+    pass
+
+class CustomGetAttrClass(object):
+    def __getattr__(self, name):
+        raise GetAttrException('FAIL, %s' % name)
+
+class GetAttrTest(unittest.TestCase):
+    '''
+        Test for an issue occurring when __getatttr__() raises an exception
+        causing NameMapper to raise a NotFound exception
+    '''
+    def test_ValidException(self):
+        o = CustomGetAttrClass()
+        try:
+            print(o.attr)
+        except GetAttrException, e:
+            # expected
+            return
+        except:
+            self.fail('Invalid exception raised: %s' % e)
+        self.fail('Should have had an exception raised')
+
+    def test_NotFoundException(self):
+        template = '''
+            #def raiseme()
+                $obj.attr
+            #end def'''
+
+        template = Cheetah.Template.Template.compile(template, compilerSettings={}, keepRefToGeneratedCode=True)
+        template = template(searchList=[{'obj' : CustomGetAttrClass()}])
+        assert template, 'We should have a valid template object by now'
+
+        self.failUnlessRaises(GetAttrException, template.raiseme)
+
+
+class InlineImportTest(unittest.TestCase):
+    def test_FromFooImportThing(self):
+        '''
+            Verify that a bug introduced in v2.1.0 where an inline:
+                #from module import class
+            would result in the following code being generated:
+                import class
+        '''
+        template = '''
+            #def myfunction()
+                #if True
+                    #from os import path
+                    #return 17
+                    Hello!
+                #end if
+            #end def
+        '''
+        template = Cheetah.Template.Template.compile(template, compilerSettings={'useLegacyImportMode' : False}, keepRefToGeneratedCode=True)
+        template = template(searchList=[{}])
+
+        assert template, 'We should have a valid template object by now'
+
+        rc = template.myfunction()
+        assert rc == 17, (template, 'Didn\'t get a proper return value')
+
+    def test_ImportFailModule(self):
+        template = '''
+            #try
+                #import invalidmodule
+            #except
+                #set invalidmodule = dict(FOO='BAR!')
+            #end try
+
+            $invalidmodule.FOO
+        '''
+        template = Cheetah.Template.Template.compile(template, compilerSettings={'useLegacyImportMode' : False}, keepRefToGeneratedCode=True)
+        template = template(searchList=[{}])
+
+        assert template, 'We should have a valid template object by now'
+        assert str(template), 'We weren\'t able to properly generate the result from the template'
+
+    def test_ProperImportOfBadModule(self):
+        template = '''
+            #from invalid import fail
+                
+            This should totally $fail
+        '''
+        self.failUnlessRaises(ImportError, Cheetah.Template.Template.compile, template, compilerSettings={'useLegacyImportMode' : False}, keepRefToGeneratedCode=True)
+
+    def test_AutoImporting(self):
+        template = '''
+            #extends FakeyTemplate
+
+            Boo!
+        '''
+        self.failUnlessRaises(ImportError, Cheetah.Template.Template.compile, template)
+
+    def test_StuffBeforeImport_Legacy(self):
+        template = '''
+###
+### I like comments before import
+###
+#extends Foo
+Bar
+'''
+        self.failUnlessRaises(ImportError, Cheetah.Template.Template.compile, template, compilerSettings={'useLegacyImportMode' : True}, keepRefToGeneratedCode=True)
+
+
+class Mantis_Issue_11_Regression_Test(unittest.TestCase):
+    ''' 
+        Test case for bug outlined in Mantis issue #11:
+            
+        Output:
+        Traceback (most recent call last):
+          File "test.py", line 12, in <module>
+            t.respond()
+          File "DynamicallyCompiledCheetahTemplate.py", line 86, in respond
+          File "/usr/lib64/python2.6/cgi.py", line 1035, in escape
+            s = s.replace("&", "&") # Must be done first! 
+    '''
+    def test_FailingBehavior(self):
+        import cgi
+        template = Cheetah.Template.Template("$escape($request)", searchList=[{'escape' : cgi.escape, 'request' : 'foobar'}])
+        assert template
+        self.failUnlessRaises(AttributeError, template.respond)
+
+
+    def test_FailingBehaviorWithSetting(self):
+        import cgi
+        template = Cheetah.Template.Template("$escape($request)", 
+                searchList=[{'escape' : cgi.escape, 'request' : 'foobar'}], 
+                compilerSettings={'prioritizeSearchListOverSelf' : True})
+        assert template
+        assert template.respond()
+
+class Mantis_Issue_21_Regression_Test(unittest.TestCase):
+    ''' 
+        Test case for bug outlined in issue #21
+
+        Effectively @staticmethod and @classmethod
+        decorated methods in templates don't 
+        properly define the _filter local, which breaks
+        when using the NameMapper
+    '''
+    def runTest(self):
+        if isPython23():
+            return
+        template = '''
+            #@staticmethod
+            #def testMethod()
+                This is my $output
+            #end def
+        '''
+        template = Cheetah.Template.Template.compile(template)
+        assert template
+        assert template.testMethod(output='bug') # raises a NameError: global name '_filter' is not defined
+
+
+class Mantis_Issue_22_Regression_Test(unittest.TestCase):
+    ''' 
+        Test case for bug outlined in issue #22
+
+        When using @staticmethod and @classmethod
+        in conjunction with the #filter directive
+        the generated code for the #filter is reliant
+        on the `self` local, breaking the function
+    '''
+    def test_NoneFilter(self):
+        # XXX: Disabling this test for now
+        return
+        if isPython23():
+            return
+        template = '''
+            #@staticmethod
+            #def testMethod()
+                #filter None
+                    This is my $output
+                #end filter
+            #end def
+        '''
+        template = Cheetah.Template.Template.compile(template)
+        assert template
+        assert template.testMethod(output='bug')
+
+    def test_DefinedFilter(self):
+        # XXX: Disabling this test for now
+        return
+        if isPython23():
+            return
+        template = '''
+            #@staticmethod
+            #def testMethod()
+                #filter Filter
+                    This is my $output
+                #end filter
+            #end def
+        '''
+        # The generated code for the template's testMethod() should look something
+        # like this in the 'error' case:
+        '''
+        @staticmethod
+        def testMethod(**KWS):
+            ## CHEETAH: generated from #def testMethod() at line 3, col 13.
+            trans = DummyTransaction()
+            _dummyTrans = True
+            write = trans.response().write
+            SL = [KWS]
+            _filter = lambda x, **kwargs: unicode(x)
+
+            ########################################
+            ## START - generated method body
+
+            _orig_filter_18517345 = _filter
+            filterName = u'Filter'
+            if self._CHEETAH__filters.has_key("Filter"):
+                _filter = self._CHEETAH__currentFilter = self._CHEETAH__filters[filterName]
+            else:
+                _filter = self._CHEETAH__currentFilter = \
+                            self._CHEETAH__filters[filterName] = getattr(self._CHEETAH__filtersLib, filterName)(self).filter
+            write(u'                    This is my ')
+            _v = VFFSL(SL,"output",True) # u'$output' on line 5, col 32
+            if _v is not None: write(_filter(_v, rawExpr=u'$output')) # from line 5, col 32.
+
+            ########################################
+            ## END - generated method body
+
+            return _dummyTrans and trans.response().getvalue() or ""
+        '''
+        template = Cheetah.Template.Template.compile(template)
+        assert template
+        assert template.testMethod(output='bug')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/SyntaxAndOutput.py b/cheetah/Tests/SyntaxAndOutput.py
new file mode 100644 (file)
index 0000000..80e2b3f
--- /dev/null
@@ -0,0 +1,3253 @@
+#!/usr/bin/env python
+# -*- coding: latin-1 -*-
+
+'''
+Syntax and Output tests.
+
+TODO
+- #finally
+- #filter
+- #errorCatcher
+- #echo
+- #silent
+'''
+
+
+##################################################
+## DEPENDENCIES ##
+
+import sys
+import types
+import re
+from copy import deepcopy
+import os
+import os.path
+import warnings
+import unittest
+
+from Cheetah.NameMapper import NotFound
+from Cheetah.NameMapper import C_VERSION as NameMapper_C_VERSION
+from Cheetah.Template import Template
+from Cheetah.Parser import ParseError
+from Cheetah.Compiler import Compiler, DEFAULT_COMPILER_SETTINGS
+
+class Unspecified(object):
+    pass
+
+majorVer, minorVer = sys.version_info[0], sys.version_info[1]
+versionTuple = (majorVer, minorVer)
+
+def testdecorator(func):
+    return func
+
+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 methWithPercentSignDefaultArg(self, arg1="110%"):
+        return str(arg1)
+
+    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,
+    'aStrToBeIncluded': "$aStr $anInt",
+    'none': None,  
+    'emptyString': '',
+    'numOne': 1,
+    'numTwo': 2,
+    'zero': 0,
+    'tenDigits': 1234567890,
+    'webSafeTest': 'abc <=> &',
+    'strip1': '  \t   strippable whitespace   \t\t  \n',
+    'strip2': '  \t   strippable whitespace   \t\t  ',
+    'strip3': '  \t   strippable whitespace   \t\t\n1 2  3\n',
+    
+    'blockToBeParsed': """$numOne $numTwo""",
+    'includeBlock2': """$numOne $numTwo $aSetVar""",
+    
+    'includeFileName': 'parseTest.txt',
+    'listOfLambdas': [lambda x: x, lambda x: x, lambda x: x,],
+    'list': [
+       {'index': 0, 'numOne': 1, 'numTwo': 2},
+       {'index': 1, 'numOne': 1, 'numTwo': 2},
+        ],
+    'nameList': [('john', 'doe'), ('jane', 'smith')],
+    'letterList': ['a', 'b', 'c'],
+    '_': lambda x: 'Translated: ' + x,
+    'unicodeData': u'aoeu12345\u1234',
+    }
+
+
+##################################################
+## TEST BASE CLASSES
+
+class OutputTest(unittest.TestCase):
+    report = '''
+Template output mismatch: 
+
+    Input Template =
+%(template)s%(end)s
+
+    Expected Output =
+%(expected)s%(end)s
+
+    Actual Output =
+%(actual)s%(end)s'''
+
+    convertEOLs = True
+    _EOLreplacement = None
+    _debugEOLReplacement = False
+
+    DEBUGLEV = 0
+    _searchList = [defaultTestNameSpace]
+
+    _useNewStyleCompilation = True
+    #_useNewStyleCompilation = False
+
+    _extraCompileKwArgs = None
+
+    def searchList(self):
+        return self._searchList
+
+    def verify(self, input, expectedOutput,
+               inputEncoding=None,
+               outputEncoding=None,
+               convertEOLs=Unspecified):
+        if self._EOLreplacement:
+            if convertEOLs is Unspecified:
+                convertEOLs = self.convertEOLs
+            if convertEOLs:
+                input = input.replace('\n', self._EOLreplacement)
+                expectedOutput = expectedOutput.replace('\n', self._EOLreplacement)
+
+        self._input = input
+        if self._useNewStyleCompilation:
+            extraKwArgs = self._extraCompileKwArgs or {}
+            
+            templateClass = Template.compile(
+                source=input,
+                compilerSettings=self._getCompilerSettings(),
+                keepRefToGeneratedCode=True,
+                **extraKwArgs
+                )
+            moduleCode = templateClass._CHEETAH_generatedModuleCode
+            searchList = self.searchList() or self._searchList
+            self.template = templateObj = templateClass(searchList=searchList)
+        else:
+            self.template = templateObj = Template(
+                input,
+                searchList=self.searchList(),
+                compilerSettings=self._getCompilerSettings(),
+                )
+            moduleCode = templateObj._CHEETAH_generatedModuleCode
+        if self.DEBUGLEV >= 1:
+            print(moduleCode)
+        try:
+            output = templateObj.respond() # rather than __str__, because of unicode
+            assert output==expectedOutput, self._outputMismatchReport(output, expectedOutput)
+        finally:
+            templateObj.shutdown()
+
+    def _getCompilerSettings(self):
+        return {}
+            
+    def _outputMismatchReport(self, output, expectedOutput):
+        if self._debugEOLReplacement and self._EOLreplacement:
+            EOLrepl = self._EOLreplacement
+            marker = '*EOL*'
+            return self.report % {'template': self._input.replace(EOLrepl, marker),
+                                  'expected': expectedOutput.replace(EOLrepl, marker),
+                                  'actual': output.replace(EOLrepl, marker),
+                                  'end': '(end)'}
+        else:
+            return self.report % {'template': self._input,
+                                  'expected': expectedOutput,
+                                  'actual': output,
+                                  'end': '(end)'}
+        
+    def genClassCode(self):
+        if hasattr(self, 'template'):
+            return self.template.generatedClassCode()
+
+    def genModuleCode(self):
+        if hasattr(self, 'template'):
+            return self.template.generatedModuleCode()
+
+##################################################
+## TEST CASE CLASSES
+
+class EmptyTemplate(OutputTest):
+    convertEOLs = False
+    def test1(self):
+        """an empty string for the template"""
+        
+        warnings.filterwarnings('error',
+                                'You supplied an empty string for the source!',
+                                UserWarning)
+        try:
+            self.verify("", "")
+        except UserWarning:
+            pass
+        else:
+            self.fail("Should warn about empty source strings.")
+        
+        try:
+            self.verify("#implements foo", "")
+        except NotImplementedError:
+            pass
+        else:
+            self.fail("This should barf about respond() not being implemented.")
+
+        self.verify("#implements respond", "")
+
+        self.verify("#implements respond(foo=1234)", "")
+
+
+class Backslashes(OutputTest):
+    convertEOLs = False
+
+    def setUp(self):
+        fp = open('backslashes.txt', 'w')
+        fp.write(r'\ #LogFormat "%h %l %u %t \"%r\" %>s %b"' + '\n\n\n\n\n\n\n')
+        fp.flush()
+        fp.close
+    
+    def tearDown(self):
+        if os.path.exists('backslashes.txt'):
+            os.remove('backslashes.txt')
+        
+    def test1(self):
+        """ a single \\ using rawstrings"""
+        self.verify(r"\ ",
+                    r"\ ")
+
+    def test2(self):
+        """ a single \\ using rawstrings and lots of lines"""
+        self.verify(r"\ " + "\n\n\n\n\n\n\n\n\n",
+                    r"\ " + "\n\n\n\n\n\n\n\n\n")
+
+    def test3(self):
+        """ a single \\ without using rawstrings"""
+        self.verify("\ \ ",
+                    "\ \ ")
+
+    def test4(self):
+        """ single line from an apache conf file"""
+        self.verify(r'#LogFormat "%h %l %u %t \"%r\" %>s %b"',
+                    r'#LogFormat "%h %l %u %t \"%r\" %>s %b"')
+
+    def test5(self):
+        """ single line from an apache conf file with many NEWLINES
+
+        The NEWLINES are used to make sure that MethodCompiler.commitStrConst()
+        is handling long and short strings in the same fashion.  It uses
+        triple-quotes for strings with lots of \\n in them and repr(theStr) for
+        shorter strings with only a few newlines."""
+        
+        self.verify(r'#LogFormat "%h %l %u %t \"%r\" %>s %b"' + '\n\n\n\n\n\n\n',
+                    r'#LogFormat "%h %l %u %t \"%r\" %>s %b"' + '\n\n\n\n\n\n\n')
+
+    def test6(self):
+        """ test backslash handling in an included file"""
+        self.verify(r'#include "backslashes.txt"',
+                    r'\ #LogFormat "%h %l %u %t \"%r\" %>s %b"' + '\n\n\n\n\n\n\n')
+
+    def test7(self):
+        """ a single \\ without using rawstrings plus many NEWLINES"""
+        self.verify("\ \ " + "\n\n\n\n\n\n\n\n\n",
+                    "\ \ " + "\n\n\n\n\n\n\n\n\n")
+
+    def test8(self):
+        """ single line from an apache conf file with single quotes and many NEWLINES 
+        """
+        
+        self.verify(r"""#LogFormat '%h %l %u %t \"%r\" %>s %b'""" + '\n\n\n\n\n\n\n',
+                    r"""#LogFormat '%h %l %u %t \"%r\" %>s %b'""" + '\n\n\n\n\n\n\n')
+        
+class NonTokens(OutputTest):
+    def test1(self):
+        """dollar signs not in Cheetah $vars"""
+        self.verify("$ $$ $5 $. $ test",
+                    "$ $$ $5 $. $ test")
+
+    def test2(self):
+        """hash not in #directives"""
+        self.verify("# \# #5 ",
+                    "# # #5 ")
+
+    def test3(self):
+        """escapted comments"""
+        self.verify("  \##escaped comment  ",
+                    "  ##escaped comment  ")
+
+    def test4(self):
+        """escapted multi-line comments"""
+        self.verify("  \#*escaped comment \n*#  ",
+                    "  #*escaped comment \n*#  ")
+
+    def test5(self):
+        """1 dollar sign"""
+        self.verify("$",
+                    "$")
+    def _X_test6(self):
+        """1 dollar sign followed by hash"""
+        self.verify("\n$#\n",
+                    "\n$#\n")
+
+    def test6(self):
+        """1 dollar sign followed by EOL Slurp Token"""
+        if DEFAULT_COMPILER_SETTINGS['EOLSlurpToken']:
+            self.verify("\n$%s\n"%DEFAULT_COMPILER_SETTINGS['EOLSlurpToken'],
+                        "\n$")
+        else:
+            self.verify("\n$#\n",
+                        "\n$#\n")
+            
+class Comments_SingleLine(OutputTest):
+    def test1(self):
+        """## followed by WS"""
+        self.verify("##    ",
+                    "")
+
+    def test2(self):
+        """## followed by NEWLINE"""
+        self.verify("##\n",
+                    "")
+
+    def test3(self):
+        """## followed by text then NEWLINE"""
+        self.verify("## oeuao aoe uaoe \n",
+                    "")
+    def test4(self):
+        """## gobbles leading WS"""
+        self.verify("    ## oeuao aoe uaoe \n",
+                    "")
+
+    def test5(self):
+        """## followed by text then NEWLINE, + leading WS"""
+        self.verify("    ## oeuao aoe uaoe \n",
+                    "")
+
+    def test6(self):
+        """## followed by EOF"""
+        self.verify("##",
+                    "")
+        
+    def test7(self):
+        """## followed by EOF with leading WS"""
+        self.verify("    ##",
+                    "")
+        
+    def test8(self):
+        """## gobble line
+        with text on previous and following lines"""
+        self.verify("line1\n   ## aoeu 1234   \nline2",
+                    "line1\nline2")
+
+    def test9(self):
+        """## don't gobble line
+        with text on previous and following lines"""
+        self.verify("line1\n 12 ## aoeu 1234   \nline2",
+                    "line1\n 12 \nline2")
+
+    def test10(self):
+        """## containing $placeholders
+        """
+        self.verify("##$a$b $c($d)",
+                    "")
+
+    def test11(self):
+        """## containing #for directive
+        """
+        self.verify("##for $i in range(15)",
+                    "")
+
+
+class Comments_MultiLine_NoGobble(OutputTest):
+    """
+    Multiline comments used to not gobble whitespace.  They do now, but this can
+    be turned off with a compilerSetting    
+    """
+
+    def _getCompilerSettings(self):
+        return {'gobbleWhitespaceAroundMultiLineComments':False}
+
+    def test1(self):
+        """#* *# followed by WS
+        Shouldn't gobble WS
+        """
+        self.verify("#* blarg *#   ",
+                    "   ")
+        
+    def test2(self):
+        """#* *# preceded and followed by WS
+        Shouldn't gobble WS
+        """
+        self.verify("   #* blarg *#   ",
+                    "      ")
+        
+    def test3(self):
+        """#* *# followed by WS, with NEWLINE
+        Shouldn't gobble WS
+        """
+        self.verify("#* \nblarg\n *#   ",
+                    "   ")
+        
+    def test4(self):
+        """#* *# preceded and followed by WS, with NEWLINE
+        Shouldn't gobble WS
+        """
+        self.verify("   #* \nblarg\n *#   ",
+                    "      ")
+
+class Comments_MultiLine(OutputTest):
+    """
+    Note: Multiline comments don't gobble whitespace!
+    """
+    
+    def test1(self):
+        """#* *# followed by WS
+        Should gobble WS
+        """
+        self.verify("#* blarg *#   ",
+                    "")
+        
+    def test2(self):
+        """#* *# preceded and followed by WS
+        Should gobble WS
+        """
+        self.verify("   #* blarg *#   ",
+                    "")
+        
+    def test3(self):
+        """#* *# followed by WS, with NEWLINE
+        Shouldn't gobble WS
+        """
+        self.verify("#* \nblarg\n *#   ",
+                    "")
+        
+    def test4(self):
+        """#* *# preceded and followed by WS, with NEWLINE
+        Shouldn't gobble WS
+        """
+        self.verify("   #* \nblarg\n *#   ",
+                    "")
+
+    def test5(self):
+        """#* *# containing nothing 
+        """
+        self.verify("#**#",
+                    "")
+        
+    def test6(self):
+        """#* *# containing only NEWLINES
+        """
+        self.verify("  #*\n\n\n\n\n\n\n\n*#  ",
+                    "")
+
+    def test7(self):
+        """#* *# containing $placeholders
+        """
+        self.verify("#* $var $var(1234*$c) *#",
+                    "")
+        
+    def test8(self):
+        """#* *# containing #for directive
+        """
+        self.verify("#* #for $i in range(15) *#",
+                    "")
+
+    def test9(self):
+        """ text around #* *# containing #for directive
+        """
+        self.verify("foo\nfoo bar #* #for $i in range(15) *# foo\n",
+                    "foo\nfoo bar  foo\n")
+
+    def test9(self):
+        """ text around #* *# containing #for directive and trailing whitespace
+        which should be gobbled
+        """
+        self.verify("foo\nfoo bar #* #for $i in range(15) *#   \ntest",
+                    "foo\nfoo bar \ntest")
+
+    def test10(self):
+        """ text around #* *# containing #for directive and newlines: trailing whitespace
+        which should be gobbled.
+        """
+        self.verify("foo\nfoo bar #* \n\n#for $i in range(15) \n\n*#   \ntest",
+                    "foo\nfoo bar \ntest")
+
+class Placeholders(OutputTest):
+    def test1(self):
+        """1 placeholder"""
+        self.verify("$aStr", "blarg")
+        
+    def test2(self):
+        """2 placeholders"""
+        self.verify("$aStr $anInt", "blarg 1")
+
+    def test3(self):
+        """2 placeholders, back-to-back"""
+        self.verify("$aStr$anInt", "blarg1")
+
+    def test4(self):
+        """1 placeholder enclosed in ()"""
+        self.verify("$(aStr)", "blarg")
+        
+    def test5(self):
+        """1 placeholder enclosed in {}"""
+        self.verify("${aStr}", "blarg")
+
+    def test6(self):
+        """1 placeholder enclosed in []"""
+        self.verify("$[aStr]", "blarg")
+
+    def test7(self):
+        """1 placeholder enclosed in () + WS
+
+        Test to make sure that $(<WS><identifier>.. matches
+        """
+        self.verify("$( aStr   )", "blarg")
+
+    def test8(self):
+        """1 placeholder enclosed in {} + WS"""
+        self.verify("${ aStr   }", "blarg")
+
+    def test9(self):
+        """1 placeholder enclosed in [] + WS"""
+        self.verify("$[ aStr   ]", "blarg")
+
+    def test10(self):
+        """1 placeholder enclosed in () + WS + * cache
+
+        Test to make sure that $*(<WS><identifier>.. matches
+        """
+        self.verify("$*( aStr   )", "blarg")
+
+    def test11(self):
+        """1 placeholder enclosed in {} + WS + *cache"""
+        self.verify("$*{ aStr   }", "blarg")
+
+    def test12(self):
+        """1 placeholder enclosed in [] + WS + *cache"""
+        self.verify("$*[ aStr   ]", "blarg")
+
+    def test13(self):
+        """1 placeholder enclosed in {} + WS + *<int>*cache"""
+        self.verify("$*5*{ aStr   }", "blarg")
+
+    def test14(self):
+        """1 placeholder enclosed in [] + WS + *<int>*cache"""
+        self.verify("$*5*[ aStr   ]", "blarg")
+
+    def test15(self):
+        """1 placeholder enclosed in {} + WS + *<float>*cache"""
+        self.verify("$*0.5d*{ aStr   }", "blarg")
+
+    def test16(self):
+        """1 placeholder enclosed in [] + WS + *<float>*cache"""
+        self.verify("$*.5*[ aStr   ]", "blarg")
+
+    def test17(self):
+        """1 placeholder + *<int>*cache"""
+        self.verify("$*5*aStr", "blarg")
+
+    def test18(self):
+        """1 placeholder *<float>*cache"""
+        self.verify("$*0.5h*aStr", "blarg")
+
+    def test19(self):
+        """1 placeholder surrounded by single quotes and multiple newlines"""
+        self.verify("""'\n\n\n\n'$aStr'\n\n\n\n'""",
+                    """'\n\n\n\n'blarg'\n\n\n\n'""")
+
+    def test20(self):
+        """silent mode $!placeholders """
+        self.verify("$!aStr$!nonExistant$!*nonExistant$!{nonExistant}", "blarg")
+
+        try:
+            self.verify("$!aStr$nonExistant",
+            "blarg")
+        except NotFound:
+            pass
+        else:
+            self.fail('should raise NotFound exception')
+
+    def test21(self):
+        """Make sure that $*caching is actually working"""
+        namesStr = 'You Me Them Everyone'
+        names = namesStr.split()
+
+        tmpl = Template.compile('#for name in $names: $name ', baseclass=dict)
+        assert str(tmpl({'names':names})).strip()==namesStr
+
+        tmpl = tmpl.subclass('#for name in $names: $*name ')
+        assert str(tmpl({'names':names}))=='You '*len(names)
+
+        tmpl = tmpl.subclass('#for name in $names: $*1*name ')
+        assert str(tmpl({'names':names}))=='You '*len(names)
+
+        tmpl = tmpl.subclass('#for name in $names: $*1*(name) ')
+        assert str(tmpl({'names':names}))=='You '*len(names)
+
+        if versionTuple > (2, 2):
+            tmpl = tmpl.subclass('#for name in $names: $*1*(name) ')
+            assert str(tmpl(names=names))=='You '*len(names)
+
+class Placeholders_Vals(OutputTest):
+    convertEOLs = False
+    def test1(self):
+        """string"""
+        self.verify("$aStr", "blarg")
+
+    def test2(self):
+        """string - with whitespace"""
+        self.verify(" $aStr ", " blarg ")
+
+    def test3(self):
+        """empty string - with whitespace"""
+        self.verify("$emptyString", "")
+
+    def test4(self):
+        """int"""
+        self.verify("$anInt", "1")
+
+    def test5(self):
+        """float"""
+        self.verify("$aFloat", "1.5")
+
+    def test6(self):
+        """list"""
+        self.verify("$aList", "['item0', 'item1', 'item2']")
+
+    def test7(self):
+        """None
+
+        The default output filter is ReplaceNone.
+        """
+        self.verify("$none", "")
+
+    def test8(self):
+        """True, False
+        """
+        self.verify("$True $False", "%s %s"%(repr(True), repr(False)))
+
+    def test9(self):
+        """$_
+        """
+        self.verify("$_('foo')", "Translated: foo")
+
+class PlaceholderStrings(OutputTest):
+    def test1(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("$str(c'$aStr')", "blarg")
+
+    def test2(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("$str(c'$aStr.upper')", "BLARG")
+
+    def test3(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("$str(c'$(aStr.upper.replace(c\"A$str()\",\"\"))')", "BLRG")
+
+    def test4(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("#echo $str(c'$(aStr.upper)')", "BLARG")
+
+    def test5(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("#if 1 then $str(c'$(aStr.upper)') else 0", "BLARG")
+
+    def test6(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("#if 1\n$str(c'$(aStr.upper)')#slurp\n#else\n0#end if", "BLARG")
+
+    def test7(self):
+        """some c'text $placeholder text' strings"""
+        self.verify("#def foo(arg=c'$(\"BLARG\")')\n"
+                    "$arg#slurp\n"
+                    "#end def\n"
+                    "$foo()$foo(c'$anInt')#slurp",
+                    
+                    "BLARG1")
+
+
+
+class UnicodeStrings(OutputTest):
+    def test1(self):
+        """unicode data in placeholder
+        """
+        #self.verify(u"$unicodeData", defaultTestNameSpace['unicodeData'], outputEncoding='utf8')
+        self.verify(u"$unicodeData", defaultTestNameSpace['unicodeData'])
+
+    def test2(self):
+        """unicode data in body
+        """
+        self.verify(u"aoeu12345\u1234", u"aoeu12345\u1234")
+        #self.verify(u"#encoding utf8#aoeu12345\u1234", u"aoeu12345\u1234")
+
+class EncodingDirective(OutputTest):
+    def test1(self):
+        """basic #encoding """
+        self.verify("#encoding utf-8\n1234",
+                    "1234")
+
+    def test2(self):
+        """basic #encoding """
+        self.verify("#encoding ascii\n1234",
+                    "1234")
+
+    def test3(self):
+        """basic #encoding """
+        self.verify("#encoding utf-8\n\xe1\x88\xb4",
+                    u'\u1234', outputEncoding='utf8')
+
+    def test4(self):
+        """basic #encoding """
+        self.verify("#encoding latin-1\n\xe1\x88\xb4",
+                    u"\xe1\x88\xb4")
+
+    def test5(self):
+        """basic #encoding """
+        self.verify("#encoding latin-1\nAndr\202",
+                    u'Andr\202')
+
+    def test6(self):
+        '''Using #encoding on the second line'''
+        self.verify("""### Comments on the first line
+#encoding utf-8\n\xe1\x88\xb4""",
+                    u'\u1234', outputEncoding='utf8')
+
+class UnicodeDirective(OutputTest):
+    def test1(self):
+        """basic #unicode """
+        self.verify("#unicode utf-8\n1234",
+                    u"1234")
+        
+        self.verify("#unicode ascii\n1234",
+                    u"1234")
+
+        self.verify("#unicode latin-1\n1234",
+                    u"1234")
+
+        self.verify("#unicode latin-1\n1234ü",
+                    u"1234ü")
+        self.verify("#unicode: latin-1\n1234ü",
+                    u"1234ü")
+        self.verify("#  unicode  : latin-1\n1234ü",
+                    u"1234ü")
+
+        self.verify(u"#unicode latin-1\n1234ü",
+                    u"1234ü")
+
+        self.verify("#encoding latin-1\n1234ü",
+                    u"1234ü")
+
+class Placeholders_Esc(OutputTest):
+    convertEOLs = False
+    def test1(self):
+        """1 escaped placeholder"""
+        self.verify("\$var",
+                    "$var")
+    
+    def test2(self):
+        """2 escaped placeholders"""
+        self.verify("\$var \$_",
+                    "$var $_")
+
+    def test3(self):
+        """2 escaped placeholders - back to back"""
+        self.verify("\$var\$_",
+                    "$var$_")
+
+    def test4(self):
+        """2 escaped placeholders - nested"""
+        self.verify("\$var(\$_)",
+                    "$var($_)")
+
+    def test5(self):
+        """2 escaped placeholders - nested and enclosed"""
+        self.verify("\$(var(\$_)",
+                    "$(var($_)")
+
+
+class Placeholders_Calls(OutputTest):
+    def test1(self):
+        """func placeholder - no ()"""
+        self.verify("$aFunc",
+                    "Scooby")
+
+    def test2(self):
+        """func placeholder - with ()"""
+        self.verify("$aFunc()",
+                    "Scooby")
+
+    def test3(self):
+        r"""func placeholder - with (\n\n)"""
+        self.verify("$aFunc(\n\n)",
+                    "Scooby", convertEOLs=False)
+
+    def test4(self):
+        r"""func placeholder - with (\n\n) and $() enclosure"""
+        self.verify("$(aFunc(\n\n))",
+                    "Scooby", convertEOLs=False)
+
+    def test5(self):
+        r"""func placeholder - with (\n\n) and ${} enclosure"""
+        self.verify("${aFunc(\n\n)}",
+                    "Scooby", convertEOLs=False)
+        
+    def test6(self):
+        """func placeholder - with (int)"""
+        self.verify("$aFunc(1234)",
+                    "1234")
+
+    def test7(self):
+        r"""func placeholder - with (\nint\n)"""
+        self.verify("$aFunc(\n1234\n)",
+                    "1234", convertEOLs=False)
+    def test8(self):
+        """func placeholder - with (string)"""
+        self.verify("$aFunc('aoeu')",
+                    "aoeu")
+        
+    def test9(self):
+        """func placeholder - with ('''string''')"""
+        self.verify("$aFunc('''aoeu''')",
+                    "aoeu")
+    def test10(self):
+        r"""func placeholder - with ('''\nstring\n''')"""
+        self.verify("$aFunc('''\naoeu\n''')",
+                    "\naoeu\n", convertEOLs=False)
+
+    def test11(self):
+        r"""func placeholder - with ('''\nstring'\n''')"""
+        self.verify("$aFunc('''\naoeu'\n''')",
+                    "\naoeu'\n", convertEOLs=False)
+
+    def test12(self):
+        r'''func placeholder - with ("""\nstring\n""")'''
+        self.verify('$aFunc("""\naoeu\n""")',
+                    "\naoeu\n", convertEOLs=False)
+
+    def test13(self):
+        """func placeholder - with (string*int)"""
+        self.verify("$aFunc('aoeu'*2)",
+                    "aoeuaoeu")
+
+    def test14(self):
+        """func placeholder - with (int*int)"""
+        self.verify("$aFunc(2*2)",
+                    "4")
+
+    def test15(self):
+        """func placeholder - with (int*float)"""
+        self.verify("$aFunc(2*2.0)",
+                    "4.0")
+
+    def test16(self):
+        r"""func placeholder - with (int\n*\nfloat)"""
+        self.verify("$aFunc(2\n*\n2.0)",
+                    "4.0", convertEOLs=False)
+
+    def test17(self):
+        """func placeholder - with ($arg=float)"""
+        self.verify("$aFunc($arg=4.0)",
+                    "4.0")
+
+    def test18(self):
+        """func placeholder - with (arg=float)"""
+        self.verify("$aFunc(arg=4.0)",
+                    "4.0")
+
+    def test19(self):
+        """deeply nested argstring, no enclosure"""
+        self.verify("$aFunc($arg=$aMeth($arg=$aFunc(1)))",
+                    "1")
+
+    def test20(self):
+        """deeply nested argstring, no enclosure + with WS"""
+        self.verify("$aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) )",
+                    "1")
+    def test21(self):
+        """deeply nested argstring, () enclosure + with WS"""
+        self.verify("$(aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) ) )",
+                    "1")
+        
+    def test22(self):
+        """deeply nested argstring, {} enclosure + with WS"""
+        self.verify("${aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) ) }",
+                    "1")
+
+    def test23(self):
+        """deeply nested argstring, [] enclosure + with WS"""
+        self.verify("$[aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) ) ]",
+                    "1")
+
+    def test24(self):
+        """deeply nested argstring, () enclosure + *cache"""
+        self.verify("$*(aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) ) )",
+                    "1")
+    def test25(self):
+        """deeply nested argstring, () enclosure + *15*cache"""
+        self.verify("$*15*(aFunc(  $arg = $aMeth( $arg = $aFunc( 1 ) ) ) )",
+                    "1")
+
+    def test26(self):
+        """a function call with the Python None kw."""
+        self.verify("$aFunc(None)",
+                    "")
+
+class NameMapper(OutputTest):
+    def test1(self):
+        """autocalling"""
+        self.verify("$aFunc! $aFunc().",
+                    "Scooby! Scooby.")
+
+    def test2(self):
+        """nested autocalling"""
+        self.verify("$aFunc($aFunc).",
+                    "Scooby.")
+
+    def test3(self):
+        """list subscription"""
+        self.verify("$aList[0]",
+                    "item0")
+
+    def test4(self):
+        """list slicing"""
+        self.verify("$aList[:2]",
+                    "['item0', 'item1']")
+        
+    def test5(self):
+        """list slicing and subcription combined"""
+        self.verify("$aList[:2][0]",
+                    "item0")
+
+    def test6(self):
+        """dictionary access - NameMapper style"""
+        self.verify("$aDict.one",
+                    "item1")
+        
+    def test7(self):
+        """dictionary access - Python style"""
+        self.verify("$aDict['one']",
+                    "item1")
+
+    def test8(self):
+        """dictionary access combined with autocalled string method"""
+        self.verify("$aDict.one.upper",
+                    "ITEM1")
+
+    def test9(self):
+        """dictionary access combined with string method"""
+        self.verify("$aDict.one.upper()",
+                    "ITEM1")
+
+    def test10(self):
+        """nested dictionary access - NameMapper style"""
+        self.verify("$aDict.nestedDict.two",
+                    "nestedItem2")
+        
+    def test11(self):
+        """nested dictionary access - Python style"""
+        self.verify("$aDict['nestedDict']['two']",
+                    "nestedItem2")
+
+    def test12(self):
+        """nested dictionary access - alternating style"""
+        self.verify("$aDict['nestedDict'].two",
+                    "nestedItem2")
+
+    def test13(self):
+        """nested dictionary access using method - alternating style"""
+        self.verify("$aDict.get('nestedDict').two",
+                    "nestedItem2")
+
+    def test14(self):
+        """nested dictionary access - NameMapper style - followed by method"""
+        self.verify("$aDict.nestedDict.two.upper",
+                    "NESTEDITEM2")
+
+    def test15(self):
+        """nested dictionary access - alternating style - followed by method"""
+        self.verify("$aDict['nestedDict'].two.upper",
+                    "NESTEDITEM2")
+
+    def test16(self):
+        """nested dictionary access - NameMapper style - followed by method, then slice"""
+        self.verify("$aDict.nestedDict.two.upper[:4]",
+                    "NEST")
+
+    def test17(self):
+        """nested dictionary access - Python style using a soft-coded key"""
+        self.verify("$aDict[$anObj.meth('nestedDict')].two",
+                    "nestedItem2")
+
+    def test18(self):
+        """object method access"""
+        self.verify("$anObj.meth1",
+                    "doo")
+
+    def test19(self):
+        """object method access, followed by complex slice"""
+        self.verify("$anObj.meth1[0: ((4/4*2)*2)/$anObj.meth1(2) ]",
+                    "do")
+
+    def test20(self):
+        """object method access, followed by a very complex slice
+        If it can pass this one, it's safe to say it works!!"""
+        self.verify("$( anObj.meth1[0:\n (\n(4/4*2)*2)/$anObj.meth1(2)\n ] )",
+                    "do")
+
+    def test21(self):
+        """object method access with % in the default arg for the meth.
+
+        This tests a bug that Jeff Johnson found and submitted a patch to SF
+        for."""
+        
+        self.verify("$anObj.methWithPercentSignDefaultArg",
+                    "110%")
+
+
+#class NameMapperDict(OutputTest):
+#
+#    _searchList = [{"update": "Yabba dabba doo!"}]
+#
+#    def test1(self):
+#        if NameMapper_C_VERSION:
+#            return # This feature is not in the C version yet.
+#        self.verify("$update", "Yabba dabba doo!")
+#
+
+class CacheDirective(OutputTest):
+    
+    def test1(self):
+        r"""simple #cache """
+        self.verify("#cache:$anInt",
+                    "1")
+
+    def test2(self):
+        r"""simple #cache + WS"""
+        self.verify("  #cache  \n$anInt#end cache",
+                    "1")
+
+    def test3(self):
+        r"""simple #cache ... #end cache"""
+        self.verify("""#cache id='cache1', timer=150m
+$anInt
+#end cache
+$aStr""",
+                    "1\nblarg")
+        
+    def test4(self):
+        r"""2 #cache ... #end cache blocks"""
+        self.verify("""#slurp
+#def foo
+#cache ID='cache1', timer=150m
+$anInt
+#end cache
+#cache id='cache2', timer=15s
+ #for $i in range(5)
+$i#slurp
+ #end for
+#end cache
+$aStr#slurp
+#end def
+$foo$foo$foo$foo$foo""",
+                    "1\n01234blarg"*5)
+
+
+    def test5(self):
+        r"""nested #cache blocks"""
+        self.verify("""#slurp
+#def foo      
+#cache ID='cache1', timer=150m
+$anInt
+#cache id='cache2', timer=15s
+ #for $i in range(5)
+$i#slurp
+ #end for
+$*(6)#slurp
+#end cache
+#end cache
+$aStr#slurp
+#end def
+$foo$foo$foo$foo$foo""",
+                    "1\n012346blarg"*5)
+        
+    def test6(self):
+        r"""Make sure that partial directives don't match"""
+        self.verify("#cache_foo",
+                    "#cache_foo")
+        self.verify("#cached",
+                    "#cached")
+
+class CallDirective(OutputTest):
+    
+    def test1(self):
+        r"""simple #call """
+        self.verify("#call int\n$anInt#end call",
+                    "1")
+        # single line version
+        self.verify("#call int: $anInt",
+                    "1")
+        self.verify("#call int: 10\n$aStr",
+                    "10\nblarg")
+
+    def test2(self):
+        r"""simple #call + WS"""
+        self.verify("#call int\n$anInt  #end call",
+                    "1")
+
+    def test3(self):
+        r"""a longer #call"""
+        self.verify('''\
+#def meth(arg)
+$arg.upper()#slurp
+#end def
+#call $meth
+$(1234+1) foo#slurp
+#end call''',
+        "1235 FOO")
+
+    def test4(self):
+        r"""#call with keyword #args"""
+        self.verify('''\
+#def meth(arg1, arg2)
+$arg1.upper() - $arg2.lower()#slurp
+#end def
+#call self.meth
+#arg arg1
+$(1234+1) foo#slurp
+#arg arg2
+UPPER#slurp
+#end call''',
+        "1235 FOO - upper")
+
+    def test5(self):
+        r"""#call with single-line keyword #args """
+        self.verify('''\
+#def meth(arg1, arg2)
+$arg1.upper() - $arg2.lower()#slurp
+#end def
+#call self.meth
+#arg arg1:$(1234+1) foo#slurp
+#arg arg2:UPPER#slurp
+#end call''',
+        "1235 FOO - upper")
+        
+    def test6(self):
+        """#call with python kwargs and cheetah output for the 1s positional
+        arg"""
+        
+        self.verify('''\
+#def meth(arg1, arg2)
+$arg1.upper() - $arg2.lower()#slurp
+#end def
+#call self.meth arg2="UPPER"
+$(1234+1) foo#slurp
+#end call''',
+        "1235 FOO - upper")
+
+    def test7(self):
+        """#call with python kwargs and #args"""
+        self.verify('''\
+#def meth(arg1, arg2, arg3)
+$arg1.upper() - $arg2.lower() - $arg3#slurp
+#end def
+#call self.meth arg2="UPPER", arg3=999
+#arg arg1:$(1234+1) foo#slurp
+#end call''',
+        "1235 FOO - upper - 999")
+        
+    def test8(self):
+        """#call with python kwargs and #args, and using a function to get the
+        function that will be called"""
+        self.verify('''\
+#def meth(arg1, arg2, arg3)
+$arg1.upper() - $arg2.lower() - $arg3#slurp
+#end def
+#call getattr(self, "meth") arg2="UPPER", arg3=999
+#arg arg1:$(1234+1) foo#slurp
+#end call''',
+        "1235 FOO - upper - 999")
+
+    def test9(self):
+        """nested #call directives"""
+        self.verify('''\
+#def meth(arg1)
+$arg1#slurp
+#end def
+#def meth2(x,y)
+$x$y#slurp
+#end def
+##
+#call self.meth
+1#slurp
+#call self.meth
+2#slurp
+#call self.meth
+3#slurp
+#end call 3
+#set two = 2
+#call self.meth2 y=c"$(10/$two)"
+#arg x
+4#slurp
+#end call 4
+#end call 2
+#end call 1''',
+        "12345")
+
+
+
+class I18nDirective(OutputTest):   
+    def test1(self):
+        r"""simple #call """
+        self.verify("#i18n \n$anInt#end i18n",
+                    "1")
+        
+        # single line version
+        self.verify("#i18n: $anInt",
+                    "1")
+        self.verify("#i18n: 10\n$aStr",
+                    "10\nblarg")
+
+
+class CaptureDirective(OutputTest):
+    def test1(self):
+        r"""simple #capture"""
+        self.verify('''\
+#capture cap1
+$(1234+1) foo#slurp
+#end capture
+$cap1#slurp
+''',
+        "1235 foo")
+
+
+    def test2(self):
+        r"""slightly more complex #capture"""
+        self.verify('''\
+#def meth(arg)
+$arg.upper()#slurp
+#end def
+#capture cap1
+$(1234+1) $anInt $meth("foo")#slurp
+#end capture
+$cap1#slurp
+''',
+        "1235 1 FOO")
+
+
+class SlurpDirective(OutputTest):
+    def test1(self):
+        r"""#slurp with 1 \n """
+        self.verify("#slurp\n",
+                    "")
+
+    def test2(self):
+        r"""#slurp with 1 \n, leading whitespace
+        Should gobble"""
+        self.verify("       #slurp\n",
+                    "")
+        
+    def test3(self):
+        r"""#slurp with 1 \n, leading content
+        Shouldn't gobble"""
+        self.verify(" 1234 #slurp\n",
+                    " 1234 ")
+        
+    def test4(self):
+        r"""#slurp with WS then \n, leading content
+        Shouldn't gobble"""
+        self.verify(" 1234 #slurp    \n",
+                    " 1234 ")
+
+    def test5(self):
+        r"""#slurp with garbage chars then \n, leading content
+        Should eat the garbage"""
+        self.verify(" 1234 #slurp garbage   \n",
+                    " 1234 ")
+
+
+
+class EOLSlurpToken(OutputTest):
+    _EOLSlurpToken = DEFAULT_COMPILER_SETTINGS['EOLSlurpToken']
+    def test1(self):
+        r"""#slurp with 1 \n """
+        self.verify("%s\n"%self._EOLSlurpToken,
+                    "")
+
+    def test2(self):
+        r"""#slurp with 1 \n, leading whitespace
+        Should gobble"""
+        self.verify("       %s\n"%self._EOLSlurpToken,
+                    "")
+    def test3(self):
+        r"""#slurp with 1 \n, leading content
+        Shouldn't gobble"""
+        self.verify(" 1234 %s\n"%self._EOLSlurpToken,
+                    " 1234 ")
+        
+    def test4(self):
+        r"""#slurp with WS then \n, leading content
+        Shouldn't gobble"""
+        self.verify(" 1234 %s    \n"%self._EOLSlurpToken,
+                    " 1234 ")
+
+    def test5(self):
+        r"""#slurp with garbage chars then \n, leading content
+        Should NOT eat the garbage"""
+        self.verify(" 1234 %s garbage   \n"%self._EOLSlurpToken,
+                    " 1234 %s garbage   \n"%self._EOLSlurpToken)
+
+if not DEFAULT_COMPILER_SETTINGS['EOLSlurpToken']:
+    del EOLSlurpToken
+
+class RawDirective(OutputTest):
+    def test1(self):
+        """#raw till EOF"""
+        self.verify("#raw\n$aFunc().\n\n",
+                    "$aFunc().\n\n")
+
+    def test2(self):
+        """#raw till #end raw"""
+        self.verify("#raw\n$aFunc().\n#end raw\n$anInt",
+                    "$aFunc().\n1")
+        
+    def test3(self):
+        """#raw till #end raw gobble WS"""
+        self.verify("  #raw  \n$aFunc().\n   #end raw  \n$anInt",
+                    "$aFunc().\n1")
+
+    def test4(self):
+        """#raw till #end raw using explicit directive closure
+        Shouldn't gobble"""
+        self.verify("  #raw  #\n$aFunc().\n   #end raw  #\n$anInt",
+                    "  \n$aFunc().\n\n1")
+
+    def test5(self):
+        """single-line short form #raw: """
+        self.verify("#raw: $aFunc().\n\n",
+                    "$aFunc().\n\n")
+
+        self.verify("#raw: $aFunc().\n$anInt",
+                    "$aFunc().\n1")
+
+    def test6(self):
+        """ Escape characters in a #raw block """
+        self.verify( """#raw: This escape should be preserved: \\$unexpanded So should this one: \\#blah The string "\\012" should not disappear.""",
+                r"""This escape should be preserved: \$unexpanded So should this one: \#blah The string "\012" should not disappear.""")
+
+
+class BreakpointDirective(OutputTest):
+    def test1(self):
+        """#breakpoint part way through source code"""
+        self.verify("$aFunc(2).\n#breakpoint\n$anInt",
+                    "2.\n")
+
+    def test2(self):
+        """#breakpoint at BOF"""
+        self.verify("#breakpoint\n$anInt",
+                    "")
+
+    def test3(self):
+        """#breakpoint at EOF"""
+        self.verify("$anInt\n#breakpoint",
+                    "1\n")
+
+
+class StopDirective(OutputTest):
+    def test1(self):
+        """#stop part way through source code"""
+        self.verify("$aFunc(2).\n#stop\n$anInt",
+                    "2.\n")
+
+    def test2(self):
+        """#stop at BOF"""
+        self.verify("#stop\n$anInt",
+                    "")
+
+    def test3(self):
+        """#stop at EOF"""
+        self.verify("$anInt\n#stop",
+                    "1\n")
+
+    def test4(self):
+        """#stop in pos test block"""
+        self.verify("""$anInt
+#if 1
+inside the if block
+#stop
+#end if
+blarg""",
+        "1\ninside the if block\n")
+
+    def test5(self):
+        """#stop in neg test block"""
+        self.verify("""$anInt
+#if 0
+inside the if block
+#stop
+#end if
+blarg""",
+        "1\nblarg")
+
+
+class ReturnDirective(OutputTest):
+    
+    def test1(self):
+        """#return'ing an int """
+        self.verify("""1
+$str($test-6)
+3
+#def test
+#if 1
+#return (3   *2)  \
+  + 2 
+#else
+aoeuoaeu
+#end if
+#end def
+""",
+                    "1\n2\n3\n")
+
+    def test2(self):
+        """#return'ing an string """
+        self.verify("""1
+$str($test[1])
+3
+#def test
+#if 1
+#return '123'
+#else
+aoeuoaeu
+#end if
+#end def
+""",
+                    "1\n2\n3\n")
+
+    def test3(self):
+        """#return'ing an string AND streaming other output via the transaction"""
+        self.verify("""1
+$str($test(trans=trans)[1])
+3
+#def test
+1.5
+#if 1
+#return '123'
+#else
+aoeuoaeu
+#end if
+#end def
+""",
+                    "1\n1.5\n2\n3\n")
+
+
+class YieldDirective(OutputTest):
+    convertEOLs = False
+    def test1(self):
+        """simple #yield """
+        
+        src1 = """#for i in range(10)\n#yield i\n#end for"""
+        src2 = """#for i in range(10)\n$i#slurp\n#yield\n#end for"""
+        src3 = ("#def iterator\n"
+               "#for i in range(10)\n#yield i\n#end for\n"
+               "#end def\n"
+               "#for i in $iterator\n$i#end for"
+               )
+
+
+        for src in (src1, src2, src3):
+            klass = Template.compile(src, keepRefToGeneratedCode=True)
+            #print klass._CHEETAH_generatedModuleCode
+            iter = klass().respond()
+            output = [str(i) for i in iter]
+            assert ''.join(output)=='0123456789'
+            #print ''.join(output)
+
+        # @@TR: need to expand this to cover error conditions etc.
+
+if versionTuple < (2, 3):
+    del YieldDirective
+        
+class ForDirective(OutputTest):
+
+    def test1(self):
+        """#for loop with one local var"""
+        self.verify("#for $i in range(5)\n$i\n#end for",
+                    "0\n1\n2\n3\n4\n")
+
+        self.verify("#for $i in range(5):\n$i\n#end for",
+                    "0\n1\n2\n3\n4\n")
+
+        self.verify("#for $i in range(5): ##comment\n$i\n#end for",
+                    "0\n1\n2\n3\n4\n")
+
+        self.verify("#for $i in range(5) ##comment\n$i\n#end for",
+                    "0\n1\n2\n3\n4\n")
+
+
+    def test2(self):
+        """#for loop with WS in loop"""
+        self.verify("#for $i in range(5)\n$i \n#end for",
+                    "0 \n1 \n2 \n3 \n4 \n")
+        
+    def test3(self):
+        """#for loop gobble WS"""
+        self.verify("   #for $i in range(5)   \n$i \n   #end for   ",
+                    "0 \n1 \n2 \n3 \n4 \n")
+
+    def test4(self):
+        """#for loop over list"""
+        self.verify("#for $i, $j in [(0,1),(2,3)]\n$i,$j\n#end for",
+                    "0,1\n2,3\n")
+        
+    def test5(self):
+        """#for loop over list, with #slurp"""
+        self.verify("#for $i, $j in [(0,1),(2,3)]\n$i,$j#slurp\n#end for",
+                    "0,12,3")
+
+    def test6(self):
+        """#for loop with explicit closures"""
+        self.verify("#for $i in range(5)#$i#end for#",
+                    "01234")
+
+    def test7(self):
+        """#for loop with explicit closures and WS"""
+        self.verify("  #for $i in range(5)#$i#end for#  ",
+                    "  01234  ")
+
+    def test8(self):
+        """#for loop using another $var"""
+        self.verify("  #for $i in range($aFunc(5))#$i#end for#  ",
+                    "  01234  ")
+
+    def test9(self):
+        """test methods in for loops"""
+        self.verify("#for $func in $listOfLambdas\n$func($anInt)\n#end for",
+                    "1\n1\n1\n")
+
+
+    def test10(self):
+        """#for loop over list, using methods of the items"""
+        self.verify("#for i, j in [('aa','bb'),('cc','dd')]\n$i.upper,$j.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+        self.verify("#for $i, $j in [('aa','bb'),('cc','dd')]\n$i.upper,$j.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+
+    def test11(self):
+        """#for loop over list, using ($i,$j) style target list"""
+        self.verify("#for (i, j) in [('aa','bb'),('cc','dd')]\n$i.upper,$j.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+        self.verify("#for ($i, $j) in [('aa','bb'),('cc','dd')]\n$i.upper,$j.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+
+    def test12(self):
+        """#for loop over list, using i, (j,k) style target list"""
+        self.verify("#for i, (j, k) in enumerate([('aa','bb'),('cc','dd')])\n$j.upper,$k.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+        self.verify("#for $i, ($j, $k) in enumerate([('aa','bb'),('cc','dd')])\n$j.upper,$k.upper\n#end for",
+                    "AA,BB\nCC,DD\n")
+
+    def test13(self):
+        """single line #for"""
+        self.verify("#for $i in range($aFunc(5)): $i",
+                    "01234")
+
+    def test14(self):
+        """single line #for with 1 extra leading space"""
+        self.verify("#for $i in range($aFunc(5)):  $i",
+                    " 0 1 2 3 4")
+
+    def test15(self):
+        """2 times single line #for"""
+        self.verify("#for $i in range($aFunc(5)): $i#slurp\n"*2,
+                    "01234"*2)
+
+    def test16(self):
+        """false single line #for """
+        self.verify("#for $i in range(5): \n$i\n#end for",
+                    "0\n1\n2\n3\n4\n")
+
+if versionTuple < (2, 3):
+    del ForDirective.test12
+
+class RepeatDirective(OutputTest):
+
+    def test1(self):
+        """basic #repeat"""
+        self.verify("#repeat 3\n1\n#end repeat",
+                    "1\n1\n1\n")
+        self.verify("#repeat 3: \n1\n#end repeat",
+                    "1\n1\n1\n")
+
+        self.verify("#repeat 3 ##comment\n1\n#end repeat",
+                    "1\n1\n1\n")
+
+        self.verify("#repeat 3: ##comment\n1\n#end repeat",
+                    "1\n1\n1\n")
+
+    def test2(self):
+        """#repeat with numeric expression"""
+        self.verify("#repeat 3*3/3\n1\n#end repeat",
+                    "1\n1\n1\n")
+    
+    def test3(self):
+        """#repeat with placeholder"""
+        self.verify("#repeat $numTwo\n1\n#end repeat",
+                    "1\n1\n")
+    
+    def test4(self):
+        """#repeat with placeholder * num"""
+        self.verify("#repeat $numTwo*1\n1\n#end repeat",
+                    "1\n1\n")
+        
+    def test5(self):
+        """#repeat with placeholder and WS"""
+        self.verify("   #repeat $numTwo   \n1\n   #end repeat   ",
+                    "1\n1\n")
+
+    def test6(self):
+        """single-line #repeat"""
+        self.verify("#repeat $numTwo: 1",
+                    "11")
+        self.verify("#repeat $numTwo: 1\n"*2,
+                    "1\n1\n"*2)
+
+        #false single-line
+        self.verify("#repeat 3:  \n1\n#end repeat",
+                    "1\n1\n1\n")
+
+
+class AttrDirective(OutputTest):
+
+    def test1(self):
+        """#attr with int"""
+        self.verify("#attr $test = 1234\n$test",
+                    "1234")
+
+    def test2(self):
+        """#attr with string"""
+        self.verify("#attr $test = 'blarg'\n$test",
+                    "blarg")
+
+    def test3(self):
+        """#attr with expression"""
+        self.verify("#attr $test = 'blarg'.upper()*2\n$test",
+                    "BLARGBLARG")
+
+    def test4(self):
+        """#attr with string + WS
+        Should gobble"""
+        self.verify("     #attr $test = 'blarg'   \n$test",
+                    "blarg")
+
+    def test5(self):
+        """#attr with string + WS + leading text
+        Shouldn't gobble"""
+        self.verify("  --   #attr $test = 'blarg'   \n$test",
+                    "  --   \nblarg")
+
+
+class DefDirective(OutputTest):
+
+    def test1(self):
+        """#def without argstring"""
+        self.verify("#def testMeth\n1234\n#end def\n$testMeth",
+                    "1234\n")
+
+        self.verify("#def testMeth ## comment\n1234\n#end def\n$testMeth",
+                    "1234\n")
+
+        self.verify("#def testMeth: ## comment\n1234\n#end def\n$testMeth",
+                    "1234\n")
+
+    def test2(self):
+        """#def without argstring, gobble WS"""
+        self.verify("   #def testMeth  \n1234\n    #end def   \n$testMeth",
+                    "1234\n")
+
+    def test3(self):
+        """#def with argstring, gobble WS"""
+        self.verify("  #def testMeth($a=999)   \n1234-$a\n  #end def\n$testMeth",
+                    "1234-999\n")
+
+    def test4(self):
+        """#def with argstring, gobble WS, string used in call"""
+        self.verify("  #def testMeth($a=999)   \n1234-$a\n  #end def\n$testMeth('ABC')",
+                    "1234-ABC\n")
+
+    def test5(self):
+        """#def with argstring, gobble WS, list used in call"""
+        self.verify("  #def testMeth($a=999)   \n1234-$a\n  #end def\n$testMeth([1,2,3])",
+                    "1234-[1, 2, 3]\n")
+
+    def test6(self):
+        """#def with 2 args, gobble WS, list used in call"""
+        self.verify("  #def testMeth($a, $b='default')   \n1234-$a$b\n  #end def\n$testMeth([1,2,3])",
+                    "1234-[1, 2, 3]default\n")
+
+    def test7(self):
+        """#def with *args, gobble WS"""
+        self.verify("  #def testMeth($*args)   \n1234-$args\n  #end def\n$testMeth",
+                    "1234-()\n")
+
+    def test8(self):
+        """#def with **KWs, gobble WS"""
+        self.verify("  #def testMeth($**KWs)   \n1234-$KWs\n  #end def\n$testMeth",
+                    "1234-{}\n")
+
+    def test9(self):
+        """#def with *args + **KWs, gobble WS"""
+        self.verify("  #def testMeth($*args, $**KWs)   \n1234-$args-$KWs\n  #end def\n$testMeth",
+                    "1234-()-{}\n")
+
+    def test10(self):
+        """#def with *args + **KWs, gobble WS"""
+        self.verify(
+            "  #def testMeth($*args, $**KWs)   \n1234-$args-$KWs.a\n  #end def\n$testMeth(1,2, a=1)",
+            "1234-(1, 2)-1\n")
+
+
+    def test11(self):
+        """single line #def with extra WS"""
+        self.verify(
+            "#def testMeth: aoeuaoeu\n- $testMeth -",
+            "- aoeuaoeu -")
+
+    def test12(self):
+        """single line #def with extra WS and nested $placeholders"""
+        self.verify(
+            "#def testMeth: $anInt $aFunc(1234)\n- $testMeth -",
+            "- 1 1234 -")
+
+    def test13(self):
+        """single line #def escaped $placeholders"""
+        self.verify(
+            "#def testMeth: \$aFunc(\$anInt)\n- $testMeth -",
+            "- $aFunc($anInt) -")
+
+    def test14(self):
+        """single line #def 1 escaped $placeholders"""
+        self.verify(
+            "#def testMeth: \$aFunc($anInt)\n- $testMeth -",
+            "- $aFunc(1) -")
+
+    def test15(self):
+        """single line #def 1 escaped $placeholders + more WS"""
+        self.verify(
+            "#def testMeth    : \$aFunc($anInt)\n- $testMeth -",
+            "- $aFunc(1) -")
+
+    def test16(self):
+        """multiline #def with $ on methodName"""
+        self.verify("#def $testMeth\n1234\n#end def\n$testMeth",
+                    "1234\n")
+
+    def test17(self):
+        """single line #def with $ on methodName"""
+        self.verify("#def $testMeth:1234\n$testMeth",
+                    "1234")
+
+    def test18(self):
+        """single line #def with an argument"""
+        self.verify("#def $testMeth($arg=1234):$arg\n$testMeth",
+                    "1234")
+
+    def test19(self):
+        """#def that extends over two lines with arguments"""
+        self.verify("#def $testMeth($arg=1234,\n"
+                    +"  $arg2=5678)\n"
+                    +"$arg $arg2\n"
+                    +"#end def\n"
+                    +"$testMeth",
+                    "1234 5678\n")
+
+class DecoratorDirective(OutputTest):
+    def test1(self):
+        """single line #def with decorator"""
+
+        self.verify("#@ blah", "#@ blah")
+        self.verify("#@23 blah", "#@23 blah")
+        self.verify("#@@TR: comment", "#@@TR: comment")
+
+        self.verify("#from Cheetah.Tests.SyntaxAndOutput import testdecorator\n"
+                    +"#@testdecorator"
+                    +"\n#def $testMeth():1234\n$testMeth",
+                    
+                    "1234")
+
+        self.verify("#from Cheetah.Tests.SyntaxAndOutput import testdecorator\n"
+                    +"#@testdecorator"
+                    +"\n#block $testMeth():1234",
+                    
+                    "1234")
+
+        try:
+            self.verify(
+                "#from Cheetah.Tests.SyntaxAndOutput import testdecorator\n"
+                +"#@testdecorator\n sdf"
+                +"\n#def $testMeth():1234\n$testMeth",                        
+
+                "1234")
+        except ParseError:
+            pass
+        else:
+            self.fail('should raise a ParseError')
+
+    def test2(self):
+        """#def with multiple decorators"""
+        self.verify("#from Cheetah.Tests.SyntaxAndOutput import testdecorator\n"
+                    +"#@testdecorator\n"
+                    +"#@testdecorator\n"
+                    +"#def testMeth\n"
+                    +"1234\n"
+                    "#end def\n"
+                    "$testMeth",
+                    "1234\n")
+
+if versionTuple < (2, 4):
+    del DecoratorDirective
+
+class BlockDirective(OutputTest):
+
+    def test1(self):
+        """#block without argstring"""
+        self.verify("#block testBlock\n1234\n#end block",
+                    "1234\n")
+
+        self.verify("#block testBlock ##comment\n1234\n#end block",
+                    "1234\n")
+
+    def test2(self):
+        """#block without argstring, gobble WS"""
+        self.verify("  #block testBlock   \n1234\n  #end block  ",
+                    "1234\n")
+
+    def test3(self):
+        """#block with argstring, gobble WS
+
+        Because blocks can be reused in multiple parts of the template arguments
+        (!!with defaults!!) can be given."""
+        
+        self.verify("  #block testBlock($a=999)   \n1234-$a\n  #end block  ",
+                    "1234-999\n")
+
+    def test4(self):
+        """#block with 2 args, gobble WS"""
+        self.verify("  #block testBlock($a=999, $b=444)   \n1234-$a$b\n  #end block  ",
+                    "1234-999444\n")
+
+
+    def test5(self):
+        """#block with 2 nested blocks
+
+        Blocks can be nested to any depth and the name of the block is optional
+        for the #end block part: #end block OR #end block [name] """
+        
+        self.verify("""#block testBlock
+this is a test block
+#block outerNest
+outer
+#block innerNest
+inner
+#end block innerNest
+#end block outerNest
+---
+#end block testBlock
+""",
+                    "this is a test block\nouter\ninner\n---\n")
+
+
+    def test6(self):
+        """single line #block """
+        self.verify(
+            "#block testMeth: This is my block",
+            "This is my block")
+
+    def test7(self):
+        """single line #block with WS"""
+        self.verify(
+            "#block testMeth: This is my block",
+            "This is my block")
+
+    def test8(self):
+        """single line #block 1 escaped $placeholders"""
+        self.verify(
+            "#block testMeth: \$aFunc($anInt)",
+            "$aFunc(1)")
+
+    def test9(self):
+        """single line #block 1 escaped $placeholders + WS"""
+        self.verify(
+            "#block testMeth: \$aFunc( $anInt )",
+            "$aFunc( 1 )")
+
+    def test10(self):
+        """single line #block 1 escaped $placeholders + more WS"""
+        self.verify(
+            "#block testMeth  : \$aFunc( $anInt )",
+            "$aFunc( 1 )")
+
+    def test11(self):
+        """multiline #block $ on argstring"""
+        self.verify("#block $testBlock\n1234\n#end block",
+                    "1234\n")
+
+    def test12(self):
+        """single line #block with $ on methodName """
+        self.verify(
+            "#block $testMeth: This is my block",
+            "This is my block")
+
+    def test13(self):
+        """single line #block with an arg """
+        self.verify(
+            "#block $testMeth($arg='This is my block'): $arg",
+            "This is my block")
+
+    def test14(self):
+        """single line #block with None for content"""
+        self.verify(
+            """#block $testMeth: $None\ntest $testMeth-""",
+            "test -")
+
+    def test15(self):
+        """single line #block with nothing for content"""
+        self.verify(
+            """#block $testMeth: \nfoo\n#end block\ntest $testMeth-""",
+            "foo\ntest foo\n-")
+
+class IncludeDirective(OutputTest):
+
+    def setUp(self):
+        fp = open('parseTest.txt', 'w')
+        fp.write("$numOne $numTwo")
+        fp.flush()
+        fp.close
+
+    def tearDown(self):
+        if os.path.exists('parseTest.txt'):
+            os.remove('parseTest.txt')
+
+    def test1(self):
+        """#include raw of source $emptyString"""
+        self.verify("#include raw source=$emptyString",
+                    "")
+
+    def test2(self):
+        """#include raw of source $blockToBeParsed"""
+        self.verify("#include raw source=$blockToBeParsed",
+                    "$numOne $numTwo")
+
+    def test3(self):
+        """#include raw of 'parseTest.txt'"""
+        self.verify("#include raw 'parseTest.txt'",
+                    "$numOne $numTwo")
+
+    def test4(self):
+        """#include raw of $includeFileName"""
+        self.verify("#include raw $includeFileName",
+                    "$numOne $numTwo")
+
+    def test5(self):
+        """#include raw of $includeFileName, with WS"""
+        self.verify("       #include raw $includeFileName      ",
+                    "$numOne $numTwo")
+
+    def test6(self):
+        """#include raw of source= , with WS"""
+        self.verify("       #include raw source='This is my $Source '*2      ",
+                    "This is my $Source This is my $Source ")
+
+    def test7(self):
+        """#include of $blockToBeParsed"""
+        self.verify("#include source=$blockToBeParsed",
+                    "1 2")
+        
+    def test8(self):
+        """#include of $blockToBeParsed, with WS"""
+        self.verify("   #include source=$blockToBeParsed   ",
+                    "1 2")
+
+    def test9(self):
+        """#include of 'parseTest.txt', with WS"""
+        self.verify("   #include source=$blockToBeParsed   ",
+                    "1 2")
+
+    def test10(self):
+        """#include of "parseTest.txt", with WS"""
+        self.verify("   #include source=$blockToBeParsed   ",
+                    "1 2")
+        
+    def test11(self):
+        """#include of 'parseTest.txt', with WS and surrounding text"""
+        self.verify("aoeu\n  #include source=$blockToBeParsed  \naoeu",
+                    "aoeu\n1 2aoeu")
+
+    def test12(self):
+        """#include of 'parseTest.txt', with WS and explicit closure"""
+        self.verify("  #include source=$blockToBeParsed#  ",
+                    "  1 2  ")
+
+
+class SilentDirective(OutputTest):
+
+    def test1(self):
+        """simple #silent"""
+        self.verify("#silent $aFunc",
+                    "")
+
+    def test2(self):
+        """simple #silent"""
+        self.verify("#silent $anObj.callIt\n$anObj.callArg",
+                    "1234")
+
+        self.verify("#silent $anObj.callIt ##comment\n$anObj.callArg",
+                    "1234")
+
+    def test3(self):
+        """simple #silent"""
+        self.verify("#silent $anObj.callIt(99)\n$anObj.callArg",
+                    "99")
+
+class SetDirective(OutputTest):
+
+    def test1(self):
+        """simple #set"""
+        self.verify("#set $testVar = 'blarg'\n$testVar",
+                    "blarg")
+        self.verify("#set testVar = 'blarg'\n$testVar",
+                    "blarg")
+
+
+        self.verify("#set testVar = 'blarg'##comment\n$testVar",
+                    "blarg")
+
+    def test2(self):
+        """simple #set with no WS between operands"""
+        self.verify("#set       $testVar='blarg'",
+                    "")
+    def test3(self):
+        """#set + use of var"""
+        self.verify("#set $testVar = 'blarg'\n$testVar",
+                    "blarg")
+        
+    def test4(self):
+        """#set + use in an #include"""
+        self.verify("#set global $aSetVar = 1234\n#include source=$includeBlock2",
+                    "1 2 1234")
+
+    def test5(self):
+        """#set with a dictionary"""
+        self.verify(     """#set $testDict = {'one':'one1','two':'two2','three':'three3'}
+$testDict.one
+$testDict.two""",
+                         "one1\ntwo2")
+
+    def test6(self):
+        """#set with string, then used in #if block"""
+    
+        self.verify("""#set $test='a string'\n#if $test#blarg#end if""",
+                    "blarg")
+
+    def test7(self):
+        """simple #set, gobble WS"""
+        self.verify("   #set $testVar = 'blarg'   ",
+                    "")
+
+    def test8(self):
+        """simple #set, don't gobble WS"""
+        self.verify("  #set $testVar = 'blarg'#---",
+                    "  ---")
+
+    def test9(self):
+        """simple #set with a list"""
+        self.verify("   #set $testVar = [1, 2, 3]  \n$testVar",
+                    "[1, 2, 3]")
+
+    def test10(self):
+        """simple #set global with a list"""
+        self.verify("   #set global $testVar = [1, 2, 3]  \n$testVar",
+                    "[1, 2, 3]")
+
+    def test11(self):
+        """simple #set global with a list and *cache
+
+        Caching only works with global #set vars.  Local vars are not accesible
+        to the cache namespace.
+        """
+        
+        self.verify("   #set global $testVar = [1, 2, 3]  \n$*testVar",
+                    "[1, 2, 3]")
+
+    def test12(self):
+        """simple #set global with a list and *<int>*cache"""
+        self.verify("   #set global $testVar = [1, 2, 3]  \n$*5*testVar",
+                    "[1, 2, 3]")
+
+    def test13(self):
+        """simple #set with a list and *<float>*cache"""
+        self.verify("   #set global $testVar = [1, 2, 3]  \n$*.5*testVar",
+                    "[1, 2, 3]")
+
+    def test14(self):
+        """simple #set without NameMapper on"""
+        self.verify("""#compiler useNameMapper = 0\n#set $testVar = 1 \n$testVar""",
+                    "1")
+
+    def test15(self):
+        """simple #set without $"""
+        self.verify("""#set testVar = 1 \n$testVar""",
+                    "1")
+
+    def test16(self):
+        """simple #set global without $"""
+        self.verify("""#set global testVar = 1 \n$testVar""",
+                    "1")
+
+    def test17(self):
+        """simple #set module without $"""
+        self.verify("""#set module __foo__ = 'bar'\n$__foo__""",
+                    "bar")
+
+    def test18(self):
+        """#set with i,j=list style assignment"""
+        self.verify("""#set i,j = [1,2]\n$i$j""",
+                    "12")
+        self.verify("""#set $i,$j = [1,2]\n$i$j""",
+                    "12")
+
+    def test19(self):
+        """#set with (i,j)=list style assignment"""
+        self.verify("""#set (i,j) = [1,2]\n$i$j""",
+                    "12")
+        self.verify("""#set ($i,$j) = [1,2]\n$i$j""",
+                    "12")
+
+    def test20(self):
+        """#set with i, (j,k)=list style assignment"""
+        self.verify("""#set i, (j,k) = [1,(2,3)]\n$i$j$k""",
+                    "123")
+        self.verify("""#set $i, ($j,$k) = [1,(2,3)]\n$i$j$k""",
+                    "123")
+
+
+class IfDirective(OutputTest):
+
+    def test1(self):
+        """simple #if block"""
+        self.verify("#if 1\n$aStr\n#end if\n",
+                    "blarg\n")
+
+        self.verify("#if 1:\n$aStr\n#end if\n",
+                    "blarg\n")
+
+        self.verify("#if 1:   \n$aStr\n#end if\n",
+                    "blarg\n")
+
+        self.verify("#if 1: ##comment \n$aStr\n#end if\n",
+                        "blarg\n")
+
+        self.verify("#if 1 ##comment \n$aStr\n#end if\n",
+                        "blarg\n")
+
+        self.verify("#if 1##for i in range(10)#$i#end for##end if",
+                    '0123456789')
+
+        self.verify("#if 1: #for i in range(10)#$i#end for",
+                    '0123456789')
+
+        self.verify("#if 1: #for i in range(10):$i",
+                    '0123456789')
+
+    def test2(self):
+        """simple #if block, with WS"""
+        self.verify("   #if 1\n$aStr\n  #end if  \n",
+                    "blarg\n")
+    def test3(self):
+        """simple #if block, with WS and explicit closures"""
+        self.verify("   #if 1#\n$aStr\n  #end if #--\n",
+                    "   \nblarg\n  --\n")
+
+    def test4(self):
+        """#if block using $numOne"""
+        self.verify("#if $numOne\n$aStr\n#end if\n",
+                    "blarg\n")
+
+    def test5(self):
+        """#if block using $zero"""
+        self.verify("#if $zero\n$aStr\n#end if\n",
+                    "")
+    def test6(self):
+        """#if block using $emptyString"""
+        self.verify("#if $emptyString\n$aStr\n#end if\n",
+                    "")
+    def test7(self):
+        """#if ... #else ... block using a $emptyString"""
+        self.verify("#if $emptyString\n$anInt\n#else\n$anInt - $anInt\n#end if",
+                    "1 - 1\n")
+        
+    def test8(self):
+        """#if ... #elif ... #else ... block using a $emptyString"""
+        self.verify("#if $emptyString\n$c\n#elif $numOne\n$numOne\n#else\n$c - $c\n#end if",
+                    "1\n")
+
+    def test9(self):
+        """#if 'not' test, with #slurp"""
+        self.verify("#if not $emptyString\n$aStr#slurp\n#end if\n",
+                    "blarg")
+
+    def test10(self):
+        """#if block using $*emptyString
+
+        This should barf
+        """
+        try:
+            self.verify("#if $*emptyString\n$aStr\n#end if\n",
+                        "")
+        except ParseError:
+            pass
+        else:
+            self.fail('This should barf')
+
+    def test11(self):
+        """#if block using invalid top-level $(placeholder) syntax - should barf"""
+
+        for badSyntax in ("#if $*5*emptyString\n$aStr\n#end if\n",
+                          "#if ${emptyString}\n$aStr\n#end if\n",
+                          "#if $(emptyString)\n$aStr\n#end if\n",
+                          "#if $[emptyString]\n$aStr\n#end if\n",
+                          "#if $!emptyString\n$aStr\n#end if\n",
+                          ):
+            try:
+                self.verify(badSyntax, "")
+            except ParseError:
+                pass
+            else:
+                self.fail('This should barf')
+            
+    def test12(self):
+        """#if ... #else if ... #else ... block using a $emptyString
+        Same as test 8 but using else if instead of elif"""
+        self.verify("#if $emptyString\n$c\n#else if $numOne\n$numOne\n#else\n$c - $c\n#end if",
+                    "1\n")
+
+
+    def test13(self):
+        """#if# ... #else # ... block using a $emptyString with """
+        self.verify("#if $emptyString# $anInt#else#$anInt - $anInt#end if",
+                    "1 - 1")
+
+    def test14(self):
+        """single-line #if: simple"""
+        self.verify("#if $emptyString then 'true' else 'false'",
+                    "false")
+
+    def test15(self):
+        """single-line #if: more complex"""
+        self.verify("#if $anInt then 'true' else 'false'",
+                    "true")
+
+    def test16(self):
+        """single-line #if: with the words 'else' and 'then' in the output """
+        self.verify("#if ($anInt and not $emptyString==''' else ''') then $str('then') else 'else'",
+                    "then")
+
+    def test17(self):
+        """single-line #if:  """
+        self.verify("#if 1: foo\n#if 0: bar\n#if 1: foo",
+                    "foo\nfoo")
+
+
+        self.verify("#if 1: foo\n#if 0: bar\n#if 1: foo",
+                    "foo\nfoo")
+
+    def test18(self):
+        """single-line #if: \n#else: """
+        self.verify("#if 1: foo\n#elif 0: bar",
+                    "foo\n")
+
+        self.verify("#if 1: foo\n#elif 0: bar\n#else: blarg\n",
+                    "foo\n")
+
+        self.verify("#if 0: foo\n#elif 0: bar\n#else: blarg\n",
+                    "blarg\n")
+
+class UnlessDirective(OutputTest):
+    
+    def test1(self):
+        """#unless 1"""
+        self.verify("#unless 1\n 1234 \n#end unless",
+                    "")
+
+        self.verify("#unless 1:\n 1234 \n#end unless",
+                    "")
+
+        self.verify("#unless 1: ##comment\n 1234 \n#end unless",
+                    "")
+
+        self.verify("#unless 1 ##comment\n 1234 \n#end unless",
+                    "")
+
+
+    def test2(self):
+        """#unless 0"""
+        self.verify("#unless 0\n 1234 \n#end unless",
+                    " 1234 \n")
+
+    def test3(self):
+        """#unless $none"""
+        self.verify("#unless $none\n 1234 \n#end unless",
+                    " 1234 \n")
+
+    def test4(self):
+        """#unless $numTwo"""
+        self.verify("#unless $numTwo\n 1234 \n#end unless",
+                    "")
+
+    def test5(self):
+        """#unless $numTwo with WS"""
+        self.verify("   #unless $numTwo   \n 1234 \n    #end unless   ",
+                    "")
+
+    def test6(self):
+        """single-line #unless"""
+        self.verify("#unless 1: 1234", "")
+        self.verify("#unless 0: 1234", "1234")
+        self.verify("#unless 0: 1234\n"*2, "1234\n"*2)
+        
+class PSP(OutputTest):
+    def searchList(self):
+        return None
+    
+    def test1(self):
+        """simple <%= [int] %>"""
+        self.verify("<%= 1234 %>",  "1234")
+
+    def test2(self):
+        """simple <%= [string] %>"""
+        self.verify("<%= 'blarg' %>", "blarg")
+
+    def test3(self):
+        """simple <%= None %>"""
+        self.verify("<%= None %>", "")
+    def test4(self):
+        """simple <%= [string] %> + $anInt"""
+        self.verify("<%= 'blarg' %>$anInt", "blarg1")
+
+    def test5(self):
+        """simple <%= [EXPR] %> + $anInt"""
+        self.verify("<%= ('blarg'*2).upper() %>$anInt", "BLARGBLARG1")
+
+    def test6(self):
+        """for loop in <%%>"""
+        self.verify("<% for i in range(5):%>1<%end%>", "11111")
+
+    def test7(self):
+        """for loop in <%%> and using <%=i%>"""
+        self.verify("<% for i in range(5):%><%=i%><%end%>", "01234")
+
+    def test8(self):
+        """for loop in <% $%> and using <%=i%>"""
+        self.verify("""<% for i in range(5):
+    i=i*2$%><%=i%><%end%>""", "02468")
+
+    def test9(self):
+        """for loop in <% $%> and using <%=i%> plus extra text"""
+        self.verify("""<% for i in range(5):
+    i=i*2$%><%=i%>-<%end%>""", "0-2-4-6-8-")
+
+    def test10(self):
+        """ Using getVar and write within a PSP """
+        self._searchList = [{'me' : 1}]
+        template = '''This is my template
+<%
+me = self.getVar('me')
+if isinstance(me, int):
+    write('Bork')
+else:
+    write('Nork')
+%>'''
+        self.verify(template, 'This is my template\nBork')
+
+
+class WhileDirective(OutputTest):
+    def test1(self):
+        """simple #while with a counter"""
+        self.verify("#set $i = 0\n#while $i < 5\n$i#slurp\n#set $i += 1\n#end while",
+                    "01234")
+
+class ContinueDirective(OutputTest):
+    def test1(self):
+        """#continue with a #while"""
+        self.verify("""#set $i = 0
+#while $i < 5
+#if $i == 3
+  #set $i += 1        
+  #continue
+#end if
+$i#slurp
+#set $i += 1
+#end while""",
+        "0124")
+
+    def test2(self):
+        """#continue with a #for"""
+        self.verify("""#for $i in range(5)
+#if $i == 3
+  #continue
+#end if
+$i#slurp
+#end for""",
+        "0124")
+
+class BreakDirective(OutputTest):
+    def test1(self):
+        """#break with a #while"""
+        self.verify("""#set $i = 0
+#while $i < 5
+#if $i == 3
+  #break
+#end if
+$i#slurp
+#set $i += 1
+#end while""",
+        "012")
+
+    def test2(self):
+        """#break with a #for"""
+        self.verify("""#for $i in range(5)
+#if $i == 3
+  #break
+#end if
+$i#slurp
+#end for""",
+        "012")
+
+
+class TryDirective(OutputTest):
+
+    def test1(self):
+        """simple #try 
+        """
+        self.verify("#try\n1234\n#except\nblarg\n#end try",
+                    "1234\n")
+
+    def test2(self):
+        """#try / #except with #raise
+        """
+        self.verify("#try\n#raise ValueError\n#except\nblarg\n#end try",
+                    "blarg\n")
+        
+    def test3(self):
+        """#try / #except with #raise + WS
+
+        Should gobble
+        """
+        self.verify("  #try  \n  #raise ValueError \n  #except \nblarg\n  #end try",
+                    "blarg\n")
+
+
+    def test4(self):
+        """#try / #except with #raise + WS and leading text
+        
+        Shouldn't gobble
+        """
+        self.verify("--#try  \n  #raise ValueError \n  #except \nblarg\n  #end try#--",
+                    "--\nblarg\n  --")
+
+    def test5(self):
+        """nested #try / #except with #raise
+        """
+        self.verify(
+"""#try
+  #raise ValueError
+#except
+  #try
+   #raise ValueError
+  #except
+blarg
+  #end try
+#end try""",
+                    "blarg\n")
+
+class PassDirective(OutputTest):
+    def test1(self):
+        """#pass in a #try / #except block
+        """
+        self.verify("#try\n#raise ValueError\n#except\n#pass\n#end try",
+                    "")
+
+    def test2(self):
+        """#pass in a #try / #except block + WS
+        """
+        self.verify("  #try  \n  #raise ValueError  \n  #except  \n   #pass   \n   #end try",
+                    "")
+
+
+class AssertDirective(OutputTest):
+    def test1(self):
+        """simple #assert 
+        """
+        self.verify("#set $x = 1234\n#assert $x == 1234",
+                    "")
+
+    def test2(self):
+        """simple #assert that fails
+        """
+        def test(self=self):
+            self.verify("#set $x = 1234\n#assert $x == 999",
+                        ""),
+        self.failUnlessRaises(AssertionError, test)
+        
+    def test3(self):
+        """simple #assert with WS
+        """
+        self.verify("#set $x = 1234\n   #assert $x == 1234   ",
+                    "")
+
+
+class RaiseDirective(OutputTest):
+    def test1(self):
+        """simple #raise ValueError
+
+        Should raise ValueError
+        """
+        def test(self=self):
+            self.verify("#raise ValueError",
+                        ""),
+        self.failUnlessRaises(ValueError, test)
+                              
+    def test2(self):
+        """#raise ValueError in #if block
+
+        Should raise ValueError
+        """
+        def test(self=self):
+            self.verify("#if 1\n#raise ValueError\n#end if\n",
+                        "")
+        self.failUnlessRaises(ValueError, test)
+
+
+    def test3(self):
+        """#raise ValueError in #if block
+
+        Shouldn't raise ValueError
+        """
+        self.verify("#if 0\n#raise ValueError\n#else\nblarg#end if\n",
+                    "blarg\n")
+
+
+
+class ImportDirective(OutputTest):
+    def test1(self):
+        """#import math
+        """
+        self.verify("#import math",
+                    "")
+
+    def test2(self):
+        """#import math + WS
+
+        Should gobble
+        """
+        self.verify("    #import math    ",
+                    "")
+
+    def test3(self):
+        """#import math + WS + leading text
+        
+        Shouldn't gobble
+        """
+        self.verify("  --  #import math    ",
+                    "  --  ")
+        
+    def test4(self):
+        """#from math import syn
+        """
+        self.verify("#from math import cos",
+                    "")
+
+    def test5(self):
+        """#from math import cos  + WS
+        Should gobble
+        """
+        self.verify("    #from math import cos  ",
+                    "")
+
+    def test6(self):
+        """#from math import cos  + WS + leading text
+        Shouldn't gobble
+        """
+        self.verify("  --  #from math import cos  ",
+                    "  --  ")
+
+    def test7(self):
+        """#from math import cos -- use it
+        """
+        self.verify("#from math import cos\n$cos(0)",
+                    "1.0")
+
+    def test8(self):
+        """#from math import cos,tan,sin -- and use them
+        """
+        self.verify("#from math import cos, tan, sin\n$cos(0)-$tan(0)-$sin(0)",
+                    "1.0-0.0-0.0")
+
+    def test9(self):
+        """#import os.path -- use it
+        """
+        
+        self.verify("#import os.path\n$os.path.exists('.')",
+                    repr(True))
+
+    def test10(self):
+        """#import os.path -- use it with NameMapper turned off
+        """
+        self.verify("""##
+#compiler-settings
+useNameMapper=False
+#end compiler-settings
+#import os.path
+$os.path.exists('.')""",
+                    repr(True))
+
+    def test11(self):
+        """#from math import *
+        """
+        
+        self.verify("#from math import *\n$pow(1,2) $log10(10)",
+                    "1.0 1.0")
+
+class CompilerDirective(OutputTest):
+    def test1(self):
+        """overriding the commentStartToken
+        """
+        self.verify("""$anInt##comment
+#compiler commentStartToken = '//'
+$anInt//comment
+""",
+                    "1\n1\n")
+
+    def test2(self):
+        """overriding and resetting the commentStartToken
+        """
+        self.verify("""$anInt##comment
+#compiler commentStartToken = '//'
+$anInt//comment
+#compiler reset
+$anInt//comment
+""",
+                    "1\n1\n1//comment\n")
+
+
+class CompilerSettingsDirective(OutputTest):
+    
+    def test1(self):
+        """overriding the cheetahVarStartToken
+        """
+        self.verify("""$anInt
+#compiler-settings
+cheetahVarStartToken = @
+#end compiler-settings
+@anInt
+#compiler-settings reset
+$anInt
+""",
+                    "1\n1\n1\n")
+
+    def test2(self):
+        """overriding the directiveStartToken
+        """
+        self.verify("""#set $x = 1234
+$x
+#compiler-settings
+directiveStartToken = @
+#end compiler-settings
+@set $x = 1234
+$x
+""",
+                    "1234\n1234\n")
+
+    def test3(self):
+        """overriding the commentStartToken
+        """
+        self.verify("""$anInt##comment
+#compiler-settings
+commentStartToken = //
+#end compiler-settings
+$anInt//comment
+""",
+                    "1\n1\n")
+
+if sys.platform.startswith('java'):
+    del CompilerDirective
+    del CompilerSettingsDirective
+
+class ExtendsDirective(OutputTest):
+
+    def test1(self):
+        """#extends Cheetah.Templates._SkeletonPage"""
+        self.verify("""#from Cheetah.Templates._SkeletonPage import _SkeletonPage
+#extends _SkeletonPage
+#implements respond
+$spacer()
+""",
+                    '<img src="spacer.gif" width="1" height="1" alt="" />\n')
+
+
+        self.verify("""#from Cheetah.Templates._SkeletonPage import _SkeletonPage
+#extends _SkeletonPage
+#implements respond(foo=1234)
+$spacer()$foo
+""",
+                    '<img src="spacer.gif" width="1" height="1" alt="" />1234\n')
+
+    def test2(self):
+        """#extends Cheetah.Templates.SkeletonPage without #import"""
+        self.verify("""#extends Cheetah.Templates.SkeletonPage
+#implements respond
+$spacer()
+""",
+                    '<img src="spacer.gif" width="1" height="1" alt="" />\n')
+
+    def test3(self):
+        """#extends Cheetah.Templates.SkeletonPage.SkeletonPage without #import"""
+        self.verify("""#extends Cheetah.Templates.SkeletonPage.SkeletonPage
+#implements respond
+$spacer()
+""",
+                    '<img src="spacer.gif" width="1" height="1" alt="" />\n')
+
+    def test4(self):
+        """#extends with globals and searchList test"""
+        self.verify("""#extends Cheetah.Templates.SkeletonPage
+#set global g="Hello"
+#implements respond
+$g $numOne
+""",
+                    'Hello 1\n')
+
+
+class SuperDirective(OutputTest):
+    def test1(self):
+        tmpl1 = Template.compile('''$foo $bar(99)
+        #def foo: this is base foo
+        #def bar(arg): super-$arg''')
+
+        tmpl2 = tmpl1.subclass('''
+        #implements dummy
+        #def foo
+          #super
+          This is child foo
+          #super(trans=trans)
+          $bar(1234)
+        #end def
+        #def bar(arg): #super($arg)
+        ''')
+        expected = ('this is base foo          '
+                    'This is child foo\nthis is base foo          '
+                    'super-1234\n super-99')
+        assert str(tmpl2()).strip()==expected
+
+
+class ImportantExampleCases(OutputTest):
+    def test1(self):
+        """how to make a comma-delimited list"""
+        self.verify("""#set $sep = ''
+#for $letter in $letterList
+$sep$letter#slurp
+#set $sep = ', '
+#end for
+""",
+                    "a, b, c")
+
+class FilterDirective(OutputTest):
+    convertEOLs=False
+
+    def _getCompilerSettings(self):
+        return {'useFilterArgsInPlaceholders':True}
+    
+    def test1(self):
+        """#filter Filter
+        """
+        self.verify("#filter Filter\n$none#end filter",
+                    "")
+
+        self.verify("#filter Filter: $none",
+                    "")
+
+    def test2(self):
+        """#filter ReplaceNone with WS
+        """
+        self.verify("#filter Filter  \n$none#end filter",
+                    "")
+
+    def test3(self):
+        """#filter MaxLen -- maxlen of 5"""
+
+        self.verify("#filter MaxLen  \n${tenDigits, $maxlen=5}#end filter",
+                    "12345")
+
+    def test4(self):
+        """#filter MaxLen -- no maxlen
+        """
+        self.verify("#filter MaxLen  \n${tenDigits}#end filter",
+                    "1234567890")
+
+    def test5(self):
+        """#filter WebSafe -- basic usage
+        """
+        self.verify("#filter WebSafe  \n$webSafeTest#end filter",
+                    "abc &lt;=&gt; &amp;")
+
+    def test6(self):
+        """#filter WebSafe -- also space
+        """
+        self.verify("#filter WebSafe  \n${webSafeTest, $also=' '}#end filter",
+                    "abc&nbsp;&lt;=&gt;&nbsp;&amp;")
+        
+    def test7(self):
+        """#filter WebSafe -- also space, without $ on the args
+        """
+        self.verify("#filter WebSafe  \n${webSafeTest, also=' '}#end filter",
+                    "abc&nbsp;&lt;=&gt;&nbsp;&amp;")
+
+    def test8(self):
+        """#filter Strip -- trailing newline
+        """
+        self.verify("#filter Strip\n$strip1#end filter",
+                    "strippable whitespace\n")
+
+    def test9(self):
+        """#filter Strip -- no trailing newine
+        """
+        self.verify("#filter Strip\n$strip2#end filter",
+                    "strippable whitespace")
+
+    def test10(self):
+        """#filter Strip -- multi-line
+        """
+        self.verify("#filter Strip\n$strip3#end filter",
+                    "strippable whitespace\n1 2  3\n")
+
+    def test11(self):
+        """#filter StripSqueeze -- canonicalize all whitespace to ' '
+        """
+        self.verify("#filter StripSqueeze\n$strip3#end filter",
+                    "strippable whitespace 1 2 3")
+
+
+class EchoDirective(OutputTest):
+    def test1(self):
+        """#echo 1234
+        """
+        self.verify("#echo 1234",
+                    "1234")
+
+class SilentDirective(OutputTest):
+    def test1(self):
+        """#silent 1234
+        """
+        self.verify("#silent 1234",
+                    "")
+
+class ErrorCatcherDirective(OutputTest):
+    pass
+
+
+class VarExists(OutputTest):               # Template.varExists()
+    
+    def test1(self):
+        """$varExists('$anInt')
+        """
+        self.verify("$varExists('$anInt')",
+                    repr(True))
+
+    def test2(self):
+        """$varExists('anInt')
+        """
+        self.verify("$varExists('anInt')",
+                    repr(True))
+
+    def test3(self):
+        """$varExists('$anInt')
+        """
+        self.verify("$varExists('$bogus')",
+                    repr(False))
+
+    def test4(self):
+        """$varExists('$anInt') combined with #if false
+        """
+        self.verify("#if $varExists('$bogus')\n1234\n#else\n999\n#end if",
+                    "999\n")
+
+    def test5(self):
+        """$varExists('$anInt') combined with #if true
+        """
+        self.verify("#if $varExists('$anInt')\n1234\n#else\n999#end if",
+                    "1234\n")
+
+class GetVar(OutputTest):               # Template.getVar()
+    def test1(self):
+        """$getVar('$anInt')
+        """
+        self.verify("$getVar('$anInt')",
+                    "1")
+
+    def test2(self):
+        """$getVar('anInt')
+        """
+        self.verify("$getVar('anInt')",
+                    "1")
+
+    def test3(self):
+        """$self.getVar('anInt')
+        """
+        self.verify("$self.getVar('anInt')",
+                    "1")
+        
+    def test4(self):
+        """$getVar('bogus', 1234)
+        """
+        self.verify("$getVar('bogus',  1234)",
+                    "1234")
+        
+    def test5(self):
+        """$getVar('$bogus', 1234)
+        """
+        self.verify("$getVar('$bogus',  1234)",
+                    "1234")
+
+
+class MiscComplexSyntax(OutputTest):
+    def test1(self):
+        """Complex use of {},[] and () in a #set expression
+        ----
+        #set $c = {'A':0}[{}.get('a', {'a' : 'A'}['a'])]
+        $c
+        """
+        self.verify("#set $c = {'A':0}[{}.get('a', {'a' : 'A'}['a'])]\n$c",
+                    "0")
+
+
+class CGI(OutputTest):
+    """CGI scripts with(out) the CGI environment and with(out) GET variables.
+    """
+    convertEOLs=False
+    
+    def _beginCGI(self):  
+        os.environ['REQUEST_METHOD'] = "GET"
+    def _endCGI(self):  
+        try:
+            del os.environ['REQUEST_METHOD']
+        except KeyError:
+            pass
+    _guaranteeNoCGI = _endCGI
+
+
+    def test1(self):
+        """A regular template."""
+        self._guaranteeNoCGI()
+        source = "#extends Cheetah.Tools.CGITemplate\n" + \
+                 "#implements respond\n" + \
+                 "$cgiHeaders#slurp\n" + \
+                 "Hello, world!" 
+        self.verify(source, "Hello, world!")
+
+
+    def test2(self):
+        """A CGI script."""
+        self._beginCGI()
+        source = "#extends Cheetah.Tools.CGITemplate\n" + \
+                 "#implements respond\n" + \
+                 "$cgiHeaders#slurp\n" + \
+                 "Hello, world!" 
+        self.verify(source, "Content-type: text/html\n\nHello, world!")
+        self._endCGI()
+
+
+    def test3(self):
+        """A (pseudo) Webware servlet.
+           
+           This uses the Python syntax escape to set
+           self._CHEETAH__isControlledByWebKit.           
+           We could instead do '#silent self._CHEETAH__isControlledByWebKit = True',
+           taking advantage of the fact that it will compile unchanged as long
+           as there's no '$' in the statement.  (It won't compile with an '$'
+           because that would convert to a function call, and you can't assign
+           to a function call.)  Because this isn't really being called from
+           Webware, we'd better not use any Webware services!  Likewise, we'd
+           better not call $cgiImport() because it would be misled.
+        """
+        self._beginCGI()
+        source = "#extends Cheetah.Tools.CGITemplate\n" + \
+                 "#implements respond\n" + \
+                 "<% self._CHEETAH__isControlledByWebKit = True %>#slurp\n" + \
+                 "$cgiHeaders#slurp\n" + \
+                 "Hello, world!"
+        self.verify(source, "Hello, world!")
+        self._endCGI()
+
+
+    def test4(self):
+        """A CGI script with a GET variable."""
+        self._beginCGI()
+        os.environ['QUERY_STRING'] = "cgiWhat=world"
+        source = "#extends Cheetah.Tools.CGITemplate\n" + \
+                 "#implements respond\n" + \
+                 "$cgiHeaders#slurp\n" + \
+                 "#silent $webInput(['cgiWhat'])##slurp\n" + \
+                 "Hello, $cgiWhat!"
+        self.verify(source, 
+                    "Content-type: text/html\n\nHello, world!")
+        del os.environ['QUERY_STRING']
+        self._endCGI()
+
+
+
+class WhitespaceAfterDirectiveTokens(OutputTest):
+    def _getCompilerSettings(self):
+        return {'allowWhitespaceAfterDirectiveStartToken':True}
+
+    def test1(self):
+        self.verify("# for i in range(10): $i",
+                    "0123456789")
+        self.verify("# for i in range(10)\n$i# end for",
+                    "0123456789")
+        self.verify("# for i in range(10)#$i#end for",
+                    "0123456789")
+
+
+
+class DefmacroDirective(OutputTest):
+    def _getCompilerSettings(self):
+        def aMacro(src):
+            return '$aStr'
+        
+        return {'macroDirectives':{'aMacro':aMacro
+                                   }}
+
+    def test1(self):
+        self.verify("""\
+#defmacro inc: #set @src +=1
+#set i = 1
+#inc: $i
+$i""",
+                    "2")
+
+
+
+        self.verify("""\
+#defmacro test
+#for i in range(10): @src
+#end defmacro
+#test: $i-foo#slurp
+#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo012")
+
+        self.verify("""\
+#defmacro test
+#for i in range(10): @src
+#end defmacro
+#test: $i-foo
+#for i in range(3): $i""",
+  "0-foo\n1-foo\n2-foo\n3-foo\n4-foo\n5-foo\n6-foo\n7-foo\n8-foo\n9-foo\n012")
+
+
+        self.verify("""\
+#defmacro test: #for i in range(10): @src
+#test: $i-foo#slurp
+-#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo-012")
+
+        self.verify("""\
+#defmacro test##for i in range(10): @src#end defmacro##slurp
+#test: $i-foo#slurp
+-#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo-012")
+
+        self.verify("""\
+#defmacro testFoo: nothing
+#defmacro test(foo=1234): #for i in range(10): @src
+#test foo=234: $i-foo#slurp
+-#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo-012")
+
+        self.verify("""\
+#defmacro testFoo: nothing
+#defmacro test(foo=1234): #for i in range(10): @src@foo
+#test foo='-foo'#$i#end test#-#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo-012")
+
+        self.verify("""\
+#defmacro testFoo: nothing
+#defmacro test(foo=1234): #for i in range(10): @src.strip()@foo
+#test foo='-foo': $i
+-#for i in range(3): $i""",
+                    "0-foo1-foo2-foo3-foo4-foo5-foo6-foo7-foo8-foo9-foo-012")
+
+    def test2(self):
+        self.verify("#aMacro: foo",
+                    "blarg")
+        self.verify("#defmacro nested: @macros.aMacro(@src)\n#nested: foo",
+                    "blarg")
+
+
+class Indenter(OutputTest):
+    convertEOLs=False
+    
+    source = """
+public class X
+{
+    #for $method in $methods
+        $getMethod($method)
+        
+    #end for
+}
+//end of class
+
+#def getMethod($method)
+    #indent ++
+    public $getType($method) ${method.Name}($getParams($method.Params));
+    #indent --
+#end def
+
+#def getParams($params)
+    #indent off
+
+    #for $counter in $range($len($params))
+        #if $counter == len($params) - 1
+                       $params[$counter]#slurp
+        #else:
+                       $params[$counter], 
+        #end if
+    #end for
+    #indent on
+#end def
+
+#def getType($method)
+    #indent push
+    #indent=0
+    #if $method.Type == "VT_VOID"
+        void#slurp
+    #elif $method.Type == "VT_INT"
+        int#slurp
+    #elif $method.Type == "VT_VARIANT"
+        Object#slurp
+    #end if
+    #indent pop
+#end def
+"""
+
+    control = """
+public class X
+{
+    public void Foo(
+                       _input, 
+                       _output);
+
+
+    public int Bar(
+                       _str1, 
+                       str2, 
+                       _str3);
+
+
+    public Object Add(
+                       value1, 
+                       value);
+
+
+}
+//end of class
+
+
+
+"""
+    def _getCompilerSettings(self):
+        return {'useFilterArgsInPlaceholders':True}
+
+    def searchList(self):    # Inside Indenter class.
+        class Method:
+            def __init__(self, _name, _type, *_params):
+                self.Name = _name
+                self.Type = _type
+                self.Params = _params
+        methods = [Method("Foo", "VT_VOID", "_input", "_output"),
+                   Method("Bar", "VT_INT", "_str1", "str2", "_str3"),
+                   Method("Add", "VT_VARIANT", "value1", "value")]
+        return [{"methods": methods}]
+
+    def test1(self):    # Inside Indenter class.
+        self.verify(self.source, self.control)
+
+
+##################################################
+## CREATE CONVERTED EOL VERSIONS OF THE TEST CASES
+
+if OutputTest._useNewStyleCompilation and versionTuple >= (2, 3):
+    extraCompileKwArgsForDiffBaseclass = {'baseclass':dict}
+else:
+    extraCompileKwArgsForDiffBaseclass = {'baseclass':object}
+    
+
+def install_eols():
+    klasses = [v for v in globals().values() if isinstance(v, type) and issubclass(v, unittest.TestCase)]
+    for klass in klasses:
+        name = klass.__name__        
+        if hasattr(klass, 'convertEOLs') and klass.convertEOLs:
+            win32Src = r"class %(name)s_Win32EOL(%(name)s): _EOLreplacement = '\r\n'"%locals()
+            macSrc = r"class %(name)s_MacEOL(%(name)s): _EOLreplacement = '\r'"%locals()
+            exec(win32Src, globals()) 
+            exec(macSrc, globals())
+
+        if versionTuple >= (2, 3):
+            src = r"class %(name)s_DiffBaseClass(%(name)s): "%locals()
+            src += " _extraCompileKwArgs = extraCompileKwArgsForDiffBaseclass"
+            exec(src, globals())
+
+        del name
+        del klass
+
+##################################################
+## if run from the command line ##
+        
+if __name__ == '__main__':
+    install_eols()
+    unittest.main()
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Tests/Template.py b/cheetah/Tests/Template.py
new file mode 100644 (file)
index 0000000..89c84ed
--- /dev/null
@@ -0,0 +1,363 @@
+#!/usr/bin/env python
+
+import pdb
+import sys
+import types
+import os
+import os.path
+import tempfile
+import shutil
+import unittest
+from Cheetah.Template import Template
+
+majorVer, minorVer = sys.version_info[0], sys.version_info[1]
+versionTuple = (majorVer, minorVer)
+
+class TemplateTest(unittest.TestCase):
+    pass
+
+class ClassMethods_compile(TemplateTest):
+    """I am using the same Cheetah source for each test to root out clashes
+    caused by the compile caching in Template.compile().
+    """
+    
+    def test_basicUsage(self):
+        klass = Template.compile(source='$foo')
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+
+    def test_baseclassArg(self):
+        klass = Template.compile(source='$foo', baseclass=dict)
+        t = klass({'foo':1234})
+        assert str(t)=='1234'
+
+        klass2 = Template.compile(source='$foo', baseclass=klass)
+        t = klass2({'foo':1234})
+        assert str(t)=='1234'
+
+        klass3 = Template.compile(source='#implements dummy\n$bar', baseclass=klass2)
+        t = klass3({'foo':1234})
+        assert str(t)=='1234'
+
+        klass4 = Template.compile(source='$foo', baseclass='dict')
+        t = klass4({'foo':1234})
+        assert str(t)=='1234'
+
+    def test_moduleFileCaching(self):
+        if versionTuple < (2, 3):
+            return
+        tmpDir = tempfile.mkdtemp()
+        try:
+            #print tmpDir
+            assert os.path.exists(tmpDir)
+            klass = Template.compile(source='$foo',
+                                     cacheModuleFilesForTracebacks=True,
+                                     cacheDirForModuleFiles=tmpDir)
+            mod = sys.modules[klass.__module__]
+            #print mod.__file__
+            assert os.path.exists(mod.__file__)
+            assert os.path.dirname(mod.__file__)==tmpDir
+        finally:
+            shutil.rmtree(tmpDir, True)
+
+    def test_classNameArg(self):
+        klass = Template.compile(source='$foo', className='foo123')
+        assert klass.__name__=='foo123'
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+
+    def test_moduleNameArg(self):
+        klass = Template.compile(source='$foo', moduleName='foo99')
+        mod = sys.modules['foo99']
+        assert klass.__name__=='foo99'
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+
+
+        klass = Template.compile(source='$foo',
+                                 moduleName='foo1',
+                                 className='foo2')
+        mod = sys.modules['foo1']
+        assert klass.__name__=='foo2'
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+
+
+    def test_mainMethodNameArg(self):
+        klass = Template.compile(source='$foo',
+                                 className='foo123',
+                                 mainMethodName='testMeth')
+        assert klass.__name__=='foo123'
+        t = klass(namespaces={'foo':1234})
+        #print t.generatedClassCode()
+        assert str(t)=='1234'
+        assert t.testMeth()=='1234'
+
+        klass = Template.compile(source='$foo',
+                                 moduleName='fooXXX',                                 
+                                 className='foo123',
+                                 mainMethodName='testMeth',
+                                 baseclass=dict)
+        assert klass.__name__=='foo123'
+        t = klass({'foo':1234})
+        #print t.generatedClassCode()
+        assert str(t)=='1234'
+        assert t.testMeth()=='1234'
+
+
+
+    def test_moduleGlobalsArg(self):
+        klass = Template.compile(source='$foo',
+                                 moduleGlobals={'foo':1234})
+        t = klass()
+        assert str(t)=='1234'
+
+        klass2 = Template.compile(source='$foo', baseclass='Test1',
+                                  moduleGlobals={'Test1':dict})
+        t = klass2({'foo':1234})
+        assert str(t)=='1234'
+
+        klass3 = Template.compile(source='$foo', baseclass='Test1',
+                                  moduleGlobals={'Test1':dict, 'foo':1234})
+        t = klass3()
+        assert str(t)=='1234'
+
+
+    def test_keepRefToGeneratedCodeArg(self):
+        klass = Template.compile(source='$foo',
+                                 className='unique58',
+                                 cacheCompilationResults=False,
+                                 keepRefToGeneratedCode=False)
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        assert not t.generatedModuleCode()
+
+
+        klass2 = Template.compile(source='$foo',
+                                 className='unique58',
+                                 keepRefToGeneratedCode=True)
+        t = klass2(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        assert t.generatedModuleCode()
+
+        klass3 = Template.compile(source='$foo',
+                                 className='unique58',
+                                 keepRefToGeneratedCode=False)
+        t = klass3(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        # still there as this class came from the cache
+        assert t.generatedModuleCode() 
+
+
+    def test_compilationCache(self):
+        klass = Template.compile(source='$foo',
+                                 className='unique111',
+                                 cacheCompilationResults=False)
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        assert not klass._CHEETAH_isInCompilationCache
+
+
+        # this time it will place it in the cache
+        klass = Template.compile(source='$foo',
+                                 className='unique111',
+                                 cacheCompilationResults=True)
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        assert klass._CHEETAH_isInCompilationCache
+
+        # by default it will be in the cache
+        klass = Template.compile(source='$foo',
+                                 className='unique999099')
+        t = klass(namespaces={'foo':1234})
+        assert str(t)=='1234'
+        assert klass._CHEETAH_isInCompilationCache
+
+
+class ClassMethods_subclass(TemplateTest):
+
+    def test_basicUsage(self):
+        klass = Template.compile(source='$foo', baseclass=dict)
+        t = klass({'foo':1234})
+        assert str(t)=='1234'
+
+        klass2 = klass.subclass(source='$foo')
+        t = klass2({'foo':1234})
+        assert str(t)=='1234'
+
+        klass3 = klass2.subclass(source='#implements dummy\n$bar')
+        t = klass3({'foo':1234})
+        assert str(t)=='1234'
+        
+
+class Preprocessors(TemplateTest):
+
+    def test_basicUsage1(self):
+        src='''\
+        %set foo = @a
+        $(@foo*10)
+        @a'''
+        src = '\n'.join([ln.strip() for ln in src.splitlines()])
+        preprocessors = {'tokens':'@ %',
+                         'namespaces':{'a':99}
+                         }
+        klass = Template.compile(src, preprocessors=preprocessors)
+        assert str(klass())=='990\n99'
+
+    def test_normalizePreprocessorArgVariants(self):
+        src='%set foo = 12\n%%comment\n$(@foo*10)'
+
+        class Settings1: tokens = '@ %' 
+        Settings1 = Settings1()
+            
+        from Cheetah.Template import TemplatePreprocessor
+        settings = Template._normalizePreprocessorSettings(Settings1)
+        preprocObj = TemplatePreprocessor(settings)
+
+        def preprocFunc(source, file):
+            return '$(12*10)', None
+
+        class TemplateSubclass(Template):
+            pass
+
+        compilerSettings = {'cheetahVarStartToken': '@',
+                            'directiveStartToken': '%',
+                            'commentStartToken': '%%',
+                            }
+        
+        for arg in ['@ %',
+                    {'tokens':'@ %'},
+                    {'compilerSettings':compilerSettings},
+                    {'compilerSettings':compilerSettings,
+                     'templateInitArgs':{}},
+                    {'tokens':'@ %',
+                     'templateAPIClass':TemplateSubclass},
+                    Settings1,
+                    preprocObj,
+                    preprocFunc,                    
+                    ]:
+            
+            klass = Template.compile(src, preprocessors=arg)
+            assert str(klass())=='120'
+
+
+    def test_complexUsage(self):
+        src='''\
+        %set foo = @a
+        %def func1: #def func(arg): $arg("***")
+        %% comment
+        $(@foo*10)
+        @func1
+        $func(lambda x:c"--$x--@a")'''
+        src = '\n'.join([ln.strip() for ln in src.splitlines()])
+
+        
+        for arg in [{'tokens':'@ %', 'namespaces':{'a':99} },
+                    {'tokens':'@ %', 'namespaces':{'a':99} },
+                    ]:
+            klass = Template.compile(src, preprocessors=arg)
+            t = klass()
+            assert str(t)=='990\n--***--99'
+
+
+
+    def test_i18n(self):
+        src='''\
+        %i18n: This is a $string that needs translation
+        %i18n id="foo", domain="root": This is a $string that needs translation
+        '''
+        src = '\n'.join([ln.strip() for ln in src.splitlines()])
+        klass = Template.compile(src, preprocessors='@ %', baseclass=dict)
+        t = klass({'string':'bit of text'})
+        #print str(t), repr(str(t))
+        assert str(t)==('This is a bit of text that needs translation\n'*2)[:-1]
+
+
+class TryExceptImportTest(TemplateTest):
+    def test_FailCase(self):
+        ''' Test situation where an inline #import statement will get relocated '''
+        source = '''
+            #def myFunction()
+                Ahoy!
+                #try
+                    #import sys
+                #except ImportError
+                    $print "This will never happen!"
+                #end try
+            #end def
+            '''
+        # This should raise an IndentationError (if the bug exists)
+        klass = Template.compile(source=source, compilerSettings={'useLegacyImportMode' : False})
+        t = klass(namespaces={'foo' : 1234})
+
+class ClassMethodSupport(TemplateTest):
+    def test_BasicDecorator(self):
+        if sys.version_info[0] == 2 and sys.version_info[1] == 3:
+                print('This version of Python doesn\'t support decorators, skipping tests')
+                return
+        template = '''
+            #@classmethod
+            #def myClassMethod()
+                #return '$foo = %s' % $foo
+            #end def
+        '''
+        template = Template.compile(source=template)
+        try:
+            rc = template.myClassMethod(foo='bar')
+            assert rc == '$foo = bar', (rc, 'Template class method didn\'t return what I expected')
+        except AttributeError, ex:
+            self.fail(ex)
+
+class StaticMethodSupport(TemplateTest):
+    def test_BasicDecorator(self):
+        if sys.version_info[0] == 2 and sys.version_info[1] == 3:
+                print('This version of Python doesn\'t support decorators, skipping tests')
+                return
+        template = '''
+            #@staticmethod
+            #def myStaticMethod()
+                #return '$foo = %s' % $foo
+            #end def
+        '''
+        template = Template.compile(source=template)
+        try:
+            rc = template.myStaticMethod(foo='bar')
+            assert rc == '$foo = bar', (rc, 'Template class method didn\'t return what I expected')
+        except AttributeError, ex:
+            self.fail(ex)
+
+class Useless(object):
+    def boink(self):
+        return [1, 2, 3]
+
+class MultipleInheritanceSupport(TemplateTest):
+    def runTest(self):
+        template = '''
+            #extends Template, Useless
+            #def foo()
+                #return [4,5] + $boink()
+            #end def
+        '''
+        template = Template.compile(template,
+                moduleGlobals={'Useless' : Useless},
+                compilerSettings={'autoImportForExtendsDirective' : False})
+        template = template()
+        result = template.foo()
+        assert result == [4, 5, 1, 2, 3], (result, 'Unexpected result')
+
+class SubclassSearchListTest(TemplateTest):
+    '''
+        Verify that if we subclass Template, we can still
+        use attributes on that subclass in the searchList
+    '''
+    def runTest(self):
+        class Sub(Template):
+            greeting = 'Hola'
+        tmpl = Sub('''When we meet, I say "${greeting}"''')
+        self.assertEquals(unicode(tmpl), 'When we meet, I say "Hola"')
+
+##################################################
+## if run from the command line ##
+        
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/Test.py b/cheetah/Tests/Test.py
new file mode 100755 (executable)
index 0000000..8e78a8e
--- /dev/null
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+'''
+Core module of Cheetah's Unit-testing framework
+
+TODO
+================================================================================
+# combo tests
+# negative test cases for expected exceptions
+# black-box vs clear-box testing
+# do some tests that run the Template for long enough to check that the refresh code works
+'''
+
+import sys
+import unittest
+
+from Cheetah.Tests import SyntaxAndOutput
+from Cheetah.Tests import NameMapper
+from Cheetah.Tests import Misc
+from Cheetah.Tests import Filters
+from Cheetah.Tests import Template
+from Cheetah.Tests import Cheps
+from Cheetah.Tests import Parser
+from Cheetah.Tests import Regressions
+from Cheetah.Tests import Unicode
+from Cheetah.Tests import CheetahWrapper
+from Cheetah.Tests import Analyzer
+
+SyntaxAndOutput.install_eols()
+
+suites = [
+   unittest.findTestCases(SyntaxAndOutput),
+   unittest.findTestCases(NameMapper),
+   unittest.findTestCases(Filters),
+   unittest.findTestCases(Template),
+   #unittest.findTestCases(Cheps),
+   unittest.findTestCases(Regressions),
+   unittest.findTestCases(Unicode),
+   unittest.findTestCases(Misc),
+   unittest.findTestCases(Parser),
+   unittest.findTestCases(Analyzer),
+]
+
+if not sys.platform.startswith('java'):
+    suites.append(unittest.findTestCases(CheetahWrapper))
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    if 'xml' in sys.argv:
+        import xmlrunner
+        runner = xmlrunner.XMLTestRunner(filename='Cheetah-Tests.xml')
+    
+    results = runner.run(unittest.TestSuite(suites))
+
diff --git a/cheetah/Tests/Unicode.py b/cheetah/Tests/Unicode.py
new file mode 100644 (file)
index 0000000..c881d86
--- /dev/null
@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+# -*- encoding: utf8 -*-
+
+from Cheetah.Template import Template
+from Cheetah import CheetahWrapper
+from Cheetah import DummyTransaction
+import imp
+import os
+import sys
+import tempfile
+import unittest
+
+class CommandLineTest(unittest.TestCase):
+    def createAndCompile(self, source):
+        sourcefile = '-'
+        while sourcefile.find('-') != -1:
+            sourcefile = tempfile.mktemp()
+        
+        fd = open('%s.tmpl' % sourcefile, 'w')
+        fd.write(source)
+        fd.close()
+
+        wrap = CheetahWrapper.CheetahWrapper()
+        wrap.main(['cheetah', 'compile', '--quiet', '--nobackup', sourcefile])
+        module_path, module_name = os.path.split(sourcefile)
+        module = loadModule(module_name, [module_path])
+        template = getattr(module, module_name)
+        return template
+
+class JBQ_UTF8_Test1(unittest.TestCase):
+    def runTest(self):
+        t = Template.compile(source="""Main file with |$v|
+
+        $other""")
+
+        otherT = Template.compile(source="Other template with |$v|")
+        other = otherT()
+        t.other = other
+
+        t.v = u'Unicode String'
+        t.other.v = u'Unicode String'
+
+        assert unicode(t())
+
+class JBQ_UTF8_Test2(unittest.TestCase):
+    def runTest(self):
+        t = Template.compile(source="""Main file with |$v|
+
+        $other""")
+
+        otherT = Template.compile(source="Other template with |$v|")
+        other = otherT()
+        t.other = other
+
+        t.v = u'Unicode String with eacute Ã©'
+        t.other.v = u'Unicode String'
+
+        assert unicode(t())
+
+
+class JBQ_UTF8_Test3(unittest.TestCase):
+    def runTest(self):
+        t = Template.compile(source="""Main file with |$v|
+
+        $other""")
+
+        otherT = Template.compile(source="Other template with |$v|")
+        other = otherT()
+        t.other = other
+
+        t.v = u'Unicode String with eacute Ã©'
+        t.other.v = u'Unicode String and an eacute Ã©'
+
+        assert unicode(t())
+
+class JBQ_UTF8_Test4(unittest.TestCase):
+    def runTest(self):
+        t = Template.compile(source="""#encoding utf-8
+        Main file with |$v| and eacute in the template Ã©""")
+
+        t.v = 'Unicode String'
+
+        assert unicode(t())
+
+class JBQ_UTF8_Test5(unittest.TestCase):
+    def runTest(self):
+        t = Template.compile(source="""#encoding utf-8
+        Main file with |$v| and eacute in the template Ã©""")
+
+        t.v = u'Unicode String'
+
+        assert unicode(t())
+
+def loadModule(moduleName, path=None):
+    if path:
+        assert isinstance(path, list)
+    try:
+        mod = sys.modules[moduleName]
+    except KeyError:
+        fp = None
+
+        try:
+            fp, pathname, description = imp.find_module(moduleName, path)
+            mod = imp.load_module(moduleName, fp, pathname, description)
+        finally:
+            if fp:
+                fp.close()
+    return mod
+
+class JBQ_UTF8_Test6(unittest.TestCase):
+    def runTest(self):
+        source = """#encoding utf-8
+        #set $someUnicodeString = u"Bébé"
+        Main file with |$v| and eacute in the template Ã©"""
+        t = Template.compile(source=source)
+
+        t.v = u'Unicode String'
+
+        assert unicode(t())
+
+class JBQ_UTF8_Test7(CommandLineTest):
+    def runTest(self):
+        source = """#encoding utf-8
+        #set $someUnicodeString = u"Bébé"
+        Main file with |$v| and eacute in the template Ã©"""
+
+        template = self.createAndCompile(source)
+        template.v = u'Unicode String'
+
+        assert unicode(template())
+
+class JBQ_UTF8_Test8(CommandLineTest):
+    def testStaticCompile(self):
+        source = """#encoding utf-8
+#set $someUnicodeString = u"Bébé"
+$someUnicodeString"""
+
+        template = self.createAndCompile(source)()
+
+        a = unicode(template).encode("utf-8")
+        self.assertEquals("Bébé", a)
+
+    def testDynamicCompile(self):
+        source = """#encoding utf-8
+#set $someUnicodeString = u"Bébé"
+$someUnicodeString"""
+
+        template = Template(source = source)
+
+        a = unicode(template).encode("utf-8")
+        self.assertEquals("Bébé", a)
+
+class EncodeUnicodeCompatTest(unittest.TestCase):
+    """
+        Taken initially from Red Hat's bugzilla #529332
+        https://bugzilla.redhat.com/show_bug.cgi?id=529332
+    """
+    def runTest(self):
+        t = Template("""Foo ${var}""", filter='EncodeUnicode')
+        t.var = u"Text with some non-ascii characters: Ã¥Ã¤Ã¶"
+        
+        rc = t.respond()
+        assert isinstance(rc, unicode), ('Template.respond() should return unicode', rc)
+        
+        rc = str(t)
+        assert isinstance(rc, str), ('Template.__str__() should return a UTF-8 encoded string', rc)
+
+
+class Unicode_in_SearchList_Test(CommandLineTest):
+    def test_BasicASCII(self):
+        source = '''This is $adjective'''
+
+        template = self.createAndCompile(source)
+        assert template and issubclass(template, Template)
+        template = template(searchList=[{'adjective' : u'neat'}])
+        assert template.respond()
+
+    def test_Thai(self):
+        # The string is something in Thai
+        source = '''This is $foo $adjective'''
+        template = self.createAndCompile(source)
+        assert template and issubclass(template, Template)
+        template = template(searchList=[{'foo' : 'bar', 
+            'adjective' : u'\u0e22\u0e34\u0e19\u0e14\u0e35\u0e15\u0e49\u0e2d\u0e19\u0e23\u0e31\u0e1a'}])
+        assert template.respond()
+
+    def test_Thai_utf8(self):
+        utf8 = '\xe0\xb8\xa2\xe0\xb8\xb4\xe0\xb8\x99\xe0\xb8\x94\xe0\xb8\xb5\xe0\xb8\x95\xe0\xb9\x89\xe0\xb8\xad\xe0\xb8\x99\xe0\xb8\xa3\xe0\xb8\xb1\xe0\xb8\x9a'
+
+        source = '''This is $adjective'''
+        template = self.createAndCompile(source)
+        assert template and issubclass(template, Template)
+        template = template(searchList=[{'adjective' : utf8}])
+        assert template.respond()
+
+
+class InlineSpanishTest(unittest.TestCase):
+    def setUp(self):
+        super(InlineSpanishTest, self).setUp()
+        self.template = '''
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Pagina del vendedor</title>
+  </head>
+  <body>
+    $header
+    <h2>Bienvenido $nombre.</h2>
+    <br /><br /><br />
+    <center>
+      Usted tiene $numpedidos_noconf <a href="">pedidós</a> sin confirmar.
+      <br /><br />
+      Bodega tiene fecha para $numpedidos_bodega <a href="">pedidos</a>.
+    </center>
+  </body>
+</html>
+        '''
+
+    def test_failure(self):
+        """ Test a template lacking a proper #encoding tag """
+        self.failUnlessRaises(UnicodeDecodeError, Template, self.template, searchList=[{'header' : '',
+                        'nombre' : '', 'numpedidos_bodega' : '',
+                        'numpedidos_noconf' : ''}])
+
+    def test_success(self):
+        """ Test a template with a proper #encoding tag """
+        template = '#encoding utf-8\n%s' % self.template
+        template = Template(template, searchList=[{'header' : '',
+                        'nombre' : '', 'numpedidos_bodega' : '',
+                        'numpedidos_noconf' : ''}])
+        self.assertTrue(unicode(template))
+
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cheetah/Tests/__init__.py b/cheetah/Tests/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/cheetah/Tests/xmlrunner.py b/cheetah/Tests/xmlrunner.py
new file mode 100644 (file)
index 0000000..36b5d8d
--- /dev/null
@@ -0,0 +1,381 @@
+"""
+XML Test Runner for PyUnit
+"""
+
+# Written by Sebastian Rittau <srittau@jroger.in-berlin.de> and placed in
+# the Public Domain. With contributions by Paolo Borelli.
+
+__revision__ = "$Id: /private/python/stdlib/xmlrunner.py 16654 2007-11-12T12:46:35.368945Z srittau  $"
+
+import os.path
+import re
+import sys
+import time
+import traceback
+import unittest
+from StringIO import StringIO
+from xml.sax.saxutils import escape
+
+from StringIO import StringIO
+
+
+
+class _TestInfo(object):
+
+    """Information about a particular test.
+    
+    Used by _XMLTestResult.
+    
+    """
+
+    def __init__(self, test, time):
+        _pieces = test.id().split('.')
+        (self._class, self._method) = ('.'.join(_pieces[:-1]), _pieces[-1])
+        self._time = time
+        self._error = None
+        self._failure = None
+
+
+    def print_report(self, stream):
+        """Print information about this test case in XML format to the
+        supplied stream.
+
+        """
+        stream.write('  <testcase classname="%(class)s" name="%(method)s" time="%(time).4f">' % \
+            {
+                "class": self._class,
+                "method": self._method,
+                "time": self._time,
+            })
+        if self._failure != None:
+            self._print_error(stream, 'failure', self._failure)
+        if self._error != None:
+            self._print_error(stream, 'error', self._error)
+        stream.write('</testcase>\n')
+
+    def _print_error(self, stream, tagname, error):
+        """Print information from a failure or error to the supplied stream."""
+        text = escape(str(error[1]))
+        stream.write('\n')
+        stream.write('    <%s type="%s">%s\n' \
+            % (tagname, issubclass(error[0], Exception) and error[0].__name__ or str(error[0]), text))
+        tb_stream = StringIO()
+        traceback.print_tb(error[2], None, tb_stream)
+        stream.write(escape(tb_stream.getvalue()))
+        stream.write('    </%s>\n' % tagname)
+        stream.write('  ')
+
+# Module level functions since Python 2.3 doesn't grok decorators
+def create_success(test, time):
+    """Create a _TestInfo instance for a successful test."""
+    return _TestInfo(test, time)
+
+def create_failure(test, time, failure):
+    """Create a _TestInfo instance for a failed test."""
+    info = _TestInfo(test, time)
+    info._failure = failure
+    return info
+
+def create_error(test, time, error):
+    """Create a _TestInfo instance for an erroneous test."""
+    info = _TestInfo(test, time)
+    info._error = error
+    return info
+
+class _XMLTestResult(unittest.TestResult):
+
+    """A test result class that stores result as XML.
+
+    Used by XMLTestRunner.
+
+    """
+
+    def __init__(self, classname):
+        unittest.TestResult.__init__(self)
+        self._test_name = classname
+        self._start_time = None
+        self._tests = []
+        self._error = None
+        self._failure = None
+
+    def startTest(self, test):
+        unittest.TestResult.startTest(self, test)
+        self._error = None
+        self._failure = None
+        self._start_time = time.time()
+
+    def stopTest(self, test):
+        time_taken = time.time() - self._start_time
+        unittest.TestResult.stopTest(self, test)
+        if self._error:
+            info = create_error(test, time_taken, self._error)
+        elif self._failure:
+            info = create_failure(test, time_taken, self._failure)
+        else:
+            info = create_success(test, time_taken)
+        self._tests.append(info)
+
+    def addError(self, test, err):
+        unittest.TestResult.addError(self, test, err)
+        self._error = err
+
+    def addFailure(self, test, err):
+        unittest.TestResult.addFailure(self, test, err)
+        self._failure = err
+
+    def print_report(self, stream, time_taken, out, err):
+        """Prints the XML report to the supplied stream.
+        
+        The time the tests took to perform as well as the captured standard
+        output and standard error streams must be passed in.a
+
+        """
+        stream.write('<testsuite errors="%(e)d" failures="%(f)d" ' % \
+            { "e": len(self.errors), "f": len(self.failures) })
+        stream.write('name="%(n)s" tests="%(t)d" time="%(time).3f">\n' % \
+            {
+                "n": self._test_name,
+                "t": self.testsRun,
+                "time": time_taken,
+            })
+        for info in self._tests:
+            info.print_report(stream)
+        stream.write('  <system-out><![CDATA[%s]]></system-out>\n' % out)
+        stream.write('  <system-err><![CDATA[%s]]></system-err>\n' % err)
+        stream.write('</testsuite>\n')
+
+
+class XMLTestRunner(object):
+
+    """A test runner that stores results in XML format compatible with JUnit.
+
+    XMLTestRunner(stream=None) -> XML test runner
+
+    The XML file is written to the supplied stream. If stream is None, the
+    results are stored in a file called TEST-<module>.<class>.xml in the
+    current working directory (if not overridden with the path property),
+    where <module> and <class> are the module and class name of the test class.
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        self._stream = kwargs.get('stream')
+        self._filename = kwargs.get('filename')
+        self._path = "."
+
+    def run(self, test):
+        """Run the given test case or test suite."""
+        class_ = test.__class__
+        classname = class_.__module__ + "." + class_.__name__
+        if self._stream == None:
+            filename = "TEST-%s.xml" % classname
+            if self._filename:
+                filename = self._filename
+            stream = file(os.path.join(self._path, filename), "w")
+            stream.write('<?xml version="1.0" encoding="utf-8"?>\n')
+        else:
+            stream = self._stream
+
+        result = _XMLTestResult(classname)
+        start_time = time.time()
+
+        # TODO: Python 2.5: Use the with statement
+        old_stdout = sys.stdout
+        old_stderr = sys.stderr
+        sys.stdout = StringIO()
+        sys.stderr = StringIO()
+
+        try:
+            test(result)
+            try:
+                out_s = sys.stdout.getvalue()
+            except AttributeError:
+                out_s = ""
+            try:
+                err_s = sys.stderr.getvalue()
+            except AttributeError:
+                err_s = ""
+        finally:
+            sys.stdout = old_stdout
+            sys.stderr = old_stderr
+
+        time_taken = time.time() - start_time
+        result.print_report(stream, time_taken, out_s, err_s)
+        if self._stream == None:
+            stream.close()
+
+        return result
+
+    def _set_path(self, path):
+        self._path = path
+
+    path = property(lambda self: self._path, _set_path, None,
+            """The path where the XML files are stored.
+            
+            This property is ignored when the XML file is written to a file
+            stream.""")
+
+
+class XMLTestRunnerTest(unittest.TestCase):
+    def setUp(self):
+        self._stream = StringIO()
+
+    def _try_test_run(self, test_class, expected):
+
+        """Run the test suite against the supplied test class and compare the
+        XML result against the expected XML string. Fail if the expected
+        string doesn't match the actual string. All time attribute in the
+        expected string should have the value "0.000". All error and failure
+        messages are reduced to "Foobar".
+
+        """
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(test_class))
+
+        got = self._stream.getvalue()
+        # Replace all time="X.YYY" attributes by time="0.000" to enable a
+        # simple string comparison.
+        got = re.sub(r'time="\d+\.\d+"', 'time="0.000"', got)
+        # Likewise, replace all failure and error messages by a simple "Foobar"
+        # string.
+        got = re.sub(r'(?s)<failure (.*?)>.*?</failure>', r'<failure \1>Foobar</failure>', got)
+        got = re.sub(r'(?s)<error (.*?)>.*?</error>', r'<error \1>Foobar</error>', got)
+
+        self.assertEqual(expected, got)
+
+    def test_no_tests(self):
+        """Regression test: Check whether a test run without any tests
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            pass
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="0" time="0.000">
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_success(self):
+        """Regression test: Check whether a test run with a successful test
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                pass
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_failure(self):
+        """Regression test: Check whether a test run with a failing test
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                self.assert_(False)
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="1" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
+    <failure type="exceptions.AssertionError">Foobar</failure>
+  </testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_error(self):
+        """Regression test: Check whether a test run with a erroneous test
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                raise IndexError()
+        self._try_test_run(TestTest, """<testsuite errors="1" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000">
+    <error type="exceptions.IndexError">Foobar</error>
+  </testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_stdout_capture(self):
+        """Regression test: Check whether a test run with output to stdout
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                print("Test")
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[Test
+]]></system-out>
+  <system-err><![CDATA[]]></system-err>
+</testsuite>
+""")
+
+    def test_stderr_capture(self):
+        """Regression test: Check whether a test run with output to stderr
+        matches a previous run.
+        
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stderr.write('Test\n')
+        self._try_test_run(TestTest, """<testsuite errors="0" failures="0" name="unittest.TestSuite" tests="1" time="0.000">
+  <testcase classname="__main__.TestTest" name="test_foo" time="0.000"></testcase>
+  <system-out><![CDATA[]]></system-out>
+  <system-err><![CDATA[Test
+]]></system-err>
+</testsuite>
+""")
+
+    class NullStream(object):
+        """A file-like object that discards everything written to it."""
+        def write(self, buffer):
+            pass
+
+    def test_unittests_changing_stdout(self):
+        """Check whether the XMLTestRunner recovers gracefully from unit tests
+        that change stdout, but don't change it back properly.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stdout = XMLTestRunnerTest.NullStream()
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(TestTest))
+
+    def test_unittests_changing_stderr(self):
+        """Check whether the XMLTestRunner recovers gracefully from unit tests
+        that change stderr, but don't change it back properly.
+
+        """
+        class TestTest(unittest.TestCase):
+            def test_foo(self):
+                sys.stderr = XMLTestRunnerTest.NullStream()
+
+        runner = XMLTestRunner(self._stream)
+        runner.run(unittest.makeSuite(TestTest))
+
+
+class XMLTestProgram(unittest.TestProgram):
+    def runTests(self):
+        if self.testRunner is None:
+            self.testRunner = XMLTestRunner()
+        unittest.TestProgram.runTests(self)
+
+main = XMLTestProgram
+
+
+if __name__ == "__main__":
+    main(module=None)
diff --git a/cheetah/Tools/CGITemplate.py b/cheetah/Tools/CGITemplate.py
new file mode 100644 (file)
index 0000000..1349b5b
--- /dev/null
@@ -0,0 +1,77 @@
+# $Id: CGITemplate.py,v 1.6 2006/01/29 02:09:59 tavis_rudd Exp $
+"""A subclass of Cheetah.Template for use in CGI scripts.
+
+Usage in a template:
+    #extends Cheetah.Tools.CGITemplate
+    #implements respond
+    $cgiHeaders#slurp
+
+Usage in a template inheriting a Python class:
+1. The template
+    #extends MyPythonClass
+    #implements respond
+    $cgiHeaders#slurp
+
+2. The Python class
+    from Cheetah.Tools import CGITemplate
+    class MyPythonClass(CGITemplate):
+        def cgiHeadersHook(self):
+            return "Content-Type: text/html; charset=koi8-r\n\n"
+
+To read GET/POST variables, use the .webInput method defined in
+Cheetah.Utils.WebInputMixin (available in all templates without importing
+anything), use Python's 'cgi' module, or make your own arrangements.
+
+This class inherits from Cheetah.Template to make it usable in Cheetah's
+single-inheritance model.  
+
+
+Meta-Data
+================================================================================
+Author: Mike Orr <iron@mso.oz.net>
+License: This software is released for unlimited distribution under the
+         terms of the MIT license.  See the LICENSE file.
+Version: $Revision: 1.6 $
+Start Date: 2001/10/03
+Last Revision Date: $Date: 2006/01/29 02:09:59 $
+""" 
+__author__ = "Mike Orr <iron@mso.oz.net>"
+__revision__ = "$Revision: 1.6 $"[11:-2]
+
+import os
+from Cheetah.Template import Template
+
+class CGITemplate(Template):
+    """Methods useful in CGI scripts.
+
+       Any class that inherits this mixin must also inherit Cheetah.Servlet.
+    """
+    
+
+    def cgiHeaders(self):
+        """Outputs the CGI headers if this is a CGI script.
+
+           Usage:  $cgiHeaders#slurp
+           Override .cgiHeadersHook() if you want to customize the headers.
+        """
+        if self.isCgi():
+            return self.cgiHeadersHook()
+
+
+
+    def cgiHeadersHook(self):
+        """Override if you want to customize the CGI headers.
+        """
+        return "Content-type: text/html\n\n"
+
+
+    def isCgi(self):
+        """Is this a CGI script?
+        """
+        env = 'REQUEST_METHOD' in os.environ 
+        wk = self._CHEETAH__isControlledByWebKit
+        return env and not wk
+
+
+    
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Tools/MondoReport.py b/cheetah/Tools/MondoReport.py
new file mode 100644 (file)
index 0000000..de4a8b5
--- /dev/null
@@ -0,0 +1,464 @@
+"""
+@@TR: This code is pretty much unsupported.
+
+MondoReport.py -- Batching module for Python and Cheetah.
+
+Version 2001-Nov-18.  Doesn't do much practical yet, but the companion
+testMondoReport.py passes all its tests.
+-Mike Orr (Iron)
+
+TODO: BatchRecord.prev/next/prev_batches/next_batches/query, prev.query,
+next.query.
+
+How about Report: .page(), .all(), .summary()?  Or PageBreaker.
+"""
+import operator
+try:
+    from functools import reduce
+except ImportError:
+    # If functools doesn't exist, we must be on an old 
+    # enough version that has reduce() in builtins
+    pass
+
+try:
+    from Cheetah.NameMapper import valueForKey as lookup_func
+except ImportError:
+    def lookup_func(obj, name):
+        if hasattr(obj, name):
+            return getattr(obj, name)
+        else:
+            return obj[name] # Raises KeyError.
+
+########## PUBLIC GENERIC FUNCTIONS ##############################
+
+class NegativeError(ValueError):
+    pass
+
+def isNumeric(v):
+    return isinstance(v, (int, float))
+
+def isNonNegative(v):
+    ret = isNumeric(v)
+    if ret and v < 0:
+        raise NegativeError(v)
+
+def isNotNone(v):
+    return v is not None
+
+def Roman(n):
+    n = int(n) # Raises TypeError.
+    if n < 1:
+        raise ValueError("roman numeral for zero or negative undefined: " + n)
+    roman = ''
+    while n >= 1000:
+            n = n - 1000
+            roman = roman + 'M'
+    while n >= 500:
+            n = n - 500
+            roman = roman + 'D'
+    while n >= 100:
+            n = n - 100
+            roman = roman + 'C'
+    while n >= 50:
+            n = n - 50
+            roman = roman + 'L'
+    while n >= 10:
+            n = n - 10
+            roman = roman + 'X'
+    while n >= 5:
+            n = n - 5
+            roman = roman + 'V'
+    while n < 5 and n >= 1:
+            n = n - 1
+            roman = roman + 'I'
+    roman = roman.replace('DCCCC', 'CM')
+    roman = roman.replace('CCCC', 'CD')
+    roman = roman.replace('LXXXX', 'XC')
+    roman = roman.replace('XXXX', 'XL')
+    roman = roman.replace('VIIII', 'IX')
+    roman = roman.replace('IIII', 'IV')
+    return roman
+
+
+def sum(lis):
+    return reduce(operator.add, lis, 0)
+    
+def mean(lis):
+    """Always returns a floating-point number.
+    """
+    lis_len = len(lis)
+    if lis_len == 0:
+        return 0.00 # Avoid ZeroDivisionError (not raised for floats anyway)
+    total = float( sum(lis) )
+    return total / lis_len
+
+def median(lis):
+    lis = sorted(lis[:])
+    return lis[int(len(lis)/2)]
+
+
+def variance(lis):
+    raise NotImplementedError()
+    
+def variance_n(lis):
+    raise NotImplementedError()
+    
+def standardDeviation(lis):
+    raise NotImplementedError()
+    
+def standardDeviation_n(lis):
+    raise NotImplementedError()
+
+
+
+class IndexFormats:
+    """Eight ways to display a subscript index.
+       ("Fifty ways to leave your lover....")
+    """
+    def __init__(self, index, item=None):
+        self._index = index
+        self._number = index + 1
+        self._item = item
+
+    def index(self):
+        return self._index
+
+    __call__ = index
+
+    def number(self):
+        return self._number
+
+    def even(self):
+        return self._number % 2 == 0
+
+    def odd(self):
+        return not self.even()
+
+    def even_i(self):
+        return self._index % 2 == 0
+
+    def odd_i(self):
+        return not self.even_i()
+
+    def letter(self):
+        return self.Letter().lower()
+
+    def Letter(self):
+        n = ord('A') + self._index
+        return chr(n)
+
+    def roman(self):
+        return self.Roman().lower()
+
+    def Roman(self):
+        return Roman(self._number)
+
+    def item(self):
+        return self._item
+
+
+
+########## PRIVATE CLASSES ##############################
+
+class ValuesGetterMixin:
+    def __init__(self, origList):
+        self._origList = origList
+
+    def _getValues(self, field=None, criteria=None):
+        if field:
+            ret = [lookup_func(elm, field) for elm in self._origList]
+        else:
+            ret = self._origList
+        if criteria:
+            ret = list(filter(criteria, ret))
+        return ret
+
+
+class RecordStats(IndexFormats, ValuesGetterMixin):
+    """The statistics that depend on the current record.
+    """
+    def __init__(self, origList, index):
+        record = origList[index] # Raises IndexError.
+        IndexFormats.__init__(self, index, record)
+        ValuesGetterMixin.__init__(self, origList)
+    
+    def length(self):
+        return len(self._origList)
+
+    def first(self):
+        return self._index == 0
+        
+    def last(self):
+        return self._index >= len(self._origList) - 1
+
+    def _firstOrLastValue(self, field, currentIndex, otherIndex):
+        currentValue = self._origList[currentIndex] # Raises IndexError.
+        try:
+            otherValue = self._origList[otherIndex]
+        except IndexError:
+            return True
+        if field:
+            currentValue = lookup_func(currentValue, field)
+            otherValue = lookup_func(otherValue, field)
+        return currentValue != otherValue
+
+    def firstValue(self, field=None):
+        return self._firstOrLastValue(field, self._index, self._index - 1)
+
+    def lastValue(self, field=None):
+        return self._firstOrLastValue(field, self._index, self._index + 1)
+
+    # firstPage and lastPage not implemented.  Needed?
+
+    def percentOfTotal(self, field=None, suffix='%', default='N/A', decimals=2):
+        rec = self._origList[self._index]
+        if field:
+            val = lookup_func(rec, field)
+        else:
+            val = rec
+        try:
+            lis = self._getValues(field, isNumeric)
+        except NegativeError:
+            return default
+        total = sum(lis)
+        if total == 0.00: # Avoid ZeroDivisionError.
+            return default
+        val = float(val)
+        try:
+            percent = (val / total) * 100
+        except ZeroDivisionError:
+            return default
+        if decimals == 0:
+            percent = int(percent)
+        else:
+            percent = round(percent, decimals)
+        if suffix:
+            return str(percent) + suffix # String.
+        else:
+            return percent # Numeric.
+
+    def __call__(self): # Overrides IndexFormats.__call__
+        """This instance is not callable, so we override the super method.
+        """
+        raise NotImplementedError()
+
+    def prev(self):
+        if self._index == 0:
+            return None
+        else:
+            length = self.length()
+            start = self._index - length
+            return PrevNextPage(self._origList, length, start)
+
+    def next(self):
+        if self._index + self.length() == self.length():
+            return None
+        else:
+            length = self.length()
+            start = self._index + length
+            return PrevNextPage(self._origList, length, start)
+            
+    def prevPages(self):
+        raise NotImplementedError()
+        
+    def nextPages(self):
+        raise NotImplementedError()
+
+    prev_batches = prevPages
+    next_batches = nextPages
+
+    def summary(self):
+        raise NotImplementedError()
+
+
+
+    def _prevNextHelper(self, start, end, size, orphan, sequence):
+        """Copied from Zope's DT_InSV.py's "opt" function.
+        """
+        if size < 1:
+            if start > 0 and end > 0 and end >= start:
+                size=end+1-start
+            else: size=7
+
+        if start > 0:
+
+            try: sequence[start-1]
+            except: start=len(sequence)
+            # if start > l: start=l
+
+            if end > 0:
+                if end < start: end=start
+            else:
+                end=start+size-1
+                try: sequence[end+orphan-1]
+                except: end=len(sequence)
+                # if l - end < orphan: end=l
+        elif end > 0:
+            try: sequence[end-1]
+            except: end=len(sequence)
+            # if end > l: end=l
+            start=end+1-size
+            if start - 1 < orphan: start=1
+        else:
+            start=1
+            end=start+size-1
+            try: sequence[end+orphan-1]
+            except: end=len(sequence)
+            # if l - end < orphan: end=l
+        return start, end, size
+
+
+
+class Summary(ValuesGetterMixin):
+    """The summary statistics, that don't depend on the current record.
+    """
+    def __init__(self, origList):
+        ValuesGetterMixin.__init__(self, origList)
+        
+    def sum(self, field=None):
+        lis = self._getValues(field, isNumeric)
+        return sum(lis)
+
+    total = sum
+
+    def count(self, field=None):
+        lis = self._getValues(field, isNotNone)
+        return len(lis)
+        
+    def min(self, field=None):
+        lis = self._getValues(field, isNotNone)
+        return min(lis) # Python builtin function min.
+        
+    def max(self, field=None):
+        lis = self._getValues(field, isNotNone)
+        return max(lis) # Python builtin function max.
+
+    def mean(self, field=None):
+        """Always returns a floating point number.
+        """
+        lis = self._getValues(field, isNumeric)
+        return mean(lis)
+
+    average = mean
+
+    def median(self, field=None):
+        lis = self._getValues(field, isNumeric)
+        return median(lis)
+
+    def variance(self, field=None):
+        raiseNotImplementedError()
+
+    def variance_n(self, field=None):
+        raiseNotImplementedError()
+
+    def standardDeviation(self, field=None):
+        raiseNotImplementedError()
+
+    def standardDeviation_n(self, field=None):
+        raiseNotImplementedError()
+
+
+class PrevNextPage:
+    def __init__(self, origList, size, start):
+        end = start + size
+        self.start = IndexFormats(start, origList[start])
+        self.end = IndexFormats(end, origList[end])
+        self.length = size
+        
+
+########## MAIN PUBLIC CLASS ##############################
+class MondoReport:
+    _RecordStatsClass = RecordStats
+    _SummaryClass = Summary
+
+    def __init__(self, origlist):
+        self._origList = origlist
+
+    def page(self, size, start, overlap=0, orphan=0):
+        """Returns list of ($r, $a, $b)
+        """
+        if overlap != 0:
+            raise NotImplementedError("non-zero overlap")
+        if orphan != 0:
+            raise NotImplementedError("non-zero orphan")
+        origList = self._origList
+        origList_len = len(origList)
+        start = max(0, start)
+        end = min( start + size, len(self._origList) )
+        mySlice = origList[start:end]
+        ret = []
+        for rel in range(size):
+            abs_ = start + rel
+            r = mySlice[rel]
+            a = self._RecordStatsClass(origList, abs_)
+            b = self._RecordStatsClass(mySlice, rel)
+            tup = r, a, b
+            ret.append(tup)
+        return ret
+
+
+    batch = page
+
+    def all(self):
+        origList_len = len(self._origList)
+        return self.page(origList_len, 0, 0, 0)
+    
+    
+    def summary(self):
+        return self._SummaryClass(self._origList)
+
+"""
+**********************************
+    Return a pageful of records from a sequence, with statistics.
+
+       in : origlist, list or tuple.  The entire set of records.  This is
+              usually a list of objects or a list of dictionaries.
+            page, int >= 0.  Which page to display.
+            size, int >= 1.  How many records per page.
+            widow, int >=0.  Not implemented.
+            orphan, int >=0.  Not implemented.
+            base, int >=0.  Number of first page (usually 0 or 1).
+
+       out: list of (o, b) pairs.  The records for the current page.  'o' is
+              the original element from 'origlist' unchanged.  'b' is a Batch
+              object containing meta-info about 'o'.
+       exc: IndexError if 'page' or 'size' is < 1.  If 'origlist' is empty or
+              'page' is too high, it returns an empty list rather than raising
+              an error.
+        
+        origlist_len = len(origlist)
+        start = (page + base) * size
+        end = min(start + size, origlist_len)
+        ret = []
+        # widow, orphan calculation: adjust 'start' and 'end' up and down, 
+        # Set 'widow', 'orphan', 'first_nonwidow', 'first_nonorphan' attributes.
+        for i in range(start, end):
+            o = origlist[i]
+            b = Batch(origlist, size, i)
+            tup = o, b
+            ret.append(tup)
+        return ret
+
+    def prev(self):
+        # return a PrevNextPage or None
+
+    def next(self):
+        # return a PrevNextPage or None
+
+    def prev_batches(self):
+        # return a list of SimpleBatch for the previous batches
+
+    def next_batches(self):
+        # return a list of SimpleBatch for the next batches
+
+########## PUBLIC MIXIN CLASS FOR CHEETAH TEMPLATES ##############
+class MondoReportMixin:
+    def batch(self, origList, size=None, start=0, overlap=0, orphan=0):
+        bat = MondoReport(origList)
+        return bat.batch(size, start, overlap, orphan)
+    def batchstats(self, origList):
+        bat = MondoReport(origList)
+        return bat.stats()
+"""
+
+# vim: shiftwidth=4 tabstop=4 expandtab textwidth=79
diff --git a/cheetah/Tools/MondoReportDoc.txt b/cheetah/Tools/MondoReportDoc.txt
new file mode 100644 (file)
index 0000000..29a026d
--- /dev/null
@@ -0,0 +1,391 @@
+MondoReport Documentation  
+Version 0.01 alpha 24-Nov-2001.  iron@mso.oz.net or mso@oz.net.
+Copyright (c) 2001 Mike Orr.  License: same as Python or Cheetah.
+
+* * * * *
+STATUS:  previous/next batches and query string are not implemented yet.
+Sorting not designed yet.  Considering "click on this column header to sort by
+this field" and multiple ascending/descending sort fields for a future version.
+
+Tested with Python 2.2b1.  May work with Python 2.1 or 2.0.
+
+* * * * *
+OVERVIEW
+
+MondoReport -- provide information about a list that is useful in generating
+any kind of report.  The module consists of one main public class, and some
+generic functions you may find useful in other programs.  This file contains an
+overview, syntax reference and examples.  The module is designed both for
+standalone use and for integration with the Cheetah template system
+(http://www.cheetahtemplate.org/), so the examples are in both Python and
+Cheetah.  The main uses of MondoReport are: 
+
+(A) to iterate through a list.  In this sense MR is a for-loop enhancer,
+providing information that would be verbose to calculate otherwise.
+
+(B) to separate a list into equal-size "pages" (or "batches"--the two terms are
+interchangeable) and only display the current page, plus limited information
+about the previous and next pages.
+
+(C) to extract summary statistics about a certain column ("field") in the list.
+
+* * * * *
+MAIN PUBLIC CLASS
+
+To create a MondoReport instance, supply a list to operate on.
+
+    mr = MondoReport(origList)
+
+The list may be a list of anything, but if you use the 'field' argument in any
+of the methods below, the elements must be instances or dictionaries.
+
+MondoReport assumes it's operating on an unchanging list.  Do not modify the
+list or any of its elements until you are completely finished with the
+ModoReport object and its sub-objects.  Otherwise, you may get an exception or
+incorrect results.
+
+MondoReport instances have three methods:
+
+    .page(size, start, overlap=0, orphan=0
+        sort=None, reverse=False)                => list of (r, a, b).
+
+'size' is an integer >= 1.  'start', 'overlap' and 'orphan' are integers >= 0.
+The list returned contains one triple for each record in the current page.  'r'
+is the original record.  'a' is a BatchRecord instance for the current record
+in relation to all records in the origList.  'b' is a BatchRecord instance for
+the current record in relation to all the records in that batch/page.  (There
+is a .batch method that's identical to .page.)
+
+The other options aren't implemented yet, but 'overlap' duplicates this many
+records on adjacent batches.  'orphan' moves this many records or fewer, if
+they are on a page alone, onto the neighboring page.  'sort' (string) specifies
+a field to sort the records by.  It may be suffixed by ":desc" to sort in
+descending order.  'reverse' (boolean) reverses the sort order.  If both
+":desc" and 'reverse' are specified, they will cancel each other out.  This
+sorting/reversal happens on a copy of the origList, and all objects returned
+by this method use the sorted list, except when resorting the next time.
+To do more complicated sorting, such as a hierarchy of columns, do it to the
+original list before creating the ModoReport object.
+
+    .all(sort=None, reverse=False)              => list of (r, a).
+
+Same, but the current page spans the entire origList.
+
+    .summary()                                  => Summary instance.
+
+Summary statistics for the entire origList.
+
+In Python, use .page or .all in a for loop:
+
+    from Cheetah.Tools.MondoReport import MondoReport
+    mr = MondoReport(myList)
+    for r, a, b in mr.page(20, 40):
+        # Do something with r, a and b.  The current page is the third page,
+        # with twenty records corresponding to origList[40:60].
+    if not myList:
+        # Warn the user there are no records in the list.
+
+It works the same way in Cheetah, just convert to Cheetah syntax.  This example
+assumes the template doubles as a Webware servlet, so we use the servlet's
+'$request' method to look up the CGI parameter 'start'.  The default value is 0
+for the first page.
+
+    #from Cheetah.Tools.MondoReport import MondoReport
+    #set $mr = $MondoReport($bigList)
+    #set $start = $request.field("start", 0)
+    #for $o, $a, $b in $mr.page(20, $start)
+        ... do something with $o, $a and $b ...
+    #end for
+    #unless $bigList
+        This is displayed if the original list has no elements.
+        It's equivalent to the "else" part Zope DTML's <dtml-in>.
+    #end unless
+
+* * * * *
+USING 'r' RECORDS
+
+Use 'r' just as you would the original element.  For instance:
+
+    print r.attribute     # If r is an instance.
+    print r['key']        # If r is a dictionary.
+    print r               # If r is numeric or a string.
+
+In Cheetah, you can take advantage of Universal Dotted Notation and autocalling:
+
+    $r.name        ## 'name' may be an attribute or key of 'r'.  If 'r' and/or
+                   ## 'name' is a function or method, it will be called without
+                   ## arguments.
+    $r.attribute
+    $r['key']
+    $r
+    $r().attribute()['key']()
+
+If origList is a list of name/value pairs (2-tuples or 2-lists), you may
+prefer to do this:
+
+    for (key, value), a, b in mr.page(20, 40):
+        print key, "=>", value
+    
+    #for ($key, $value), $a, $b in $mr.page(20, $start)
+        $key =&gt; $value
+    #end for
+
+* * * * *
+STATISTICS METHODS AND FIELD VALUES
+
+Certain methods below have an optional argument 'field'.  If specified, 
+MondoReport will look up that field in each affected record and use its value
+in the calculation.  MondoReport uses Cheetah's NameMapper if available,
+otherwise it uses a minimal NameMapper substitute that looks for an attribute
+or dictionary key called "field".  You'll get an exception if any record is a
+type without attributes or keys, or if one or more records is missing that
+attribute/key.
+
+If 'field' is None, MondoReport will use the entire record in its
+calculation.  This makes sense mainly if the records are a numeric type.
+
+All statistics methods filter out None values from their calculations, and
+reduce the number of records accordingly.  Most filter out non-numeric fields
+(or records).  Some raise NegativeError if a numeric field (or record) is
+negative.
+
+
+* * * * *
+BatchRecord METHODS
+
+The 'a' and 'b' objects of MondoReport.page() and MondoReport.all() provide
+these methods.
+
+    .index()
+
+The current subscript.  For 'a', this is the true subscript into origList.
+For 'b', this is relative to the current page, so the first record will be 0.
+Hint: In Cheetah, use autocalling to skip the parentheses: '$b.index'.
+
+    .number()
+
+The record's position starting from 1.  This is always '.index() + 1'.
+
+    .Letter()
+
+The letter ("A", "B", "C") corresponding to .number().  Undefined if .number()
+> 26.  The current implementation just adds the offset to 'a' and returns
+whatever character it happens to be.  
+
+To make a less dumb implementation (e.g., "Z, AA, BB" or "Z, A1, B1"):
+1) Subclass BatchRecord and override the .Letter method. 
+2) Subclass MondoReport and set the class variable .BatchRecordClass to your
+new improved class.
+
+    .letter()
+
+Same but lower case.
+
+    .Roman()
+
+The Roman numeral corresponding to .number().
+
+    .roman()
+
+Same but lower case.
+
+    .even()
+
+True if .number() is even.
+
+    .odd()
+
+True if .number() is odd.
+
+    .even_i()
+
+True if .index() is even.
+
+    .odd_i()
+
+True if .index() is odd.
+
+    .length()
+
+For 'a', number of records in origList.  For 'b', number of records on this
+page.
+
+    .item()
+
+The record itself.  You don't need this in the normal case since it's the same
+as 'r', but it's useful for previous/next batches.
+
+    .size()
+
+The 'size' argument used when this BatchRecord was created.  
+'a.size() == b.size()'.
+
+    .first()
+
+True if this is the first record.
+
+    .last()
+
+True if this is the last record.
+
+    .firstValue(field=None)
+
+True if there is no previous record, or if the previous field/record has a 
+different value.  Used for to print section headers.  For instance, if you
+are printing addresses by country, this will be true at the first occurrance
+of each country.  Or for indexes, you can have a non-printing field showing
+which letter of the alphablet this entry starts with, and then print a "B"
+header before printing the first record starting with "B".
+
+    .lastValue(field=None)
+
+True if this is the last record containing the current value in the
+field/record.
+
+    .percentOfTotal(field=None, suffix="%", default="N/A", decimals=2)
+
+Returns the percent that the current field/record is of all fields/records.
+If 'suffix' is None, returns a number; otherwise it returns a string with
+'suffix' suffixed.  If the current value is non-numeric, returns 'default'
+instead (without 'suffix').  'decimals' tells the number of decimal places to
+return; if 0, there will be no decimal point.
+
+    .prev()
+
+Returns a PrevNextBatch instance for the previous page.  If there is no
+previous page, returns None.  [Not implemented yet.]
+
+    .next()
+
+Returns a PrevNextBatch instance for the next page.  If there is no next page,
+returns None.  [Not implemented yet.]
+
+    .prevPages()
+
+Returns a list of PrevNextPage instances for every previous page, or [] if no
+previous pages.  [Not implemented yet.]
+
+    .nextPages()
+
+Returns a list of PrevNextPage instances for every next page, or [] if no next
+pages.  [Not implemented yet.]
+
+    .query(start=None, label=None, attribName="start", attribs=[])
+
+[Not implemented yet.]
+
+With no arguments, returns the HTML query string with start value removed (so
+you can append a new start value in your hyperlink).  The query string is taken
+from the 'QUERY_STRING' environmental variable, or "" if missing.  (This is
+Webware compatible.)  
+
+With 'start' (an integer >= 0), returns the query string with an updated start
+value, normally for the next or previous batch.
+
+With 'label' (a string), returns a complete HTML hyperlink:
+'<A HREF="?new_query_string">label</A>'.  You'll get a TypeError if you specify
+'label' but not 'start'.
+
+With 'attribName' (a string), uses this attribute name rather than "start".
+Useful if you have another CGI parameter "start" that's used for something
+else.
+
+With 'attribs' (a dictionary), adds these attributes to the hyperlink.
+For instance, 'attribs={"target": "_blank"}'.  Ignored unless 'label' is 
+specified too.
+
+This method assumes the start parameter is a GET variable, not a POST variable.
+
+    .summary()
+
+Returns a Summary instance.  'a.summary()' refers to all records in the
+origList, so it's the same as MondoReport.summary().  'b.summary()' refers only
+to the records on the current page.  [Not implemented yet.]
+
+* * * * *
+PrevNextPage INSTANCES
+
+[Not implemented yet.]
+
+PrevNextPage instances have the following methods:
+
+    .start()
+
+The index (true index of origList) that that page starts at.  You may also use
+'.start().index()', '.start().number()', etc.  Also
+'.start().item(field=None)'.  (Oh, so *that*'s what .item is for!)
+
+    .end()
+
+The index (true index of origList) that that page ends at.  You may also use
+'.end().index()', '.end().number()', etc.   Also
+'.end().item(field=None)'.
+
+    .length()
+
+Number of records on that page.
+
+    .query(label=None, attribName="start", attribs={}, before="", after="")
+
+[Not implemented yet.]
+
+Similar to 'a.query()' and 'b.query()', but automatically calculates the start
+value for the appropriate page.  
+
+For fancy HTML formatting, 'before' is prepended to the returned text and 
+'after' is appended.  (There was an argument 'else_' for if there is no such
+batch, but it was removed because you can't even get to this method at all in
+that case.)
+
+* * * * * *
+SUMMARY STATISTICS
+
+These methods are supported by the Summary instances returned by
+MondoReport.Summary():
+
+    .sum(field=None)
+
+Sum of all numeric values in a field, or sum of all records.
+
+    .total(field=None)
+
+Same.
+
+    .count(field=None)
+
+Number of fields/records with non-None values.
+
+    .min(field=None)
+
+Minimum value in that field/record.  Ignores None values.
+
+    .max(field=None)
+
+Maximum value in that field/record.  Ignores None values.
+
+    .mean(field=None)
+
+The mean (=average) of all numeric values in that field/record.
+
+    .average(field=None)
+
+Same.
+
+    .median(field=None)
+
+The median of all numeric values in that field/record.  This is done by sorting
+the values and taking the middle value.
+
+    .variance(field=None), .variance_n(field=None)
+    .standardDeviation(field=None), .standardDeviation_n(field=None)
+
+[Not implemented yet.]
+
+
+* * * * *
+To run the regression tests (requires unittest.py, which is standard with
+Python 2.2), run MondoReportTest.py from the command line.  The regression test
+double as usage examples.
+
+
+# vim: shiftwidth=4 tabstop=4 expandtab textwidth=79
diff --git a/cheetah/Tools/RecursiveNull.py b/cheetah/Tools/RecursiveNull.py
new file mode 100644 (file)
index 0000000..d5c1ef0
--- /dev/null
@@ -0,0 +1,28 @@
+"""
+Nothing, but in a friendly way.  Good for filling in for objects you want to
+hide.  If $form.f1 is a RecursiveNull object, then
+$form.f1.anything["you"].might("use") will resolve to the empty string.
+
+This module was contributed by Ian Bicking.
+"""
+
+class RecursiveNull(object):
+    def __getattr__(self, attr):
+        return self
+    def __getitem__(self, item):
+        return self
+    def __call__(self, *args, **kwargs):
+        return self
+    def __str__(self):
+        return ''
+    def __repr__(self):
+        return ''
+    def __nonzero__(self):
+        return 0
+    def __eq__(self, x):
+        if x:
+            return False
+        return True
+    def __ne__(self, x):
+        return x and True or False
+
diff --git a/cheetah/Tools/SiteHierarchy.py b/cheetah/Tools/SiteHierarchy.py
new file mode 100644 (file)
index 0000000..dece01e
--- /dev/null
@@ -0,0 +1,166 @@
+# $Id: SiteHierarchy.py,v 1.1 2001/10/11 03:25:54 tavis_rudd Exp $
+"""Create menus and crumbs from a site hierarchy.
+
+You define the site hierarchy as lists/tuples.  Each location in the hierarchy
+is a (url, description) tuple.  Each list has the base URL/text in the 0
+position, and all the children coming after it.  Any child can be a list,
+representing further depth to the hierarchy.  See the end of the file for an
+example hierarchy.
+
+Use Hierarchy(contents, currentURL), where contents is this hierarchy, and
+currentURL is the position you are currently in.  The menubar and crumbs methods
+give you the HTML output.
+
+There are methods you can override to customize the HTML output.
+"""
+
+##################################################
+## DEPENDENCIES
+import string
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+##################################################
+## CLASSES
+
+class Hierarchy:
+    def __init__(self, hierarchy, currentURL, prefix='', menuCSSClass=None,
+                 crumbCSSClass=None):
+        """
+        hierarchy is described above, currentURL should be somewhere in
+        the hierarchy.  prefix will be added before all of the URLs (to
+        help mitigate the problems with absolute URLs), and if given,
+        cssClass will be used for both links *and* nonlinks.
+        """
+
+        self._contents = hierarchy
+        self._currentURL = currentURL
+        if menuCSSClass:
+            self._menuCSSClass = ' class="%s"' % menuCSSClass
+        else:
+            self._menuCSSClass = ''
+        if crumbCSSClass:
+            self._crumbCSSClass = ' class="%s"' % crumbCSSClass
+        else:
+            self._crumbCSSClass = ''
+        self._prefix=prefix
+
+    
+    ## Main output methods
+    
+    def menuList(self, menuCSSClass=None):
+        """An indented menu list"""
+        if menuCSSClass:
+            self._menuCSSClass = ' class="%s"' % menuCSSClass
+        
+        stream = StringIO()
+        for item in self._contents[1:]:
+            self._menubarRecurse(item, 0, stream)
+        return stream.getvalue()
+
+    def crumbs(self, crumbCSSClass=None):
+        """The home>where>you>are crumbs"""
+        if crumbCSSClass:
+            self._crumbCSSClass = ' class="%s"' % crumbCSSClass
+        
+        path = []
+        pos = self._contents
+        while True:
+            ## This is not the fastest algorithm, I'm afraid.
+            ## But it probably won't be for a huge hierarchy anyway.
+            foundAny = False
+            path.append(pos[0])
+            for item in pos[1:]:
+                if self._inContents(item):
+                    if isinstance(item, tuple):
+                        path.append(item)
+                        break
+                    else:
+                        pos = item
+                        foundAny = True
+                        break
+            if not foundAny:
+                break
+        if len(path) == 1:
+            return self.emptyCrumb()
+        return string.join(map(lambda x, self=self: self.crumbLink(x[0], x[1]),
+                               path), self.crumbSeperator()) + \
+                               self.crumbTerminator()
+
+    ## Methods to control the Aesthetics
+    #  - override these methods for your own look
+        
+    def menuLink(self, url, text, indent):
+        if url == self._currentURL or self._prefix + url == self._currentURL:
+            return '%s<B%s>%s</B> <BR>\n' % ('&nbsp;'*2*indent,
+                             self._menuCSSClass, text)
+        else:
+            return '%s<A HREF="%s%s"%s>%s</A> <BR>\n' % \
+                   ('&nbsp;'*2*indent, self._prefix, url,
+                    self._menuCSSClass, text)
+        
+    def crumbLink(self, url, text):
+        if url == self._currentURL or self._prefix + url == self._currentURL:
+            return '<B%s>%s</B>' % (text, self._crumbCSSClass)
+        else:
+            return '<A HREF="%s%s"%s>%s</A>' % \
+                   (self._prefix, url, self._crumbCSSClass, text)
+        
+    def crumbSeperator(self):
+        return '&nbsp;&gt;&nbsp;'
+    
+    def crumbTerminator(self):
+        return ''
+    
+    def emptyCrumb(self):
+        """When you are at the homepage"""
+        return ''
+                
+    ## internal methods
+    
+    def _menubarRecurse(self, contents, indent, stream):
+        if isinstance(contents, tuple):
+            url, text = contents
+            rest = []
+        else:
+            url, text = contents[0]
+            rest = contents[1:]
+        stream.write(self.menuLink(url, text, indent))
+        if self._inContents(contents):
+            for item in rest:
+                self._menubarRecurse(item, indent+1, stream)
+
+    def _inContents(self, contents):
+        if isinstance(contents, tuple):
+            return self._currentURL == contents[0]
+        for item in contents:
+            if self._inContents(item):
+                return True
+        return False
+##################################################
+## from the command line
+
+if __name__ == '__main__':
+    hierarchy = [('/', 'home'),
+            ('/about', 'About Us'),
+            [('/services', 'Services'),
+             [('/services/products', 'Products'),
+              ('/services/products/widget', 'The Widget'),
+              ('/services/products/wedge', 'The Wedge'),
+              ('/services/products/thimble', 'The Thimble'),
+              ],
+             ('/services/prices', 'Prices'),
+             ],
+            ('/contact', 'Contact Us'),
+            ]
+
+    for url in ['/', '/services', '/services/products/widget', '/contact']:
+        print('<p>', '='*50)
+        print('<br> %s: <br>\n' % url)
+        n = Hierarchy(hierarchy, url, menuCSSClass='menu', crumbCSSClass='crumb',
+                  prefix='/here')
+        print(n.menuList())
+        print('<p>', '-'*50)
+        print(n.crumbs())
diff --git a/cheetah/Tools/__init__.py b/cheetah/Tools/__init__.py
new file mode 100644 (file)
index 0000000..506503b
--- /dev/null
@@ -0,0 +1,8 @@
+"""This package contains classes, functions, objects and packages contributed
+   by Cheetah users.  They are not used by Cheetah itself.  There is no
+   guarantee that this directory will be included in Cheetah releases, that
+   these objects will remain here forever, or that they will remain
+   backward-compatible.
+"""
+
+# vim: shiftwidth=5 tabstop=5 expandtab
diff --git a/cheetah/Tools/turbocheetah/__init__.py b/cheetah/Tools/turbocheetah/__init__.py
new file mode 100644 (file)
index 0000000..584583e
--- /dev/null
@@ -0,0 +1,5 @@
+from turbocheetah import cheetahsupport
+
+TurboCheetah = cheetahsupport.TurboCheetah
+
+__all__ = ["TurboCheetah"]
\ No newline at end of file
diff --git a/cheetah/Tools/turbocheetah/cheetahsupport.py b/cheetah/Tools/turbocheetah/cheetahsupport.py
new file mode 100644 (file)
index 0000000..1a70286
--- /dev/null
@@ -0,0 +1,110 @@
+"Template support for Cheetah"
+
+import sys, os, imp
+
+from Cheetah import Compiler
+import pkg_resources
+
+def _recompile_template(package, basename, tfile, classname):
+    tmpl = pkg_resources.resource_string(package, "%s.tmpl" % basename)
+    c = Compiler.Compiler(source=tmpl, mainClassName='GenTemplate')
+    code = str(c)
+    mod = imp.new_module(classname)
+    ns = dict()
+    exec(code, ns)
+    tempclass = ns.get("GenTemplate",
+                       ns.get('DynamicallyCompiledCheetahTemplate'))
+    assert tempclass
+    tempclass.__name__ = basename
+    setattr(mod, basename, tempclass)
+    sys.modules[classname] = mod
+    return mod
+
+class TurboCheetah:
+    extension = "tmpl"
+    
+    def __init__(self, extra_vars_func=None, options=None):
+        if options is None:
+            options = dict()
+        self.get_extra_vars = extra_vars_func
+        self.options = options
+        self.compiledTemplates = {}
+        self.search_path = []
+    
+    def load_template(self, template=None,
+                      template_string=None, template_file=None,
+                      loadingSite=False):
+        """Searches for a template along the Python path.
+
+        Template files must end in ".tmpl" and be in legitimate packages.
+        """
+        given = len([_f for _f in (template, template_string, template_file) if _f])
+        if given > 1:
+            raise TypeError(
+                "You may give only one of template, template_string, and "
+                "template_file")
+        if not given:
+            raise TypeError(
+                "You must give one of template, template_string, or "
+                "template_file")
+        if template:
+            return self.load_template_module(template)
+        elif template_string:
+            return self.load_template_string(template_string)
+        elif template_file:
+            return self.load_template_file(template_file)
+
+    def load_template_module(self, classname):
+
+        ct = self.compiledTemplates
+
+        divider = classname.rfind(".")
+        if divider > -1:
+            package = classname[0:divider]
+            basename = classname[divider+1:]
+        else:
+            raise ValueError("All templates must be in a package")
+
+        if not self.options.get("cheetah.precompiled", False):
+            tfile = pkg_resources.resource_filename(package, 
+                                                    "%s.%s" % 
+                                                    (basename,
+                                                    self.extension))
+            if classname in ct:
+                mtime = os.stat(tfile).st_mtime
+                if ct[classname] != mtime:
+                    ct[classname] = mtime
+                    del sys.modules[classname]
+                    mod = _recompile_template(package, basename, 
+                                              tfile, classname)
+                else:
+                    mod = __import__(classname, dict(), dict(), [basename])
+            else:
+                ct[classname] = os.stat(tfile).st_mtime
+                mod = _recompile_template(package, basename, 
+                                          tfile, classname)
+        else:
+            mod = __import__(classname, dict(), dict(), [basename])
+        tempclass = getattr(mod, basename)
+        return tempclass
+
+    def load_template_string(self, content):
+        raise NotImplementedError
+
+    def load_template_file(self, filename):
+        raise NotImplementedError
+
+    def render(self, info, format="html", fragment=False, template=None,
+               template_string=None, template_file=None):
+        tclass = self.load_template(
+            template=template, template_string=template_string,
+            template_file=template_file)
+        if self.get_extra_vars:
+            extra = self.get_extra_vars()
+        else:
+            extra = {}
+        tempobj = tclass(searchList=[info, extra])
+        if fragment:
+            return tempobj.fragment()
+        else:
+            return tempobj.respond()
diff --git a/cheetah/Tools/turbocheetah/tests/__init__.py b/cheetah/Tools/turbocheetah/tests/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/cheetah/Tools/turbocheetah/tests/test_template.py b/cheetah/Tools/turbocheetah/tests/test_template.py
new file mode 100644 (file)
index 0000000..a6196e3
--- /dev/null
@@ -0,0 +1,66 @@
+import os
+from turbocheetah import TurboCheetah
+
+here = os.path.dirname(__file__)
+
+values = {
+    'v': 'VV',
+    'one': 1,
+    }
+
+def test_normal():
+    plugin = TurboCheetah()
+    # Make sure a simple test works:
+    s = plugin.render(values, template='turbocheetah.tests.simple1')
+    assert s.strip() == 'This is a test: VV'
+    # Make sure one template can inherit from another:
+    s = plugin.render(values, template='turbocheetah.tests.import_inherit')
+    assert s.strip() == 'Inherited: import'
+    
+def test_path():
+    plugin = TurboCheetah()
+    plugin.search_path = [here]
+    # Make sure we pick up filenames (basic test):
+    s = plugin.render(values, template_file='simple1')
+    assert s.strip() == 'This is a test: VV'
+    # Make sure we pick up subdirectories:
+    s = plugin.render(values, template_file='sub/master')
+    assert s.strip() == 'sub1: 1'
+
+def test_search():
+    plugin = TurboCheetah()
+    plugin.search_path = [os.path.join(here, 'sub'),
+                          os.path.join(here, 'sub2'),
+                          here]
+    # Pick up from third entry:
+    s = plugin.render(values, template_file='simple1')
+    assert s.strip() == 'This is a test: VV'
+    # Pick up from sub/master, non-ambiguous:
+    s = plugin.render(values, template_file='master')
+    assert s.strip() == 'sub1: 1'
+    # Pick up from sub/page, inherit from sub/template:
+    s = plugin.render(values, template_file='page')
+    assert s.strip() == 'SUB: sub content'
+    # Pick up from sub2/page_over, inherit from sub/template:
+    s = plugin.render(values, template_file='page_over')
+    assert s.strip() == 'SUB: override content'
+    # Pick up from sub/page_template_over, inherit from
+    # sub2/template_over:
+    s = plugin.render(values, template_file='page_template_over')
+    assert s.strip() == 'OVER: sub content'
+    # Change page, make sure that undoes overrides:
+    plugin.search_path = [os.path.join(here, 'sub'),
+                          here]
+    s = plugin.render(values, template_file='page_over')
+    assert s.strip() == 'SUB: sub content'
+
+def test_string():
+    # Make sure simple string evaluation works:
+    plugin = TurboCheetah()
+    s = plugin.render(values, template_string="""Hey $v""")
+    assert s == "Hey VV"
+    # Make sure a string can inherit from a file:
+    plugin.search_path = [here]
+    s = plugin.render(values, template_string="#extends inherit_from\ns value")
+    assert s.strip() == 'inherit: s value'
+    
diff --git a/cheetah/Unspecified.py b/cheetah/Unspecified.py
new file mode 100644 (file)
index 0000000..89c5176
--- /dev/null
@@ -0,0 +1,9 @@
+try:
+    from ds.sys.Unspecified import Unspecified
+except ImportError:
+    class _Unspecified:
+        def __repr__(self):
+            return 'Unspecified'        
+        def __str__(self):
+            return 'Unspecified'
+    Unspecified = _Unspecified()
diff --git a/cheetah/Utils/Indenter.py b/cheetah/Utils/Indenter.py
new file mode 100644 (file)
index 0000000..52c142d
--- /dev/null
@@ -0,0 +1,123 @@
+"""
+Indentation maker.
+@@TR: this code is unsupported and largely undocumented ...
+
+This version is based directly on code by Robert Kuzelj
+<robert_kuzelj@yahoo.com> and uses his directive syntax.  Some classes and
+attributes have been renamed.  Indentation is output via
+$self._CHEETAH__indenter.indent() to prevent '_indenter' being looked up on the
+searchList and another one being found.  The directive syntax will
+soon be changed somewhat.
+""" 
+
+import re
+import sys
+
+def indentize(source):
+    return IndentProcessor().process(source)
+
+class IndentProcessor(object):
+    """Preprocess #indent tags."""
+    LINE_SEP = '\n'
+    ARGS = "args"
+    INDENT_DIR = re.compile(r'[ \t]*#indent[ \t]*(?P<args>.*)')
+    DIRECTIVE = re.compile(r"[ \t]*#")
+    WS = "ws"
+    WHITESPACES = re.compile(r"(?P<ws>[ \t]*)")
+
+    INC = "++"
+    DEC = "--"
+    
+    SET = "="
+    CHAR = "char"
+    
+    ON = "on"
+    OFF = "off"
+
+    PUSH = "push"
+    POP = "pop"
+    
+    def process(self, _txt):
+        result = []
+
+        for line in _txt.splitlines():
+            match = self.INDENT_DIR.match(line)
+            if match:
+                #is indention directive
+                args = match.group(self.ARGS).strip()
+                if args == self.ON:
+                    line = "#silent $self._CHEETAH__indenter.on()"
+                elif args == self.OFF:
+                    line = "#silent $self._CHEETAH__indenter.off()"
+                elif args == self.INC:
+                    line = "#silent $self._CHEETAH__indenter.inc()"
+                elif args == self.DEC:
+                    line = "#silent $self._CHEETAH__indenter.dec()"
+                elif args.startswith(self.SET):
+                    level = int(args[1:])
+                    line = "#silent $self._CHEETAH__indenter.setLevel(%(level)d)" % {"level":level}
+                elif args.startswith('chars'):
+                    self.indentChars = eval(args.split('=')[1])
+                    line = "#silent $self._CHEETAH__indenter.setChars(%(level)d)" % {"level":level}
+                elif args.startswith(self.PUSH):
+                    line = "#silent $self._CHEETAH__indenter.push()"
+                elif args.startswith(self.POP):
+                    line = "#silent $self._CHEETAH__indenter.pop()"
+            else:
+                match = self.DIRECTIVE.match(line)
+                if not match:
+                    #is not another directive
+                    match = self.WHITESPACES.match(line)
+                    if match:
+                        size = len(match.group("ws").expandtabs(4))
+                        line = ("${self._CHEETAH__indenter.indent(%(size)d)}" % {"size":size}) + line.lstrip()
+                    else:
+                        line = "${self._CHEETAH__indenter.indent(0)}" + line
+            result.append(line)
+
+        return self.LINE_SEP.join(result)
+
+class Indenter(object):
+    """
+    A class that keeps track of the current indentation level.
+    .indent() returns the appropriate amount of indentation.
+    """
+    On = 1
+    Level = 0
+    Chars = '    '
+    LevelStack = []
+
+    def on(self):
+        self.On = 1
+    def off(self):
+        self.On = 0
+    def inc(self):
+        self.Level += 1
+    def dec(self):
+        """decrement can only be applied to values greater zero
+            values below zero don't make any sense at all!"""
+        if self.Level > 0:
+            self.Level -= 1
+    def push(self):
+        self.LevelStack.append(self.Level)
+    def pop(self):
+        """the levestack can not become -1. any attempt to do so
+           sets the level to 0!"""
+        if len(self.LevelStack) > 0:
+            self.Level = self.LevelStack.pop()
+        else:
+            self.Level = 0
+    def setLevel(self, _level):
+        """the leve can't be less than zero. any attempt to do so
+           sets the level automatically to zero!"""
+        if _level < 0:
+            self.Level = 0
+        else:
+            self.Level = _level
+    def setChar(self, _chars):
+        self.Chars = _chars
+    def indent(self, _default=0):
+        if self.On:
+            return self.Chars * self.Level
+        return " " * _default
+
diff --git a/cheetah/Utils/Misc.py b/cheetah/Utils/Misc.py
new file mode 100644 (file)
index 0000000..81949d1
--- /dev/null
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+"""
+    Miscellaneous functions/objects used by Cheetah but also useful standalone.
+""" 
+import os          # Used in mkdirsWithPyInitFile.
+import sys         # Used in die.
+
+##################################################
+## MISCELLANEOUS FUNCTIONS
+
+def die(reason):
+    sys.stderr.write(reason + '\n')
+    sys.exit(1)
+
+def useOrRaise(thing, errmsg=''):
+    """Raise 'thing' if it's a subclass of Exception.  Otherwise return it.
+
+    Called by: Cheetah.Servlet.cgiImport()
+    """
+    if isinstance(thing, type) and issubclass(thing, Exception):
+        raise thing(errmsg)
+    return thing
+
+
+def checkKeywords(dic, legalKeywords, what='argument'):
+    """Verify no illegal keyword arguments were passed to a function.
+
+    in : dic, dictionary (**kw in the calling routine).
+         legalKeywords, list of strings, the keywords that are allowed.
+         what, string, suffix for error message (see function source).
+    out: None.
+    exc: TypeError if 'dic' contains a key not in 'legalKeywords'.
+    called by: Cheetah.Template.__init__()
+    """
+    # XXX legalKeywords could be a set when sets get added to Python.
+    for k in dic.keys(): # Can be dic.iterkeys() if Python >= 2.2.
+        if k not in legalKeywords: 
+            raise TypeError("'%s' is not a valid %s" % (k, what))
+
+
+def removeFromList(list_, *elements):
+    """Save as list_.remove(each element) but don't raise an error if
+       element is missing.  Modifies 'list_' in place!  Returns None.
+    """
+    for elm in elements:
+        try:
+            list_.remove(elm)
+        except ValueError:
+            pass
+
+
+def mkdirsWithPyInitFiles(path):
+    """Same as os.makedirs (mkdir 'path' and all missing parent directories)
+       but also puts a Python '__init__.py' file in every directory it
+       creates.  Does nothing (without creating an '__init__.py' file) if the
+       directory already exists.  
+    """
+    dir, fil = os.path.split(path)
+    if dir and not os.path.exists(dir):
+        mkdirsWithPyInitFiles(dir)
+    if not os.path.exists(path):
+        os.mkdir(path)
+        init = os.path.join(path, "__init__.py")
+        f = open(init, 'w') # Open and close to produce empty file.
+        f.close()
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Utils/WebInputMixin.py b/cheetah/Utils/WebInputMixin.py
new file mode 100644 (file)
index 0000000..52b6220
--- /dev/null
@@ -0,0 +1,102 @@
+# $Id: WebInputMixin.py,v 1.10 2006/01/06 21:56:54 tavis_rudd Exp $
+"""Provides helpers for Template.webInput(), a method for importing web
+transaction variables in bulk.  See the docstring of webInput for full details.
+
+Meta-Data
+================================================================================
+Author: Mike Orr <iron@mso.oz.net>
+License: This software is released for unlimited distribution under the
+         terms of the MIT license.  See the LICENSE file.
+Version: $Revision: 1.10 $
+Start Date: 2002/03/17
+Last Revision Date: $Date: 2006/01/06 21:56:54 $
+""" 
+__author__ = "Mike Orr <iron@mso.oz.net>"
+__revision__ = "$Revision: 1.10 $"[11:-2]
+
+from Cheetah.Utils.Misc import useOrRaise
+
+class NonNumericInputError(ValueError): pass
+
+##################################################
+## PRIVATE FUNCTIONS AND CLASSES
+
+class _Converter:
+    """A container object for info about type converters.
+    .name, string, name of this converter (for error messages).
+    .func, function, factory function.
+    .default, value to use or raise if the real value is missing.
+    .error, value to use or raise if .func() raises an exception.
+    """
+    def __init__(self, name, func, default, error):
+        self.name = name
+        self.func = func
+        self.default = default
+        self.error = error
+
+
+def _lookup(name, func, multi, converters):
+    """Look up a Webware field/cookie/value/session value.  Return
+    '(realName, value)' where 'realName' is like 'name' but with any
+    conversion suffix strips off.  Applies numeric conversion and
+    single vs multi values according to the comments in the source.
+    """
+    # Step 1 -- split off the conversion suffix from 'name'; e.g. "height:int".
+    # If there's no colon, the suffix is "".  'longName' is the name with the 
+    # suffix, 'shortName' is without.    
+    # XXX This implementation assumes "height:" means "height".
+    colon = name.find(':')
+    if colon != -1:
+        longName = name
+        shortName, ext = name[:colon], name[colon+1:]
+    else:
+        longName = shortName = name
+        ext = ''
+
+    # Step 2 -- look up the values by calling 'func'.
+    if longName != shortName:
+        values = func(longName, None) or func(shortName, None)
+    else:
+        values = func(shortName, None)
+    # 'values' is a list of strings, a string or None.
+
+    # Step 3 -- Coerce 'values' to a list of zero, one or more strings.
+    if   values is None:
+        values = []
+    elif isinstance(values, str):
+        values = [values]
+
+    # Step 4 -- Find a _Converter object or raise TypeError.
+    try:
+        converter = converters[ext]
+    except KeyError:
+        fmt = "'%s' is not a valid converter name in '%s'"
+        tup = (ext, longName)
+        raise TypeError(fmt % tup)    
+
+    # Step 5 -- if there's a converter func, run it on each element.
+    # If the converter raises an exception, use or raise 'converter.error'.
+    if converter.func is not None:
+        tmp = values[:]
+        values = []
+        for elm in tmp:
+            try:
+                elm = converter.func(elm)
+            except (TypeError, ValueError):
+                tup = converter.name, elm
+                errmsg = "%s '%s' contains invalid characters" % tup
+                elm = useOrRaise(converter.error, errmsg)
+            values.append(elm)
+    # 'values' is now a list of strings, ints or floats.
+
+    # Step 6 -- If we're supposed to return a multi value, return the list
+    # as is.  If we're supposed to return a single value and the list is
+    # empty, return or raise 'converter.default'.  Otherwise, return the
+    # first element in the list and ignore any additional values.
+    if   multi:
+        return shortName, values
+    if len(values) == 0:
+        return shortName, useOrRaise(converter.default)
+    return shortName, values[0]
+
+# vim: sw=4 ts=4 expandtab
diff --git a/cheetah/Utils/__init__.py b/cheetah/Utils/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/cheetah/Utils/htmlDecode.py b/cheetah/Utils/htmlDecode.py
new file mode 100644 (file)
index 0000000..2832a74
--- /dev/null
@@ -0,0 +1,14 @@
+"""This is a copy of the htmlDecode function in Webware.
+
+@@TR: It implemented more efficiently.
+
+"""
+
+from Cheetah.Utils.htmlEncode import htmlCodesReversed
+
+def htmlDecode(s, codes=htmlCodesReversed):
+    """ Returns the ASCII decoded version of the given HTML string. This does
+    NOT remove normal HTML tags like <p>. It is the inverse of htmlEncode()."""
+    for code in codes:
+        s = s.replace(code[1], code[0])
+    return s
diff --git a/cheetah/Utils/htmlEncode.py b/cheetah/Utils/htmlEncode.py
new file mode 100644 (file)
index 0000000..f76c77e
--- /dev/null
@@ -0,0 +1,21 @@
+"""This is a copy of the htmlEncode function in Webware.
+
+
+@@TR: It implemented more efficiently.
+
+"""
+htmlCodes = [
+    ['&', '&amp;'],
+    ['<', '&lt;'],
+    ['>', '&gt;'],
+    ['"', '&quot;'],
+]
+htmlCodesReversed = htmlCodes[:]
+htmlCodesReversed.reverse()
+
+def htmlEncode(s, codes=htmlCodes):
+    """ Returns the HTML encoded version of the given string. This is useful to
+    display a plain ASCII text string on a web page."""
+    for code in codes:
+        s = s.replace(code[0], code[1])
+    return s
diff --git a/cheetah/Utils/statprof.py b/cheetah/Utils/statprof.py
new file mode 100644 (file)
index 0000000..0431628
--- /dev/null
@@ -0,0 +1,304 @@
+## statprof.py
+## Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
+## Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
+
+## This library is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public
+## License as published by the Free Software Foundation; either
+## version 2.1 of the License, or (at your option) any later version.
+##
+## This library is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+## Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public
+## License along with this program; if not, contact:
+##
+## Free Software Foundation           Voice:  +1-617-542-5942
+## 59 Temple Place - Suite 330        Fax:    +1-617-542-2652
+## Boston, MA  02111-1307,  USA       gnu@gnu.org
+
+"""
+statprof is intended to be a fairly simple statistical profiler for
+python. It was ported directly from a statistical profiler for guile,
+also named statprof, available from guile-lib [0].
+
+[0] http://wingolog.org/software/guile-lib/statprof/
+
+To start profiling, call statprof.start():
+>>> start()
+
+Then run whatever it is that you want to profile, for example:
+>>> import test.pystone; test.pystone.pystones()
+
+Then stop the profiling and print out the results:
+>>> stop()
+>>> display()
+  %   cumulative      self          
+ time    seconds   seconds  name    
+ 26.72      1.40      0.37  pystone.py:79:Proc0
+ 13.79      0.56      0.19  pystone.py:133:Proc1
+ 13.79      0.19      0.19  pystone.py:208:Proc8
+ 10.34      0.16      0.14  pystone.py:229:Func2
+  6.90      0.10      0.10  pystone.py:45:__init__
+  4.31      0.16      0.06  pystone.py:53:copy
+    ...
+
+All of the numerical data with the exception of the calls column is
+statistically approximate. In the following column descriptions, and
+in all of statprof, "time" refers to execution time (both user and
+system), not wall clock time.
+
+% time
+    The percent of the time spent inside the procedure itself (not
+    counting children).
+
+cumulative seconds
+    The total number of seconds spent in the procedure, including
+    children.
+
+self seconds
+    The total number of seconds spent in the procedure itself (not
+    counting children).
+
+name
+    The name of the procedure.
+
+By default statprof keeps the data collected from previous runs. If you
+want to clear the collected data, call reset():
+>>> reset()
+
+reset() can also be used to change the sampling frequency. For example,
+to tell statprof to sample 50 times a second:
+>>> reset(50)
+
+This means that statprof will sample the call stack after every 1/50 of
+a second of user + system time spent running on behalf of the python
+process. When your process is idle (for example, blocking in a read(),
+as is the case at the listener), the clock does not advance. For this
+reason statprof is not currently not suitable for profiling io-bound
+operations.
+
+The profiler uses the hash of the code object itself to identify the
+procedures, so it won't confuse different procedures with the same name.
+They will show up as two different rows in the output.
+
+Right now the profiler is quite simplistic.  I cannot provide
+call-graphs or other higher level information.  What you see in the
+table is pretty much all there is. Patches are welcome :-)
+
+
+Threading
+---------
+
+Because signals only get delivered to the main thread in Python,
+statprof only profiles the main thread. However because the time
+reporting function uses per-process timers, the results can be
+significantly off if other threads' work patterns are not similar to the
+main thread's work patterns.
+
+
+Implementation notes
+--------------------
+
+The profiler works by setting the unix profiling signal ITIMER_PROF to
+go off after the interval you define in the call to reset(). When the
+signal fires, a sampling routine is run which looks at the current
+procedure that's executing, and then crawls up the stack, and for each
+frame encountered, increments that frame's code object's sample count.
+Note that if a procedure is encountered multiple times on a given stack,
+it is only counted once. After the sampling is complete, the profiler
+resets profiling timer to fire again after the appropriate interval.
+
+Meanwhile, the profiler keeps track, via os.times(), how much CPU time
+(system and user -- which is also what ITIMER_PROF tracks), has elapsed
+while code has been executing within a start()/stop() block.
+
+The profiler also tries to avoid counting or timing its own code as
+much as possible.
+"""
+
+
+
+
+try:
+    import itimer
+except ImportError:
+    raise ImportError('''statprof requires the itimer python extension.
+To install it, enter the following commands from a terminal:
+
+wget http://www.cute.fi/~torppa/py-itimer/py-itimer.tar.gz
+tar zxvf py-itimer.tar.gz
+cd py-itimer
+sudo python setup.py install
+''')
+
+import signal
+import os
+
+
+__all__ = ['start', 'stop', 'reset', 'display']
+
+
+###########################################################################
+## Utils
+
+def clock():
+    times = os.times()
+    return times[0] + times[1]
+
+
+###########################################################################
+## Collection data structures
+
+class ProfileState(object):
+    def __init__(self, frequency=None):
+        self.reset(frequency)
+
+    def reset(self, frequency=None):
+        # total so far
+        self.accumulated_time = 0.0
+        # start_time when timer is active
+        self.last_start_time = None
+        # total count of sampler calls
+        self.sample_count = 0
+        # a float
+        if frequency:
+            self.sample_interval = 1.0/frequency
+        elif not hasattr(self, 'sample_interval'):
+            # default to 100 Hz
+            self.sample_interval = 1.0/100.0
+        else:
+            # leave the frequency as it was
+            pass
+        self.remaining_prof_time = None
+        # for user start/stop nesting
+        self.profile_level = 0
+        # whether to catch apply-frame
+        self.count_calls = False
+        # gc time between start() and stop()
+        self.gc_time_taken = 0
+
+    def accumulate_time(self, stop_time):
+        self.accumulated_time += stop_time - self.last_start_time
+
+state = ProfileState()
+
+## call_data := { code object: CallData }
+call_data = {}
+class CallData(object):
+    def __init__(self, code):
+        self.name = code.co_name
+        self.filename = code.co_filename
+        self.lineno = code.co_firstlineno
+        self.call_count = 0
+        self.cum_sample_count = 0
+        self.self_sample_count = 0
+        call_data[code] = self
+
+def get_call_data(code):
+    return call_data.get(code, None) or CallData(code)
+
+
+###########################################################################
+## SIGPROF handler
+
+def sample_stack_procs(frame):
+    state.sample_count += 1
+    get_call_data(frame.f_code).self_sample_count += 1
+
+    code_seen = {}
+    while frame:
+        code_seen[frame.f_code] = True
+        frame = frame.f_back
+    for code in code_seen.iterkeys():
+        get_call_data(code).cum_sample_count += 1
+
+def profile_signal_handler(signum, frame):
+    if state.profile_level > 0:
+        state.accumulate_time(clock())
+        sample_stack_procs(frame)
+        itimer.setitimer(itimer.ITIMER_PROF,
+            state.sample_interval, 0.0)
+        state.last_start_time = clock()
+
+
+###########################################################################
+## Profiling API
+
+def is_active():
+    return state.profile_level > 0
+
+def start():
+    state.profile_level += 1
+    if state.profile_level == 1:
+        state.last_start_time = clock()
+        rpt = state.remaining_prof_time
+        state.remaining_prof_time = None
+        signal.signal(signal.SIGPROF, profile_signal_handler)
+        itimer.setitimer(itimer.ITIMER_PROF,
+            rpt or state.sample_interval, 0.0)
+        state.gc_time_taken = 0 # dunno
+  
+def stop():
+    state.profile_level -= 1
+    if state.profile_level == 0:
+        state.accumulate_time(clock())
+        state.last_start_time = None
+        rpt = itimer.setitimer(itimer.ITIMER_PROF, 0.0, 0.0)
+        signal.signal(signal.SIGPROF, signal.SIG_IGN)
+        state.remaining_prof_time = rpt[0]
+        state.gc_time_taken = 0 # dunno
+    
+def reset(frequency=None):
+    assert state.profile_level == 0, "Can't reset() while statprof is running"
+    call_data.clear()
+    state.reset(frequency)
+    
+
+###########################################################################
+## Reporting API
+
+class CallStats(object):
+    def __init__(self, call_data):
+        self_samples = call_data.self_sample_count
+        cum_samples = call_data.cum_sample_count
+        nsamples = state.sample_count
+        secs_per_sample = state.accumulated_time / nsamples
+        basename = os.path.basename(call_data.filename)
+
+        self.name = '%s:%d:%s' % (basename, call_data.lineno, call_data.name)
+        self.pcnt_time_in_proc = self_samples / nsamples * 100
+        self.cum_secs_in_proc = cum_samples * secs_per_sample
+        self.self_secs_in_proc = self_samples * secs_per_sample
+        self.num_calls = None
+        self.self_secs_per_call = None
+        self.cum_secs_per_call = None
+
+    def display(self):
+        print('%6.2f %9.2f %9.2f  %s' % (self.pcnt_time_in_proc,
+                                         self.cum_secs_in_proc,
+                                         self.self_secs_in_proc,
+                                         self.name))
+
+
+def display():
+    if state.sample_count == 0:
+        print('No samples recorded.')
+        return
+
+    l = [CallStats(x) for x in call_data.itervalues()]
+    l = [(x.self_secs_in_proc, x.cum_secs_in_proc, x) for x in l]
+    l.sort(reverse=True)
+    l = [x[2] for x in l]
+
+    print('%5.5s %10.10s   %7.7s  %-8.8s' % ('%  ', 'cumulative', 'self', ''))
+    print('%5.5s  %9.9s  %8.8s  %-8.8s' % ("time", "seconds", "seconds", "name"))
+
+    for x in l:
+        x.display()
+
+    print('---')
+    print('Sample count: %d' % state.sample_count)
+    print('Total time: %f seconds' % state.accumulated_time)
diff --git a/cheetah/Version.py b/cheetah/Version.py
new file mode 100644 (file)
index 0000000..f01d1cd
--- /dev/null
@@ -0,0 +1,58 @@
+Version = '2.4.4'
+VersionTuple = (2, 4, 4, 'development', 0)
+
+MinCompatibleVersion = '2.0rc6'
+MinCompatibleVersionTuple = (2, 0, 0, 'candidate', 6)
+
+####
+def convertVersionStringToTuple(s):
+    versionNum = [0, 0, 0]
+    releaseType = 'final'
+    releaseTypeSubNum = 0
+    if s.find('a')!=-1:
+        num, releaseTypeSubNum = s.split('a')
+        releaseType = 'alpha'
+    elif s.find('b')!=-1:
+        num, releaseTypeSubNum = s.split('b')
+        releaseType = 'beta'
+    elif s.find('rc')!=-1:
+        num, releaseTypeSubNum = s.split('rc')
+        releaseType = 'candidate'
+    else:
+        num = s
+    num = num.split('.')
+    for i in range(len(num)):
+        versionNum[i] = int(num[i])
+    if len(versionNum)<3:
+        versionNum += [0]
+    releaseTypeSubNum = int(releaseTypeSubNum)
+
+    return tuple(versionNum+[releaseType, releaseTypeSubNum])
+
+
+if __name__ == '__main__':
+    c = convertVersionStringToTuple
+    print(c('2.0a1'))
+    print(c('2.0b1'))
+    print(c('2.0rc1'))
+    print(c('2.0'))
+    print(c('2.0.2'))
+
+
+    assert c('0.9.19b1') < c('0.9.19')
+    assert c('0.9b1') < c('0.9.19')
+
+    assert c('2.0a2') > c('2.0a1')
+    assert c('2.0b1') > c('2.0a2')
+    assert c('2.0b2') > c('2.0b1')
+    assert c('2.0b2') == c('2.0b2')
+
+    assert c('2.0rc1') > c('2.0b1')
+    assert c('2.0rc2') > c('2.0rc1')
+    assert c('2.0rc2') > c('2.0b1')
+
+    assert c('2.0') > c('2.0a1')
+    assert c('2.0') > c('2.0b1')
+    assert c('2.0') > c('2.0rc1')
+    assert c('2.0.1') > c('2.0')
+    assert c('2.0rc1') > c('2.0b1')
diff --git a/cheetah/__init__.py b/cheetah/__init__.py
new file mode 100644 (file)
index 0000000..910574b
--- /dev/null
@@ -0,0 +1,20 @@
+'''
+Cheetah is an open source template engine and code generation tool.
+
+It can be used standalone or combined with other tools and frameworks. Web
+development is its principle use, but Cheetah is very flexible and is also being
+used to generate C++ game code, Java, sql, form emails and even Python code.
+
+Homepage
+    http://www.cheetahtemplate.org/
+
+Documentation
+    http://cheetahtemplate.org/learn.html
+
+Mailing list
+cheetahtemplate-discuss@lists.sourceforge.net
+Subscribe at 
+    http://lists.sourceforge.net/lists/listinfo/cheetahtemplate-discuss
+'''
+
+from Version import *
diff --git a/cheetah/c/Cheetah.h b/cheetah/c/Cheetah.h
new file mode 100644 (file)
index 0000000..d149c15
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * (c) 2009, R. Tyler Ballance <tyler@slide.com>
+ */
+
+#ifndef _CHEETAH_H_
+#define _CHEETAH_H_
+
+#include <Python.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Python 2.3 compatibility
+ */
+#ifndef Py_RETURN_TRUE
+#define Py_RETURN_TRUE Py_INCREF(Py_True);\
+    return Py_True
+#endif
+#ifndef Py_RETURN_FALSE
+#define Py_RETURN_FALSE Py_INCREF(Py_False);\
+    return Py_False
+#endif 
+#ifndef Py_RETURN_NONE
+#define Py_RETURN_NONE Py_INCREF(Py_None);\
+    return Py_None
+#endif
+
+
+/*
+ * Filter Module
+ */
+typedef struct {
+    PyObject_HEAD
+    /* type specific fields */
+} PyFilter;
+
+/*
+ * End Filter Module 
+ */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/cheetah/c/_namemapper.c b/cheetah/c/_namemapper.c
new file mode 100644 (file)
index 0000000..a114658
--- /dev/null
@@ -0,0 +1,494 @@
+/* ***************************************************************************
+This is the C language version of NameMapper.py.  See the comments and
+DocStrings in NameMapper for details on the purpose and interface of this
+module.
+
+===============================================================================
+$Id: _namemapper.c,v 1.34 2007/12/10 18:25:20 tavis_rudd Exp $
+Authors: Tavis Rudd <tavis@damnsimple.com>
+Version: $Revision: 1.34 $
+Start Date: 2001/08/07
+Last Revision Date: $Date: 2007/12/10 18:25:20 $
+*/
+
+/* *************************************************************************** */
+#include <Python.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "cheetah.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+static PyObject *NotFound;   /* locally-raised exception */
+static PyObject *TooManyPeriods;   /* locally-raised exception */
+static PyObject* pprintMod_pformat; /* used for exception formatting */
+
+
+/* *************************************************************************** */
+/* First the c versions of the functions */
+/* *************************************************************************** */
+
+static void setNotFoundException(char *key, PyObject *namespace)
+{
+    PyObject *exceptionStr = NULL;
+    exceptionStr = PyUnicode_FromFormat("cannot find \'%s\'", key);
+    PyErr_SetObject(NotFound, exceptionStr);
+    Py_XDECREF(exceptionStr);
+}
+
+static int wrapInternalNotFoundException(char *fullName, PyObject *namespace)
+{
+    PyObject *excType, *excValue, *excTraceback, *isAlreadyWrapped = NULL;
+    PyObject *newExcValue = NULL;
+    if (!ALLOW_WRAPPING_OF_NOTFOUND_EXCEPTIONS) {
+        return 0;
+    } 
+
+    if (!PyErr_Occurred()) {
+        return 0;
+    }
+
+    if (PyErr_GivenExceptionMatches(PyErr_Occurred(), NotFound)) {
+        PyErr_Fetch(&excType, &excValue, &excTraceback);
+        isAlreadyWrapped = PyObject_CallMethod(excValue, "find", "s", "while searching");
+
+        if (isAlreadyWrapped != NULL) {
+            if (PyLong_AsLong(isAlreadyWrapped) == -1) {
+                newExcValue = PyUnicode_FromFormat("%U while searching for \'%s\'",
+                        excValue, fullName);
+            }
+            Py_DECREF(isAlreadyWrapped);
+        }
+        else {
+           newExcValue = excValue; 
+        }
+        PyErr_Restore(excType, newExcValue, excTraceback);
+        return -1;
+    } 
+    return 0;
+}
+
+
+static int isInstanceOrClass(PyObject *nextVal) {
+#ifndef IS_PYTHON3
+    /* old style classes or instances */
+    if((PyInstance_Check(nextVal)) || (PyClass_Check(nextVal))) {
+        return 1;
+    }
+#endif 
+
+    if (!PyObject_HasAttrString(nextVal, "__class__")) {
+        return 0;
+    }
+
+    /* new style classes or instances */
+    if (PyType_Check(nextVal) || PyObject_HasAttrString(nextVal, "mro")) {
+        return 1;
+    }
+
+    if (strncmp(nextVal->ob_type->tp_name, "function", 9) == 0)
+        return 0;
+
+    /* method, func, or builtin func */
+    if (PyObject_HasAttrString(nextVal, "im_func") 
+        || PyObject_HasAttrString(nextVal, "func_code")
+        || PyObject_HasAttrString(nextVal, "__self__")) {
+        return 0;
+    }
+
+    /* instance */
+    if ((!PyObject_HasAttrString(nextVal, "mro")) &&
+            PyObject_HasAttrString(nextVal, "__init__")) {
+        return 1;
+    }
+
+    return 0;
+}
+
+
+static int getNameChunks(char *nameChunks[], char *name, char *nameCopy) 
+{
+    char c;
+    char *currChunk;
+    int currChunkNum = 0;
+
+    currChunk = nameCopy;
+    while ('\0' != (c = *nameCopy)){
+    if ('.' == c) {
+        if (currChunkNum >= (MAXCHUNKS-2)) { /* avoid overflowing nameChunks[] */
+            PyErr_SetString(TooManyPeriods, name); 
+            return 0;
+        }
+
+        *nameCopy ='\0';
+        nameChunks[currChunkNum++] = currChunk;
+        nameCopy++;
+        currChunk = nameCopy;
+    } else 
+        nameCopy++;
+    }
+    if (nameCopy > currChunk) {
+        nameChunks[currChunkNum++] = currChunk;
+    }
+    return currChunkNum;
+}
+
+
+static int PyNamemapper_hasKey(PyObject *obj, char *key)
+{
+    if (PyMapping_Check(obj) && PyMapping_HasKeyString(obj, key)) {
+        return TRUE;
+    } else if (PyObject_HasAttrString(obj, key)) {
+        return TRUE;
+    }
+    return FALSE;
+}
+
+
+static PyObject *PyNamemapper_valueForKey(PyObject *obj, char *key)
+{
+    PyObject *theValue = NULL;
+
+    if (PyMapping_Check(obj) && PyMapping_HasKeyString(obj, key)) {
+        theValue = PyMapping_GetItemString(obj, key);
+    } else if (PyObject_HasAttrString(obj, key)) {
+        theValue = PyObject_GetAttrString(obj, key);
+    } else {
+        setNotFoundException(key, obj);
+    }
+    return theValue;
+}
+
+static PyObject *PyNamemapper_valueForName(PyObject *obj, char *nameChunks[], int numChunks, int executeCallables)
+{
+    int i;
+    char *currentKey;
+    PyObject *currentVal = NULL;
+    PyObject *nextVal = NULL;
+
+    currentVal = obj;
+    for (i=0; i < numChunks;i++) {
+        currentKey = nameChunks[i];
+        if (PyErr_CheckSignals()) {    /* not sure if I really need to do this here, but what the hell */
+            if (i>0) {
+                Py_DECREF(currentVal);
+            }
+            return NULL;
+        }
+        
+        if (PyMapping_Check(currentVal) && PyMapping_HasKeyString(currentVal, currentKey)) {
+            nextVal = PyMapping_GetItemString(currentVal, currentKey);
+        } 
+        else {
+          PyObject *exc;
+          nextVal = PyObject_GetAttrString(currentVal, currentKey);
+          exc = PyErr_Occurred();
+          if (exc != NULL) {
+            // if exception == AttributeError, report our own exception
+            if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
+                setNotFoundException(currentKey, currentVal);
+            }
+            // any exceptions results in failure
+            if (i > 0) {
+                Py_DECREF(currentVal);
+            }
+            return NULL;
+          }
+        }
+        if (i > 0) {
+            Py_DECREF(currentVal);
+        }
+
+        if (executeCallables && PyCallable_Check(nextVal) && 
+                (isInstanceOrClass(nextVal) == 0) ) {
+            if (!(currentVal = PyObject_CallObject(nextVal, NULL))) {
+                Py_DECREF(nextVal);
+                return NULL;
+            }
+            Py_DECREF(nextVal);
+        } else {
+            currentVal = nextVal;
+        }
+    }
+
+    return currentVal;
+}
+
+
+/* *************************************************************************** */
+/* Now the wrapper functions to export into the Python module */
+/* *************************************************************************** */
+
+
+static PyObject *namemapper_valueForKey(PyObject *self, PyObject *args)
+{
+    PyObject *obj;
+    char *key;
+
+    if (!PyArg_ParseTuple(args, "Os", &obj, &key)) {
+        return NULL;
+    }
+
+    return PyNamemapper_valueForKey(obj, key);
+}
+
+static PyObject *namemapper_valueForName(PYARGS)
+{
+    PyObject *obj;
+    char *name;
+    int executeCallables = 0;
+
+    char *nameCopy = NULL;
+    char *tmpPntr1 = NULL;
+    char *tmpPntr2 = NULL;
+    char *nameChunks[MAXCHUNKS];
+    int numChunks;
+
+    PyObject *theValue;
+
+    static char *kwlist[] = {"obj", "name", "executeCallables", NULL};
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Os|i", kwlist,  &obj, &name, &executeCallables)) {
+        return NULL;
+    }
+
+    createNameCopyAndChunks();  
+
+    theValue = PyNamemapper_valueForName(obj, nameChunks, numChunks, executeCallables);
+    free(nameCopy);
+    if (wrapInternalNotFoundException(name, obj)) {
+        theValue = NULL;
+    }
+    return theValue;
+}
+
+static PyObject *namemapper_valueFromSearchList(PYARGS)
+{
+    PyObject *searchList;
+    char *name;
+    int executeCallables = 0;
+
+    char *nameCopy = NULL;
+    char *tmpPntr1 = NULL;
+    char *tmpPntr2 = NULL;
+    char *nameChunks[MAXCHUNKS];
+    int numChunks;
+
+    PyObject *nameSpace = NULL;
+    PyObject *theValue = NULL;
+    PyObject *iterator = NULL;
+
+    static char *kwlist[] = {"searchList", "name", "executeCallables", NULL};
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Os|i", kwlist, &searchList, &name, &executeCallables)) {
+        return NULL;
+    }
+
+    createNameCopyAndChunks();
+
+    iterator = PyObject_GetIter(searchList);
+    if (iterator == NULL) {
+        PyErr_SetString(PyExc_TypeError,"This searchList is not iterable!");
+        goto done;
+    }
+
+    while ((nameSpace = PyIter_Next(iterator))) {
+        checkForNameInNameSpaceAndReturnIfFound(TRUE);
+        Py_DECREF(nameSpace);
+        if(PyErr_CheckSignals()) {
+        theValue = NULL;
+        goto done;
+        }
+    }
+    if (PyErr_Occurred()) {
+        theValue = NULL;
+        goto done;
+    }
+
+    setNotFoundException(nameChunks[0], searchList);
+
+done:
+    Py_XDECREF(iterator);
+    free(nameCopy);
+    return theValue;
+}
+
+static PyObject *namemapper_valueFromFrameOrSearchList(PyObject *self, PyObject *args, PyObject *keywds)
+{
+    /* python function args */
+    char *name;
+    int executeCallables = 0;
+    PyObject *searchList = NULL;
+
+    /* locals */
+    char *nameCopy = NULL;
+    char *tmpPntr1 = NULL;
+    char *tmpPntr2 = NULL;
+    char *nameChunks[MAXCHUNKS];
+    int numChunks;
+
+    PyObject *nameSpace = NULL;
+    PyObject *theValue = NULL;
+    PyObject *excString = NULL;
+    PyObject *iterator = NULL;
+
+    static char *kwlist[] = {"searchList", "name", "executeCallables", NULL};
+
+    if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os|i", kwlist,  &searchList, &name, 
+                    &executeCallables)) {
+        return NULL;
+    }
+
+    createNameCopyAndChunks();
+
+    nameSpace = PyEval_GetLocals();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);  
+
+    iterator = PyObject_GetIter(searchList);
+    if (iterator == NULL) {
+        PyErr_SetString(PyExc_TypeError,"This searchList is not iterable!");
+        goto done;
+    }
+    while ( (nameSpace = PyIter_Next(iterator)) ) {
+        checkForNameInNameSpaceAndReturnIfFound(TRUE);
+        Py_DECREF(nameSpace);
+        if(PyErr_CheckSignals()) {
+            theValue = NULL;
+            goto done;
+        }
+    }
+    if (PyErr_Occurred()) {
+        theValue = NULL;
+        goto done;
+    }
+
+    nameSpace = PyEval_GetGlobals();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);
+
+    nameSpace = PyEval_GetBuiltins();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);
+
+    excString = Py_BuildValue("s", "[locals()]+searchList+[globals(), __builtins__]");
+    setNotFoundException(nameChunks[0], excString);
+    Py_DECREF(excString);
+
+done:
+    Py_XDECREF(iterator);
+    free(nameCopy);
+    return theValue;
+}
+
+static PyObject *namemapper_valueFromFrame(PyObject *self, PyObject *args, PyObject *keywds)
+{
+    /* python function args */
+    char *name;
+    int executeCallables = 0;
+
+    /* locals */
+    char *tmpPntr1 = NULL;
+    char *tmpPntr2 = NULL;
+
+    char *nameCopy = NULL;
+    char *nameChunks[MAXCHUNKS];
+    int numChunks;
+
+    PyObject *nameSpace = NULL;
+    PyObject *theValue = NULL;
+    PyObject *excString = NULL;
+
+    static char *kwlist[] = {"name", "executeCallables", NULL};
+
+    if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|i", kwlist, &name, &executeCallables)) {
+        return NULL;
+    }
+
+    createNameCopyAndChunks();
+
+    nameSpace = PyEval_GetLocals();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);
+
+    nameSpace = PyEval_GetGlobals();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);
+
+    nameSpace = PyEval_GetBuiltins();
+    checkForNameInNameSpaceAndReturnIfFound(FALSE);
+
+    excString = Py_BuildValue("s", "[locals(), globals(), __builtins__]");
+    setNotFoundException(nameChunks[0], excString);
+    Py_DECREF(excString);
+done:
+    free(nameCopy);
+    return theValue;
+}
+
+/* *************************************************************************** */
+/* Method registration table: name-string -> function-pointer */
+
+static struct PyMethodDef namemapper_methods[] = {
+  {"valueForKey", namemapper_valueForKey,  1},
+  {"valueForName", (PyCFunction)namemapper_valueForName,  METH_VARARGS|METH_KEYWORDS},
+  {"valueFromSearchList", (PyCFunction)namemapper_valueFromSearchList,  METH_VARARGS|METH_KEYWORDS},
+  {"valueFromFrame", (PyCFunction)namemapper_valueFromFrame,  METH_VARARGS|METH_KEYWORDS},
+  {"valueFromFrameOrSearchList", (PyCFunction)namemapper_valueFromFrameOrSearchList,  METH_VARARGS|METH_KEYWORDS},
+  {NULL,         NULL}
+};
+
+
+/* *************************************************************************** */
+/* Initialization function (import-time) */
+
+#ifdef IS_PYTHON3
+static struct PyModuleDef namemappermodule = {
+    PyModuleDef_HEAD_INIT,
+    "_namemapper",
+    NULL, /* docstring */
+    -1, 
+    namemapper_methods,
+    NULL,
+    NULL,
+    NULL,
+    NULL};
+
+PyMODINIT_FUNC PyInit__namemapper(void)
+{
+    PyObject *m = PyModule_Create(&namemappermodule);
+#else
+DL_EXPORT(void) init_namemapper(void)
+{
+    PyObject *m = Py_InitModule3("_namemapper", namemapper_methods, NULL);
+#endif 
+
+    PyObject *d, *pprintMod;
+
+    /* add symbolic constants to the module */
+    d = PyModule_GetDict(m);
+    NotFound = PyErr_NewException("NameMapper.NotFound",PyExc_LookupError,NULL);
+    TooManyPeriods = PyErr_NewException("NameMapper.TooManyPeriodsInName",NULL,NULL);
+    PyDict_SetItemString(d, "NotFound", NotFound);
+    PyDict_SetItemString(d, "TooManyPeriodsInName", TooManyPeriods);
+    pprintMod = PyImport_ImportModule("pprint");
+    if (!pprintMod) {
+#ifdef IS_PYTHON3
+        return NULL;
+#else
+        return;
+#endif
+    }
+    pprintMod_pformat = PyObject_GetAttrString(pprintMod, "pformat");
+    Py_DECREF(pprintMod);
+    /* check for errors */
+    if (PyErr_Occurred()) {
+        Py_FatalError("Can't initialize module _namemapper");
+    }
+#ifdef IS_PYTHON3
+    return m;
+#endif
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cheetah/c/cheetah.h b/cheetah/c/cheetah.h
new file mode 100644 (file)
index 0000000..f596431
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2009, R. Tyler Ballance <tyler@monkeypox.org>
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ *  1. Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ * 
+ *  2. Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in
+ *     the documentation and/or other materials provided with the
+ *     distribution.
+ * 
+ *  3. Neither the name of R. Tyler Ballance nor the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */ 
+
+#ifndef _CHEETAH_H_
+#define _CHEETAH_H_
+
+#include <Python.h>
+
+#if PY_MAJOR_VERSION >= 3
+#define IS_PYTHON3
+#endif
+
+#define TRUE 1
+#define FALSE 0
+
+#define PYARGS PyObject *self, PyObject *args, PyObject *kwargs
+
+
+/*
+ * _namemapper.c specific definitions 
+ */
+#define MAXCHUNKS 15           /* max num of nameChunks for the arrays */
+#define ALLOW_WRAPPING_OF_NOTFOUND_EXCEPTIONS 1
+#define createNameCopyAndChunks() {\
+    nameCopy = malloc(strlen(name) + 1);\
+    tmpPntr1 = name; \
+    tmpPntr2 = nameCopy;\
+    while ((*tmpPntr2++ = *tmpPntr1++)); \
+        numChunks = getNameChunks(nameChunks, name, nameCopy); \
+    if (PyErr_Occurred()) {    /* there might have been TooManyPeriods */\
+        free(nameCopy);\
+        return NULL;\
+    }\
+}
+
+#define checkForNameInNameSpaceAndReturnIfFound(namespace_decref) { \
+    if ( PyNamemapper_hasKey(nameSpace, nameChunks[0]) ) {\
+        theValue = PyNamemapper_valueForName(nameSpace, nameChunks, numChunks, executeCallables);\
+        if (namespace_decref) {\
+            Py_DECREF(nameSpace);\
+        }\
+        if (wrapInternalNotFoundException(name, nameSpace)) {\
+            theValue = NULL;\
+        }\
+        goto done;\
+    }\
+}
+
+#endif
diff --git a/cheetah/convertTmplPathToModuleName.py b/cheetah/convertTmplPathToModuleName.py
new file mode 100644 (file)
index 0000000..264f18e
--- /dev/null
@@ -0,0 +1,20 @@
+import os.path
+import string
+
+letters = None
+try:
+    letters = string.ascii_letters
+except AttributeError:
+    letters = string.letters
+
+l = ['_'] * 256
+for c in string.digits + letters:
+    l[ord(c)] = c
+_pathNameTransChars = ''.join(l)
+del l, c
+
+def convertTmplPathToModuleName(tmplPath,
+                                _pathNameTransChars=_pathNameTransChars,
+                                splitdrive=os.path.splitdrive,
+                                ):
+    return splitdrive(tmplPath)[1].translate(_pathNameTransChars)
diff --git a/packaging/python-cheetah.changes b/packaging/python-cheetah.changes
new file mode 100644 (file)
index 0000000..589ac8b
--- /dev/null
@@ -0,0 +1,19 @@
+* Mon Feb 20 2012 Jian-feng Ding <jian-feng.ding@intel.com> - 2.4.4
+- Update to 2.4.4
+
+* Fri Apr 02 2010 Priya Vijayan <priya.vijayan@intel.com> - 2.4.2.1
+- Update to 2.4.2.1
+
+* Sat Feb 13 2010 Anas Nashif <anas.nashif@intel.com> - 2.4.1
+- build for other distros
+
+* Wed Dec 23 2009 Anas Nashif <anas.nashif@intel.com> - 2.4.1
+- rename to python-cheetah and comply with python module naming
+- Update to 2.4.1
+
+* Wed Nov 04 2009 Priya Vijayan <priya.vijayan@intel.com> 2.4.0
+- Update to 2.4.0
+
+* Tue Jul 28 2009 Priya Vijayan <priya.vijayan@intel.com> 2.2.1
+- Initial Import to Moblin
+
diff --git a/packaging/python-cheetah.manifest b/packaging/python-cheetah.manifest
new file mode 100644 (file)
index 0000000..017d22d
--- /dev/null
@@ -0,0 +1,5 @@
+<manifest>
+ <request>
+    <domain name="_"/>
+ </request>
+</manifest>
diff --git a/packaging/python-cheetah.spec b/packaging/python-cheetah.spec
new file mode 100644 (file)
index 0000000..a79527a
--- /dev/null
@@ -0,0 +1,105 @@
+%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
+
+Name:           python-cheetah
+Version:        2.4.4
+Release:        1
+Summary:        Template engine and code-generator
+
+Group:          Development/Libraries
+License:        MIT
+URL:            http://cheetahtemplate.org/
+Source:         http://pypi.python.org/packages/source/C/Cheetah/Cheetah-%{version}.tar.gz
+Source1001: packaging/python-cheetah.manifest 
+
+BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root
+
+BuildRequires:  python-devel
+%if 0%{?suse_version}
+BuildRequires:  python-setuptools
+%else
+BuildRequires:  python-setuptools-devel
+%endif
+BuildRequires:  python-lxml, python-pygments, python-markdown
+
+%description
+Cheetah is an open source template engine and code generation tool,
+written in Python. It can be used standalone or combined with other
+tools and frameworks. Web development is its principle use, but
+Cheetah is very flexible and is also being used to generate C++ game
+code, Java, sql, form emails and even Python code.
+
+%prep
+%setup -q -n Cheetah-%{version}
+
+%build
+cp %{SOURCE1001} .
+export CHEETAH_USE_SETUPTOOLS=1
+%{__python} setup.py build
+
+%install
+export CHEETAH_USE_SETUPTOOLS=1
+%if 0%{?suse_version}
+%{__python} setup.py install --prefix=%{_prefix} --root=%{buildroot} --record-rpm=INSTALLED_FILES  
+%else
+%{__python} setup.py install --skip-build --root %{buildroot} --prefix=%{_prefix}
+%endif
+
+%check
+export PATH="%{buildroot}/%{_bindir}:$PATH"
+export PYTHONPATH="%{buildroot}/%{python_sitearch}"
+%{__python} %{buildroot}/%{python_sitearch}/Cheetah/Tests/Test.py
+
+%clean
+rm -rf %{buildroot}
+
+%if 0%{?suse_version}
+%files -f INSTALLED_FILES
+%manifest python-cheetah.manifest
+%defattr(-,root,root,-)
+%else
+%files
+%manifest python-cheetah.manifest
+%defattr(-,root,root,-)
+%dir %{python_sitearch}/Cheetah
+%{python_sitearch}/Cheetah/*.py
+%{python_sitearch}/Cheetah/*.pyc
+%{python_sitearch}/Cheetah/_namemapper.so
+%{python_sitearch}/Cheetah/*.pyo
+
+%dir %{python_sitearch}/Cheetah/Macros
+%{python_sitearch}/Cheetah/Macros/*.py
+%{python_sitearch}/Cheetah/Macros/*.pyc
+%{python_sitearch}/Cheetah/Macros/*.pyo
+
+%dir %{python_sitearch}/Cheetah/Templates
+%{python_sitearch}/Cheetah/Templates/*.py
+%{python_sitearch}/Cheetah/Templates/*.pyc
+%{python_sitearch}/Cheetah/Templates/*.tmpl
+%{python_sitearch}/Cheetah/Templates/*.pyo
+
+%dir %{python_sitearch}/Cheetah/Tests
+%{python_sitearch}/Cheetah/Tests/*.py
+%{python_sitearch}/Cheetah/Tests/*.pyc
+%{python_sitearch}/Cheetah/Tests/*.pyo
+
+%dir %{python_sitearch}/Cheetah/Tools
+%{python_sitearch}/Cheetah/Tools/*.py
+%{python_sitearch}/Cheetah/Tools/*.pyc
+%{python_sitearch}/Cheetah/Tools/*.txt
+%{python_sitearch}/Cheetah/Tools/*.pyo
+
+%dir %{python_sitearch}/Cheetah/Utils
+%{python_sitearch}/Cheetah/Utils/*.py
+%{python_sitearch}/Cheetah/Utils/*.pyc
+%{python_sitearch}/Cheetah/Utils/*.pyo
+
+%dir %{python_sitearch}/Cheetah-%{version}-*.egg-info
+%{python_sitearch}/Cheetah-%{version}-*.egg-info/PKG-INFO
+%{python_sitearch}/Cheetah-%{version}-*.egg-info/*.txt
+
+%endif
+%doc CHANGES LICENSE TODO
+%{_bindir}/cheetah
+%{_bindir}/cheetah-compile
+%{_bindir}/cheetah-analyze
+
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..861a9f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..b8d1b14
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# $Id: setup.py,v 1.15 2005/01/03 17:58:41 tavis_rudd Exp $
+import os
+
+try:
+    os.remove('MANIFEST')               # to avoid those bloody out-of-date manifests!!
+except:
+    pass
+    
+import SetupTools
+import SetupConfig
+configurations = (SetupConfig,)
+SetupTools.run_setup( configurations )
+
+
+
+