0001"""
0002simple, elegant templating
0003(part of web.py)
0004"""
0005
0006import re, glob, os, os.path
0007from types import FunctionType as function
0008from utils import storage, group
0009from net import websafe
0010
0011# differences from python:
0012#  - for: has an optional else: that gets called if the loop never runs
0013# differences to add:
0014#  - you can use the expression inside if, while blocks
0015#  - special for loop attributes, like django?
0016#  - you can check to see if a variable is defined (perhaps w/ get func?)
0017# all these are probably good ideas for python...
0018
0019# todo:
0020#  inline tuple
0021#  relax constraints on spacing
0022#  continue, break, etc.
0023#  tracebacks
0024
0025global_globals = {'None':None, 'False':False, 'True': True}
0026MAX_ITERS = 100000
0027
0028WHAT = 0
0029ARGS = 4
0030KWARGS = 6
0031NAME = 2
0032BODY = 4
0033CLAUSE = 2
0034ELIF = 6
0035ELSE = 8
0036IN = 6
0037NAME = 2
0038EXPR = 4
0039FILTER = 4
0040THING = 2
0041ATTR = 4
0042ITEM = 4
0043NEGATE = 4
0044X = 2
0045OP = 4
0046Y = 6
0047LINENO = -1
0048
0049# http://docs.python.org/ref/identifiers.html
0050r_var = '[a-zA-Z_][a-zA-Z0-9_]*'
0051
0052class ParseError(Exception): pass
0053class Parser:
0054    def __init__(self, text):
0055        self.t = text
0056        self.p = 0
0057        self._lock = [False]
0058
0059    def lock(self):
0060        self._lock[-1] = True
0061
0062    def curline(self):
0063        return self.t[:self.p].count(' ')+1
0064
0065    def csome(self):
0066        return repr(self.t[self.p:self.p+5]+'...')
0067
0068    def Error(self, x, y=None):
0069        if y is None: y = self.csome()
0070        raise ParseError, "expected %s, got %s (line %s)" % (x, y, self.curline())
0071
0072    def q(self, f):
0073        def internal(*a, **kw):
0074            checkp = self.p
0075            self._lock.append(False)
0076            try:
0077                q = f(*a, **kw)
0078            except ParseError:
0079                if self._lock[-1]:
0080                    raise
0081                self.p = checkp
0082                self._lock.pop()
0083                return False
0084            self._lock.pop()
0085            return q or True
0086        return internal
0087
0088    def tokr(self, t):
0089        text = self.c(len(t))
0090        if text != t:
0091            self.Error(repr(t), repr(text))
0092        return t
0093
0094    def ltokr(self, *l):
0095        for x in l:
0096            o = self.tokq(x)
0097            if o: return o
0098        self.Error('one of '+repr(l))
0099
0100    def rer(self, r):
0101        x = re.match(r, self.t[self.p:]) #@@re_compile
0102        if not x:
0103            self.Error('r'+repr(r))
0104        return self.tokr(x.group())
0105
0106    def endr(self):
0107        if self.p != len(self.t):
0108            self.Error('EOF')
0109
0110    def c(self, n=1):
0111        out = self.t[self.p:self.p+n]
0112        if out == '' and n != 0:
0113            self.Error('character', 'EOF')
0114        self.p += n
0115        return out
0116
0117    def lookbehind(self, t):
0118        return self.t[self.p-len(t):self.p] == t
0119
0120    def __getattr__(self, a):
0121        if a.endswith('q'):
0122            return self.q(getattr(self, a[:-1]+'r'))
0123        raise AttributeError, a
0124
0125class TemplateParser(Parser):
0126    def __init__(self, *a, **kw):
0127        Parser.__init__(self, *a, **kw)
0128        self.curws = ''
0129        self.curind = ''
0130
0131    def o(self, *a):
0132        return a+('lineno', self.curline())
0133
0134    def go(self):
0135        # maybe try to do some traceback parsing/hacking
0136        return self.gor()
0137
0138    def gor(self):
0139        header = self.defwithq()
0140        results = self.lines(start=True)
0141        self.endr()
0142        return header, results
0143
0144    def ws(self):
0145        n = 0
0146        while self.tokq(" "): n += 1
0147        return " " * n
0148
0149    def defwithr(self):
0150        self.tokr('$def with ')
0151        self.lock()
0152        self.tokr('(')
0153        args = []
0154        kw = []
0155        x = self.req(r_var)
0156        while x:
0157            if self.tokq('='):
0158                v = self.exprr()
0159                kw.append((x, v))
0160            else:
0161                args.append(x)
0162            x = self.tokq(', ') and self.req(r_var)
0163        self.tokr(') ')
0164        return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw)
0165
0166    def literalr(self):
0167        o = (
0168          self.req('"[^"]*"') or #@@ no support for escapes
0169          self.req("'[^']*'")
0170        )
0171        if o is False:
0172            o = self.req('\-?[0-9]+(\.[0-9]*)?')
0173            if o is not False:
0174                if '.' in o: o = float(o)
0175                else: o = int(o)
0176
0177        if o is False: self.Error('literal')
0178        return self.o('literal', 'thing', o)
0179
0180    def listr(self):
0181        self.tokr('[')
0182        self.lock()
0183        x = []
0184        if not self.tokq(']'):
0185            while True:
0186                t = self.exprr()
0187                x.append(t)
0188                if not self.tokq(', '): break
0189            self.tokr(']')
0190        return self.o('list', 'thing', x)
0191
0192    def dictr(self):
0193        self.tokr('{')
0194        self.lock()
0195        x = {}
0196        if not self.tokq('}'):
0197            while True:
0198                k = self.exprr()
0199                self.tokr(': ')
0200                v = self.exprr()
0201                x[k] = v
0202                if not self.tokq(', '): break
0203            self.tokr('}')
0204        return self.o('dict', 'thing', x)
0205
0206    def parenr(self):
0207        self.tokr('(')
0208        self.lock()
0209        o = self.exprr() # todo: allow list
0210        self.tokr(')')
0211        return self.o('paren', 'thing', o)
0212
0213    def atomr(self):
0214        """returns var, literal, paren, dict, or list"""
0215        o = (
0216          self.varq() or
0217          self.parenq() or
0218          self.dictq() or
0219          self.listq() or
0220          self.literalq()
0221        )
0222        if o is False: self.Error('atom')
0223        return o
0224
0225    def primaryr(self):
0226        """returns getattr, call, or getitem"""
0227        n = self.atomr()
0228        while 1:
0229            if self.tokq('.'):
0230                v = self.req(r_var)
0231                if not v:
0232                    self.p -= 1 # get rid of the '.'
0233                    break
0234                else:
0235                    n = self.o('getattr', 'thing', n, 'attr', v)
0236            elif self.tokq('('):
0237                args = []
0238                kw = []
0239
0240                while 1:
0241                    # need to see if we're doing a keyword argument
0242                    checkp = self.p
0243                    k = self.req(r_var)
0244                    if k and self.tokq('='): # yup
0245                        v = self.exprr()
0246                        kw.append((k, v))
0247                    else:
0248                        self.p = checkp
0249                        x = self.exprq()
0250                        if x: # at least it's something
0251                            args.append(x)
0252                        else:
0253                            break
0254
0255                    if not self.tokq(', '): break
0256                self.tokr(')')
0257                n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw)
0258            elif self.tokq('['):
0259                v = self.exprr()
0260                self.tokr(']')
0261                n = self.o('getitem', 'thing', n, 'item', v)
0262            else:
0263                break
0264
0265        return n
0266
0267    def exprr(self):
0268        negate = self.tokq('not ')
0269        x = self.primaryr()
0270        if self.tokq(' '):
0271            operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%')
0272            self.tokr(' ')
0273            y = self.exprr()
0274            x = self.o('test', 'x', x, 'op', operator, 'y', y)
0275
0276        return self.o('expr', 'thing', x, 'negate', negate)
0277
0278    def varr(self):
0279        return self.o('var', 'name', self.rer(r_var))
0280
0281    def liner(self):
0282        out = []
0283        o = self.curws
0284        while 1:
0285            c = self.c()
0286            self.lock()
0287            if c == ' ':
0288                self.p -= 1
0289                break
0290            if c == '$':
0291                if self.lookbehind('\$'):
0292                    o = o[:-1] + c
0293                else:
0294                    filter = not bool(self.tokq(':'))
0295
0296                    if self.tokq('{'):
0297                        out.append(o)
0298                        out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter))
0299                        self.tokr('}')
0300                        o = ''
0301                    else:
0302                        g = self.primaryq()
0303                        if g:
0304                            out.append(o)
0305                            out.append(self.o('itpl', 'name', g, 'filter', filter))
0306                            o = ''
0307                        else:
0308                            o += c
0309            else:
0310                o += c
0311        self.tokr(' ')
0312        if not self.lookbehind('\ '):
0313            o += ' '
0314        else:
0315            o = o[:-1]
0316        out.append(o)
0317        return self.o('line', 'thing', out)
0318
0319    def varsetr(self):
0320        self.tokr('$var ')
0321        self.lock()
0322        what = self.rer(r_var)
0323        self.tokr(':')
0324        body = self.lines()
0325        return self.o('varset', 'name', what, 'body', body)
0326
0327    def ifr(self):
0328        self.tokr("$if ")
0329        self.lock()
0330        expr = self.exprr()
0331        self.tokr(":")
0332        ifc = self.lines()
0333
0334        elifs = []
0335        while self.tokq(self.curws + self.curind + '$elif '):
0336            v = self.exprr()
0337            self.tokr(':')
0338            c = self.lines()
0339            elifs.append(self.o('elif', 'clause', v, 'body', c))
0340
0341        if self.tokq(self.curws + self.curind + "$else:"):
0342            elsec = self.lines()
0343        else:
0344            elsec = None
0345
0346        return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec)
0347
0348    def forr(self):
0349        self.tokr("$for ")
0350        self.lock()
0351        v = self.setabler()
0352        self.tokr(" in ")
0353        g = self.exprr()
0354        self.tokr(":")
0355        l = self.lines()
0356
0357        if self.tokq(self.curws + self.curind + '$else:'):
0358            elsec = self.lines()
0359        else:
0360            elsec = None
0361
0362        return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec)
0363
0364    def whiler(self):
0365        self.tokr('$while ')
0366        self.lock()
0367        v = self.exprr()
0368        self.tokr(":")
0369        l = self.lines()
0370
0371        if self.tokq(self.curws + self.curind + '$else:'):
0372            elsec = self.lines()
0373        else:
0374            elsec = None
0375
0376        return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec)
0377
0378    def assignr(self):
0379        self.tokr('$ ')
0380        assign = self.rer(r_var) # NOTE: setable
0381        self.tokr(' = ')
0382        expr = self.exprr()
0383        self.tokr(' ')
0384
0385        return self.o('assign', 'name', assign, 'expr', expr)
0386
0387    def commentr(self):
0388        self.tokr('$#')
0389        self.lock()
0390        while self.c() != ' ': pass
0391        return self.o('comment')
0392
0393    def setabler(self):
0394        out = [self.varr()] #@@ not quite right
0395        while self.tokq(', '):
0396             out.append(self.varr())
0397        return out
0398
0399    def lines(self, start=False):
0400        """
0401        This function gets called from two places:
0402          1. at the start, where it's matching the document itself
0403          2. after any command, where it matches one line or an indented block
0404        """
0405        o = []
0406        if not start: # try to match just one line
0407            singleline = self.tokq(' ') and self.lineq()
0408            if singleline:
0409                return [singleline]
0410            else:
0411                self.rer(' *') #@@slurp space?
0412                self.tokr(' ')
0413                oldind = self.curind
0414                self.curind += '    '
0415        while 1:
0416            oldws = self.curws
0417            t = self.tokq(oldws + self.curind)
0418            if not t: break
0419
0420            self.curws += self.ws()
0421            x = t and (
0422              self.varsetq() or
0423              self.ifq() or
0424              self.forq() or
0425              self.whileq() or
0426              self.assignq() or
0427              self.commentq() or
0428              self.lineq())
0429            self.curws = oldws
0430            if not x:
0431                break
0432            elif x[WHAT] == 'comment':
0433                pass
0434            else:
0435                o.append(x)
0436
0437        if not start: self.curind = oldind
0438        return o
0439
0440class Stowage(storage):
0441    def __str__(self): return self.get('_str')
0442    #@@ edits in place
0443    def __add__(self, other):
0444        if isinstance(other, (unicode, str)):
0445            self._str += other
0446            return self
0447        else:
0448            raise TypeError, 'cannot add'
0449    def __radd__(self, other):
0450        if isinstance(other, (unicode, str)):
0451            self._str = other + self._str
0452            return self
0453        else:
0454            raise TypeError, 'cannot add'
0455
0456class WTF(AssertionError): pass
0457class SecurityError(Exception):
0458    """The template seems to be trying to do something naughty."""
0459    pass
0460
0461Required = object()
0462class Template:
0463    globals = {}
0464    def __init__(self, text, filter=None):
0465        self.filter = filter
0466        # universal newlines:
0467        text = text.replace(' ', ' ').replace(' ', ' ')
0468        if not text.endswith(' '): text += ' '
0469        header, tree = TemplateParser(text).go()
0470        self.tree = tree
0471        if header:
0472            self.h_defwith(header)
0473        else:
0474            self.args, self.kwargs = (), {}
0475
0476    def __call__(self, *a, **kw):
0477        d = self.globals.copy()
0478        d.update(self._parseargs(a, kw))
0479        f = Fill(self.tree, d=d)
0480        if self.filter: f.filter = self.filter
0481        return f.go()
0482
0483    def _parseargs(self, inargs, inkwargs):
0484        # difference from Python:
0485        #   no error on setting a keyword arg twice
0486        d = {}
0487        for arg in self.args:
0488            d[arg] = Required
0489        for kw, val in self.kwargs:
0490            d[kw] = val
0491
0492        for n, val in enumerate(inargs):
0493            if n < len(self.args):
0494                d[self.args[n]] = val
0495            elif n < len(self.args)+len(self.kwargs):
0496                kw = self.kwargs[n - len(self.args)][0]
0497                d[kw] = val
0498
0499        for kw, val in inkwargs.iteritems():
0500            d[kw] = val
0501
0502        unset = []
0503        for k, v in d.iteritems():
0504            if v is Required:
0505                unset.append(k)
0506        if unset:
0507            raise TypeError, 'values for %s are required' % unset
0508
0509        return d
0510
0511    def h_defwith(self, header):
0512        assert header[WHAT] == 'defwith'
0513        f = Fill(self.tree, d={})
0514
0515        self.args = header[ARGS]
0516        self.kwargs = []
0517        for var, valexpr in header[KWARGS]:
0518            self.kwargs.append((var, f.h(valexpr)))
0519
0520class Handle:
0521    def __init__(self, parsetree, **kw):
0522        self._funccache = {}
0523        self.parsetree = parsetree
0524        for (k, v) in kw.iteritems(): setattr(self, k, v)
0525
0526    def h(self, item):
0527        return getattr(self, 'h_' + item[WHAT])(item)
0528
0529class Fill(Handle):
0530    builtins = global_globals
0531    def filter(self, text):
0532        if text is None: return ''
0533        else: return str(text)
0534        # later: can do stuff like WebSafe
0535
0536    def h_literal(self, i):
0537        item = i[THING]
0538        if isinstance(item, str) and item[0] in ['"', "'"]:
0539            item = item[1:-1]
0540        elif isinstance(item, (float, int)):
0541            pass
0542        return item
0543
0544    def h_list(self, i):
0545        x = i[THING]
0546        out = []
0547        for item in x:
0548            out.append(self.h(item))
0549        return out
0550
0551    def h_dict(self, i):
0552        x = i[THING]
0553        out = {}
0554        for k, v in x.iteritems():
0555            out[self.h(k)] = self.h(v)
0556        return out
0557
0558    def h_paren(self, i):
0559        item = i[THING]
0560        if isinstance(item, list):
0561            raise NotImplementedError, 'tuples'
0562        return self.h(item)
0563
0564    def h_getattr(self, i):
0565        thing, attr = i[THING], i[ATTR]
0566        thing = self.h(thing)
0567        if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'):
0568            raise SecurityError, 'tried to get ' + attr
0569        try:
0570            if thing in self.builtins:
0571                raise SecurityError, 'tried to getattr on ' + repr(thing)
0572        except TypeError:
0573            pass # raised when testing an unhashable object
0574        try:
0575            return getattr(thing, attr)
0576        except AttributeError:
0577            if isinstance(thing, list) and attr == 'join':
0578                return lambda s: s.join(thing)
0579            else:
0580                raise
0581
0582    def h_call(self, i):
0583        call = self.h(i[THING])
0584        args = [self.h(x) for x in i[ARGS]]
0585        kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]])
0586        return call(*args, **kw)
0587
0588    def h_getitem(self, i):
0589        thing, item = i[THING], i[ITEM]
0590        thing = self.h(thing)
0591        item = self.h(item)
0592        return thing[item]
0593
0594    def h_expr(self, i):
0595        item = self.h(i[THING])
0596        if i[NEGATE]:
0597            item = not item
0598        return item
0599
0600    def h_test(self, item):
0601        ox, op, oy = item[X], item[OP], item[Y]
0602        # for short-circuiting to work, we can't eval these here
0603        e = self.h
0604        if op == 'is':
0605            return e(ox) is e(oy)
0606        elif op == 'is not':
0607            return e(ox) is not e(oy)
0608        elif op == 'in':
0609            return e(ox) in e(oy)
0610        elif op == 'not in':
0611            return e(ox) not in e(oy)
0612        elif op == '==':
0613            return e(ox) == e(oy)
0614        elif op == '!=':
0615            return e(ox) != e(oy)
0616        elif op == '>':
0617            return e(ox) > e(oy)
0618        elif op == '<':
0619            return e(ox) < e(oy)
0620        elif op == '<=':
0621            return e(ox) <= e(oy)
0622        elif op == '>=':
0623            return e(ox) >= e(oy)
0624        elif op == 'and':
0625            return e(ox) and e(oy)
0626        elif op == 'or':
0627            return e(ox) or e(oy)
0628        elif op == '+':
0629            return e(ox) + e(oy)
0630        elif op == '-':
0631            return e(ox) - e(oy)
0632        elif op == '*':
0633            return e(ox) * e(oy)
0634        elif op == '/':
0635            return e(ox) / e(oy)
0636        elif op == '%':
0637            return e(ox) % e(oy)
0638        else:
0639            raise WTF, 'op ' + op
0640
0641    def h_var(self, i):
0642        v = i[NAME]
0643        if v in self.d:
0644            return self.d[v]
0645        elif v in self.builtins:
0646            return self.builtins[v]
0647        elif v == 'self':
0648            return self.output
0649        else:
0650            raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO])
0651
0652    def h_line(self, i):
0653        out = []
0654        for x in i[THING]:
0655            if isinstance(x, str):
0656                out.append(x)
0657            elif x[WHAT] == 'itpl':
0658                o = self.h(x[NAME])
0659                if x[FILTER]:
0660                    o = self.filter(o)
0661                else:
0662                    if isinstance(o, Stowage):
0663                        o = o._str
0664                out.append(o)
0665            else:
0666                raise WTF, x
0667        return ''.join(out)
0668
0669    def h_varset(self, i):
0670        self.output[i[NAME]] = ''.join(self.h_lines(i[BODY]))
0671        return ''
0672
0673    def h_if(self, i):
0674        expr = self.h(i[CLAUSE])
0675        if expr:
0676            do = i[BODY]
0677        else:
0678            for e in i[ELIF]:
0679                expr = self.h(e[CLAUSE])
0680                if expr:
0681                    do = e[BODY]
0682                    break
0683            else:
0684                do = i[ELSE]
0685        return ''.join(self.h_lines(do))
0686
0687    def h_for(self, i):
0688        out = []
0689        assert i[IN][WHAT] == 'expr'
0690        invar = self.h(i[IN])
0691        forvar = i[NAME]
0692        if invar:
0693            for nv in invar:
0694                if len(forvar) == 1:
0695                    fv = forvar[0]
0696                    assert fv[WHAT] == 'var'
0697                    self.d[fv[NAME]] = nv # same (lack of) scoping as Python
0698                else:
0699                    for x, y in zip(forvar, nv):
0700                        assert x[WHAT] == 'var'
0701                        self.d[x[NAME]] = y
0702
0703                out.extend(self.h_lines(i[BODY]))
0704        else:
0705            if i[ELSE]:
0706                out.extend(self.h_lines(i[ELSE]))
0707        return ''.join(out)
0708
0709    def h_while(self, i):
0710        out = []
0711        expr = self.h(i[CLAUSE])
0712        if not expr:
0713            return ''.join(self.h_lines(i[ELSE]))
0714        c = 0
0715        while expr:
0716            c += 1
0717            if c >= MAX_ITERS:
0718                raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO]
0719            out.extend(self.h_lines(i[BODY]))
0720            expr = self.h(i[CLAUSE])
0721        return ''.join(out)
0722
0723    def h_assign(self, i):
0724        self.d[i[NAME]] = self.h(i[EXPR])
0725        return ''
0726
0727    def h_comment(self, i): pass
0728
0729    def h_lines(self, lines):
0730        if lines is None: return []
0731        return map(self.h, lines)
0732
0733    def go(self):
0734        self.output = Stowage()
0735        self.output._str = ''.join(map(self.h, self.parsetree))
0736        if self.output.keys() == ['_str']:
0737            self.output = self.output['_str']
0738        return self.output
0739
0740class render:
0741    def __init__(self, loc='templates/', cache=True):
0742        self.loc = loc
0743        if cache:
0744            self.cache = {}
0745        else:
0746            self.cache = False
0747
0748    def _do(self, name, filter=None):
0749        if self.cache is False or name not in self.cache:
0750            p = glob.glob(self.loc + name + '.*')
0751            if not p and os.path.isdir(self.loc + name):
0752                return render(self.loc + name + '/', cache=self.cache)
0753            elif not p:
0754                raise AttributeError, 'no template named ' + name
0755            p = p[0]
0756            c = Template(open(p).read())
0757            if self.cache is not False: self.cache[name] = (p, c)
0758
0759        if self.cache is not False: p, c = self.cache[name]
0760
0761        if p.endswith('.html'):
0762            import webapi as web
0763            if 'headers' in web.ctx:
0764                web.header('Content-Type', 'text/html; charset=utf-8', unique=True)
0765            if not filter: c.filter = websafe
0766        elif p.endswith('.xml'):
0767            if not filter: c.filter = websafe
0768
0769        return c
0770
0771    def __getattr__(self, p):
0772        return self._do(p)
0773
0774def frender(fn, *a, **kw):
0775    return Template(open(fn).read(), *a, **kw)
0776
0777def test():
0778    import sys
0779    verbose = '-v' in sys.argv
0780    def assertEqual(a, b):
0781        if a == b:
0782            if verbose:
0783                sys.stderr.write('.')
0784                sys.stderr.flush()
0785        else:
0786            assert a == b, " expected: %s got: %s" % (repr(a), repr(b))
0787
0788    from utils import storage, group
0789    t = Template
0790
0791    tests = [
0792        lambda: t('1')(),                                                   '1 ',
0793        lambda: t('$def with () 1')(),                                     '1 ',
0794        lambda: t('$def with (a) $a')(1),                                  '1 ',
0795        lambda: t('$def with (a=0) $a')(1),                                '1 ',
0796        lambda: t('$def with (a=0) $a')(a=1),                              '1 ',
0797        lambda: t('$if 1: 1')(),                                            '1 ',
0798        lambda: t('$if 1:     1')(),                                       '1 ',
0799        lambda: t('$if 0: 0 $elif 1: 1')(),                                '1 ',
0800        lambda: t('$if 0: 0 $elif None: 0 $else: 1')(),                   '1 ',
0801        lambda: t('$if (0 < 1) and (1 < 2): 1')(),                          '1 ',
0802        lambda: t('$for x in [1, 2, 3]: $x')(),                             '1 2 3 ',
0803        lambda: t('$for x in []: 0 $else: 1')(),                           '1 ',
0804        lambda: t('$def with (a) $while a and a.pop(): 1')([1, 2, 3]),     '1 1 1 ',
0805        lambda: t('$while 0: 0 $else: 1')(),                               '1 ',
0806        lambda: t('$ a = 1 $a')(),                                         '1 ',
0807        lambda: t('$# 0')(),                                                '',
0808        lambda: t('$def with (d) $for k, v in d.iteritems(): $k')({1: 1}), '1 ',
0809        lambda: t('$def with (a) $(a)')(1),                                '1 ',
0810        lambda: t('$def with (a) $a')(1),                                  '1 ',
0811        lambda: t('$def with (a) $a.b')(storage(b=1)),                     '1 ',
0812        lambda: t('$def with (a) $a[0]')([1]),                             '1 ',
0813        lambda: t('${0 or 1}')(),                                           '1 ',
0814        lambda: t('$ a = [1] $a[0]')(),                                    '1 ',
0815        lambda: t('$ a = {1: 1} $a.keys()[0]')(),                          '1 ',
0816        lambda: t('$ a = [] $if not a: 1')(),                              '1 ',
0817        lambda: t('$ a = {} $if not a: 1')(),                              '1 ',
0818        lambda: t('$ a = -1 $a')(),                                        '-1 ',
0819        lambda: t('$ a = "1" $a')(),                                       '1 ',
0820        lambda: t('$if 1 is 1: 1')(),                                       '1 ',
0821        lambda: t('$if not 0: 1')(),                                        '1 ',
0822        lambda: t('$if 1:     $if 1: 1')(),                                '1 ',
0823        lambda: t('$ a = 1 $a')(),                                         '1 ',
0824        lambda: t('$ a = 1. $a')(),                                        '1.0 ',
0825        lambda: t('$({1: 1}.keys()[0])')(),                                 '1 ',
0826    ]
0827
0828    for func, value in group(tests, 2):
0829        assertEqual(func(), value)
0830
0831    j = Template("$var foo: bar")()
0832    assertEqual(str(j), '')
0833    assertEqual(j.foo, 'bar ')
0834    if verbose: sys.stderr.write(' ')
0835
0836
0837if __name__ == "__main__":
0838    test()