diff --git a/README.md b/README.md index b5b7fef..40e49fe 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,25 @@ # Stache +NOTE see hard-fork https://github.com/clach04/stache (and issue tracker there). I may not continue updating this repo. + Trimmed mustache logic-less templates +Fork of https://github.com/hyperturtle/Stache with: + + * support for Cpython 2.2 up to and including Python 3.x + * Also works with Jython 2.x. + * Fix for trailing blank newlines (https://github.com/hyperturtle/Stache/issues/2) + * regular Python unittest based test suite + +Also see https://github.com/SmithSamuelM/staching which includes a fix for https://github.com/hyperturtle/Stache/issues/2 but doesn't support older Python versions, use if older Python support is not required. + Implements everything from [Mustache.5](http://mustache.github.com/mustache.5.html) **except for lambdas** in < 200 lines of code. Plus four new things. Implied closing tags `{{/}}`, Self referencer `{{.}}`, Existence check `{{?exists}}{{/exists}}` and data pusher `{{< blah}}{{/blah}}`, `{{:default}}` + # Also, the ability to compile to javascript code! ## render_js(template_string) @@ -220,9 +232,17 @@ Custom Footer pip install stache +Optionally install Nose for running nosetests, there is a regular unittest that does not require nose). + + pip install nose + # Test -You can run `python test.py` or if you have nosetests: +Pure python tests can be ran with: + + python testsuite.py + +The Python and javascript tests can be ran with `python test.py` or if you have nosetests: cd stache nosetests @@ -280,4 +300,4 @@ The main reason I liked Mustache in the first place is because of possibility of template reuse. Some future ideas I have is rendering to javascript templates to be used on browser -frontend, bypassing the need for a client side script to compile it into javascript \ No newline at end of file +frontend, bypassing the need for a client side script to compile it into javascript diff --git a/__init__.py b/__init__.py index 55ff995..e2326b7 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,28 @@ -import itertools -from cgi import escape +from __future__ import generators +import sys +try: + # py 3.8+ + from html import escape +except ImportError: + # py2 + from cgi import escape + +try: + raise ImportError + import itertools + itertools_takewhile = itertools.takewhile +except ImportError: + # fake it + + def takewhile(predicate, iterable): + # takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4 + for x in iterable: + if predicate(x): + yield x + else: + break + + itertools_takewhile = takewhile try: @@ -7,6 +30,14 @@ except ImportError: pass +py_ver = sys.version[:3] +py_v3 = py_ver >= '3.0' + +if py_v3: + string_func = str +else: + string_func = unicode + TOKEN_RAW = intern('raw') TOKEN_TAGOPEN = intern('tagopen') TOKEN_TAGINVERT = intern('taginvert') @@ -100,13 +131,16 @@ def _checkprefix(tag, prefix): - return tag[1:].strip() if tag and tag[0] == prefix else None + if tag and tag[0] == prefix: + return tag[1:].strip() + else: + return None def _lookup(data, datum): for scope in data: if datum == '.': - return str(scope) + return string_func(scope) elif datum in scope: return scope[datum] elif hasattr(scope, datum): @@ -221,23 +255,127 @@ def _tokenize(self, template): while rest and len(rest) > 0: pre_section = rest.split(self.otag, 1) - pre, rest = pre_section if len(pre_section) == 2 else (pre_section[0], None) - taglabel, rest = rest.split(self.ctag, 1) if rest else (None, None) - taglabel = taglabel.strip() if taglabel else '' + + + if len(pre_section) == 2: + pre, rest = pre_section + else: + pre, rest = (pre_section[0], None) + if rest: + taglabel, rest = rest.split(self.ctag, 1) + else: + taglabel, rest = (None, None) + + if taglabel: + taglabel = taglabel.strip() + else: + taglabel = '' open_tag = _checkprefix(taglabel, '#') - invert_tag = _checkprefix(taglabel, '^') if not open_tag else None - close_tag = _checkprefix(taglabel, '/') if not invert_tag else None - comment_tag = _checkprefix(taglabel, '!') if not close_tag else None - partial_tag = _checkprefix(taglabel, '>') if not comment_tag else None - push_tag = _checkprefix(taglabel, '<') if not partial_tag else None - bool_tag = _checkprefix(taglabel, '?') if not push_tag else None - booltern_tag = _checkprefix(taglabel, ':') if not bool_tag else None - unescape_tag = _checkprefix(taglabel, '{') if not booltern_tag else None - rest = rest[1:] if unescape_tag else rest - unescape_tag = (unescape_tag or _checkprefix(taglabel, '&')) if not booltern_tag else None - delim_tag = taglabel[1:-1] if not unescape_tag and len(taglabel) >= 2 and taglabel[0] == '=' and taglabel[-1] == '=' else None - delim_tag = delim_tag.split(' ', 1) if delim_tag else None - delim_tag = delim_tag if delim_tag and len(delim_tag) == 2 else None + if not open_tag: + invert_tag = _checkprefix(taglabel, '^') + else: + invert_tag = None + if not invert_tag: + close_tag = _checkprefix(taglabel, '/') + else: + close_tag = None + comment_tag = None + partial_tag = None + push_tag = None + bool_tag = None + booltern_tag = None + unescape_tag = None + + if not close_tag: + comment_tag = _checkprefix(taglabel, '!') + if not comment_tag: + partial_tag = _checkprefix(taglabel, '>') + if not partial_tag: + push_tag = _checkprefix(taglabel, '<') + if not push_tag: + bool_tag = _checkprefix(taglabel, '?') + if not bool_tag: + booltern_tag = _checkprefix(taglabel, ':') + if not booltern_tag: + unescape_tag = _checkprefix(taglabel, '{') + + if unescape_tag: + rest = rest[1:] + else: + rest = rest # FIXME seems like a NOOP + + if not booltern_tag: + unescape_tag = (unescape_tag or _checkprefix(taglabel, '&')) + else: + unescape_tag = None + if not unescape_tag and len(taglabel) >= 2 and taglabel[0] == '=' and taglabel[-1] == '=': + delim_tag = taglabel[1:-1] + else: + delim_tag = None + if delim_tag: + delim_tag = delim_tag.split(' ', 1) + else: + delim_tag = None + + if delim_tag and len(delim_tag) == 2: + delim_tag = delim_tag + else: + delim_tag = None + + # fix for https://github.com/hyperturtle/Stache/issues/2 from https://github.com/SmithSamuelM/staching/commit/f2c591ec69cc922c6ffec67e0d66f8047f2f2bf3 + if ( open_tag or invert_tag or comment_tag or + partial_tag or push_tag or bool_tag or + booltern_tag or unescape_tag or delim_tag): # not a variable + inline = False + if rest: # strip trailing whitespace and linefeed if present + front, sep, back = rest.partition("\n") # partition at linefeed + if sep: + if not front.strip(): # only whitespace before linefeed + rest = back # removed whitespace and linefeed + #if _debug: print( "open rest strip front: \n%s" % rest) + else: #inline + inline = True + #if _debug: print( "open inline:") + if not inline and pre: #strip trailing whitespace after linefeed if present + front, sep, back = pre.rpartition("\n") + if sep: + if not back.strip(): # only whitespace after linefeed + pre = ''.join((front, sep)) # restore linefeed + #if _debug: print( "open pre strip back: \n%s" % pre) + else: + pre = back.rstrip() #no linefeed so rstrip + #if _debug: print( "open pre rstrip back: \n%s" % pre) + + elif close_tag: + inline = True # section is inline + follow = False # followed by inline + post = '' + + if rest: # see if inline follows + front, sep, back = rest.partition("\n") + if front.strip(): # not empty before linefeed so inline follows + follow = True # inline follows + #if _debug: print( "close follow:") + + if pre: #strip trailing whitespace after prev linefeed if present + front, sep, back = pre.rpartition("\n") + if sep and not back.strip(): # only whitespace after linefeed + inline = False + #if _debug: print() "close not inline:" ) + if follow: + post = back # save spacing for following inline + pre = ''.join((front, sep)) # restore upto linefeed + #if _debug: print( "close pre strip back: \n%s" % pre) + + if not inline and rest: # strip trailing whitespace and linefeed if present + if follow: # restore saved spacing + rest = post + rest + #print( "close follow rest: \n%s" % rest) + front, sep, back = rest.partition("\n") # partition at linefeed + if sep: + if not front.strip(): # only whitespace before linefeed + rest = back # remove trailing whitespace and linefeed + #if _debug: print( "close rest strip front: \n%s" % rest) if push_tag: pre = pre.rstrip() @@ -258,7 +396,7 @@ def _tokenize(self, template): elif close_tag is not None: current_scope = scope.pop() if close_tag: - assert (current_scope == close_tag), 'Mismatch open/close blocks' + assert (current_scope == close_tag), 'Mismatch open/close blocks, %r != %r' % (current_scope, close_tag) # TODO replace with a check, assertions can be optimized out yield TOKEN_TAGCLOSE, current_scope, len(scope)+1 elif booltern_tag: scope.append(booltern_tag) @@ -280,10 +418,10 @@ def _tokenize(self, template): def _parse(self, tokens, *data): for token in tokens: - #print ' token:' + str(token) + #print ' token:' + string_func(token) tag, content, scope = token if tag == TOKEN_RAW: - yield str(content) + yield string_func(content) elif tag == TOKEN_TAG: tagvalue = _lookup(data, content) #cant use if tagvalue because we need to render tagvalue if it's 0 @@ -292,17 +430,17 @@ def _parse(self, tokens, *data): try: if len(tagvalue) > 0: if scope: - yield str(tagvalue) + yield string_func(tagvalue) else: - yield escape(str(tagvalue)) + yield escape(string_func(tagvalue)) except TypeError: if scope: - yield str(tagvalue) + yield string_func(tagvalue) else: - yield escape(str(tagvalue)) + yield escape(string_func(tagvalue)) elif tag == TOKEN_TAGOPEN or tag == TOKEN_TAGINVERT: tagvalue = _lookup(data, content) - untilclose = itertools.takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) + untilclose = itertools_takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) if (tag == TOKEN_TAGOPEN and tagvalue) or (tag == TOKEN_TAGINVERT and not tagvalue): if hasattr(tagvalue, 'items'): #print ' its a dict!', tagvalue, untilclose @@ -330,7 +468,7 @@ def _parse(self, tokens, *data): pass elif tag == TOKEN_BOOL: tagvalue = _lookup(data, content) - untilclose = itertools.takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) + untilclose = itertools_takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) if tagvalue: for part in self._parse(untilclose, *data): yield part @@ -342,7 +480,7 @@ def _parse(self, tokens, *data): for part in self._parse(iter(list(self.templates[content])), *data): yield part elif tag == TOKEN_PUSH: - untilclose = itertools.takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) + untilclose = itertools_takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) data[-1][content] = ''.join(self._parse(untilclose, *data)) elif tag == TOKEN_TAGDELIM: self.otag, self.ctag = content @@ -353,7 +491,7 @@ def _jsparse(self, tokens): for token in tokens: tag, content, scope = token if tag == TOKEN_RAW: - yield "'{0}'".format(str(content)) + yield "'{0}'".format(string_func(content)) elif tag == TOKEN_TAG: if content != '': if scope: @@ -361,7 +499,7 @@ def _jsparse(self, tokens): else: yield "htmlEncode(lookup(data, '{0}'))".format(content) elif tag == TOKEN_TAGOPEN or tag == TOKEN_TAGINVERT or tag == TOKEN_BOOL: - untilclose = itertools.takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) + untilclose = itertools_takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) inside = self._jsparse(untilclose) if tag == TOKEN_TAGOPEN: pre = "return section(data, lookup(data, tag), function (data) {" @@ -381,7 +519,7 @@ def _jsparse(self, tokens): elif tag == TOKEN_PARTIAL: yield "templates['{0}'](data)".format(content) elif tag == TOKEN_PUSH: - untilclose = itertools.takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) + untilclose = itertools_takewhile(lambda x: x != (TOKEN_TAGCLOSE, content, scope), tokens) self.hoist_data[content] = _renderjsfunction(self._jsparse(untilclose), params="data") elif tag == TOKEN_TAGDELIM: self.otag, self.ctag = content diff --git a/test.py b/test.py index f0770b3..b8abc6d 100644 --- a/test.py +++ b/test.py @@ -1,54 +1,105 @@ +from __future__ import generators +import sys +import warnings try: - from __init__ import Stache, render, render_js + import timeit except ImportError: - from . import Stache, render, render_js + # TODO fake it? + timeit = None +try: + import subprocess +except ImportError: + subprocess = None + +try: + import json +except ImportError: + # TODO import simplejson, etc. + json = None + +from stache import Stache, render, render_js + -import timeit -import subprocess +skip_bool = False +if sys.version_info < (2, 3): + skip_bool = True + +def mydict(*args, **kwargs): + """Emulate Python 2.3 dict keyword support + """ + if args: + raise NotImplementedError('args not supported') + if kwargs: + return kwargs + else: + return {} def verify(output, template, data): print("%s with %s" % (template, data)) result = render(template, data) result_iter = ''.join(Stache().render_iter(template, data)) + print("Output: %s\n" % output) print("Result: %s\n" % result) assert result == output assert result == result_iter def verify_js(output, template, data): - import json + if json is None: + warnings.warn('json module missing, skipping this js test') + return script = render_js(template) print("%s with %s" % (template, data)) result = subprocess.check_output(["node", "-e", "console.log({0}({1}))".format(script, json.dumps(data))]).strip() print("Result: %s\n" % result) assert output.lower() == result -def verify_partial(stachio, output, template, data={}): - print("%s with %s" % (template, data)) - result = stachio.render_template(template, data) - print("Result: %s\n" % result) - assert output == result - def verify_js_partial(stachio, output, template, data={}): - import json + if json is None: + warnings.warn('json module missing, skipping this js test') + return print("%s with %s" % (template, data)) script = stachio.render_js_template(template) result = subprocess.check_output(["node", "-e", "console.log({0}({1}))".format(script, json.dumps(data))]).strip() print("Result: %s\n" % result) assert output.lower() == result +def bare_js_partial(stachio, output, template, data={}): + if json is None: + warnings.warn('json module missing, skipping this js test') + return + return stachio.render_js_template(template) + +def verify_partial(stachio, output, template, data={}): + print("%s with %s" % (template, data)) + result = stachio.render_template(template, data) + print("Result: %s\n" % result) + assert output == result + def bench(output, template, data): + if timeit is None: + warnings.warn('timeit module missing, skipping this test') + return t = timeit.Timer("render('%s', %s)" % (template, data), "from __main__ import render") print("%.2f\tusec/test > python %s with %s" % (1000000 * t.timeit(number=10000)/10000, template, data)) def bench_js(output, template, data): + if timeit is None: + warnings.warn('timeit module missing, skipping this test') + return t = timeit.Timer("render_js('%s')" % (template), "from __main__ import render_js") print("%.2f\tusec/test > js %s with %s" % (1000000 * t.timeit(number=10000)/10000, template, data)) def bench_partial(stachio, output, template, data={}): + if timeit is None: + warnings.warn('timeit module missing, skipping this test') + return t = timeit.Timer("s.render_template('%s', %s)" % (template, data), "from __main__ import test_partials, s") print("%.2f\tusec/test > python partial %s with %s" % (1000000 * t.timeit(number=10000)/10000, template, data)) def bench_js_partial(stachio, output, template, data={}): + if timeit is None: + warnings.warn('timeit module missing, skipping this test') + return t = timeit.Timer("s.render_template('%s', %s)" % (template, data), "from __main__ import test_partials, s") print("%.2f\tusec/test > js_partial %s with %s" % (1000000 * t.timeit(number=10000)/10000, template, data)) @@ -61,64 +112,67 @@ def bare_js(output, template, data): def bare_partial(stachio, output, template, data={}): return stachio.render_template(template, data) -def bare_js_partial(stachio, output, template, data={}): - return stachio.render_js_template(template) - def test(method=bare): # test tag lookup - yield method, 'a10c', 'a{{b}}c', dict(b=10) - yield method, 'a10c', 'a{{b}}c', dict(b=10) - yield method, 'ac', 'a{{b}}c', dict(c=10) - yield method, 'a10c', 'a{{b}}c', dict(b='10') - yield method, 'acde', 'a{{!b}}cde', dict(b='10') - yield method, 'aTrue', 'a{{b}}', dict(b=True) - yield method, 'a123', 'a{{b}}{{c}}{{d}}', dict(b=1,c=2,d=3) + yield method, 'a10c', 'a{{b}}c', mydict(b=10) + yield method, 'a10c', 'a{{b}}c', mydict(b=10) + yield method, 'ac', 'a{{b}}c', mydict(c=10) + yield method, 'a10c', 'a{{b}}c', mydict(b='10') + yield method, 'acde', 'a{{!b}}cde', mydict(b='10') + if not skip_bool: + yield method, 'aTrue', 'a{{b}}', mydict(b=True) + yield method, 'a123', 'a{{b}}{{c}}{{d}}', mydict(b=1,c=2,d=3) # test falsy #sections - yield method, 'ab', 'a{{#b}}b{{/b}}', dict(b=True) - yield method, 'a', 'a{{^b}}b{{/b}}', dict(b=True) - yield method, 'a', 'a{{#b}}b{{/}}', dict(b=False) - yield method, 'ab', 'a{{^b}}b{{/b}}', dict(b=False) + if not skip_bool: + yield method, 'ab', 'a{{#b}}b{{/b}}', mydict(b=True) + yield method, 'a', 'a{{^b}}b{{/b}}', mydict(b=True) + yield method, 'a', 'a{{#b}}b{{/}}', mydict(b=False) + yield method, 'ab', 'a{{^b}}b{{/b}}', mydict(b=False) #test invert sections - yield method, 'ab', 'a{{#b}}ignore me{{/b}}{{^b}}b{{/}}', dict(b=[]) - yield method, 'ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', dict(b=[1]) - yield method, 'ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', dict(b=True) + yield method, 'ab', 'a{{#b}}ignore me{{/b}}{{^b}}b{{/}}', mydict(b=[]) + yield method, 'ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', mydict(b=[1]) + if not skip_bool: + yield method, 'ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', mydict(b=True) #test ?sections - yield method, 'a- 1 2 3 4', 'a{{?b}}-{{#b}} {{.}}{{/}}{{/}}', dict(b=[1,2,3,4]) - yield method, 'a', 'a{{?b}}ignoreme{{/}}', dict(b=False) - yield method, 'a', 'a{{?b}}ignoreme{{/}}', dict(b=[]) - yield method, 'a', 'a{{?b}}ignoreme{{/}}', dict() - yield method, 'abb', 'a{{?b}}b{{/}}{{?b}}b{{/}}', dict(b=[1,2,3]) - yield method, 'ab123d', 'a{{?b}}b{{#b}}{{.}}{{/}}d{{/}}', dict(b=[1,2,3]) + yield method, 'a- 1 2 3 4', 'a{{?b}}-{{#b}} {{.}}{{/}}{{/}}', mydict(b=[1,2,3,4]) + if not skip_bool: + yield method, 'a', 'a{{?b}}ignoreme{{/}}', mydict(b=False) + yield method, 'a', 'a{{?b}}ignoreme{{/}}', mydict(b=[]) + yield method, 'a', 'a{{?b}}ignoreme{{/}}', mydict() + yield method, 'abb', 'a{{?b}}b{{/}}{{?b}}b{{/}}', mydict(b=[1,2,3]) + yield method, 'ab123d', 'a{{?b}}b{{#b}}{{.}}{{/}}d{{/}}', mydict(b=[1,2,3]) #test #section scope - yield method, 'abbbb', 'a{{#b}}b{{/b}}', dict(b=[1,2,3,4]) - yield method, 'a1234', 'a{{#b}}{{.}}{{/b}}', dict(b=[1,2,3,4]) - yield method, 'a a=1 a=2 a=0 a', 'a {{#b}}a={{a}} {{/b}}a', dict(a=0,b=[{'a':1},{'a':2},{'c':1}]) - yield method, '1', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', dict(a={'b':{'c':1}}) - yield method, '12', '{{#a}}{{#b}}{{c}}{{/}}{{/a}}', dict(a={'b':[{'c':1}, {'c':2}]}) - yield method, '12', '{{#a}}{{#b}}{{c}}{{/b}}{{/}}', dict(a=[{'b':{'c':1}},{'b':{'c':2}}]) - yield method, '132', '{{#a}}{{#b}}{{c}}{{/}}{{/}}', dict(a=[{'b':[{'c':1}, {'c':3}]},{'b':{'c':2}}]) - yield method, '132456', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', dict(a=[{'b':[{'c':1}, {'c':3}]},{'b':{'c':2}},{'b':[{'c':4}, {'c':5}]},{'b':{'c':6}}]) - yield method, '1', '{{#a}}{{#a}}{{c}}{{/a}}{{/a}}', dict(a={'a':{'c':1}}) - yield method, '<3><3><3>', '<{{id}}><{{# a? }}{{id}}{{/ a? }}><{{# b? }}{{id}}{{/ b? }}>', {'id':3,'a?':True, 'b?':True} + yield method, 'abbbb', 'a{{#b}}b{{/b}}', mydict(b=[1,2,3,4]) + yield method, 'a1234', 'a{{#b}}{{.}}{{/b}}', mydict(b=[1,2,3,4]) + yield method, 'a a=1 a=2 a=0 a', 'a {{#b}}a={{a}} {{/b}}a', mydict(a=0,b=[{'a':1},{'a':2},{'c':1}]) + yield method, '1', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', mydict(a={'b':{'c':1}}) + yield method, '12', '{{#a}}{{#b}}{{c}}{{/}}{{/a}}', mydict(a={'b':[{'c':1}, {'c':2}]}) + yield method, '12', '{{#a}}{{#b}}{{c}}{{/b}}{{/}}', mydict(a=[{'b':{'c':1}},{'b':{'c':2}}]) + yield method, '132', '{{#a}}{{#b}}{{c}}{{/}}{{/}}', mydict(a=[{'b':[{'c':1}, {'c':3}]},{'b':{'c':2}}]) + yield method, '132456', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', mydict(a=[{'b':[{'c':1}, {'c':3}]},{'b':{'c':2}},{'b':[{'c':4}, {'c':5}]},{'b':{'c':6}}]) + yield method, '1', '{{#a}}{{#a}}{{c}}{{/a}}{{/a}}', mydict(a={'a':{'c':1}}) + if not skip_bool: + yield method, '<3><3><3>', '<{{id}}><{{# a? }}{{id}}{{/ a? }}><{{# b? }}{{id}}{{/ b? }}>', {'id':3,'a?':True, 'b?':True} #test delim - yield method, 'delim{{a}}', '{{=<% %>=}}<%a%>{{a}}', dict(a='delim') - yield method, 'delim{{a}}delim<%a%>', '{{=<% %>=}}<%a%>{{a}}<%={{ }}=%>{{a}}<%a%>', dict(a='delim') + yield method, 'delim{{a}}', '{{=<% %>=}}<%a%>{{a}}', mydict(a='delim') + yield method, 'delim{{a}}delim<%a%>', '{{=<% %>=}}<%a%>{{a}}<%={{ }}=%>{{a}}<%a%>', mydict(a='delim') #test : - yield method, '123test', '123{{:hi}}abc{{/}}', dict(hi='test') - yield method, '123test', '123{{:hi}}{{:hi}}abc{{/}}{{/}}', dict(hi='test') - yield method, '123abc', '123{{:hi}}{{:hi2}}abc{{/}}{{/}}', dict() - yield method, '123cba', '123{{:hi}}{{:hi2}}abc{{/}}{{/}}', dict(hi2='cba') - yield method, '123abc', '123{{:hi}}abc{{/}}', dict() - yield method, '123abc', '123{{:hi}}abc{{/}}', dict(hi=False) - yield method, '123abc', '123{{:hi}}abc{{/}}', dict(hi=[]) - yield method, '123test', '{{<', '{{&a}}', dict(a='><') - yield method, '><', '{{{a}}}', dict(a='><') + yield method, '><', '{{a}}', mydict(a='><') + yield method, '><', '{{&a}}', mydict(a='><') + yield method, '><', '{{{a}}}', mydict(a='><') s = Stache() s.add_template('a', '1') @@ -138,23 +192,24 @@ def test(method=bare): s.add_template('o', 'a{{?b}}b{{#b}}{{.}}{{/}}d{{/}}') def test_partials(method=bare_partial): - yield method, s, '1', 'a', dict() - yield method, s, '1', 'b', dict() - yield method, s, '11', 'c', dict() - yield method, s, '', 'd', dict() - yield method, s, '555', 'd', dict(a={'b':555}) - yield method, s, '123', 'g', dict(a={}) - yield method, s, '555', 'e', dict(a={'b':555}) - yield method, s, '555', 'f', dict(a={'b':555}) - yield method, s, 'i=123', 'h', dict() - yield method, s, 'i=', 'i', dict() - yield method, s, 'showme', 'j', dict() - yield method, s, 'default', 'k', dict() - yield method, s, 'custom', 'k', dict(e="custom") - yield method, s, '<3><3><3>', 'l', {'id':3,'a':True, 'b':True} - yield method, s, '<3><3><3>', 'm', {'id':3,'a?':True, 'b?':True} - yield method, s, 'abb', 'n', dict(b=[1,2,3]) - yield method, s, 'ab123d', 'o', dict(b=[1,2,3]) + yield method, s, '1', 'a', mydict() + yield method, s, '1', 'b', mydict() + yield method, s, '11', 'c', mydict() + yield method, s, '', 'd', mydict() + yield method, s, '555', 'd', mydict(a={'b':555}) + yield method, s, '123', 'g', mydict(a={}) + yield method, s, '555', 'e', mydict(a={'b':555}) + yield method, s, '555', 'f', mydict(a={'b':555}) + yield method, s, 'i=123', 'h', mydict() + yield method, s, 'i=', 'i', mydict() + yield method, s, 'showme', 'j', mydict() + yield method, s, 'default', 'k', mydict() + yield method, s, 'custom', 'k', mydict(e="custom") + if not skip_bool: + yield method, s, '<3><3><3>', 'l', {'id':3,'a':True, 'b':True} + yield method, s, '<3><3><3>', 'm', {'id':3,'a?':True, 'b?':True} + yield method, s, 'abb', 'n', mydict(b=[1,2,3]) + yield method, s, 'ab123d', 'o', mydict(b=[1,2,3]) def test_js(method=verify_js): return @@ -195,10 +250,13 @@ def test_js_all(): test_js_all() print('starting individual benchmarks') run(bench, bench_partial, bench_js, bench_js_partial) - print('starting combined python benchmark') - t = timeit.Timer("run(method_js=null, method_js_partial=null)", "from __main__ import run, bare, null") - print("%.2f\tusec/all tests" % (1000000 * t.timeit(number=10000)/10000)) - print('starting combined js benchmark') - t = timeit.Timer("run(method=null, method_partial=null)", "from __main__ import run, bare, null") - print("%.2f\tusec/all js tests" % (1000000 * t.timeit(number=10000)/10000)) + if timeit is None: + warnings.warn('timeit module missing, skipping this test') + else: + print('starting combined python benchmark') + t = timeit.Timer("run(method_js=null, method_js_partial=null)", "from __main__ import run, bare, null") + print("%.2f\tusec/all tests" % (1000000 * t.timeit(number=10000)/10000)) + print('starting combined js benchmark') + t = timeit.Timer("run(method=null, method_partial=null)", "from __main__ import run, bare, null") + print("%.2f\tusec/all js tests" % (1000000 * t.timeit(number=10000)/10000)) diff --git a/testsuite.py b/testsuite.py new file mode 100644 index 0000000..763204c --- /dev/null +++ b/testsuite.py @@ -0,0 +1,343 @@ +import os +import string +import sys +try: + import unittest2 as unittest +except ImportError: + import unittest + + +import stache +from stache import render, Stache + + +# start generated code +class BaseTest(unittest.TestCase): + def stache_verify(self, output, template, data): + #print("%s with %s" % (template, data)) + result = render(template, data) + result_iter = ''.join(Stache().render_iter(template, data)) + #print("Output: %s\n" % output) + #print("Result: %s\n" % result) + #assert result == output + self.assertEqual(result, output) + #assert result == result_iter + self.assertEqual(result, result_iter) + + +class verify(BaseTest): + + # test tag lookup + def test_verify_01(self): + self.stache_verify('a10c', 'a{{b}}c', {'b': 10}) + + + def test_verify_02(self): + self.stache_verify('a10c', 'a{{b}}c', {'b': 10}) + + + def test_verify_03(self): + self.stache_verify('ac', 'a{{b}}c', {'c': 10}) + + + def test_verify_04(self): + self.stache_verify('a10c', 'a{{b}}c', {'b': '10'}) + + + def test_verify_05(self): + self.stache_verify('acde', 'a{{!b}}cde', {'b': '10'}) + + + def test_verify_06(self): + self.stache_verify('aTrue', 'a{{b}}', {'b': True}) + + + def test_verify_07(self): + self.stache_verify('a123', 'a{{b}}{{c}}{{d}}', {'c': 2, 'b': 1, 'd': 3}) + + + # test falsy #sections + def test_verify_08(self): + self.stache_verify('ab', 'a{{#b}}b{{/b}}', {'b': True}) + + + def test_verify_09(self): + self.stache_verify('a', 'a{{^b}}b{{/b}}', {'b': True}) + + + def test_verify_10(self): + self.stache_verify('a', 'a{{#b}}b{{/}}', {'b': False}) + + + def test_verify_11(self): + self.stache_verify('ab', 'a{{^b}}b{{/b}}', {'b': False}) + + + # test invert sections + def test_verify_12(self): + self.stache_verify('ab', 'a{{#b}}ignore me{{/b}}{{^b}}b{{/}}', {'b': []}) + + + def test_verify_13(self): + self.stache_verify('ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', {'b': [1]}) + + + def test_verify_14(self): + self.stache_verify('ab', 'a{{#b}}b{{/b}}{{^b}}ignore me{{/}}', {'b': True}) + + + # test ?sections + def test_verify_15(self): + self.stache_verify('a- 1 2 3 4', 'a{{?b}}-{{#b}} {{.}}{{/}}{{/}}', {'b': [1, 2, 3, 4]}) + + + def test_verify_16(self): + self.stache_verify('a', 'a{{?b}}ignoreme{{/}}', {'b': False}) + + + def test_verify_17(self): + self.stache_verify('a', 'a{{?b}}ignoreme{{/}}', {'b': []}) + + + def test_verify_18(self): + self.stache_verify('a', 'a{{?b}}ignoreme{{/}}', {}) + + + def test_verify_19(self): + self.stache_verify('abb', 'a{{?b}}b{{/}}{{?b}}b{{/}}', {'b': [1, 2, 3]}) + + + def test_verify_20(self): + self.stache_verify('ab123d', 'a{{?b}}b{{#b}}{{.}}{{/}}d{{/}}', {'b': [1, 2, 3]}) + + + # test #section scope + def test_verify_21(self): + self.stache_verify('abbbb', 'a{{#b}}b{{/b}}', {'b': [1, 2, 3, 4]}) + + + def test_verify_22(self): + self.stache_verify('a1234', 'a{{#b}}{{.}}{{/b}}', {'b': [1, 2, 3, 4]}) + + + def test_verify_23(self): + self.stache_verify('aa=1 a=2 a=0 a', 'a {{#b}}a={{a}} {{/b}}a', {'a': 0, 'b': [{'a': 1}, {'a': 2}, {'c': 1}]}) + + def test_verify_23_a(self): + self.stache_verify('a a=1 a=2 a=0 a', 'a {{#b}} a={{a}}{{/b}} a', {'a': 0, 'b': [{'a': 1}, {'a': 2}, {'c': 1}]}) + + + def test_verify_24(self): + self.stache_verify('1', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', {'a': {'b': {'c': 1}}}) + + + def test_verify_25(self): + self.stache_verify('12', '{{#a}}{{#b}}{{c}}{{/}}{{/a}}', {'a': {'b': [{'c': 1}, {'c': 2}]}}) + + + def test_verify_26(self): + self.stache_verify('12', '{{#a}}{{#b}}{{c}}{{/b}}{{/}}', {'a': [{'b': {'c': 1}}, {'b': {'c': 2}}]}) + + + def test_verify_27(self): + self.stache_verify('132', '{{#a}}{{#b}}{{c}}{{/}}{{/}}', {'a': [{'b': [{'c': 1}, {'c': 3}]}, {'b': {'c': 2}}]}) + + + def test_verify_28(self): + self.stache_verify('132456', '{{#a}}{{#b}}{{c}}{{/b}}{{/a}}', {'a': [{'b': [{'c': 1}, {'c': 3}]}, {'b': {'c': 2}}, {'b': [{'c': 4}, {'c': 5}]}, {'b': {'c': 6}}]}) + + + def test_verify_29(self): + self.stache_verify('1', '{{#a}}{{#a}}{{c}}{{/a}}{{/a}}', {'a': {'a': {'c': 1}}}) + + + def test_verify_30(self): + self.stache_verify('<3><3><3>', '<{{id}}><{{# a? }}{{id}}{{/ a? }}><{{# b? }}{{id}}{{/ b? }}>', {'b?': True, 'id': 3, 'a?': True}) + + + # test delim + def test_verify_31(self): + self.stache_verify('delim{{a}}', '{{=<% %>=}}<%a%>{{a}}', {'a': 'delim'}) + + + def test_verify_32(self): + self.stache_verify('delim{{a}}delim<%a%>', '{{=<% %>=}}<%a%>{{a}}<%={{ }}=%>{{a}}<%a%>', {'a': 'delim'}) + + + # test : + def test_verify_33(self): + self.stache_verify('123test', '123{{:hi}}abc{{/}}', {'hi': 'test'}) + + + def test_verify_34(self): + self.stache_verify('123test', '123{{:hi}}{{:hi}}abc{{/}}{{/}}', {'hi': 'test'}) + + + def test_verify_35(self): + self.stache_verify('123abc', '123{{:hi}}{{:hi2}}abc{{/}}{{/}}', {}) + + + def test_verify_36(self): + self.stache_verify('123cba', '123{{:hi}}{{:hi2}}abc{{/}}{{/}}', {'hi2': 'cba'}) + + + def test_verify_37(self): + self.stache_verify('123abc', '123{{:hi}}abc{{/}}', {}) + + + def test_verify_38(self): + self.stache_verify('123abc', '123{{:hi}}abc{{/}}', {'hi': False}) + + + def test_verify_39(self): + self.stache_verify('123abc', '123{{:hi}}abc{{/}}', {'hi': []}) + + + def test_verify_40(self): + self.stache_verify('123test', '{{<'}) + + + def test_verify_44(self): + self.stache_verify('><', '{{&a}}', {'a': '><'}) + + + def test_verify_45(self): + self.stache_verify('><', '{{{a}}}', {'a': '><'}) + + +s = Stache() +s.add_template('a', '1') +s.add_template('b', '{{>a}}') +s.add_template('c', '{{>a}}{{>b}}') +s.add_template('d', '{{#a}}{{b}}{{/a}}') +s.add_template('e', '{{>d}}') +s.add_template('f', '{{>e}}') +s.add_template('g', '{{i}}') +s.add_template('i', 'i={{e}}') +s.add_template('j', 'show{{!ignoreme}}me') +s.add_template('k', '{{:e}}default{{/}}') +s.add_template('l', '<{{id}}><{{# a }}{{id}}{{/ a }}><{{# b }}{{id}}{{/ b }}>') +s.add_template('m', '<{{id}}><{{# a? }}{{id}}{{/ a? }}><{{# b? }}{{id}}{{/ b? }}>') +s.add_template('n', 'a{{?b}}b{{/}}{{?b}}b{{/}}') +s.add_template('o', 'a{{?b}}b{{#b}}{{.}}{{/}}d{{/}}') + + +class verify_partial(BaseTest): + + def stache_verify_partial(self, output, template, data={}): + stachio = s + #print("%s with %s" % (template, data)) + result = stachio.render_template(template, data) + #print("Result: %s\n" % result) + #assert output == result + self.assertEqual(result, output) + + + + def test_verify_partial_01(self): + self.stache_verify_partial('1', 'a', {}) + + + def test_verify_partial_02(self): + self.stache_verify_partial('1', 'b', {}) + + + def test_verify_partial_03(self): + self.stache_verify_partial('11', 'c', {}) + + + def test_verify_partial_04(self): + self.stache_verify_partial('', 'd', {}) + + + def test_verify_partial_05(self): + self.stache_verify_partial('555', 'd', {'a': {'b': 555}}) + + + def test_verify_partial_06(self): + self.stache_verify_partial('123', 'g', {'a': {}}) + + + def test_verify_partial_07(self): + self.stache_verify_partial('555', 'e', {'a': {'b': 555}}) + + + def test_verify_partial_08(self): + self.stache_verify_partial('555', 'f', {'a': {'b': 555}}) + + + def test_verify_partial_09(self): + self.stache_verify_partial('i=123', 'h', {}) + + + def test_verify_partial_10(self): + self.stache_verify_partial('i=', 'i', {}) + + + def test_verify_partial_11(self): + self.stache_verify_partial('showme', 'j', {}) + + + def test_verify_partial_12(self): + self.stache_verify_partial('default', 'k', {}) + + + def test_verify_partial_13(self): + self.stache_verify_partial('custom', 'k', {'e': 'custom'}) + + + def test_verify_partial_14(self): + self.stache_verify_partial('<3><3><3>', 'l', {'a': True, 'b': True, 'id': 3}) + + + def test_verify_partial_15(self): + self.stache_verify_partial('<3><3><3>', 'm', {'b?': True, 'id': 3, 'a?': True}) + + + def test_verify_partial_16(self): + self.stache_verify_partial('abb', 'n', {'b': [1, 2, 3]}) + + + def test_verify_partial_17(self): + self.stache_verify_partial('ab123d', 'o', {'b': [1, 2, 3]}) + + +class RaisesErrors(BaseTest): + + def test_good_tags(self): + self.stache_verify('1', '{{#a}}{{b}}{{/a}}', {'a': {'b': '1'}}) + + def test_mismatched_tags(self): # compare with test_good_tags() + #self.stache_verify('1', '{{#a}}{{b}}{{/b}}', {'a': {'b': '1'}}) # 'a' not closed, attempts to close 'b' instead + self.assertRaises(AssertionError, self.stache_verify, '1', '{{#a}}{{b}}{{/b}}', {'a': {'b': '1'}}) # 'a' not closed, attempts to close 'b' instead + # TODO check error/exception documents correct tags in error + +def main(argv=None): + if argv is None: + argv = sys.argv + + print('Python %s on %s' %(sys.version, sys.platform)) + unittest.main() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + +# end generated code