diff options
Diffstat (limited to 'cheetah')
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 + "<" + rawCode + " could not be found>" + "="*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 = {' ': ' ', '"': '"'} + +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("&", "&") # Must be done first! + s = s.replace("<", "<") + s = s.replace(">", ">") + # 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 <=> &") + + def test6(self): + """#filter WebSafe -- also space + """ + self.verify("#filter WebSafe \n${webSafeTest, $also=' '}#end filter", + "abc <=> &") + + def test7(self): + """#filter WebSafe -- also space, without $ on the args + """ + self.verify("#filter WebSafe \n${webSafeTest, also=' '}#end filter", + "abc <=> &") + + 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 => $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' % (' '*2*indent, + self._menuCSSClass, text) + else: + return '%s<A HREF="%s%s"%s>%s</A> <BR>\n' % \ + (' '*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 ' > ' + + 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 = [ + ['&', '&'], + ['<', '<'], + ['>', '>'], + ['"', '"'], +] +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) |