From ab45b845d580299aa680328d8c45ace060465183 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Mar 2007 20:47:50 +0100 Subject: [PATCH] [svn] added jinja doctests and fixed problem with i18n strings in requirements --HG-- branch : trunk --- Makefile | 12 +++ jinja/datastructure.py | 13 ++- jinja/filters.py | 41 ++++---- jinja/tests.py | 22 ++--- jinja/translators/python.py | 9 +- tests/conftest.py | 55 +++++++++++ tests/test_filters.py | 191 ++++++++++++++++++++++++++++++++++++ tests/test_forloop.py | 65 ++++++++++++ tests/test_ifcondition.py | 27 +++++ tests/test_tests.py | 62 ++++++++++++ 10 files changed, 456 insertions(+), 41 deletions(-) create mode 100644 Makefile create mode 100644 tests/conftest.py create mode 100644 tests/test_filters.py create mode 100644 tests/test_forloop.py create mode 100644 tests/test_ifcondition.py create mode 100644 tests/test_tests.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..64f448a --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +# +# Jinja Makefile +# ~~~~~~~~~~~~~~ +# +# Shortcuts for various tasks. +# +# :copyright: 2007 by Armin Ronacher. +# :license: BSD, see LICENSE for more details. +# + +test: + @(cd tests; py.test $(TESTS)) diff --git a/jinja/datastructure.py b/jinja/datastructure.py index 39625c3..7da8813 100644 --- a/jinja/datastructure.py +++ b/jinja/datastructure.py @@ -244,13 +244,16 @@ class LoopContext(object): iterated = property(lambda s: s._stack[-1]['index'] > -1) index0 = property(lambda s: s._stack[-1]['index']) index = property(lambda s: s._stack[-1]['index'] + 1) - revindex0 = property(lambda s: s._stack[-1]['length'] - s._stack[-1]['index'] - 1) - revindex = property(lambda s: s._stack[-1]['length'] - s._stack[-1]['index']) + revindex0 = property(lambda s: s._stack[-1]['length'] - + s._stack[-1]['index'] - 1) + revindex = property(lambda s: s._stack[-1]['length'] - + s._stack[-1]['index']) length = property(lambda s: s._stack[-1]['length']) - even = property(lambda s: s._stack[-1]['index'] % 2 == 0) - odd = property(lambda s: s._stack[-1]['index'] % 2 == 1) + even = property(lambda s: s._stack[-1]['index'] % 2 == 1) + odd = property(lambda s: s._stack[-1]['index'] % 2 == 0) first = property(lambda s: s._stack[-1]['index'] == 0) - last = property(lambda s: s._stack[-1]['index'] == s._stack[-1]['length'] - 1) + last = property(lambda s: s._stack[-1]['index'] == + s._stack[-1]['length'] - 1) def __iter__(self): s = self._stack[-1] diff --git a/jinja/filters.py b/jinja/filters.py index 9393980..dde0be6 100644 --- a/jinja/filters.py +++ b/jinja/filters.py @@ -109,16 +109,6 @@ def do_escape(s, attribute=False): do_escape = stringfilter(do_escape) -def do_addslashes(s): - """ - Add backslashes in front of special characters to s. This method - might be useful if you try to fill javascript strings. Also have - a look at the `jsonencode` filter. - """ - return s.encode('utf-8').encode('string-escape').decode('utf-8') -do_addslashes = stringfilter(do_addslashes) - - def do_capitalize(s): """ Capitalize a value. The first character will be uppercase, all others @@ -196,11 +186,10 @@ def do_default(default_value=u'', boolean=False): {{ ''|default('the string was empty', true) }} """ def wrapped(env, context, value): - if (boolean and not v) or v in (Undefined, None): + if (boolean and not value) or value in (Undefined, None): return default_value - return v + return value return wrapped -do_default = stringfilter(do_default) def do_join(d=u''): @@ -521,21 +510,34 @@ def do_rst(s): do_rst = stringfilter(do_rst) -def do_int(): +def do_int(default=0): """ - Convert the value into an integer. + Convert the value into an integer. If the + conversion doesn't work it will return ``0``. You can + override this default using the first parameter. """ def wrapped(env, context, value): - return int(value) + try: + return int(value) + except (TypeError, ValueError): + try: + return int(float(value)) + except (TypeError, ValueError): + return default return wrapped -def do_float(): +def do_float(default=0.0): """ - Convert the value into a floating point number. + Convert the value into a floating point number. If the + conversion doesn't work it will return ``0.0``. You can + override this default using the first parameter. """ def wrapped(env, context, value): - return float(value) + try: + return float(value) + except (TypeError, ValueError): + return default return wrapped @@ -552,7 +554,6 @@ FILTERS = { 'lower': do_lower, 'escape': do_escape, 'e': do_escape, - 'addslashes': do_addslashes, 'capitalize': do_capitalize, 'title': do_title, 'default': do_default, diff --git a/jinja/tests.py b/jinja/tests.py index c732358..ae293d4 100644 --- a/jinja/tests.py +++ b/jinja/tests.py @@ -12,7 +12,7 @@ import re from jinja.datastructure import Undefined -number_re = re.compile(r'^-?\d+(\.\d+)$') +number_re = re.compile(r'^-?\d+(\.\d+)?$') regex_type = type(number_re) @@ -87,31 +87,29 @@ def test_sequence(): def test_matching(regex): - """ - Test if the variable matches the regular expression - given. If the regular expression is a string additional - slashes are automatically added, if it's a compiled regex - it's used without any modifications: + r""" + Test if the variable matches the regular expression given. Note that + you have to escape special chars using *two* backslashes, these are + *not* raw strings. .. sourcecode:: jinja - {% if var is matching('\d+$') %} + {% if var is matching('^\\d+$') %} var looks like a number {% else %} var doesn't really look like a number {% endif %} """ if isinstance(regex, unicode): - regex = re.compile(regex.encode('unicode-escape'), re.U) - elif isinstance(regex, unicode): - regex = re.compile(regex.encode('string-escape')) + regex = re.compile(regex, re.U) + elif isinstance(regex, str): + regex = re.compile(regex) elif type(regex) is not regex_type: regex = None def wrapped(environment, context, value): if regex is None: return False - else: - return regex.match(value) + return regex.search(value) is not None return wrapped TESTS = { diff --git a/jinja/translators/python.py b/jinja/translators/python.py index b341852..6d5b924 100644 --- a/jinja/translators/python.py +++ b/jinja/translators/python.py @@ -304,12 +304,13 @@ class PythonTranslator(Translator): ] # we have requirements? add them here. + body_lines = [] if requirements: for n in requirements: - lines.append(self.handle_node(n)) + body_lines.append(self.handle_node(n)) # the template body - body_lines = [self.handle_node(n) for n in node] + body_lines.extend([self.handle_node(n) for n in node]) # add translation helpers if required if self.require_translations: @@ -379,7 +380,7 @@ class PythonTranslator(Translator): nodeinfo = self.nodeinfo(node.body) if nodeinfo: write(nodeinfo) - buf.append(self.handle_node(node.body)) + buf.append(self.handle_node(node.body) or self.indent('pass')) self.indention -= 1 # else part of loop @@ -389,7 +390,7 @@ class PythonTranslator(Translator): nodeinfo = self.nodeinfo(node.else_) if nodeinfo: write(nodeinfo) - buf.append(self.handle_node(node.else_)) + buf.append(self.handle_node(node.else_) or self.indent('pass')) self.indention -= 1 # call recursive for loop! diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8ec3737 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" + conftest + ~~~~~~~~ + + Configure py.test for support stuff. + + :copyright: 2007 by Armin Ronacher, Alexander Schremmer. + :license: BSD, see LICENSE for more details. +""" + +import os +import sys +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +import py +from inspect import isclass +from jinja import Environment + + +simple_env = Environment(trim_blocks=True) + + +class Module(py.test.collect.Module): + + def __init__(self, *args, **kwargs): + self.env = simple_env + super(Module, self).__init__(*args, **kwargs) + + def join(self, name): + obj = getattr(self.obj, name) + if isclass(obj): + return JinjaClassCollector(name, parent=self) + elif hasattr(obj, 'func_code'): + return JinjaTestFunction(name, parent=self) + + +class JinjaTestFunction(py.test.collect.Function): + + def execute(self, target, *args): + co = target.func_code + if 'env' in co.co_varnames[:co.co_argcount]: + target(self.parent.env, *args) + else: + target(*args) + + +class JinjaClassCollector(py.test.collect.Class): + + Function = JinjaTestFunction + + def setup(self): + cls = self.obj + cls.env = self.parent.env + super(JinjaClassCollector, self).setup() diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 0000000..2d728b0 --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" + unit test for the filters + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Missing tests: + + - wordcount + - rst + - markdown + - textile + + :copyright: 2007 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +CAPITALIZE = '''{{ "foo bar"|capitalize }}''' +CENTER = '''{{ "foo"|center(9) }}''' +DEFAULT = '''{{ missing|default("no") }}|{{ false|default('no') }}|\ +{{ false|default('no', true) }}|{{ given|default("no") }}''' +DICTSORT = '''{{ foo|dictsort }}|\ +{{ foo|dictsort(true) }}|\ +{{ foo|dictsort(false, 'value') }}''' +ESCAPE = '''{{ '<">&'|escape }}|{{ '<">&'|escape(true) }}''' +FILESIZEFORMAT = '{{ 100|filesizeformat }}|\ +{{ 1000|filesizeformat }}|\ +{{ 1000000|filesizeformat }}|\ +{{ 1000000000|filesizeformat }}|\ +{{ 1000000000000|filesizeformat }}' +FIRST = '''{{ foo|first }}''' +FLOAT = '''{{ "42"|float }}|{{ "ajsghasjgd"|float }}|{{ "32.32"|float }}''' +INDENT = '''{{ foo|indent(2) }}|{{ foo|indent(2, true) }}''' +INT = '''{{ "42"|int }}|{{ "ajsghasjgd"|int }}|{{ "32.32"|int }}''' +JOIN = '''{{ [1, 2, 3]|join("|") }}''' +LAST = '''{{ foo|last }}''' +LENGTH = '''{{ "hello world"|length }}''' +LOWER = '''{{ "FOO"|lower }}''' +PPRINT = '''{{ data|pprint }}''' +RANDOM = '''{{ seq|random }}''' +REVERSE = '''{{ "foobar"|reverse }}|{{ [1, 2, 3]|reverse }}''' +STRING = '''{{ range(10)|string }}''' +TITLE = '''{{ "foo bar"|title }}''' +TRUNCATE = '''{{ data|truncate(15, true, ">>>") }}|\ +{{ data|truncate(15, false, ">>>") }}|\ +{{ smalldata|truncate(15) }}''' +UPPER = '''{{ "foo"|upper }}''' +URLENCODE = '''{{ "f#b"|urlencode }}''' +URLIZE = '''{{ "foo http://www.example.com/ bar"|urlize }}''' +WORDCOUNT = '''{{ "foo bar baz"|wordcount }}''' + + +def test_capitalize(env): + tmpl = env.from_string(CAPITALIZE) + assert tmpl.render() == 'Foo bar' + + +def test_center(env): + tmpl = env.from_string(CENTER) + assert tmpl.render() == ' foo ' + + +def test_default(env): + tmpl = env.from_string(DEFAULT) + assert tmpl.render(given='yes') == 'no|False|no|yes' + + +def test_dictsort(env): + tmpl = env.from_string(DICTSORT) + out = tmpl.render(foo={"a": 0, "b": 1, "c": 2, "A": 3}) + assert out == ("[('a', 0), ('A', 3), ('b', 1), ('c', 2)]|" + "[('A', 3), ('a', 0), ('b', 1), ('c', 2)]|" + "[('a', 0), ('b', 1), ('c', 2), ('A', 3)]") + + +def test_escape(env): + tmpl = env.from_string(ESCAPE) + out = tmpl.render() + assert out == '<">&|<">&' + + +def test_filesizeformat(env): + tmpl = env.from_string(FILESIZEFORMAT) + out = tmpl.render() + assert out == '100 Bytes|1000 Bytes|976.6 KB|953.7 MB|931.3 GB' + + +def test_first(env): + tmpl = env.from_string(FIRST) + out = tmpl.render(foo=range(10)) + assert out == '0' + + +def test_float(env): + tmpl = env.from_string(FLOAT) + out = tmpl.render() + assert out == '42.0|0.0|32.32' + + +def test_indent(env): + tmpl = env.from_string(INDENT) + text = '\n'.join([' '.join(['foo', 'bar'] * 2)] * 2) + out = tmpl.render(foo=text) + assert out == 'foo bar foo bar\n foo bar foo bar| ' \ + 'foo bar foo bar\n foo bar foo bar' + + +def test_int(env): + tmpl = env.from_string(INT) + out = tmpl.render() + assert out == '42|0|32' + + +def test_join(env): + tmpl = env.from_string(JOIN) + out = tmpl.render() + assert out == '1|2|3' + + +def test_last(env): + tmpl = env.from_string(LAST) + out = tmpl.render(foo=range(10)) + assert out == '9' + + +def test_length(env): + tmpl = env.from_string(LENGTH) + out = tmpl.render() + assert out == '11' + + +def test_lower(env): + tmpl = env.from_string(LOWER) + out = tmpl.render() + assert out == 'foo' + + +def test_pprint(env): + from pprint import pformat + tmpl = env.from_string(PPRINT) + data = range(10000) + assert tmpl.render(data=data) == pformat(data) + + +def test_random(env): + tmpl = env.from_string(RANDOM) + seq = range(100) + for _ in range(10): + assert int(tmpl.render(seq=seq)) in seq + + +def test_reverse(env): + tmpl = env.from_string(REVERSE) + assert tmpl.render() == 'raboof|[3, 2, 1]' + + +def test_string(env): + tmpl = env.from_string(STRING) + assert tmpl.render(foo=range(10)) == str(range(10)) + + +def test_title(env): + tmpl = env.from_string(TITLE) + assert tmpl.render() == "Foo Bar" + + +def test_truncate(env): + tmpl = env.from_string(TRUNCATE) + out = tmpl.render(data='foobar baz bar' * 1000, + smalldata='foobar baz bar') + assert out == 'foobar baz barf>>>|foobar baz >>>|foobar baz bar' + + +def test_upper(env): + tmpl = env.from_string(UPPER) + assert tmpl.render() == 'FOO' + + +def test_urlencode(env): + tmpl = env.from_string(URLENCODE) + assert tmpl.render() == 'f%23b' + + +def test_urlize(env): + tmpl = env.from_string(URLIZE) + assert tmpl.render() == 'foo '\ + 'http://www.example.com/ bar' + + +def test_wordcount(env): + tmpl = env.from_string(WORDCOUNT) + assert tmpl.render() == '3' diff --git a/tests/test_forloop.py b/tests/test_forloop.py new file mode 100644 index 0000000..af8afce --- /dev/null +++ b/tests/test_forloop.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" + unit test for loop functions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2007 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +SIMPLE = '''{% for item in seq %}{{ item }}{% endfor %}''' +ELSE = '''{% for item in seq %}XXX{% else %}...{% endfor %}''' +EMPTYBLOCKS = '''<{% for item in seq %}{% else %}{% endfor %}>''' +CONTEXTVARS = '''{% for item in seq %}\ +{{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ + loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{ + loop.even }}|{{ loop.odd }}|{{ loop.length }}###{% endfor %}''' +CYCLING = '''{% for item in seq %}{% cycle '<1>', '<2>' %}{% endfor %}\ +{% for item in seq %}{% cycle through %}{% endfor %}''' +SCOPE = '''{% for item in seq %}{% endfor %}{{ item }}''' + + +def test_simple(env): + tmpl = env.from_string(SIMPLE) + assert tmpl.render(seq=range(10)) == '0123456789' + + +def test_else(env): + tmpl = env.from_string(ELSE) + assert tmpl.render() == '...' + + +def test_empty_blocks(env): + tmpl = env.from_string(EMPTYBLOCKS) + assert tmpl.render() == '<>' + + +def test_context_vars(env): + tmpl = env.from_string(CONTEXTVARS) + one, two, _ = tmpl.render(seq=[0, 1]).split('###') + (one_index, one_index0, one_revindex, one_revindex0, one_first, + one_last, one_even, one_odd, one_length) = one.split('|') + (two_index, two_index0, two_revindex, two_revindex0, two_first, + two_last, two_even, two_odd, two_length) = two.split('|') + + assert int(one_index) == 1 and int(two_index) == 2 + assert int(one_index0) == 0 and int(two_index0) == 1 + assert int(one_revindex) == 2 and int(two_revindex) == 1 + assert int(one_revindex0) == 1 and int(two_revindex0) == 0 + assert one_first == 'True' and two_first == 'False' + assert one_last == 'False' and two_last == 'True' + assert one_even == 'False' and two_even == 'True' + assert one_odd == 'True' and two_odd == 'False' + assert one_length == two_length == '2' + + +def test_cycling(env): + tmpl = env.from_string(CYCLING) + output = tmpl.render(seq=range(4), through=('<1>', '<2>')) + assert output == '<1><2>' * 4 + + +def test_scope(env): + tmpl = env.from_string(SCOPE) + output = tmpl.render(seq=range(10)) + assert not output diff --git a/tests/test_ifcondition.py b/tests/test_ifcondition.py new file mode 100644 index 0000000..21072fc --- /dev/null +++ b/tests/test_ifcondition.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" + unit test for if conditions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2007 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +SIMPLE = '''{% if true %}...{% endif %}''' +ELIF = '''{% if false %}XXX{% elif true %}...{% else %}XXX{% endif %}''' +ELSE = '''{% if false %}XXX{% else %}...{% endif %}''' + + +def test_simple(env): + tmpl = env.from_string(SIMPLE) + assert tmpl.render() == '...' + + +def test_elif(env): + tmpl = env.from_string(ELIF) + assert tmpl.render() == '...' + + +def test_else(env): + tmpl = env.from_string(ELSE) + assert tmpl.render() == '...' diff --git a/tests/test_tests.py b/tests/test_tests.py new file mode 100644 index 0000000..22bdec4 --- /dev/null +++ b/tests/test_tests.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +""" + unit test for the test functions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2007 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +DEFINED = '''{{ missing is defined }}|{{ true is defined }}''' +EVEN = '''{{ 1 is even }}|{{ 2 is even }}''' +LOWER = '''{{ "foo" is lower }}|{{ "FOO" is lower }}''' +MATCHING = '''{{ "42" is matching('^\\d+$') }}|\ +{{ "foo" is matching('^\\d+$') }}''' +NUMERIC = '''{{ "43" is numeric }}|{{ "foo" is numeric }}|\ +{{ 42 is numeric }}''' +ODD = '''{{ 1 is odd }}|{{ 2 is odd }}''' +SEQUENCE = '''{{ [1, 2, 3] is sequence }}|\ +{{ "foo" is sequence }}|\ +{{ 42 is sequence }}''' +UPPER = '''{{ "FOO" is upper }}|{{ "foo" is upper }}''' + + +def test_defined(env): + tmpl = env.from_string(DEFINED) + assert tmpl.render() == 'False|True' + + +def test_even(env): + tmpl = env.from_string(EVEN) + assert tmpl.render() == 'False|True' + + +def test_lower(env): + tmpl = env.from_string(LOWER) + assert tmpl.render() == 'True|False' + + +def test_matching(env): + tmpl = env.from_string(MATCHING) + assert tmpl.render() == 'True|False' + + +def test_numeric(env): + tmpl = env.from_string(NUMERIC) + assert tmpl.render() == 'True|False|True' + + +def test_odd(env): + tmpl = env.from_string(ODD) + assert tmpl.render() == 'True|False' + + +def test_sequence(env): + tmpl = env.from_string(SEQUENCE) + assert tmpl.render() == 'True|True|False' + + +def test_upper(env): + tmpl = env.from_string(UPPER) + assert tmpl.render() == 'True|False' + -- 2.26.2