summaryrefslogtreecommitdiff
path: root/cheetah
diff options
context:
space:
mode:
Diffstat (limited to 'cheetah')
-rw-r--r--cheetah/CacheRegion.py136
-rw-r--r--cheetah/CacheStore.py108
-rw-r--r--cheetah/CheetahWrapper.py621
-rw-r--r--cheetah/Compiler.py2013
-rw-r--r--cheetah/Django.py16
-rw-r--r--cheetah/DummyTransaction.py95
-rw-r--r--cheetah/ErrorCatchers.py62
-rw-r--r--cheetah/FileUtils.py373
-rw-r--r--cheetah/Filters.py233
-rwxr-xr-xcheetah/ImportHooks.py138
-rwxr-xr-xcheetah/ImportManager.py565
-rw-r--r--cheetah/Macros/I18n.py67
-rw-r--r--cheetah/Macros/__init__.py1
-rw-r--r--cheetah/NameMapper.py379
-rw-r--r--cheetah/Parser.py2662
-rw-r--r--cheetah/Servlet.py112
-rw-r--r--cheetah/SettingsManager.py292
-rw-r--r--cheetah/SourceReader.py303
-rw-r--r--cheetah/Template.py1897
-rw-r--r--cheetah/TemplateCmdLineIface.py107
-rw-r--r--cheetah/Templates/SkeletonPage.py272
-rw-r--r--cheetah/Templates/SkeletonPage.tmpl44
-rw-r--r--cheetah/Templates/_SkeletonPage.py215
-rw-r--r--cheetah/Templates/__init__.py1
-rw-r--r--cheetah/Tests/CheetahWrapper.py545
-rw-r--r--cheetah/Tests/Filters.py68
-rw-r--r--cheetah/Tests/NameMapper.py526
-rw-r--r--cheetah/Tests/Regressions.py248
-rw-r--r--cheetah/Tests/SyntaxAndOutput.py3227
-rw-r--r--cheetah/Tests/Template.py353
-rwxr-xr-xcheetah/Tests/Test.py45
-rw-r--r--cheetah/Tests/Unicode.py185
-rw-r--r--cheetah/Tests/__init__.py1
-rwxr-xr-xcheetah/Tests/unittest_local_copy.py978
-rw-r--r--cheetah/Tests/xmlrunner.py381
-rw-r--r--cheetah/Tools/CGITemplate.py77
-rw-r--r--cheetah/Tools/MondoReport.py463
-rw-r--r--cheetah/Tools/MondoReportDoc.txt391
-rw-r--r--cheetah/Tools/RecursiveNull.py28
-rw-r--r--cheetah/Tools/SiteHierarchy.py182
-rw-r--r--cheetah/Tools/__init__.py8
-rw-r--r--cheetah/Tools/turbocheetah/__init__.py5
-rw-r--r--cheetah/Tools/turbocheetah/cheetahsupport.py110
-rw-r--r--cheetah/Tools/turbocheetah/tests/__init__.py1
-rw-r--r--cheetah/Tools/turbocheetah/tests/test_template.py66
-rw-r--r--cheetah/Unspecified.py9
-rw-r--r--cheetah/Utils/Indenter.py123
-rw-r--r--cheetah/Utils/Misc.py81
-rw-r--r--cheetah/Utils/VerifyType.py83
-rw-r--r--cheetah/Utils/WebInputMixin.py102
-rw-r--r--cheetah/Utils/__init__.py1
-rw-r--r--cheetah/Utils/htmlDecode.py14
-rw-r--r--cheetah/Utils/htmlEncode.py21
-rw-r--r--cheetah/Utils/memcache.py624
-rw-r--r--cheetah/Utils/statprof.py304
-rw-r--r--cheetah/Version.py58
-rw-r--r--cheetah/__init__.py20
-rw-r--r--cheetah/convertTmplPathToModuleName.py15
58 files changed, 20055 insertions, 0 deletions
diff --git a/cheetah/CacheRegion.py b/cheetah/CacheRegion.py
new file mode 100644
index 0000000..dd0d099
--- /dev/null
+++ b/cheetah/CacheRegion.py
@@ -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 not self._cacheItems.has_key(cacheItemID):
+ 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
index 0000000..9c41656
--- /dev/null
+++ b/cheetah/CacheStore.py
@@ -0,0 +1,108 @@
+'''
+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
+
+from Cheetah.Utils.memcache import Client as MemcachedClient
+
+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 self._data.has_key(key):
+ 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 self._data.has_key(key):
+ 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
+
+ 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
index 0000000..96f57d5
--- /dev/null
+++ b/cheetah/CheetahWrapper.py
@@ -0,0 +1,621 @@
+# $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='Verbosely 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)
+ opts.verbose = not opts.stdout
+
+ ##################################################
+ ## 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 Cheetah.Tests.unittest_local_copy as unittest
+ del sys.argv[1:] # Prevent unittest from misinterpreting options.
+ sys.argv.extend(self.testOpts)
+ #unittest.main(testSuite=Test.testSuite)
+ #unittest.main(testSuite=Test.testSuite)
+ unittest.main(module=Test)
+
+ 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 dstSources.has_key(b.dst):
+ dstSources[b.dst].append(b.src)
+ else:
+ dstSources[b.dst] = [b.src]
+ keys = dstSources.keys()
+ keys.sort()
+ 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()
+
+
+##################################################
+## 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
index 0000000..39c7f51
--- /dev/null
+++ b/cheetah/Compiler.py
@@ -0,0 +1,2013 @@
+'''
+ 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 _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 \$vars in the string.
+ """
+
+ token = self.setting('directiveStartToken')
+ return theString.replace('\\' + token, token)
+
+ def commitStrConst(self):
+ """Add the code for outputting the pending strConst without chopping off
+ any whitespace from it.
+ """
+ if self._pendingStrConstChunks:
+ strConst = self._unescapeCheetahVars(''.join(self._pendingStrConstChunks))
+ strConst = self._unescapeDirectives(strConst)
+ self._pendingStrConstChunks = []
+ if not strConst:
+ return
+ else:
+ reprstr = repr(strConst).replace('\\012','\n')
+ i = 0
+ out = []
+ if reprstr.startswith('u'):
+ i = 1
+ out = ['u']
+ body = escapedNewlineRE.sub('\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 self.__dict__.has_key(name):
+ 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 self._placeholderToErrorCatcherMap.has_key(rawCode):
+ 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.match(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 self.__dict__.has_key(name):
+ 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",
+ "import __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 = theVars.keys()
+ keys.sort()
+ 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/Django.py b/cheetah/Django.py
new file mode 100644
index 0000000..876fbbc
--- /dev/null
+++ b/cheetah/Django.py
@@ -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
index 0000000..26d2ea7
--- /dev/null
+++ b/cheetah/DummyTransaction.py
@@ -0,0 +1,95 @@
+
+'''
+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 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 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 ''.join(chunks)
+ except UnicodeDecodeError, ex:
+ nonunicode = [c for c in chunks if not isinstance(c, unicode)]
+ raise DummyResponseFailure('''Looks like you're trying to mix encoded strings with Unicode strings
+ (most likely utf-8 encoded ones)
+
+ This can happen if you're using the `EncodeUnicode` filter, or if you're manually
+ encoding strings as utf-8 before passing them in on the searchList (possible offenders:
+ %s)
+ (%s)''' % (nonunicode, ex))
+
+
+ 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, types.TypeType):
+ _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
index 0000000..a8b7035
--- /dev/null
+++ b/cheetah/ErrorCatchers.py
@@ -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
index 0000000..97e77f8
--- /dev/null
+++ b/cheetah/FileUtils.py
@@ -0,0 +1,373 @@
+# $Id: FileUtils.py,v 1.12 2005/11/02 22:26:07 tavis_rudd Exp $
+"""File utitilies for Python:
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+License: This software is released for unlimited distribution under the
+ terms of the MIT license. See the LICENSE file.
+Version: $Revision: 1.12 $
+Start Date: 2001/09/26
+Last Revision Date: $Date: 2005/11/02 22:26:07 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.12 $"[11:-2]
+
+
+from glob import glob
+import os
+from os import listdir
+import os.path
+import re
+from types import StringType
+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 type(patternOrRE) == StringType:
+ self._regex = re.compile(patternOrRE)
+ else:
+ self._regex = patternOrRE
+ if type(replacement) == StringType:
+ 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 locals().has_key('orig'):
+ 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 not locals().has_key('orig'):
+ orig = open(file).read()
+ new = regex.sub(subber, orig)
+ open(file, 'w').write(new)
+
+ def _subDispatcher(self, match):
+ if self._recordResults:
+ if not self._results.has_key(self._currFile):
+ 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
index 0000000..dd65f28
--- /dev/null
+++ b/cheetah/Filters.py
@@ -0,0 +1,233 @@
+'''
+ 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):
+ if encoding:
+ return val.encode(encoding)
+ else:
+ return val
+ else:
+ try:
+ return str(val)
+ except UnicodeEncodeError:
+ return unicode(val)
+ return u''
+
+RawOrEncodedUnicode = Filter
+
+class EncodeUnicode(Filter):
+ def filter(self, val,
+ encoding='utf8',
+ str=str,
+ **kw):
+ """Encode Unicode strings, by default in UTF-8.
+
+ >>> import Cheetah.Template
+ >>> t = Cheetah.Template.Template('''
+ ... $myvar
+ ... ${myvar, encoding='utf16'}
+ ... ''', searchList=[{'myvar': u'Asni\xe8res'}],
+ ... filter='EncodeUnicode')
+ >>> print t
+ """
+ if isinstance(val, unicode):
+ return val.encode(encoding)
+ if val is None:
+ return ''
+ return str(val)
+
+
+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 kw.has_key('maxlen') 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 kw.has_key('also'):
+ 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 1: # 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:", `s1`
+ print " WebSafe:", `WebSafe().filter(s1)`
+
+ print
+ print " Strip INPUT:", `s2`
+ print " Strip:", `Strip().filter(s2)`
+ print "StripSqueeze:", `StripSqueeze().filter(s2)`
+
+ print "Unicode:", `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
index 0000000..261fb01
--- /dev/null
+++ b/cheetah/ImportHooks.py
@@ -0,0 +1,138 @@
+# $Id: ImportHooks.py,v 1.27 2007/11/16 18:28:47 tavis_rudd Exp $
+
+"""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()
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+License: This software is released for unlimited distribution under the
+ terms of the MIT license. See the LICENSE file.
+Version: $Revision: 1.27 $
+Start Date: 2001/03/30
+Last Revision Date: $Date: 2007/11/16 18:28:47 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.27 $"[11:-2]
+
+import sys
+import os.path
+import types
+import __builtin__
+import new
+import imp
+from threading import RLock
+import string
+import traceback
+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 = imp.new_module(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 type(__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 type(__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
index 0000000..743360e
--- /dev/null
+++ b/cheetah/ImportManager.py
@@ -0,0 +1,565 @@
+# $Id: ImportManager.py,v 1.6 2007/04/03 01:56:24 tavis_rudd Exp $
+
+"""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
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com> based on Gordon McMillan's iu.py
+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/03/30
+Last Revision Date: $Date: 2007/04/03 01:56:24 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.6 $"[11:-2]
+
+##################################################
+## DEPENDENCIES
+
+import sys
+import imp
+import marshal
+
+##################################################
+## CONSTANTS & GLOBALS
+
+try:
+ True,False
+except NameError:
+ True, False = (1==1),(1==0)
+
+_installed = False
+
+STRINGTYPE = type('')
+
+# _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 1:
+ 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 type(thing) is STRINGTYPE:
+ 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 in 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
index 0000000..7c2b1ef
--- /dev/null
+++ b/cheetah/Macros/I18n.py
@@ -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
index 0000000..792d600
--- /dev/null
+++ b/cheetah/Macros/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/cheetah/NameMapper.py b/cheetah/NameMapper.py
new file mode 100644
index 0000000..3a6322e
--- /dev/null
+++ b/cheetah/NameMapper.py
@@ -0,0 +1,379 @@
+# $Id: NameMapper.py,v 1.32 2007/12/10 19:20:09 tavis_rudd Exp $
+
+"""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.
+
+Meta-Data
+================================================================================
+Authors: Tavis Rudd <tavis@damnsimple.com>,
+ Chuck Esterbrook <echuck@mindspring.com>
+Version: $Revision: 1.32 $
+Start Date: 2001/04/03
+Last Revision Date: $Date: 2007/12/10 19:20:09 $
+"""
+from __future__ import generators
+__author__ = "Tavis Rudd <tavis@damnsimple.com>," +\
+ "\nChuck Esterbrook <echuck@mindspring.com>"
+__revision__ = "$Revision: 1.32 $"[11:-2]
+import types
+from types import StringType, InstanceType, ClassType, TypeType
+from pprint import pformat
+import inspect
+import pdb
+
+_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 type(obj) in (InstanceType, ClassType):
+ # 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 obj.has_key(key):
+ return True
+ elif hasattr(obj, key):
+ return True
+ else:
+ return False
+
+def valueForKey(obj, key):
+ if hasattr(obj, 'has_key') and obj.has_key(key):
+ 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 obj.has_key(key):
+ nextObj = obj[key]
+ else:
+ try:
+ nextObj = getattr(obj, key)
+ except AttributeError:
+ _raiseNotFoundException(key, obj)
+
+ if executeCallables and callable(nextObj) 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 _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
index 0000000..7436e9c
--- /dev/null
+++ b/cheetah/Parser.py
@@ -0,0 +1,2662 @@
+# $Id: Parser.py,v 1.137 2008/03/10 05:25:13 tavis_rudd Exp $
+"""Parser classes for Cheetah's Compiler
+
+Classes:
+ ParseError( Exception )
+ _LowLevelParser( Cheetah.SourceReader.SourceReader ), basically a lexer
+ _HighLevelParser( _LowLevelParser )
+ Parser === _HighLevelParser (an alias)
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+Version: $Revision: 1.137 $
+Start Date: 2001/08/01
+Last Revision Date: $Date: 2008/03/10 05:25:13 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.137 $"[11:-2]
+
+import os
+import sys
+import re
+from re import DOTALL, MULTILINE
+from types import StringType, ListType, TupleType, ClassType, TypeType
+import time
+from tokenize import pseudoprog
+import inspect
+import new
+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 apply(group, choices) + '*'
+def maybe(*choices): return apply(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')
+
+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:
+ 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:
+ """Used by _LowLevelParser.getArgList()"""
+
+ def __init__(self):
+ self.argNames = []
+ self.defVals = []
+ self.i = 0
+
+ def addArgName(self, name):
+ self.argNames.append( name )
+ self.defVals.append( None )
+
+ def next(self):
+ self.i += 1
+
+ def addToDefVal(self, token):
+ i = self.i
+ if self.defVals[i] == None:
+ self.defVals[i] = ''
+ self.defVals[i] += token
+
+ def merge(self):
+ defVals = self.defVals
+ for i in range(len(defVals)):
+ if type(defVals[i]) == StringType:
+ defVals[i] = defVals[i].strip()
+
+ return map(None, [i.strip() for i in self.argNames], defVals)
+
+ 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 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 1:
+ 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 1:
+ 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.addToDefVal(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.addArgName( 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.addToDefVal(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.addArgName(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 1:
+ 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
+ #print 'CHEETAH STRING', nextToken, theStr, startPosIdx
+ 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))
+ #if not self.atEnd() and self.matches('.join('):
+ # print 'DEBUG***'
+ 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):
+ _LowLevelParser.__init__(self, 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):
+ _LowLevelParser.configureParser(self)
+ self._initDirectives()
+
+ def _initDirectives(self):
+ def normalizeParserVal(val):
+ if isinstance(val, (str,unicode)):
+ handler = getattr(self, val)
+ elif type(val) in (ClassType, TypeType):
+ handler = val(self)
+ elif callable(val):
+ 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 type(callback) in (ClassType, TypeType):
+ 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)
+ 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 1:
+ 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:
+ out = sys.stderr
+ print >> out, 'An error occurred while processing the following #compiler directive.'
+ print >> out, '-'*80
+ print >> out, self[startPos:endPos]
+ print >> out, '-'*80
+ print >> out, 'Please check the syntax of these settings.'
+ print >> out, 'A full Python exception traceback follows.'
+ 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:
+ out = sys.stderr
+ print >> out, 'An error occurred while processing the following compiler settings.'
+ print >> out, '-'*80
+ print >> out, settingsStr.strip()
+ print >> out, '-'*80
+ print >> out, 'Please check the syntax of these settings.'
+ print >> out, 'A full Python exception traceback follows.'
+ 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 not self._directiveNamesAndParsers.has_key(macroName)
+ 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 not kwArgs.has_key('src')
+ kwArgs['src'] = srcBlock
+
+ if type(macro)==new.instancemethod:
+ 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
index 0000000..f19e508
--- /dev/null
+++ b/cheetah/Servlet.py
@@ -0,0 +1,112 @@
+'''
+Provides an abstract Servlet baseclass for Cheetah's Template class
+'''
+
+import sys
+import os.path
+
+isWebwareInstalled = False
+try:
+ try:
+ from ds.appserver.Servlet import Servlet as BaseServlet
+ except:
+ from WebKit.Servlet import Servlet as BaseServlet
+ isWebwareInstalled = True
+
+ if not issubclass(BaseServlet, object):
+ class NewStyleBaseServlet(BaseServlet, object):
+ pass
+ BaseServlet = NewStyleBaseServlet
+except:
+ class BaseServlet(object):
+ _reusable = 1
+ _threadSafe = 0
+
+ def awake(self, transaction):
+ pass
+
+ def sleep(self, transaction):
+ pass
+
+ def shutdown(self):
+ pass
+
+##################################################
+## CLASSES
+
+class Servlet(BaseServlet):
+
+ """This class is an abstract baseclass for Cheetah.Template.Template.
+
+ It wraps WebKit.Servlet and provides a few extra convenience methods that
+ are also found in WebKit.Page. It doesn't do any of the HTTP method
+ resolution that is done in WebKit.HTTPServlet
+ """
+
+ transaction = None
+ application = None
+ request = None
+ session = None
+
+ def __init__(self, *args, **kwargs):
+ super(Servlet, self).__init__(*args, **kwargs)
+
+ # this default will be changed by the .awake() method
+ self._CHEETAH__isControlledByWebKit = False
+
+ ## methods called by Webware during the request-response
+
+ def awake(self, transaction):
+ super(Servlet, self).awake(transaction)
+
+ # a hack to signify that the servlet is being run directly from WebKit
+ self._CHEETAH__isControlledByWebKit = True
+
+ self.transaction = transaction
+ #self.application = transaction.application
+ self.response = response = transaction.response
+ self.request = transaction.request
+
+ # Temporary hack to accomodate bug in
+ # WebKit.Servlet.Servlet.serverSidePath: it uses
+ # self._request even though this attribute does not exist.
+ # This attribute WILL disappear in the future.
+ self._request = transaction.request()
+
+
+ self.session = transaction.session
+ self.write = response().write
+ #self.writeln = response.writeln
+
+ 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 self._CHEETAH__isControlledByWebKit:
+ return super(Servlet, self).serverSidePath(path)
+ elif 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
index 0000000..dfb396b
--- /dev/null
+++ b/cheetah/SettingsManager.py
@@ -0,0 +1,292 @@
+import sys
+import os.path
+import copy as copyModule
+from ConfigParser import ConfigParser
+import re
+from tokenize import Intnumber, Floatnumber, Number
+from types import *
+import types
+import new
+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 + '[\)]*')
+
+convertableToStrTypes = (StringType, IntType, FloatType,
+ LongType, ComplexType, NoneType,
+ UnicodeType)
+
+##################################################
+## 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.items():
+ if dict1.has_key(key) and type(val) == types.DictType and \
+ type(dict1[key]) == types.DictType:
+
+ 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.items():
+ 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) in globalsDict, newSettings
+ del newSettings['self']
+ module = new.module('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
index 0000000..0dc0e60
--- /dev/null
+++ b/cheetah/SourceReader.py
@@ -0,0 +1,303 @@
+# $Id: SourceReader.py,v 1.15 2007/04/03 01:57:42 tavis_rudd Exp $
+"""SourceReader class for Cheetah's Parser and CodeGenerator
+
+Meta-Data
+================================================================================
+Author: Tavis Rudd <tavis@damnsimple.com>
+License: This software is released for unlimited distribution under the
+ terms of the MIT license. See the LICENSE file.
+Version: $Revision: 1.15 $
+Start Date: 2001/09/19
+Last Revision Date: $Date: 2007/04/03 01:57:42 $
+"""
+__author__ = "Tavis Rudd <tavis@damnsimple.com>"
+__revision__ = "$Revision: 1.15 $"[11:-2]
+
+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:
+ def __init__(self, src, filename=None, breakPoint=None, encoding=None):
+
+ ## @@TR 2005-01-17: the following comes from a patch Terrel Shumway
+ ## contributed to add unicode support to the reading of Cheetah source
+ ## files with dynamically compiled templates. All the existing unit
+ ## tests pass but, it needs more testing and some test cases of its
+ ## own. My instinct is to move this up into the code that passes in the
+ ## src string rather than leaving it here. As implemented here it
+ ## forces all src strings to unicode, which IMO is not what we want.
+ # if encoding is None:
+ # # peek at the encoding in the first two lines
+ # m = EOLZre.search(src)
+ # pos = m.end()
+ # if pos<len(src):
+ # m = EOLZre.search(src,pos)
+ # pos = m.end()
+ # m = ENCODINGsearch(src,0,pos)
+ # if m:
+ # encoding = m.group(1)
+ # else:
+ # encoding = sys.getfilesystemencoding()
+ # self._encoding = encoding
+ # if type(src) is not unicode:
+ # src = src.decode(encoding)
+ ## end of Terrel's patch
+
+ 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):
+ 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 self._bookmarks.has_key(name)
+
+ 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
index 0000000..eeeeb95
--- /dev/null
+++ b/cheetah/Template.py
@@ -0,0 +1,1897 @@
+'''
+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 new # used to bind methods and create dummy modules
+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
+from types import StringType, ClassType
+try:
+ from types import StringTypes
+except ImportError:
+ StringTypes = (types.StringType,types.UnicodeType)
+
+try:
+ from threading import Lock
+except ImportError:
+ class Lock:
+ def acquire(self):
+ pass
+ def release(self):
+ pass
+
+try:
+ x = set()
+except NameError:
+ # Python 2.3 compatibility
+ from sets import Set as set
+
+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
+
+try:
+ from Cheetah._verifytype import *
+except ImportError:
+ from Cheetah.Utils import VerifyType
+ verifyType = VerifyType.VerifyType
+ verifyTypeClass = VerifyType.VerifyTypeClass
+
+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 = d.items()
+ items.sort()
+ 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
+
+try:
+ from Cheetah._template import valOrDefault
+except ImportError:
+ # used in Template.compile()
+ def valOrDefault(val, default):
+ if val is not Unspecified:
+ return val
+ return default
+
+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_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
+
+ def _getCompilerClass(klass, source=None, file=None):
+ return klass._CHEETAH_compilerClass
+ _getCompilerClass = classmethod(_getCompilerClass)
+
+ def _getCompilerSettings(klass, source=None, file=None):
+ return klass._CHEETAH_compilerSettings
+ _getCompilerSettings = classmethod(_getCompilerSettings)
+
+ 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=[...]) ] )
+
+ """
+ ##################################################
+ ## normalize and validate args
+ try:
+ vt = verifyType
+ vtc = verifyTypeClass
+ N = types.NoneType; S = types.StringType; U = types.UnicodeType
+ D = types.DictType; F = types.FileType
+ C = types.ClassType; M = types.ModuleType
+ I = types.IntType; B = types.BooleanType
+
+ IB = (I, B)
+ NS = (N, S)
+
+ vt(source, 'source', (N,S,U), 'string or None')
+ vt(file, 'file',(N,S,U,F), 'string, file-like object, or None')
+
+ baseclass = valOrDefault(baseclass, klass._CHEETAH_defaultBaseclassForTemplates)
+ if isinstance(baseclass, Template):
+ baseclass = baseclass.__class__
+ vt(baseclass, 'baseclass', (N,S,C,type), 'string, class or None')
+
+ cacheCompilationResults = valOrDefault(
+ cacheCompilationResults, klass._CHEETAH_cacheCompilationResults)
+ vt(cacheCompilationResults, 'cacheCompilationResults', IB, 'boolean')
+
+ useCache = valOrDefault(useCache, klass._CHEETAH_useCompilationCache)
+ vt(useCache, 'useCache', IB, 'boolean')
+
+ compilerSettings = valOrDefault(
+ compilerSettings, klass._getCompilerSettings(source, file) or {})
+ vt(compilerSettings, 'compilerSettings', (D,), 'dictionary')
+
+ compilerClass = valOrDefault(compilerClass, klass._getCompilerClass(source, file))
+ preprocessors = valOrDefault(preprocessors, klass._CHEETAH_preprocessors)
+
+ keepRefToGeneratedCode = valOrDefault(
+ keepRefToGeneratedCode, klass._CHEETAH_keepRefToGeneratedCode)
+ vt(keepRefToGeneratedCode, 'keepRefToGeneratedCode', IB, 'boolean')
+
+ vt(moduleName, 'moduleName', NS, 'string or None')
+ __orig_file__ = None
+ if not moduleName:
+ if file and type(file) in StringTypes:
+ moduleName = convertTmplPathToModuleName(file)
+ __orig_file__ = file
+ else:
+ moduleName = klass._CHEETAH_defaultModuleNameForTemplates
+
+ className = valOrDefault(
+ className, klass._CHEETAH_defaultClassNameForTemplates)
+ vt(className, 'className', NS, 'string or None')
+ className = className or moduleName
+
+ mainMethodName = valOrDefault(
+ mainMethodName, klass._CHEETAH_defaultMainMethodNameForTemplates)
+ vt(mainMethodName, 'mainMethodName', NS, 'string or None')
+
+ moduleGlobals = valOrDefault(
+ moduleGlobals, klass._CHEETAH_defaultModuleGlobalsForTemplates)
+
+ cacheModuleFilesForTracebacks = valOrDefault(
+ cacheModuleFilesForTracebacks, klass._CHEETAH_cacheModuleFilesForTracebacks)
+ vt(cacheModuleFilesForTracebacks, 'cacheModuleFilesForTracebacks', IB, 'boolean')
+
+ cacheDirForModuleFiles = valOrDefault(
+ cacheDirForModuleFiles, klass._CHEETAH_cacheDirForModuleFiles)
+ vt(cacheDirForModuleFiles, 'cacheDirForModuleFiles', NS, 'string or None')
+
+ except TypeError, reason:
+ raise TypeError(reason)
+
+ ##################################################
+ ## 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 type(baseclass) in StringTypes:
+ baseclassName = baseclass
+ elif type(baseclass) in (ClassType, 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'
+ 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 = new.module(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.encode(outputEncoding), __file__, 'exec')
+ exec co in 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
+
+ return templateClass
+ compile = classmethod(compile)
+
+ 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)
+ subclass = classmethod(subclass)
+
+ 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
+ _preprocessSource = classmethod(_preprocessSource)
+
+ 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 callable(arg):
+ 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)
+
+ _normalizePreprocessorArg = classmethod(_normalizePreprocessorArg)
+
+ 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
+ _normalizePreprocessorSettings = classmethod(_normalizePreprocessorSettings)
+
+ 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
+ _updateSettingsWithPreprocessTokens = classmethod(_updateSettingsWithPreprocessTokens)
+
+ 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 = new.instancemethod(method.im_func, None, concreteTemplateClass)
+ #print methodname, method
+ 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):
+ return getattr(self, mainMethName)()
+ elif (hasattr(concreteTemplateClass, 'respond')
+ and concreteTemplateClass.respond!=Servlet.respond):
+ def __str__(self):
+ return self.respond()
+ else:
+ def __str__(self):
+ if hasattr(self, mainMethNameAttr):
+ return getattr(self,mainMethNameAttr)()
+ elif hasattr(self, 'respond'):
+ return self.respond()
+ else:
+ return super(self.__class__, self).__str__()
+
+ __str__ = new.instancemethod(__str__, None, concreteTemplateClass)
+ setattr(concreteTemplateClass, '__str__', __str__)
+
+ _addCheetahPlumbingCodeToClass = classmethod(_addCheetahPlumbingCodeToClass)
+
+ ## end classmethods ##
+
+ 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!
+
+ """
+
+ ##################################################
+ ## Verify argument keywords and types
+
+ S = types.StringType; U = types.UnicodeType
+ L = types.ListType; T = types.TupleType
+ D = types.DictType; F = types.FileType
+ C = types.ClassType; M = types.ModuleType
+ N = types.NoneType
+ vt = verifyType
+ vtc = verifyTypeClass
+ try:
+ vt(source, 'source', (N,S,U), 'string or None')
+ vt(file, 'file', (N,S,U,F), 'string, file open for reading, or None')
+ vtc(filter, 'filter', (S,C,type), 'string or class',
+ Filters.Filter,
+ '(if class, must be subclass of Cheetah.Filters.Filter)')
+ vt(filtersLib, 'filtersLib', (S,M), 'string or module',
+ '(if module, must contain subclasses of Cheetah.Filters.Filter)')
+ vtc(errorCatcher, 'errorCatcher', (N,S,C,type), 'string, class or None',
+ ErrorCatchers.ErrorCatcher,
+ '(if class, must be subclass of Cheetah.ErrorCatchers.ErrorCatcher)')
+ if compilerSettings is not Unspecified:
+ vt(compilerSettings, 'compilerSettings', (D,), 'dictionary')
+
+ except TypeError:
+ raise
+
+ 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
+ reserved_searchlist = dir(self)
+ if searchList:
+ for namespace in searchList:
+ if isinstance(namespace, dict):
+ intersection = set(reserved_searchlist) & set(namespace.keys())
+ warn = False
+ if intersection:
+ warn = True
+ if isinstance(compilerSettings, dict) and compilerSettings.get('prioritizeSearchListOverSelf'):
+ warn = False
+ if warn:
+ print
+ print ''' *** WARNING *** '''
+ print ''' The following keys are members of the Template class and will result in NameMapper collisions! '''
+ print ''' > %s ''' % ', '.join(list(intersection))
+ print
+ print ''' Please change the key's name or use the compiler setting "prioritizeSearchListOverSelf=True" to prevent the NameMapper from using '''
+ print ''' the Template member in place of your searchList variable '''
+ print ''' *************** '''
+ print
+
+ 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 key, cregion in self.getCacheRegions():
+ cregion.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 type(errorCatcher) == ClassType:
+ 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 type(file) in StringTypes:
+ 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)
+ 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 not self._CHEETAH__cheetahIncludes.has_key(_includeID):
+ if not raw:
+ if includeFrom == 'file':
+ source = None
+ if type(srcArg) in StringTypes:
+ 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.
+
+
+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
index 0000000..16a90cf
--- /dev/null
+++ b/cheetah/TemplateCmdLineIface.py
@@ -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
index 0000000..04bf4fc
--- /dev/null
+++ b/cheetah/Templates/SkeletonPage.py
@@ -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 callable(self.transaction)):
+ 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 callable(self.transaction)):
+ 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 callable(self.transaction)):
+ 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
index 0000000..43c5ecd
--- /dev/null
+++ b/cheetah/Templates/SkeletonPage.tmpl
@@ -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
index 0000000..fe01ebf
--- /dev/null
+++ b/cheetah/Templates/_SkeletonPage.py
@@ -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 not self._stylesheets.has_key(identifier):
+ 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.items():
+ if type(details) not in (types.ListType, types.TupleType):
+ 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.items():
+ if type(details) not in (types.ListType, types.TupleType):
+ 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 metaTags.has_key('HTTP-EQUIV'):
+ for http_equiv, contents in metaTags['HTTP-EQUIV'].items():
+ metaTagsTxt += ['<meta http-equiv="', str(http_equiv), '" content="',
+ str(contents), '" />\n']
+
+ if metaTags.has_key('NAME'):
+ 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
index 0000000..8b13789
--- /dev/null
+++ b/cheetah/Templates/__init__.py
@@ -0,0 +1 @@
+
diff --git a/cheetah/Tests/CheetahWrapper.py b/cheetah/Tests/CheetahWrapper.py
new file mode 100644
index 0000000..ca7b0ae
--- /dev/null
+++ b/cheetah/Tests/CheetahWrapper.py
@@ -0,0 +1,545 @@
+#!/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 popen2
+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.
+
+
+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.
+ """
+ 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 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):
+ process = popen2.Popen4(cmd)
+ 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 1:
+ 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/Filters.py b/cheetah/Tests/Filters.py
new file mode 100644
index 0000000..bf35440
--- /dev/null
+++ b/cheetah/Tests/Filters.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+import sys
+
+import Cheetah.Template
+import Cheetah.Filters
+
+import unittest_local_copy as unittest
+
+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 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/NameMapper.py b/cheetah/Tests/NameMapper.py
new file mode 100644
index 0000000..5d6b8b6
--- /dev/null
+++ b/cheetah/Tests/NameMapper.py
@@ -0,0 +1,526 @@
+#!/usr/bin/env python
+
+from __future__ import generators
+import sys
+import types
+import os
+import os.path
+
+#import unittest_local_copy as unittest
+import unittest
+from Cheetah.NameMapper import NotFound, valueForKey, \
+ valueForName, valueFromSearchList, valueFromFrame, valueFromFrameOrSearchList
+
+
+class DummyClass:
+ 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
+
+
+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(),
+ '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,AssertionError)
+ _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 autoCallResults.has_key(name):
+ 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)
+
+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
+
+
+##################################################
+## if run from the command line ##
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cheetah/Tests/Regressions.py b/cheetah/Tests/Regressions.py
new file mode 100644
index 0000000..b5ba3d0
--- /dev/null
+++ b/cheetah/Tests/Regressions.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+
+import Cheetah.NameMapper
+import Cheetah.Template
+
+import pdb
+import sys
+
+import unittest_local_copy as unittest # This is just stupid
+
+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
index 0000000..78e0cc5
--- /dev/null
+++ b/cheetah/Tests/SyntaxAndOutput.py
@@ -0,0 +1,3227 @@
+#!/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 new
+import pdb
+import warnings
+
+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
+import unittest_local_copy as unittest
+
+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
+ self.template = templateObj = templateClass(searchList=self.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')
+
+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")
+
+ def test11(self):
+ r"""func placeholder - with ('''\nstring'\n''')"""
+ self.verify("$aFunc('''\naoeu'\n''')",
+ "\naoeu'\n")
+
+ def test12(self):
+ r'''func placeholder - with ("""\nstring\n""")'''
+ self.verify('$aFunc("""\naoeu\n""")',
+ "\naoeu\n")
+
+ 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")
+
+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 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-")
+
+
+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}
+
+
+for klass in [var for var in globals().values()
+ if type(var) == types.ClassType and issubclass(var, unittest.TestCase)]:
+ 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()
+ #print win32Src
+ #print macSrc
+ exec win32Src+'\n'
+ exec macSrc+'\n'
+
+ if versionTuple >= (2,3):
+ src = r"class %(name)s_DiffBaseClass(%(name)s): "%locals()
+ src += " _extraCompileKwArgs = extraCompileKwArgsForDiffBaseclass"
+ exec src+'\n'
+
+ del name
+ del klass
+
+##################################################
+## if run from the command line ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Tests/Template.py b/cheetah/Tests/Template.py
new file mode 100644
index 0000000..085180d
--- /dev/null
+++ b/cheetah/Tests/Template.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python
+
+import pdb
+import sys
+import types
+import os
+import os.path
+import tempfile
+import shutil
+import unittest_local_copy as 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()
+ print result
+
+
+##################################################
+## 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
index 0000000..080f4fa
--- /dev/null
+++ b/cheetah/Tests/Test.py
@@ -0,0 +1,45 @@
+#!/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_local_copy as unittest
+
+
+
+import SyntaxAndOutput
+import NameMapper
+import Template
+import CheetahWrapper
+import Regressions
+import Unicode
+import VerifyType
+
+suites = [
+ unittest.findTestCases(SyntaxAndOutput),
+ unittest.findTestCases(NameMapper),
+ unittest.findTestCases(Template),
+ unittest.findTestCases(Regressions),
+ unittest.findTestCases(Unicode),
+ unittest.findTestCases(VerifyType),
+]
+
+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
index 0000000..da59bed
--- /dev/null
+++ b/cheetah/Tests/Unicode.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+# -*- encoding: utf8 -*-
+
+from Cheetah.Template import Template
+from Cheetah import CheetahWrapper
+from Cheetah import DummyTransaction
+import imp
+import os
+import pdb
+import random
+import sys
+import tempfile
+import unittest_local_copy as unittest # This is stupid
+
+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', '--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 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_ErrorReporting(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}])
+ self.failUnlessRaises(DummyTransaction.DummyResponseFailure, template.respond)
+
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cheetah/Tests/__init__.py b/cheetah/Tests/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/cheetah/Tests/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/cheetah/Tests/unittest_local_copy.py b/cheetah/Tests/unittest_local_copy.py
new file mode 100755
index 0000000..a5f5499
--- /dev/null
+++ b/cheetah/Tests/unittest_local_copy.py
@@ -0,0 +1,978 @@
+#!/usr/bin/env python
+""" This is a hacked version of PyUnit that extends its reporting capabilities
+with optional meta data on the test cases. It also makes it possible to
+separate the standard and error output streams in TextTestRunner.
+
+It's a hack rather than a set of subclasses because a) Steve had used double
+underscore private attributes for some things I needed access to, and b) the
+changes affected so many classes that it was easier just to hack it.
+
+The changes are in the following places:
+TestCase:
+ - minor refactoring of __init__ and __call__ internals
+ - added some attributes and methods for storing and retrieving meta data
+
+_TextTestResult
+ - refactored the stream handling
+ - incorporated all the output code from TextTestRunner
+ - made the output of FAIL and ERROR information more flexible and
+ incorporated the new meta data from TestCase
+ - added a flag called 'explain' to __init__ that controls whether the new '
+ explanation' meta data from TestCase is printed along with tracebacks
+
+TextTestRunner
+ - delegated all output to _TextTestResult
+ - added 'err' and 'explain' to the __init__ signature to match the changes
+ in _TextTestResult
+
+TestProgram
+ - added -e and --explain as flags on the command line
+
+-- Tavis Rudd <tavis@redonions.net> (Sept 28th, 2001)
+
+- _TestTextResult.printErrorList(): print blank line after each traceback
+
+-- Mike Orr <mso@oz.net> (Nov 11, 2002)
+
+TestCase methods copied from unittest in Python 2.3:
+ - .assertAlmostEqual(first, second, places=7, msg=None): to N decimal places.
+ - .failIfAlmostEqual(first, second, places=7, msg=None)
+
+-- Mike Orr (Jan 5, 2004)
+
+
+Below is the original docstring for unittest.
+---------------------------------------------------------------------------
+Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's
+Smalltalk testing framework.
+
+This module contains the core framework classes that form the basis of
+specific test cases and suites (TestCase, TestSuite etc.), and also a
+text-based utility class for running the tests and reporting the results
+(TextTestRunner).
+
+Simple usage:
+
+ import unittest
+
+ class IntegerArithmenticTestCase(unittest.TestCase):
+ def testAdd(self): ## test method names begin 'test*'
+ self.assertEquals((1 + 2), 3)
+ self.assertEquals(0 + 1, 1)
+ def testMultiply(self);
+ self.assertEquals((0 * 10), 0)
+ self.assertEquals((5 * 8), 40)
+
+ if __name__ == '__main__':
+ unittest.main()
+
+Further information is available in the bundled documentation, and from
+
+ http://pyunit.sourceforge.net/
+
+Copyright (c) 1999, 2000, 2001 Steve Purcell
+This module is free software, and you may redistribute it and/or modify
+it under the same terms as Python itself, so long as this copyright message
+and disclaimer are retained in their original form.
+
+IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+"""
+
+__author__ = "Steve Purcell"
+__email__ = "stephen_purcell at yahoo dot com"
+__revision__ = "$Revision: 1.11 $"[11:-2]
+
+
+##################################################
+## DEPENDENCIES ##
+
+import os
+import re
+import string
+import sys
+import time
+import traceback
+import types
+import pprint
+
+##################################################
+## CONSTANTS & GLOBALS
+
+try:
+ True,False
+except NameError:
+ True, False = (1==1),(1==0)
+
+##############################################################################
+# Test framework core
+##############################################################################
+
+
+class TestResult:
+ """Holder for test result information.
+
+ Test results are automatically managed by the TestCase and TestSuite
+ classes, and do not need to be explicitly manipulated by writers of tests.
+
+ Each instance holds the total number of tests run, and collections of
+ failures and errors that occurred among those test runs. The collections
+ contain tuples of (testcase, exceptioninfo), where exceptioninfo is a
+ tuple of values as returned by sys.exc_info().
+ """
+ def __init__(self):
+ self.failures = []
+ self.errors = []
+ self.testsRun = 0
+ self.shouldStop = 0
+
+ def startTest(self, test):
+ "Called when the given test is about to be run"
+ self.testsRun = self.testsRun + 1
+
+ def stopTest(self, test):
+ "Called when the given test has been run"
+ pass
+
+ def addError(self, test, err):
+ "Called when an error has occurred"
+ self.errors.append((test, err))
+
+ def addFailure(self, test, err):
+ "Called when a failure has occurred"
+ self.failures.append((test, err))
+
+ def addSuccess(self, test):
+ "Called when a test has completed successfully"
+ pass
+
+ def wasSuccessful(self):
+ "Tells whether or not this result was a success"
+ return len(self.failures) == len(self.errors) == 0
+
+ def stop(self):
+ "Indicates that the tests should be aborted"
+ self.shouldStop = 1
+
+ def __repr__(self):
+ return "<%s run=%i errors=%i failures=%i>" % \
+ (self.__class__, self.testsRun, len(self.errors),
+ len(self.failures))
+
+class TestCase:
+ """A class whose instances are single test cases.
+
+ By default, the test code itself should be placed in a method named
+ 'runTest'.
+
+ If the fixture may be used for many test cases, create as
+ many test methods as are needed. When instantiating such a TestCase
+ subclass, specify in the constructor arguments the name of the test method
+ that the instance is to execute.
+
+ Test authors should subclass TestCase for their own tests. Construction
+ and deconstruction of the test's environment ('fixture') can be
+ implemented by overriding the 'setUp' and 'tearDown' methods respectively.
+
+ If it is necessary to override the __init__ method, the base class
+ __init__ method must always be called. It is important that subclasses
+ should not change the signature of their __init__ method, since instances
+ of the classes are instantiated automatically by parts of the framework
+ in order to be run.
+ """
+
+ # This attribute determines which exception will be raised when
+ # the instance's assertion methods fail; test methods raising this
+ # exception will be deemed to have 'failed' rather than 'errored'
+
+ failureException = AssertionError
+
+ # the name of the fixture. Used for displaying meta data about the test
+ name = None
+
+ def __init__(self, methodName='runTest'):
+ """Create an instance of the class that will use the named test
+ method when executed. Raises a ValueError if the instance does
+ not have a method with the specified name.
+ """
+ self._testMethodName = methodName
+ self._setupTestMethod()
+ self._setupMetaData()
+
+ def _setupTestMethod(self):
+ try:
+ self._testMethod = getattr(self, self._testMethodName)
+ except AttributeError:
+ raise ValueError, "no such test method in %s: %s" % \
+ (self.__class__, self._testMethodName)
+
+ ## meta data methods
+
+ def _setupMetaData(self):
+ """Setup the default meta data for the test case:
+
+ - id: self.__class__.__name__ + testMethodName OR self.name + testMethodName
+ - description: 1st line of Class docstring + 1st line of method docstring
+ - explanation: rest of Class docstring + rest of method docstring
+
+ """
+
+
+ testDoc = self._testMethod.__doc__ or '\n'
+ testDocLines = testDoc.splitlines()
+
+ testDescription = testDocLines[0].strip()
+ if len(testDocLines) > 1:
+ testExplanation = '\n'.join(
+ [ln.strip() for ln in testDocLines[1:]]
+ ).strip()
+ else:
+ testExplanation = ''
+
+ fixtureDoc = self.__doc__ or '\n'
+ fixtureDocLines = fixtureDoc.splitlines()
+ fixtureDescription = fixtureDocLines[0].strip()
+ if len(fixtureDocLines) > 1:
+ fixtureExplanation = '\n'.join(
+ [ln.strip() for ln in fixtureDocLines[1:]]
+ ).strip()
+ else:
+ fixtureExplanation = ''
+
+ if not self.name:
+ self.name = self.__class__
+ self._id = "%s.%s" % (self.name, self._testMethodName)
+
+ if not fixtureDescription:
+ self._description = testDescription
+ else:
+ self._description = fixtureDescription + ', ' + testDescription
+
+ if not fixtureExplanation:
+ self._explanation = testExplanation
+ else:
+ self._explanation = ['Fixture Explanation:',
+ '--------------------',
+ fixtureExplanation,
+ '',
+ 'Test Explanation:',
+ '-----------------',
+ testExplanation
+ ]
+ self._explanation = '\n'.join(self._explanation)
+
+ def id(self):
+ return self._id
+
+ def setId(self, id):
+ self._id = id
+
+ def describe(self):
+ """Returns a one-line description of the test, or None if no
+ description has been provided.
+
+ The default implementation of this method returns the first line of
+ the specified test method's docstring.
+ """
+ return self._description
+
+ shortDescription = describe
+
+ def setDescription(self, descr):
+ self._description = descr
+
+ def explain(self):
+ return self._explanation
+
+ def setExplanation(self, expln):
+ self._explanation = expln
+
+ ## core methods
+
+ def setUp(self):
+ "Hook method for setting up the test fixture before exercising it."
+ pass
+
+ def run(self, result=None):
+ return self(result)
+
+ def tearDown(self):
+ "Hook method for deconstructing the test fixture after testing it."
+ pass
+
+ def debug(self):
+ """Run the test without collecting errors in a TestResult"""
+ self.setUp()
+ self._testMethod()
+ self.tearDown()
+
+ ## internal methods
+
+ def defaultTestResult(self):
+ return TestResult()
+
+ def __call__(self, result=None):
+ if result is None:
+ result = self.defaultTestResult()
+
+ result.startTest(self)
+ try:
+ try:
+ self.setUp()
+ except:
+ result.addError(self, self.__exc_info())
+ return
+
+ ok = 0
+ try:
+ self._testMethod()
+ ok = 1
+ except self.failureException, e:
+ result.addFailure(self, self.__exc_info())
+ except:
+ result.addError(self, self.__exc_info())
+ try:
+ self.tearDown()
+ except:
+ result.addError(self, self.__exc_info())
+ ok = 0
+ if ok:
+ result.addSuccess(self)
+ finally:
+ result.stopTest(self)
+
+ return result
+
+ def countTestCases(self):
+ return 1
+
+ def __str__(self):
+ return "%s (%s)" % (self._testMethodName, self.__class__)
+
+ def __repr__(self):
+ return "<%s testMethod=%s>" % \
+ (self.__class__, self._testMethodName)
+
+ def __exc_info(self):
+ """Return a version of sys.exc_info() with the traceback frame
+ minimised; usually the top level of the traceback frame is not
+ needed.
+ """
+ exctype, excvalue, tb = sys.exc_info()
+ if sys.platform[:4] == 'java': ## tracebacks look different in Jython
+ return (exctype, excvalue, tb)
+ newtb = tb.tb_next
+ if newtb is None:
+ return (exctype, excvalue, tb)
+ return (exctype, excvalue, newtb)
+
+ ## methods for use by the test cases
+
+ def fail(self, msg=None):
+ """Fail immediately, with the given message."""
+ raise self.failureException, msg
+
+ def failIf(self, expr, msg=None):
+ "Fail the test if the expression is true."
+ if expr: raise self.failureException, msg
+
+ def failUnless(self, expr, msg=None):
+ """Fail the test unless the expression is true."""
+ if not expr: raise self.failureException, msg
+
+ def failUnlessRaises(self, excClass, callableObj, *args, **kwargs):
+ """Fail unless an exception of class excClass is thrown
+ by callableObj when invoked with arguments args and keyword
+ arguments kwargs. If a different type of exception is
+ thrown, it will not be caught, and the test case will be
+ deemed to have suffered an error, exactly as for an
+ unexpected exception.
+ """
+ try:
+ apply(callableObj, args, kwargs)
+ except excClass:
+ return
+ else:
+ if hasattr(excClass,'__name__'): excName = excClass.__name__
+ else: excName = str(excClass)
+ raise self.failureException, excName
+
+ def failUnlessEqual(self, first, second, msg=None):
+ """Fail if the two objects are unequal as determined by the '!='
+ operator.
+ """
+ if first != second:
+ raise self.failureException, (msg or '%s != %s' % (first, second))
+
+ def failIfEqual(self, first, second, msg=None):
+ """Fail if the two objects are equal as determined by the '=='
+ operator.
+ """
+ if first == second:
+ raise self.failureException, (msg or '%s == %s' % (first, second))
+
+ def failUnlessAlmostEqual(self, first, second, places=7, msg=None):
+ """Fail if the two objects are unequal as determined by their
+ difference rounded to the given number of decimal places
+ (default 7) and comparing to zero.
+
+ Note that decimal places (from zero) is usually not the same
+ as significant digits (measured from the most signficant digit).
+ """
+ if round(second-first, places) != 0:
+ raise self.failureException, \
+ (msg or '%s != %s within %s places' % (`first`, `second`, `places` ))
+
+ def failIfAlmostEqual(self, first, second, places=7, msg=None):
+ """Fail if the two objects are equal as determined by their
+ difference rounded to the given number of decimal places
+ (default 7) and comparing to zero.
+
+ Note that decimal places (from zero) is usually not the same
+ as significant digits (measured from the most signficant digit).
+ """
+ if round(second-first, places) == 0:
+ raise self.failureException, \
+ (msg or '%s == %s within %s places' % (`first`, `second`, `places`))
+
+ ## aliases
+
+ assertEqual = assertEquals = failUnlessEqual
+
+ assertNotEqual = assertNotEquals = failIfEqual
+
+ assertAlmostEqual = assertAlmostEquals = failUnlessAlmostEqual
+
+ assertNotAlmostEqual = assertNotAlmostEquals = failIfAlmostEqual
+
+ assertRaises = failUnlessRaises
+
+ assert_ = failUnless
+
+
+class FunctionTestCase(TestCase):
+ """A test case that wraps a test function.
+
+ This is useful for slipping pre-existing test functions into the
+ PyUnit framework. Optionally, set-up and tidy-up functions can be
+ supplied. As with TestCase, the tidy-up ('tearDown') function will
+ always be called if the set-up ('setUp') function ran successfully.
+ """
+
+ def __init__(self, testFunc, setUp=None, tearDown=None,
+ description=None):
+ TestCase.__init__(self)
+ self.__setUpFunc = setUp
+ self.__tearDownFunc = tearDown
+ self.__testFunc = testFunc
+ self.__description = description
+
+ def setUp(self):
+ if self.__setUpFunc is not None:
+ self.__setUpFunc()
+
+ def tearDown(self):
+ if self.__tearDownFunc is not None:
+ self.__tearDownFunc()
+
+ def runTest(self):
+ self.__testFunc()
+
+ def id(self):
+ return self.__testFunc.__name__
+
+ def __str__(self):
+ return "%s (%s)" % (self.__class__, self.__testFunc.__name__)
+
+ def __repr__(self):
+ return "<%s testFunc=%s>" % (self.__class__, self.__testFunc)
+
+
+ def describe(self):
+ if self.__description is not None: return self.__description
+ doc = self.__testFunc.__doc__
+ return doc and string.strip(string.split(doc, "\n")[0]) or None
+
+ ## aliases
+ shortDescription = describe
+
+class TestSuite:
+ """A test suite is a composite test consisting of a number of TestCases.
+
+ For use, create an instance of TestSuite, then add test case instances.
+ When all tests have been added, the suite can be passed to a test
+ runner, such as TextTestRunner. It will run the individual test cases
+ in the order in which they were added, aggregating the results. When
+ subclassing, do not forget to call the base class constructor.
+ """
+ def __init__(self, tests=(), suiteName=None):
+ self._tests = []
+ self._testMap = {}
+ self.suiteName = suiteName
+ self.addTests(tests)
+
+ def __repr__(self):
+ return "<%s tests=%s>" % (self.__class__, pprint.pformat(self._tests))
+
+ __str__ = __repr__
+
+ def countTestCases(self):
+ cases = 0
+ for test in self._tests:
+ cases = cases + test.countTestCases()
+ return cases
+
+ def addTest(self, test):
+ self._tests.append(test)
+ if isinstance(test, TestSuite) and test.suiteName:
+ name = test.suiteName
+ elif isinstance(test, TestCase):
+ #print test, test._testMethodName
+ name = test._testMethodName
+ else:
+ name = test.__class__.__name__
+ self._testMap[name] = test
+
+ def addTests(self, tests):
+ for test in tests:
+ self.addTest(test)
+
+ def getTestForName(self, name):
+ return self._testMap[name]
+
+ def run(self, result):
+ return self(result)
+
+ def __call__(self, result):
+ for test in self._tests:
+ if result.shouldStop:
+ break
+ test(result)
+ return result
+
+ def debug(self):
+ """Run the tests without collecting errors in a TestResult"""
+ for test in self._tests: test.debug()
+
+
+##############################################################################
+# Text UI
+##############################################################################
+
+class StreamWrapper:
+ def __init__(self, out=sys.stdout, err=sys.stderr):
+ self._streamOut = out
+ self._streamErr = err
+
+ def write(self, txt):
+ self._streamOut.write(txt)
+ self._streamOut.flush()
+
+ def writeln(self, *lines):
+ for line in lines:
+ self.write(line + '\n')
+ if not lines:
+ self.write('\n')
+
+ def writeErr(self, txt):
+ self._streamErr.write(txt)
+
+ def writelnErr(self, *lines):
+ for line in lines:
+ self.writeErr(line + '\n')
+ if not lines:
+ self.writeErr('\n')
+
+
+class _TextTestResult(TestResult, StreamWrapper):
+ _separatorWidth = 70
+ _sep1 = '='
+ _sep2 = '-'
+ _errorSep1 = '*'
+ _errorSep2 = '-'
+ _errorSep3 = ''
+
+ def __init__(self,
+ stream=sys.stdout,
+ errStream=sys.stderr,
+ verbosity=1,
+ explain=False):
+
+ TestResult.__init__(self)
+ StreamWrapper.__init__(self, out=stream, err=errStream)
+
+ self._verbosity = verbosity
+ self._showAll = verbosity > 1
+ self._dots = (verbosity == 1)
+ self._explain = explain
+
+ ## startup and shutdown methods
+
+ def beginTests(self):
+ self._startTime = time.time()
+
+ def endTests(self):
+ self._stopTime = time.time()
+ self._timeTaken = float(self._stopTime - self._startTime)
+
+ def stop(self):
+ self.shouldStop = 1
+
+ ## methods called for each test
+
+ def startTest(self, test):
+ TestResult.startTest(self, test)
+ if self._showAll:
+ self.write("%s (%s)" %( test.id(), test.describe() ) )
+ self.write(" ... ")
+
+ def addSuccess(self, test):
+ TestResult.addSuccess(self, test)
+ if self._showAll:
+ self.writeln("ok")
+ elif self._dots:
+ self.write('.')
+
+ def addError(self, test, err):
+ TestResult.addError(self, test, err)
+ if self._showAll:
+ self.writeln("ERROR")
+ elif self._dots:
+ self.write('E')
+ if err[0] is KeyboardInterrupt:
+ self.stop()
+
+ def addFailure(self, test, err):
+ TestResult.addFailure(self, test, err)
+ if self._showAll:
+ self.writeln("FAIL")
+ elif self._dots:
+ self.write('F')
+
+ ## display methods
+
+ def summarize(self):
+ self.printErrors()
+ self.writeSep2()
+ run = self.testsRun
+ self.writeln("Ran %d test%s in %.3fs" %
+ (run, run == 1 and "" or "s", self._timeTaken))
+ self.writeln()
+ if not self.wasSuccessful():
+ self.writeErr("FAILED (")
+ failed, errored = map(len, (self.failures, self.errors))
+ if failed:
+ self.writeErr("failures=%d" % failed)
+ if errored:
+ if failed: self.writeErr(", ")
+ self.writeErr("errors=%d" % errored)
+ self.writelnErr(")")
+ else:
+ self.writelnErr("OK")
+
+ def writeSep1(self):
+ self.writeln(self._sep1 * self._separatorWidth)
+
+ def writeSep2(self):
+ self.writeln(self._sep2 * self._separatorWidth)
+
+ def writeErrSep1(self):
+ self.writeln(self._errorSep1 * self._separatorWidth)
+
+ def writeErrSep2(self):
+ self.writeln(self._errorSep2 * self._separatorWidth)
+
+ def printErrors(self):
+ if self._dots or self._showAll:
+ self.writeln()
+ self.printErrorList('ERROR', self.errors)
+ self.printErrorList('FAIL', self.failures)
+
+ def printErrorList(self, flavour, errors):
+ for test, err in errors:
+ self.writeErrSep1()
+ self.writelnErr("%s %s (%s)" % (flavour, test.id(), test.describe() ))
+ if self._explain:
+ expln = test.explain()
+ if expln:
+ self.writeErrSep2()
+ self.writeErr( expln )
+ self.writelnErr()
+
+ self.writeErrSep2()
+ for line in apply(traceback.format_exception, err):
+ for l in line.split("\n")[:-1]:
+ self.writelnErr(l)
+ self.writelnErr("")
+
+class TextTestRunner:
+ def __init__(self,
+ stream=sys.stdout,
+ errStream=sys.stderr,
+ verbosity=1,
+ explain=False):
+
+ self._out = stream
+ self._err = errStream
+ self._verbosity = verbosity
+ self._explain = explain
+
+ ## main methods
+
+ def run(self, test):
+ result = self._makeResult()
+ result.beginTests()
+ test( result )
+ result.endTests()
+ result.summarize()
+
+ return result
+
+ ## internal methods
+
+ def _makeResult(self):
+ return _TextTestResult(stream=self._out,
+ errStream=self._err,
+ verbosity=self._verbosity,
+ explain=self._explain,
+ )
+
+##############################################################################
+# Locating and loading tests
+##############################################################################
+
+class TestLoader:
+ """This class is responsible for loading tests according to various
+ criteria and returning them wrapped in a Test
+ """
+ testMethodPrefix = 'test'
+ sortTestMethodsUsing = cmp
+ suiteClass = TestSuite
+
+ def loadTestsFromTestCase(self, testCaseClass):
+ """Return a suite of all tests cases contained in testCaseClass"""
+ return self.suiteClass(tests=map(testCaseClass,
+ self.getTestCaseNames(testCaseClass)),
+ suiteName=testCaseClass.__name__)
+
+ def loadTestsFromModule(self, module):
+ """Return a suite of all tests cases contained in the given module"""
+ tests = []
+ for name in dir(module):
+ obj = getattr(module, name)
+ if type(obj) == types.ClassType and issubclass(obj, TestCase):
+ tests.append(self.loadTestsFromTestCase(obj))
+ return self.suiteClass(tests)
+
+ def loadTestsFromName(self, name, module=None):
+ """Return a suite of all tests cases given a string specifier.
+
+ The name may resolve either to a module, a test case class, a
+ test method within a test case class, or a callable object which
+ returns a TestCase or TestSuite instance.
+
+ The method optionally resolves the names relative to a given module.
+ """
+ parts = string.split(name, '.')
+ if module is None:
+ if not parts:
+ raise ValueError, "incomplete test name: %s" % name
+ else:
+ parts_copy = parts[:]
+ while parts_copy:
+ try:
+ module = __import__(string.join(parts_copy,'.'))
+ break
+ except ImportError:
+ del parts_copy[-1]
+ if not parts_copy: raise
+ parts = parts[1:]
+ obj = module
+ for part in parts:
+ if isinstance(obj, TestSuite):
+ obj = obj.getTestForName(part)
+ else:
+ obj = getattr(obj, part)
+
+ if type(obj) == types.ModuleType:
+ return self.loadTestsFromModule(obj)
+ elif type(obj) == types.ClassType and issubclass(obj, TestCase):
+ return self.loadTestsFromTestCase(obj)
+ elif type(obj) == types.UnboundMethodType:
+ return obj.im_class(obj.__name__)
+ elif isinstance(obj, TestSuite):
+ return obj
+ elif isinstance(obj, TestCase):
+ return obj
+ elif callable(obj):
+ test = obj()
+ if not isinstance(test, TestCase) and \
+ not isinstance(test, TestSuite):
+ raise ValueError, \
+ "calling %s returned %s, not a test" %(obj,test)
+ return test
+ else:
+ raise ValueError, "don't know how to make test from: %s" % obj
+
+ def loadTestsFromNames(self, names, module=None):
+ """Return a suite of all tests cases found using the given sequence
+ of string specifiers. See 'loadTestsFromName()'.
+ """
+ suites = []
+ for name in names:
+ suites.append(self.loadTestsFromName(name, module))
+ return self.suiteClass(suites)
+
+ def getTestCaseNames(self, testCaseClass):
+ """Return a sorted sequence of method names found within testCaseClass.
+ """
+ testFnNames = [fn for fn in dir(testCaseClass) if fn.startswith(self.testMethodPrefix)]
+ if hasattr(testCaseClass, 'runTest'):
+ testFnNames.append('runTest')
+ for baseclass in testCaseClass.__bases__:
+ for testFnName in self.getTestCaseNames(baseclass):
+ if testFnName not in testFnNames: # handle overridden methods
+ testFnNames.append(testFnName)
+ if self.sortTestMethodsUsing:
+ testFnNames.sort(self.sortTestMethodsUsing)
+ return testFnNames
+
+
+
+defaultTestLoader = TestLoader()
+
+
+##############################################################################
+# Patches for old functions: these functions should be considered obsolete
+##############################################################################
+
+def _makeLoader(prefix, sortUsing, suiteClass=None):
+ loader = TestLoader()
+ loader.sortTestMethodsUsing = sortUsing
+ loader.testMethodPrefix = prefix
+ if suiteClass: loader.suiteClass = suiteClass
+ return loader
+
+def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp):
+ return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass)
+
+def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
+ return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass)
+
+def findTestCases(module, prefix='test', sortUsing=cmp, suiteClass=TestSuite):
+ return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module)
+
+##############################################################################
+# Facilities for running tests from the command line
+##############################################################################
+
+class TestProgram:
+ """A command-line program that runs a set of tests; this is primarily
+ for making test modules conveniently executable.
+ """
+ USAGE = """\
+Usage: %(progName)s [options] [test] [...]
+
+Options:
+ -h, --help Show this message
+ -v, --verbose Verbose output
+ -q, --quiet Minimal output
+ -e, --expain Output extra test details if there is a failure or error
+
+Examples:
+ %(progName)s - run default set of tests
+ %(progName)s MyTestSuite - run suite 'MyTestSuite'
+ %(progName)s MyTestSuite.MyTestCase - run suite 'MyTestSuite'
+ %(progName)s MyTestCase.testSomething - run MyTestCase.testSomething
+ %(progName)s MyTestCase - run all 'test*' test methods
+ in MyTestCase
+"""
+ def __init__(self, module='__main__', defaultTest=None,
+ argv=None, testRunner=None, testLoader=defaultTestLoader,
+ testSuite=None):
+ if type(module) == type(''):
+ self.module = __import__(module)
+ for part in string.split(module,'.')[1:]:
+ self.module = getattr(self.module, part)
+ else:
+ self.module = module
+ if argv is None:
+ argv = sys.argv
+ self.test = testSuite
+ self.verbosity = 1
+ self.explain = 0
+ self.defaultTest = defaultTest
+ self.testRunner = testRunner
+ self.testLoader = testLoader
+ self.progName = os.path.basename(argv[0])
+ self.parseArgs(argv)
+ self.runTests()
+
+ def usageExit(self, msg=None):
+ if msg: print msg
+ print self.USAGE % self.__dict__
+ sys.exit(2)
+
+ def parseArgs(self, argv):
+ import getopt
+ try:
+ options, args = getopt.getopt(argv[1:], 'hHvqer',
+ ['help','verbose','quiet','explain', 'raise'])
+ for opt, value in options:
+ if opt in ('-h','-H','--help'):
+ self.usageExit()
+ if opt in ('-q','--quiet'):
+ self.verbosity = 0
+ if opt in ('-v','--verbose'):
+ self.verbosity = 2
+ if opt in ('-e','--explain'):
+ self.explain = True
+ if len(args) == 0 and self.defaultTest is None and self.test is None:
+ self.test = self.testLoader.loadTestsFromModule(self.module)
+ return
+ if len(args) > 0:
+ self.testNames = args
+ else:
+ self.testNames = (self.defaultTest,)
+ self.createTests()
+ except getopt.error, msg:
+ self.usageExit(msg)
+
+ def createTests(self):
+ if self.test == None:
+ self.test = self.testLoader.loadTestsFromNames(self.testNames,
+ self.module)
+
+ def runTests(self):
+ if self.testRunner is None:
+ self.testRunner = TextTestRunner(verbosity=self.verbosity,
+ explain=self.explain)
+ result = self.testRunner.run(self.test)
+ self._cleanupAfterRunningTests()
+ sys.exit(not result.wasSuccessful())
+
+ def _cleanupAfterRunningTests(self):
+ """A hook method that is called immediately prior to calling
+ sys.exit(not result.wasSuccessful()) in self.runTests().
+ """
+ pass
+
+main = TestProgram
+
+
+##############################################################################
+# Executing this module from the command line
+##############################################################################
+
+if __name__ == "__main__":
+ main(module=None)
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Tests/xmlrunner.py b/cheetah/Tests/xmlrunner.py
new file mode 100644
index 0000000..dc49c56
--- /dev/null
+++ b/cheetah/Tests/xmlrunner.py
@@ -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):
+ print >>sys.stderr, "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[]]></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
index 0000000..c94ddc4
--- /dev/null
+++ b/cheetah/Tools/CGITemplate.py
@@ -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 = os.environ.has_key('REQUEST_METHOD')
+ 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
index 0000000..d0fada2
--- /dev/null
+++ b/cheetah/Tools/MondoReport.py
@@ -0,0 +1,463 @@
+"""
+@@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, types
+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.
+
+########## CONSTANTS ##############################
+
+True, False = (1==1), (1==0)
+numericTypes = types.IntType, types.LongType, types.FloatType
+
+########## PUBLIC GENERIC FUNCTIONS ##############################
+
+class NegativeError(ValueError):
+ pass
+
+def isNumeric(v):
+ return type(v) in numericTypes
+
+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 = lis[:]
+ lis.sort()
+ 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 = 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
index 0000000..29a026d
--- /dev/null
+++ b/cheetah/Tools/MondoReportDoc.txt
@@ -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
index 0000000..d5c1ef0
--- /dev/null
+++ b/cheetah/Tools/RecursiveNull.py
@@ -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
index 0000000..06da56f
--- /dev/null
+++ b/cheetah/Tools/SiteHierarchy.py
@@ -0,0 +1,182 @@
+# $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.
+
+Meta-Data
+================================================================================
+Author: Ian Bicking <ianb@colorstudy.com>
+Version: $Revision: 1.1 $
+Start Date: 2001/07/23
+Last Revision Date: $Date: 2001/10/11 03:25:54 $
+"""
+__author__ = "Ian Bicking <ianb@colorstudy.com>"
+__version__ = "$Revision: 1.1 $"[11:-2]
+
+##################################################
+## DEPENDENCIES
+import string
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+##################################################
+## GLOBALS & CONSTANTS
+
+True, False = (1==1), (0==1)
+
+##################################################
+## 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 1:
+ ## 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 type(item) is type(()):
+ 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 type(contents) is type(()):
+ 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 type(contents) is type(()):
+ 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
index 0000000..506503b
--- /dev/null
+++ b/cheetah/Tools/__init__.py
@@ -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
index 0000000..584583e
--- /dev/null
+++ b/cheetah/Tools/turbocheetah/__init__.py
@@ -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
index 0000000..682206f
--- /dev/null
+++ b/cheetah/Tools/turbocheetah/cheetahsupport.py
@@ -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 in 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(filter(None, (template, template_string, template_file)))
+ 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 ct.has_key(classname):
+ 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
index 0000000..792d600
--- /dev/null
+++ b/cheetah/Tools/turbocheetah/tests/__init__.py
@@ -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
index 0000000..a6196e3
--- /dev/null
+++ b/cheetah/Tools/turbocheetah/tests/test_template.py
@@ -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
index 0000000..89c5176
--- /dev/null
+++ b/cheetah/Unspecified.py
@@ -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
index 0000000..52c142d
--- /dev/null
+++ b/cheetah/Utils/Indenter.py
@@ -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
index 0000000..6ff5bb2
--- /dev/null
+++ b/cheetah/Utils/Misc.py
@@ -0,0 +1,81 @@
+# $Id: Misc.py,v 1.8 2005/11/02 22:26:08 tavis_rudd Exp $
+"""Miscellaneous functions/objects used by Cheetah but also useful standalone.
+
+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.8 $
+Start Date: 2001/11/07
+Last Revision Date: $Date: 2005/11/02 22:26:08 $
+"""
+__author__ = "Mike Orr <iron@mso.oz.net>"
+__revision__ = "$Revision: 1.8 $"[11:-2]
+
+import os # Used in mkdirsWithPyInitFile.
+import types # Used in useOrRaise.
+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 type(thing) == types.ClassType 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/VerifyType.py b/cheetah/Utils/VerifyType.py
new file mode 100644
index 0000000..11a435d
--- /dev/null
+++ b/cheetah/Utils/VerifyType.py
@@ -0,0 +1,83 @@
+# $Id: VerifyType.py,v 1.4 2005/11/02 22:26:08 tavis_rudd Exp $
+"""Functions to verify an argument's type
+
+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.4 $
+Start Date: 2001/11/07
+Last Revision Date: $Date: 2005/11/02 22:26:08 $
+"""
+__author__ = "Mike Orr <iron@mso.oz.net>"
+__revision__ = "$Revision: 1.4 $"[11:-2]
+
+##################################################
+## DEPENDENCIES
+
+import types # Used in VerifyTypeClass.
+
+##################################################
+## PRIVATE FUNCTIONS
+
+def _errmsg(argname, ltd, errmsgExtra=''):
+ """Construct an error message.
+
+ argname, string, the argument name.
+ ltd, string, description of the legal types.
+ errmsgExtra, string, text to append to error mssage.
+ Returns: string, the error message.
+ """
+ if errmsgExtra:
+ errmsgExtra = '\n' + errmsgExtra
+ return "arg '%s' must be %s%s" % (argname, ltd, errmsgExtra)
+
+
+##################################################
+## TYPE VERIFICATION FUNCTIONS
+
+def VerifyType(arg, argname, legalTypes, ltd, errmsgExtra=''):
+ """Verify the type of an argument.
+
+ arg, any, the argument.
+ argname, string, name of the argument.
+ legalTypes, list of type objects, the allowed types.
+ ltd, string, description of legal types (for error message).
+ errmsgExtra, string, text to append to error message.
+ Returns: None.
+ Exceptions: TypeError if 'arg' is the wrong type.
+ """
+ if type(arg) not in legalTypes:
+ m = _errmsg(argname, ltd, errmsgExtra)
+ raise TypeError(m)
+ return True
+
+
+def VerifyTypeClass(arg, argname, legalTypes, ltd, klass, errmsgExtra=''):
+ """Same, but if it's a class, verify it's a subclass of the right class.
+
+ arg, any, the argument.
+ argname, string, name of the argument.
+ legalTypes, list of type objects, the allowed types.
+ ltd, string, description of legal types (for error message).
+ klass, class, the parent class.
+ errmsgExtra, string, text to append to the error message.
+ Returns: None.
+ Exceptions: TypeError if 'arg' is the wrong type.
+ """
+ VerifyType(arg, argname, legalTypes, ltd, errmsgExtra)
+ # If no exception, the arg is a legal type.
+ if type(arg) == types.ClassType and not issubclass(arg, klass):
+ # Must test for "is class type" to avoid TypeError from issubclass().
+ m = _errmsg(argname, ltd, errmsgExtra)
+ raise TypeError(m)
+ return True
+
+# @@MO: Commented until we determine whether it's useful.
+#def VerifyClass(arg, argname, klass, ltd):
+# """Same, but allow *only* a subclass of the right class.
+# """
+# VerifyTypeClass(arg, argname, [types.ClassType], ltd, klass)
+
+# vim: shiftwidth=4 tabstop=4 expandtab
diff --git a/cheetah/Utils/WebInputMixin.py b/cheetah/Utils/WebInputMixin.py
new file mode 100644
index 0000000..52b6220
--- /dev/null
+++ b/cheetah/Utils/WebInputMixin.py
@@ -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
index 0000000..792d600
--- /dev/null
+++ b/cheetah/Utils/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/cheetah/Utils/htmlDecode.py b/cheetah/Utils/htmlDecode.py
new file mode 100644
index 0000000..2832a74
--- /dev/null
+++ b/cheetah/Utils/htmlDecode.py
@@ -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
index 0000000..f76c77e
--- /dev/null
+++ b/cheetah/Utils/htmlEncode.py
@@ -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/memcache.py b/cheetah/Utils/memcache.py
new file mode 100644
index 0000000..ee9678d
--- /dev/null
+++ b/cheetah/Utils/memcache.py
@@ -0,0 +1,624 @@
+
+"""
+client module for memcached (memory cache daemon)
+
+Overview
+========
+
+See U{the MemCached homepage<http://www.danga.com/memcached>} for more about memcached.
+
+Usage summary
+=============
+
+This should give you a feel for how this module operates::
+
+ import memcache
+ mc = memcache.Client(['127.0.0.1:11211'], debug=0)
+
+ mc.set("some_key", "Some value")
+ value = mc.get("some_key")
+
+ mc.set("another_key", 3)
+ mc.delete("another_key")
+
+ mc.set("key", "1") # note that the key used for incr/decr must be a string.
+ mc.incr("key")
+ mc.decr("key")
+
+The standard way to use memcache with a database is like this::
+
+ key = derive_key(obj)
+ obj = mc.get(key)
+ if not obj:
+ obj = backend_api.get(...)
+ mc.set(key, obj)
+
+ # we now have obj, and future passes through this code
+ # will use the object from the cache.
+
+Detailed Documentation
+======================
+
+More detailed documentation is available in the L{Client} class.
+"""
+
+import sys
+import socket
+import time
+import types
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+__author__ = "Evan Martin <martine@danga.com>"
+__version__ = "1.2_tummy5"
+__copyright__ = "Copyright (C) 2003 Danga Interactive"
+__license__ = "Python"
+
+class _Error(Exception):
+ pass
+
+class Client:
+ """
+ Object representing a pool of memcache servers.
+
+ See L{memcache} for an overview.
+
+ In all cases where a key is used, the key can be either:
+ 1. A simple hashable type (string, integer, etc.).
+ 2. A tuple of C{(hashvalue, key)}. This is useful if you want to avoid
+ making this module calculate a hash value. You may prefer, for
+ example, to keep all of a given user's objects on the same memcache
+ server, so you could use the user's unique id as the hash value.
+
+ @group Setup: __init__, set_servers, forget_dead_hosts, disconnect_all, debuglog
+ @group Insertion: set, add, replace
+ @group Retrieval: get, get_multi
+ @group Integers: incr, decr
+ @group Removal: delete
+ @sort: __init__, set_servers, forget_dead_hosts, disconnect_all, debuglog,\
+ set, add, replace, get, get_multi, incr, decr, delete
+ """
+
+ _usePickle = False
+ _FLAG_PICKLE = 1<<0
+ _FLAG_INTEGER = 1<<1
+ _FLAG_LONG = 1<<2
+
+ _SERVER_RETRIES = 10 # how many times to try finding a free server.
+
+ def __init__(self, servers, debug=0):
+ """
+ Create a new Client object with the given list of servers.
+
+ @param servers: C{servers} is passed to L{set_servers}.
+ @param debug: whether to display error messages when a server can't be
+ contacted.
+ """
+ self.set_servers(servers)
+ self.debug = debug
+ self.stats = {}
+
+ def set_servers(self, servers):
+ """
+ Set the pool of servers used by this client.
+
+ @param servers: an array of servers.
+ Servers can be passed in two forms:
+ 1. Strings of the form C{"host:port"}, which implies a default weight of 1.
+ 2. Tuples of the form C{("host:port", weight)}, where C{weight} is
+ an integer weight value.
+ """
+ self.servers = [_Host(s, self.debuglog) for s in servers]
+ self._init_buckets()
+
+ def get_stats(self):
+ '''Get statistics from each of the servers.
+
+ @return: A list of tuples ( server_identifier, stats_dictionary ).
+ The dictionary contains a number of name/value pairs specifying
+ the name of the status field and the string value associated with
+ it. The values are not converted from strings.
+ '''
+ data = []
+ for s in self.servers:
+ if not s.connect(): continue
+ name = '%s:%s (%s)' % ( s.ip, s.port, s.weight )
+ s.send_cmd('stats')
+ serverData = {}
+ data.append(( name, serverData ))
+ readline = s.readline
+ while 1:
+ line = readline()
+ if not line or line.strip() == 'END': break
+ stats = line.split(' ', 2)
+ serverData[stats[1]] = stats[2]
+
+ return(data)
+
+ def flush_all(self):
+ 'Expire all data currently in the memcache servers.'
+ for s in self.servers:
+ if not s.connect(): continue
+ s.send_cmd('flush_all')
+ s.expect("OK")
+
+ def debuglog(self, str):
+ if self.debug:
+ sys.stderr.write("MemCached: %s\n" % str)
+
+ def _statlog(self, func):
+ if not self.stats.has_key(func):
+ self.stats[func] = 1
+ else:
+ self.stats[func] += 1
+
+ def forget_dead_hosts(self):
+ """
+ Reset every host in the pool to an "alive" state.
+ """
+ for s in self.servers:
+ s.dead_until = 0
+
+ def _init_buckets(self):
+ self.buckets = []
+ for server in self.servers:
+ for i in range(server.weight):
+ self.buckets.append(server)
+
+ def _get_server(self, key):
+ if type(key) == types.TupleType:
+ serverhash = key[0]
+ key = key[1]
+ else:
+ serverhash = hash(key)
+
+ for i in range(Client._SERVER_RETRIES):
+ server = self.buckets[serverhash % len(self.buckets)]
+ if server.connect():
+ #print "(using server %s)" % server,
+ return server, key
+ serverhash = hash(str(serverhash) + str(i))
+ return None, None
+
+ def disconnect_all(self):
+ for s in self.servers:
+ s.close_socket()
+
+ def delete(self, key, time=0):
+ '''Deletes a key from the memcache.
+
+ @return: Nonzero on success.
+ @rtype: int
+ '''
+ server, key = self._get_server(key)
+ if not server:
+ return 0
+ self._statlog('delete')
+ if time != None:
+ cmd = "delete %s %d" % (key, time)
+ else:
+ cmd = "delete %s" % key
+
+ try:
+ server.send_cmd(cmd)
+ server.expect("DELETED")
+ except socket.error, msg:
+ server.mark_dead(msg[1])
+ return 0
+ return 1
+
+ def incr(self, key, delta=1):
+ """
+ Sends a command to the server to atomically increment the value for C{key} by
+ C{delta}, or by 1 if C{delta} is unspecified. Returns None if C{key} doesn't
+ exist on server, otherwise it returns the new value after incrementing.
+
+ Note that the value for C{key} must already exist in the memcache, and it
+ must be the string representation of an integer.
+
+ >>> mc.set("counter", "20") # returns 1, indicating success
+ 1
+ >>> mc.incr("counter")
+ 21
+ >>> mc.incr("counter")
+ 22
+
+ Overflow on server is not checked. Be aware of values approaching
+ 2**32. See L{decr}.
+
+ @param delta: Integer amount to increment by (should be zero or greater).
+ @return: New value after incrementing.
+ @rtype: int
+ """
+ return self._incrdecr("incr", key, delta)
+
+ def decr(self, key, delta=1):
+ """
+ Like L{incr}, but decrements. Unlike L{incr}, underflow is checked and
+ new values are capped at 0. If server value is 1, a decrement of 2
+ returns 0, not -1.
+
+ @param delta: Integer amount to decrement by (should be zero or greater).
+ @return: New value after decrementing.
+ @rtype: int
+ """
+ return self._incrdecr("decr", key, delta)
+
+ def _incrdecr(self, cmd, key, delta):
+ server, key = self._get_server(key)
+ if not server:
+ return 0
+ self._statlog(cmd)
+ cmd = "%s %s %d" % (cmd, key, delta)
+ try:
+ server.send_cmd(cmd)
+ line = server.readline()
+ return int(line)
+ except socket.error, msg:
+ server.mark_dead(msg[1])
+ return None
+
+ def add(self, key, val, time=0):
+ '''
+ Add new key with value.
+
+ Like L{set}, but only stores in memcache if the key doesn\'t already exist.
+
+ @return: Nonzero on success.
+ @rtype: int
+ '''
+ return self._set("add", key, val, time)
+ def replace(self, key, val, time=0):
+ '''Replace existing key with value.
+
+ Like L{set}, but only stores in memcache if the key already exists.
+ The opposite of L{add}.
+
+ @return: Nonzero on success.
+ @rtype: int
+ '''
+ return self._set("replace", key, val, time)
+ def set(self, key, val, time=0):
+ '''Unconditionally sets a key to a given value in the memcache.
+
+ The C{key} can optionally be an tuple, with the first element being the
+ hash value, if you want to avoid making this module calculate a hash value.
+ You may prefer, for example, to keep all of a given user's objects on the
+ same memcache server, so you could use the user's unique id as the hash
+ value.
+
+ @return: Nonzero on success.
+ @rtype: int
+ '''
+ return self._set("set", key, val, time)
+
+ def _set(self, cmd, key, val, time):
+ server, key = self._get_server(key)
+ if not server:
+ return 0
+
+ self._statlog(cmd)
+
+ flags = 0
+ if isinstance(val, types.StringTypes):
+ pass
+ elif isinstance(val, int):
+ flags |= Client._FLAG_INTEGER
+ val = "%d" % val
+ elif isinstance(val, long):
+ flags |= Client._FLAG_LONG
+ val = "%d" % val
+ elif self._usePickle:
+ flags |= Client._FLAG_PICKLE
+ val = pickle.dumps(val, 2)
+ else:
+ pass
+
+ fullcmd = "%s %s %d %d %d\r\n%s" % (cmd, key, flags, time, len(val), val)
+ try:
+ server.send_cmd(fullcmd)
+ server.expect("STORED")
+ except socket.error, msg:
+ server.mark_dead(msg[1])
+ return 0
+ return 1
+
+ def get(self, key):
+ '''Retrieves a key from the memcache.
+
+ @return: The value or None.
+ '''
+ server, key = self._get_server(key)
+ if not server:
+ return None
+
+ self._statlog('get')
+
+ try:
+ server.send_cmd("get %s" % key)
+ rkey, flags, rlen, = self._expectvalue(server)
+ if not rkey:
+ return None
+ value = self._recv_value(server, flags, rlen)
+ server.expect("END")
+ except (_Error, socket.error), msg:
+ if type(msg) is types.TupleType:
+ msg = msg[1]
+ server.mark_dead(msg)
+ return None
+ return value
+
+ def get_multi(self, keys):
+ '''
+ Retrieves multiple keys from the memcache doing just one query.
+
+ >>> success = mc.set("foo", "bar")
+ >>> success = mc.set("baz", 42)
+ >>> mc.get_multi(["foo", "baz", "foobar"]) == {"foo": "bar", "baz": 42}
+ 1
+
+ This method is recommended over regular L{get} as it lowers the number of
+ total packets flying around your network, reducing total latency, since
+ your app doesn\'t have to wait for each round-trip of L{get} before sending
+ the next one.
+
+ @param keys: An array of keys.
+ @return: A dictionary of key/value pairs that were available.
+
+ '''
+
+ self._statlog('get_multi')
+
+ server_keys = {}
+
+ # build up a list for each server of all the keys we want.
+ for key in keys:
+ server, key = self._get_server(key)
+ if not server:
+ continue
+ if not server_keys.has_key(server):
+ server_keys[server] = []
+ server_keys[server].append(key)
+
+ # send out all requests on each server before reading anything
+ dead_servers = []
+ for server in server_keys.keys():
+ try:
+ server.send_cmd("get %s" % " ".join(server_keys[server]))
+ except socket.error, msg:
+ server.mark_dead(msg[1])
+ dead_servers.append(server)
+
+ # if any servers died on the way, don't expect them to respond.
+ for server in dead_servers:
+ del server_keys[server]
+
+ retvals = {}
+ for server in server_keys.keys():
+ try:
+ line = server.readline()
+ while line and line != 'END':
+ rkey, flags, rlen = self._expectvalue(server, line)
+ # Bo Yang reports that this can sometimes be None
+ if rkey is not None:
+ val = self._recv_value(server, flags, rlen)
+ retvals[rkey] = val
+ line = server.readline()
+ except (_Error, socket.error), msg:
+ server.mark_dead(msg)
+ return retvals
+
+ def _expectvalue(self, server, line=None):
+ if not line:
+ line = server.readline()
+
+ if line[:5] == 'VALUE':
+ resp, rkey, flags, len = line.split()
+ flags = int(flags)
+ rlen = int(len)
+ return (rkey, flags, rlen)
+ else:
+ return (None, None, None)
+
+ def _recv_value(self, server, flags, rlen):
+ rlen += 2 # include \r\n
+ buf = server.recv(rlen)
+ if len(buf) != rlen:
+ raise _Error("received %d bytes when expecting %d" % (len(buf), rlen))
+
+ if len(buf) == rlen:
+ buf = buf[:-2] # strip \r\n
+
+ if flags == 0:
+ val = buf
+ elif flags & Client._FLAG_INTEGER:
+ val = int(buf)
+ elif flags & Client._FLAG_LONG:
+ val = long(buf)
+ elif self._usePickle and flags & Client._FLAG_PICKLE:
+ try:
+ val = pickle.loads(buf)
+ except:
+ self.debuglog('Pickle error...\n')
+ val = None
+ else:
+ self.debuglog("unknown flags on get: %x\n" % flags)
+
+ return val
+
+class _Host:
+ _DEAD_RETRY = 30 # number of seconds before retrying a dead server.
+
+ def __init__(self, host, debugfunc=None):
+ if isinstance(host, types.TupleType):
+ host = host[0]
+ self.weight = host[1]
+ else:
+ self.weight = 1
+
+ if host.find(":") > 0:
+ self.ip, self.port = host.split(":")
+ self.port = int(self.port)
+ else:
+ self.ip, self.port = host, 11211
+
+ if not debugfunc:
+ debugfunc = lambda x: x
+ self.debuglog = debugfunc
+
+ self.deaduntil = 0
+ self.socket = None
+
+ def _check_dead(self):
+ if self.deaduntil and self.deaduntil > time.time():
+ return 1
+ self.deaduntil = 0
+ return 0
+
+ def connect(self):
+ if self._get_socket():
+ return 1
+ return 0
+
+ def mark_dead(self, reason):
+ self.debuglog("MemCache: %s: %s. Marking dead." % (self, reason))
+ self.deaduntil = time.time() + _Host._DEAD_RETRY
+ self.close_socket()
+
+ def _get_socket(self):
+ if self._check_dead():
+ return None
+ if self.socket:
+ return self.socket
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Python 2.3-ism: s.settimeout(1)
+ try:
+ s.connect((self.ip, self.port))
+ except socket.error, msg:
+ self.mark_dead("connect: %s" % msg[1])
+ return None
+ self.socket = s
+ return s
+
+ def close_socket(self):
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+
+ def send_cmd(self, cmd):
+ if len(cmd) > 100:
+ self.socket.sendall(cmd)
+ self.socket.sendall('\r\n')
+ else:
+ self.socket.sendall(cmd + '\r\n')
+
+ def readline(self):
+ buffers = ''
+ recv = self.socket.recv
+ while 1:
+ data = recv(1)
+ if not data:
+ self.mark_dead('Connection closed while reading from %s'
+ % repr(self))
+ break
+ if data == '\n' and buffers and buffers[-1] == '\r':
+ return(buffers[:-1])
+ buffers = buffers + data
+ return(buffers)
+
+ def expect(self, text):
+ line = self.readline()
+ if line != text:
+ self.debuglog("while expecting '%s', got unexpected response '%s'" % (text, line))
+ return line
+
+ def recv(self, rlen):
+ buf = ''
+ recv = self.socket.recv
+ while len(buf) < rlen:
+ buf = buf + recv(rlen - len(buf))
+ return buf
+
+ def __str__(self):
+ d = ''
+ if self.deaduntil:
+ d = " (dead until %d)" % self.deaduntil
+ return "%s:%d%s" % (self.ip, self.port, d)
+
+def _doctest():
+ import doctest, memcache
+ servers = ["127.0.0.1:11211"]
+ mc = Client(servers, debug=1)
+ globs = {"mc": mc}
+ return doctest.testmod(memcache, globs=globs)
+
+if __name__ == "__main__":
+ print "Testing docstrings..."
+ _doctest()
+ print "Running tests:"
+ print
+ #servers = ["127.0.0.1:11211", "127.0.0.1:11212"]
+ servers = ["127.0.0.1:11211"]
+ mc = Client(servers, debug=1)
+
+ def to_s(val):
+ if not isinstance(val, types.StringTypes):
+ return "%s (%s)" % (val, type(val))
+ return "%s" % val
+ def test_setget(key, val):
+ print "Testing set/get {'%s': %s} ..." % (to_s(key), to_s(val)),
+ mc.set(key, val)
+ newval = mc.get(key)
+ if newval == val:
+ print "OK"
+ return 1
+ else:
+ print "FAIL"
+ return 0
+
+ class FooStruct:
+ def __init__(self):
+ self.bar = "baz"
+ def __str__(self):
+ return "A FooStruct"
+ def __eq__(self, other):
+ if isinstance(other, FooStruct):
+ return self.bar == other.bar
+ return 0
+
+ test_setget("a_string", "some random string")
+ test_setget("an_integer", 42)
+ if test_setget("long", long(1<<30)):
+ print "Testing delete ...",
+ if mc.delete("long"):
+ print "OK"
+ else:
+ print "FAIL"
+ print "Testing get_multi ...",
+ print mc.get_multi(["a_string", "an_integer"])
+
+ print "Testing get(unknown value) ...",
+ print to_s(mc.get("unknown_value"))
+
+ f = FooStruct()
+ test_setget("foostruct", f)
+
+ print "Testing incr ...",
+ x = mc.incr("an_integer", 1)
+ if x == 43:
+ print "OK"
+ else:
+ print "FAIL"
+
+ print "Testing decr ...",
+ x = mc.decr("an_integer", 1)
+ if x == 42:
+ print "OK"
+ else:
+ print "FAIL"
+
+
+
+# vim: ts=4 sw=4 et :
diff --git a/cheetah/Utils/statprof.py b/cheetah/Utils/statprof.py
new file mode 100644
index 0000000..55638eb
--- /dev/null
+++ b/cheetah/Utils/statprof.py
@@ -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.
+"""
+
+
+from __future__ import division
+
+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
index 0000000..747b176
--- /dev/null
+++ b/cheetah/Version.py
@@ -0,0 +1,58 @@
+Version = '2.2.2'
+VersionTuple = (2, 2, 2,'candidate', 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
index 0000000..910574b
--- /dev/null
+++ b/cheetah/__init__.py
@@ -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/convertTmplPathToModuleName.py b/cheetah/convertTmplPathToModuleName.py
new file mode 100644
index 0000000..4f9d8ea
--- /dev/null
+++ b/cheetah/convertTmplPathToModuleName.py
@@ -0,0 +1,15 @@
+import os.path
+import string
+
+l = ['_'] * 256
+for c in string.digits + string.letters:
+ l[ord(c)] = c
+_pathNameTransChars = string.join(l, '')
+del l, c
+
+def convertTmplPathToModuleName(tmplPath,
+ _pathNameTransChars=_pathNameTransChars,
+ splitdrive=os.path.splitdrive,
+ translate=string.translate,
+ ):
+ return translate(splitdrive(tmplPath)[1], _pathNameTransChars)