'''
    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
import re
import time
import random
import warnings
import copy
import codecs

from .Version import Version, VersionTuple
from .SettingsManager import SettingsManager
from .Utils.Indenter import indentize  # an undocumented preprocessor
from . import NameMapper
from .Parser import Parser, ParseError, specialVarRE, \
    STATIC_CACHE, REFRESH_CACHE, SET_GLOBAL, SET_MODULE, \
    unicodeDirectiveRE, encodingDirectiveRE, escapedNewlineRE
from .compat import PY2, string_type, unicode

from .NameMapper import 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 baseclasses 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, ''),
    ('encoding', None,
     'The encoding to read input files as (or None for ASCII)'),
]

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  # noqa: E226,E501 missing whitespace around operator
        elif timeString[-1] == 'h':
            interval = float(timeString[:-1])*60*60  # noqa: E226,E501 missing whitespace around operator
        elif timeString[-1] == 'd':
            interval = float(timeString[:-1])*60*60*24  # noqa: E226,E501 missing whitespace around operator
        elif timeString[-1] == 'w':
            interval = float(timeString[:-1])*60*60*24*7  # noqa: E226,E501 missing whitespace around operator
        else:                       # default to minutes
            interval = float(timeString)*60  # noqa: E226,E501 missing whitespace around operator
        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
          # optionally used instead of VFFSL
          VFSL = NameMapper.valueFromSearchList
          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 = name[:firstDotIdx]
                afterDot = name[firstDotIdx+1:]  # noqa: E226,E501 missing whitespace around operator
                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  # noqa: E226 missing whitespace around operator
        docStr = (ind + '"""\n' + ind
                  + ('\n' + ind).join([ln.replace('"""', "'''")
                                       for ln in self._docStringLines])
                  + '\n' + ind + '"""\n')
        return docStr

    # methods for adding code
    def addMethDocString(self, line):
        self._docStringLines.append(line.replace('%', '%%'))

    def addChunk(self, chunk):
        self.commitStrConst()
        chunk = "\n" + self.indentation() + chunk
        self._methodBodyChunks.append(chunk)

    def appendToPrevChunk(self, appendage):
        self._methodBodyChunks[-1] = self._methodBodyChunks[-1] + appendage

    def addWriteChunk(self, chunk):
        self.addChunk('write(' + chunk + ')')

    def addFilteredChunk(self, chunk, filterArgs=None,
                         rawExpr=None, lineCol=None):
        if filterArgs is None:
            filterArgs = ''
        if self.setting('includeRawExprInFilterArgs') and rawExpr:
            filterArgs += ', rawExpr=%s' % repr(rawExpr)

        if self.setting('alwaysFilterNone'):
            if rawExpr and rawExpr.find('\n') == -1 and \
                    rawExpr.find('\r') == -1:
                self.addChunk("_v = %s # %r" % (chunk, rawExpr))
                if lineCol:
                    self.appendToPrevChunk(' on line %s, col %s' % lineCol)
            else:
                self.addChunk("_v = %s" % chunk)

            if self.setting('useFilters'):
                self.addChunk("if _v is not None: write(_filter(_v%s))"
                              % filterArgs)
            else:
                self.addChunk("if _v is not None: write(str(_v))")
        else:
            if self.setting('useFilters'):
                self.addChunk("write(_filter(%s%s))" % (chunk, filterArgs))
            else:
                self.addChunk("write(str(%s))" % chunk)

    def _appendToPrevStrConst(self, strConst):
        if self._pendingStrConstChunks:
            self._pendingStrConstChunks.append(strConst)
        else:
            self._pendingStrConstChunks = [strConst]

    def commitStrConst(self):
        """
        Add the code for outputting the pending strConst without chopping off
        any whitespace from it.
        """
        if not self._pendingStrConstChunks:
            return

        strConst = ''.join(self._pendingStrConstChunks)
        self._pendingStrConstChunks = []
        if not strConst:
            return

        reprstr = repr(strConst)
        i = 0
        out = []
        if reprstr.startswith('u'):
            i = 1
            out = ['u']
        body = escapedNewlineRE.sub('\\1\n', reprstr[i+1:-1])  # noqa: E226,E501 missing whitespace around operator

        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)  # noqa: E226,E501 missing whitespace around operator

    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 arg[1] is not 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('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.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
        # attrib of current methodCompiler
        self._callRegionsStack.append((ID, callDetails))

        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

        # attrib of current methodCompiler
        self._captureRegionsStack.append((ID, captureDetails))
        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 "' + errorCatcherName + '" in self._CHEETAH__errorCatchers:')
        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
        # attrib of current methodCompiler
        self._filterRegionsStack.append((ID, filterDetails))

        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 "' + theFilter + '" in self._CHEETAH__filters:')
                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)  # noqa: E226 missing whitespace around operator
        self.addChunk('## START - generated method body')
        self.addChunk('')

    def _addAutoCleanupCode(self):
        self.addChunk('')
        self.addChunk("#"*40)  # noqa: E226 missing whitespace around operator
        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 arg[1] is not 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)  # noqa: E226,E501 missing whitespace around operator


class ClassCompiler(GenUtils):
    methodCompilerClass = AutoMethodCompiler
    methodCompilerClassForInit = MethodCompiler

    def __init__(self, className, mainMethodName='respond',
                 moduleCompiler=None,
                 fileName=None,
                 settingsManager=None):

        self._settingsManager = settingsManager
        self._fileName = fileName
        self._className = className
        self._moduleCompiler = moduleCompiler
        self._mainMethodName = mainMethodName
        self._setupState()
        methodCompiler = self._spawnMethodCompiler(
            mainMethodName,
            initialMethodComment='## CHEETAH: main method '
            'generated for this template')

        self._setActiveMethodCompiler(methodCompiler)
        if fileName and self.setting('monitorSrcFile'):
            self._addSourceFileMonitoring(fileName)

    def setting(self, key):
        return self._settingsManager.setting(key)

    def __getattr__(self, name):
        """Provide access to the methods and attributes of the MethodCompiler
        at the top of the activeMethods stack: one-way namespace sharing


        WARNING: Use .setMethods to assign the attributes of the MethodCompiler
        from the methods of this class!!! or you will be assigning to
        attributes of this object instead.
        """
        if name in self.__dict__:
            return self.__dict__[name]
        elif hasattr(self.__class__, name):
            return getattr(self.__class__, name)
        elif self._activeMethodsList and \
                hasattr(self._activeMethodsList[-1], name):
            return getattr(self._activeMethodsList[-1], name)
        else:
            raise AttributeError(name)

    def _setupState(self):
        self._classDef = None
        self._decoratorsForNextMethod = []
        self._activeMethodsList = []        # stack while parsing/generating
        self._finishedMethodsList = []      # store by order
        self._methodsIndex = {}      # store by name
        self._baseClass = 'Template'
        self._classDocStringLines = []
        # printed after methods in the gen class def:
        self._generatedAttribs = ['_CHEETAH__instanceInitialized = False']
        self._generatedAttribs.append('_CHEETAH_version = __CHEETAH_version__')
        self._generatedAttribs.append(
            '_CHEETAH_versionTuple = __CHEETAH_versionTuple__')

        if self.setting('addTimestampsToCompilerOutput'):
            self._generatedAttribs.append(
                '_CHEETAH_genTime = __CHEETAH_genTime__')
            self._generatedAttribs.append(
                '_CHEETAH_genTimestamp = __CHEETAH_genTimestamp__')

        self._generatedAttribs.append('_CHEETAH_src = __CHEETAH_src__')
        self._generatedAttribs.append(
            '_CHEETAH_srcLastModified = __CHEETAH_srcLastModified__')

        if self.setting('templateMetaclass'):
            self._generatedAttribs.append(
                '__metaclass__ = ' + self.setting('templateMetaclass'))
        self._initMethChunks = []
        self._blockMetaData = {}
        self._errorCatcherCount = 0
        self._placeholderToErrorCatcherMap = {}

    def cleanupState(self):
        while self._activeMethodsList:
            methCompiler = self._popActiveMethodCompiler()
            self._swallowMethodCompiler(methCompiler)
        self._setupInitMethod()
        if self._mainMethodName == 'respond':
            if self.setting('setup__str__method'):
                self._generatedAttribs.append(
                    'def __str__(self): return self.respond()')
        self.addAttribute(
            '_mainCheetahMethod_for_' + self._className
            + ' = ' + repr(self._mainMethodName))

    def _setupInitMethod(self):
        __init__ = self._spawnMethodCompiler(
            '__init__', klass=self.methodCompilerClassForInit)
        __init__.setMethodSignature("def __init__(self, *args, **KWs)")
        __init__.addChunk(
            'super(%s, self).__init__(*args, **KWs)' % self._className)
        __init__.addChunk(
            _initMethod_initCheetah % {'className': self._className})
        for chunk in self._initMethChunks:
            __init__.addChunk(chunk)
        __init__.cleanupState()
        self._swallowMethodCompiler(__init__, pos=0)

    def _addSourceFileMonitoring(self, fileName):
        # @@TR: this stuff needs auditing for Cheetah 2.0
        # the first bit is added to init
        self.addChunkToInit('self._filePath = ' + repr(fileName))
        self.addChunkToInit('self._fileMtime = ' + str(getmtime(fileName)))

        # the rest is added to the main output method of the class
        # ('mainMethod')
        self.addChunk(
            'if exists(self._filePath) and '
            + 'getmtime(self._filePath) > self._fileMtime:')
        self.indent()
        self.addChunk(
            'self._compile(file=self._filePath, moduleName='
            + self._className + ')')
        self.addChunk(
            'write(getattr(self, self._mainCheetahMethod_for_'
            + self._className + ')(trans=trans))')
        self.addStop()
        self.dedent()

    def setClassName(self, name):
        self._className = name

    def className(self):
        return self._className

    def setBaseClass(self, baseClassName):
        self._baseClass = baseClassName

    def setMainMethodName(self, methodName):
        if methodName == self._mainMethodName:
            return
        # change the name in the methodCompiler and add new reference
        mainMethod = self._methodsIndex[self._mainMethodName]
        mainMethod.setMethodName(methodName)
        self._methodsIndex[methodName] = mainMethod

        # make sure that fileUpdate code still works properly:
        chunkToChange = (
            'write(self.' + self._mainMethodName + '(trans=trans))')
        chunks = mainMethod._methodBodyChunks
        if chunkToChange in chunks:
            for i in range(len(chunks)):
                if chunks[i] == chunkToChange:
                    chunks[i] = ('write(self.' + methodName + '(trans=trans))')
        # get rid of the old reference and update self._mainMethodName
        del self._methodsIndex[self._mainMethodName]
        self._mainMethodName = methodName

    def setMainMethodArgs(self, argsList):
        mainMethodCompiler = self._methodsIndex[self._mainMethodName]
        for argName, defVal in argsList:
            mainMethodCompiler.addMethArg(argName, defVal)

    def _spawnMethodCompiler(self, methodName, klass=None,
                             initialMethodComment=None):
        if klass is None:
            klass = self.methodCompilerClass

        decorators = self._decoratorsForNextMethod or []
        self._decoratorsForNextMethod = []
        methodCompiler = klass(methodName, classCompiler=self,
                               decorators=decorators,
                               initialMethodComment=initialMethodComment)
        self._methodsIndex[methodName] = methodCompiler
        return methodCompiler

    def _setActiveMethodCompiler(self, methodCompiler):
        self._activeMethodsList.append(methodCompiler)

    def _getActiveMethodCompiler(self):
        return self._activeMethodsList[-1]

    def _popActiveMethodCompiler(self):
        return self._activeMethodsList.pop()

    def _swallowMethodCompiler(self, methodCompiler, pos=None):
        methodCompiler.cleanupState()
        if pos is 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 arg[1] is not None:
                chunk += '=' + arg[1]
            argStringChunks.append(chunk)
        argString = ','.join(argStringChunks)

        self.addFilteredChunk(
            'super(%(className)s, self).%(methodName)s(%(argString)s)'
            % locals())

    def addErrorCatcherCall(self, codeChunk, rawCode='', lineCol=''):
        if rawCode in self._placeholderToErrorCatcherMap:
            methodName = self._placeholderToErrorCatcherMap[rawCode]
            if not self.setting('outputRowColComments'):
                self._methodsIndex[methodName].addMethDocString(
                    'plus at line %s, col %s' % lineCol)
            return methodName

        self._errorCatcherCount += 1
        methodName = '__errorCatcher' + str(self._errorCatcherCount)
        self._placeholderToErrorCatcherMap[rawCode] = methodName

        catcherMeth = self._spawnMethodCompiler(
            methodName,
            klass=MethodCompiler,
            initialMethodComment=(
                '## CHEETAH: Generated from ' + rawCode
                + ' at line %s, col %s' % lineCol + '.')
        )
        catcherMeth.setMethodSignature(
            'def ' + methodName
            + '(self, localsDict={})')  # is this use of localsDict right?
        catcherMeth.addChunk('try:')
        catcherMeth.indent()
        catcherMeth.addChunk(
            "return eval('''" + codeChunk + "''', globals(), localsDict)")
        catcherMeth.dedent()
        catcherMeth.addChunk(
            'except self._CHEETAH__errorCatcher.exceptions() as 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,  # noqa: E226 missing whitespace around operator
                ind + '## CHEETAH GENERATED METHODS',
                '\n',
                self.methodDefs(),
            ])

        def addAttributes():
            classDefChunks.extend([
                ind + '#'*50,  # noqa: E226 missing whitespace around operator
                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):
        try:
            attribs = [self.setting('indentationStep') + str(attrib)
                       for attrib in self._generatedAttribs]
        except UnicodeEncodeError:
            attribs = [self.setting('indentationStep') + unicode(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 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, string_type):  # it's a filename.
            encoding = self.settings().get('encoding')
            if encoding:
                f = codecs.open(file, 'r', encoding=encoding)
            else:
                # if no encoding is specified, use the builtin open function
                f = open(file, 'r')
            source = f.read()
            f.close()
            self._filePath = file
            self._fileMtime = os.path.getmtime(file)
        elif hasattr(file, 'read'):
            # Can't set filename or mtime -- they're not accessible
            source = file.read()
        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, string_type):
            # By converting to unicode here we allow objects
            # such as other Templates to be passed in
            source = unicode(source)

        # Handle the #indent directive by converting it to other directives.
        # (Over the long term we'll make it a real directive.)
        if source == "":
            warnings.warn("You supplied an empty string for the source!", )

        else:
            unicodeMatch = unicodeDirectiveRE.search(source)
            encodingMatch = encodingDirectiveRE.search(source)
            if unicodeMatch:
                if encodingMatch:
                    raise ParseError(
                        self, "#encoding and #unicode are mutually exclusive! "
                        "Use one or the other.")
                source = unicodeDirectiveRE.sub('', source)
                if isinstance(source, bytes):
                    encoding = unicodeMatch.group(1) or 'ascii'
                    source = source.decode(encoding)
            elif encodingMatch:
                encodings = encodingMatch.groups()
                if len(encodings):
                    encoding = encodings[0]
                    if isinstance(source, bytes):
                        source = source.decode(encoding)
                    else:
                        source = eval(
                            repr(source).encode("ascii", "backslashreplace")
                            .decode(encoding))
            else:
                source = unicode(source)

        if source.find('#indent') != -1:  # @@TR: undocumented hack
            source = indentize(source)

        self._parser = self.parserClass(source, filename=self._filePath,
                                        compiler=self)
        self._setupCompilerState()

    def __getattr__(self, name):
        """Provide one-way access to the methods and attributes of the
        ClassCompiler, and thereby the MethodCompilers as well.

        WARNING: Use .setMethods to assign the attributes of the ClassCompiler
        from the methods of this class!!! Or you will be assigning to
        attributes of this object instead.
        """
        if name in self.__dict__:
            return self.__dict__[name]
        elif hasattr(self.__class__, name):
            return getattr(self.__class__, name)
        elif self._activeClassesList and \
                hasattr(self._activeClassesList[-1], name):
            return getattr(self._activeClassesList[-1], name)
        else:
            raise AttributeError(name)

    def _initializeSettings(self):
        self.updateSettings(copy.deepcopy(DEFAULT_COMPILER_SETTINGS))

    def _setupCompilerState(self):
        self._activeClassesList = []
        self._finishedClassesList = []      # listed by ordered
        self._finishedClassIndex = {}  # listed by name
        self._moduleDef = None
        self._moduleShBang = '#!/usr/bin/env python'
        self._moduleEncoding = 'ascii'
        self._moduleEncodingStr = ''
        self._moduleHeaderLines = []
        self._moduleDocStringLines = []
        self._specialVars = {}
        # we might have been moved to be a sub-package
        if "__spec__" in globals():
            package = __spec__.parent   # noqa: F821 undefined name '__spec__'
        else:
            package = __package__
        self._importStatements = [
            "import sys",
            "import os",
            "import os.path",
            'try:',
            '    import builtins as builtin',
            'except ImportError:',
            '    import __builtin__ as builtin',
            "from os.path import getmtime, exists",
            "import time",
            "import types",
            "from %s.Version import MinCompatibleVersion as "
            "RequiredCheetahVersion" % package,
            "from %s.Version import MinCompatibleVersionTuple "
            "as RequiredCheetahVersionTuple" % package,
            "from %s.Template import Template" % package,
            "from %s.DummyTransaction import *" % package,
            "from %s.NameMapper import NotFound, "
            "valueForName, valueFromSearchList, "
            "valueFromFrameOrSearchList" % package,
            "from %s.CacheRegion import CacheRegion" % package,
            "import %s.Filters as Filters" % package,
            "import %s.ErrorCatchers as ErrorCatchers" % package,
            "from %s.compat import unicode" % package,
        ]

        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 = []
            for klass in baseClassName.split(','):
                klass = klass.strip()
                chunks = klass.split('.')
                if len(chunks) == 1:
                    baseclasses.append(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]
                    for chunk in chunks[1:-1]:
                        if modName in self.importedVarNames():
                            needToAddImport = False
                            finalBaseClassName = klass.replace(modName + '.',
                                                               '')
                            baseclasses.append(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)
                        baseclasses.append(finalClassName)
                        importStatement = "from %s import %s" % (
                            modName, finalClassName)
                        self.addImportStatement(importStatement)
                        self.addImportedVarNames([finalClassName])

            self._getActiveClassCompiler().setBaseClass(', '.join(baseclasses))

    def setCompilerSetting(self, key, valueExpr):
        self.setSetting(key, eval(valueExpr))
        self._parser.configureParser()

    def setCompilerSettings(self, keywords, settingsStr):
        KWs = keywords

        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(',')
        # handle aliases
        importVarNames = [var.split()[-1] for var in importVarNames]
        importVarNames = [var for var in importVarNames if not var == '*']
        # used by #extend for auto-imports
        self.addImportedVarNames(importVarNames, raw_statement=impStatement)

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

    def __to_bytes(self):
        code = self.getModuleCode()
        if isinstance(code, bytes):
            return code
        return code.encode(self.getModuleEncoding())

    def __to_unicode(self):
        code = self.getModuleCode()
        if isinstance(code, bytes):
            return code.decode(self.getModuleEncoding())
        return code

    if PY2:
        __str__ = __to_bytes
        __unicode__ = __to_unicode
    else:
        __bytes__ = __to_bytes
        __str__ = __to_unicode

    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,
        }  # noqa

        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  # noqa: E226,E501 missing whitespace around operator
                + ('\n#' + ' '*offSet).join(self._moduleHeaderLines)  # noqa: E226,E501 missing whitespace around operator
                + '\n')

        return header

    def moduleDocstring(self):
        if not self._moduleDocStringLines:
            return ''

        return ('"""' + '\n'.join(self._moduleDocStringLines)
                + '\n"""\n')

    def specialVars(self):
        chunks = []
        theVars = self._specialVars
        keys = sorted(theVars.keys())
        for key in keys:
            chunks.append(key + ' = ' + repr(theVars[key]))
        return '\n'.join(chunks)

    def importStatements(self):
        return '\n'.join(self._importStatements)

    def moduleConstants(self):
        return '\n'.join(self._moduleConstants)

    def classDefs(self):
        classDefs = [klass.classDef() for klass in self._finishedClasses()]
        return '\n\n'.join(classDefs)

    def moduleFooter(self):
        return """
# CHEETAH was developed by Tavis Rudd and Mike Orr
# with code, advice and input from many other volunteers.
# For more information visit https://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
