[svn] added many new tests to jinja
authorArmin Ronacher <armin.ronacher@active-4.com>
Mon, 21 May 2007 14:44:26 +0000 (16:44 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Mon, 21 May 2007 14:44:26 +0000 (16:44 +0200)
--HG--
branch : trunk

19 files changed:
CHANGES
Makefile
docs/generate.py
docs/src/altsyntax.txt
docs/src/designerdoc.txt
docs/src/devrecipies.txt
docs/src/objects.txt
jinja/_native.py
jinja/datastructure.py
jinja/lexer.py
jinja/loaders.py
jinja/parser.py
jinja/translators/python.py
tests/conftest.py
tests/runtime/bigtable.py
tests/test_loaders.py
tests/test_parser.py [new file with mode: 0644]
tests/test_security.py [new file with mode: 0644]
tests/test_streaming.py [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index 0c0fd47ca55be4932e7aa9f9803bfc157009da3d..bda176c08384ac63dbebbde719315f72c4b9fb8a 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -81,7 +81,8 @@ Version 1.1
 
 - additional macro arguments now end up in `varargs`.
 
-- implemented `{% call %}` - unsure if this makes it into the final release.
+- implemented the `{% call %}` block. `call` and `endcall` can still be used
+  as identifiers until Jinja 1.3
 
 - it's not possible to stream templates.
 
index ca294d90636bae66e27f1ee47b0bf98b1661167b..8f40d473fe740a868cf5af756740b0e21dbdb128 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,9 @@
 test:
        @(cd tests; py.test $(TESTS))
 
+test-coverage:
+       @(cd tests; py.test -C $(TESTS))
+
 documentation:
        @(cd docs; ./generate.py)
 
index 8acf5b7626f21e0397f291ade8338803af9438f6..f8e8238dc35914e80daa047d47ef69cc4d0d1516 100755 (executable)
@@ -260,7 +260,7 @@ def generate_documentation(data, link_style):
 def handle_file(filename, fp, dst, preproc):
     now = datetime.now()
     title = os.path.basename(filename)[:-4]
-    content = fp.read()
+    content = fp.read().decode('utf-8')
     suffix = not preproc and '.html' or ''
     parts = generate_documentation(content, (lambda x: './%s%s' % (x, suffix)))
     result = file(os.path.join(dst, title + '.html'), 'w')
index 088445c1706e2c4de11abaa8649123fbab126f18..9fb3b64f654f71d8dbbb971b639455d195dceff9 100644 (file)
@@ -46,7 +46,7 @@ An example template then looks like this:
     <ul>
     <% for item in seq %>
       <li><%= item %></li>
-    <% endfor %>
+    <% endfor %>
     </ul>
 
 
@@ -82,7 +82,7 @@ Jinja 1.1 or higher.
 
 
 Block / Variable Tag Unification
---------------------------------
+================================
 
 If variable end and start tags are `None` or look the same as block tags and
 you're running Jinja 1.1 or later the parser will switch into the
@@ -103,3 +103,6 @@ This now allows smarty like templates:
     {else}
         Something is {something}.
     {endif}
+
+This feature however can cause strange looking templates because there is no
+visible difference between blocks and variables.
index ba5de713daf07c02eef4e640b1dcba62ab08403d..d48dde20f119b87f7bdab443ee5ef071994b0f65 100644 (file)
@@ -419,7 +419,11 @@ The following keywords exist and cannot be used as identifiers:
     `and`, `block`, `cycle`, `elif`, `else`, `endblock`, `endfilter`,
     `endfor`, `endif`, `endmacro`, `endraw`, `endtrans`, `extends`, `filter`,
     `for`, `if`, `in`, `include`, `is`, `macro`, `not`, `or`, `pluralize`,
-    `print`, `raw`, `recursive`, `set`, `trans`
+    `print`, `raw`, `recursive`, `set`, `trans`, `call`*, `endcall`*,
+
+keywords marked with `*` can be used until Jinja 1.3 as identifiers because
+they were introduced after the Jinja 1.0 release and can cause backwards
+compatiblity problems otherwise. You should not use them any more.
 
 If you want to use such a name you have to prefix or suffix it or use
 alternative names:
@@ -430,11 +434,6 @@ alternative names:
         {{ macro_('foo') }}
     {% endfor %}
 
-If future Jinja releases add new keywords those will be "light" keywords which
-means that they won't raise an error for several releases but yield warnings
-on the application side. But it's very unlikely that new keywords will be
-added.
-
 
 Bleeding Edge
 =============
index ae70226d6022256bfc719c45882a3328afc934e1..7f5b6c010ff1926b4a32e0cda787a5cabfa6173b 100644 (file)
@@ -23,16 +23,17 @@ this:
 
     class AutoEnvironment(Environment):
 
-        def autorender(self):
-            return self.render(sys._getframe(1).f_locals)
+        def autorender(self, template):
+            tmpl = self.get_template(template)
+            return tmpl.render(sys._getframe(1).f_locals)
 
 You can use it now like this:
 
 .. sourcecode:: python
 
-    foo_tmpl = env.get_template('foo.html')
-
     def foo():
         seq = range(10)
         foo = "blub"
-        return foo_tmpl.autorender()
+        return env.autorender('foo.html')
+
+In the template you can now access the local variables `seq` and `foo`.
index b1f86e4c410a615192b8161fcaf79ca452c5c0c7..7b0641fa5292cb4d758a3e824bd9209672313626 100644 (file)
@@ -49,6 +49,30 @@ from the template it should return the content of the context as simple html
 template. Of course you can modify the context too. For more informations about
 the context object have a look at the `context object`_ documentation.
 
+The new ``{% call %}`` tag that exists with Jinja 1.1 onwards can not only
+be used with Jinja macros but also with Python functions. If a template
+designers uses the ``{% call %}`` tag to call a function provided by the
+application Jinja will call this function with a keyword argument called
+`caller` which points to a `function`. If you call this function (optionally
+with keyword arguments that appear in the context) you get a string back
+that was the content of that block. This should explain this:
+
+.. sourcecode:: python
+
+    def make_dialog(title, caller=None):
+        body = ''
+        if caller:
+            body = caller(title=title)
+        return '<div class="dialog"><h2>%s</h2>%s</div>' % (title, body)
+
+This can be used like this in the template now:
+
+.. sourcecode:: html+jinja
+
+    {% call make_dialog('Dialog Title') %}
+        This is the body of the dialog entitled "{{ title }}".
+    {% endcall %}
+
 Deferred Values
 ===============
 
index ab7d1d3937171269b0ebc92f80b4857d9b993a6d..9fa723f53b273cdf26ac153fe7528edf1cb67088 100644 (file)
     :license: BSD, see LICENSE for more details.
 """
 from jinja.datastructure import Deferred, Undefined
+try:
+    from collections import deque
+except ImportError:
+    class deque(list):
+        """
+        Minimal subclass of list that provides the deque
+        interface used by the native `BaseContext`.
+        """
+
+        def appendleft(self, item):
+            list.insert(self, 0, item)
+
+        def popleft(self):
+            return list.pop(self, 0)
 
 
 class BaseContext(object):
@@ -21,27 +35,33 @@ class BaseContext(object):
     def __init__(self, undefined_singleton, globals, initial):
         self._undefined_singleton = undefined_singleton
         self.current = current = {}
-        self.stack = [globals, initial, current]
-        self._push = self.stack.append
-        self._pop = self.stack.pop
+        self._stack = deque([current, initial, globals])
         self.globals = globals
         self.initial = initial
 
+        self._push = self._stack.appendleft
+        self._pop = self._stack.popleft
+
+    def stack(self):
+        return list(self._stack)[::-1]
+    stack = property(stack)
+
     def pop(self):
         """
         Pop the last layer from the stack and return it.
         """
         rv = self._pop()
-        self.current = self.stack[-1]
+        self.current = self._stack[0]
         return rv
 
     def push(self, data=None):
         """
-        Push one layer to the stack. Layer must be a dict or omitted.
+        Push one layer to the stack and return it. Layer must be
+        a dict or omitted.
         """
         data = data or {}
         self._push(data)
-        self.current = self.stack[-1]
+        self.current = self._stack[0]
         return data
 
     def __getitem__(self, name):
@@ -50,10 +70,7 @@ class BaseContext(object):
         such as ``'::cycle1'``. Resolve deferreds.
         """
         if not name.startswith('::'):
-            # because the stack is usually quite small we better
-            # use [::-1] which is faster than reversed() in such
-            # a situation.
-            for d in self.stack[::-1]:
+            for d in self._stack:
                 if name in d:
                     rv = d[name]
                     if rv.__class__ is Deferred:
@@ -83,7 +100,7 @@ class BaseContext(object):
         """
         Check if the context contains a given variable.
         """
-        for layer in self.stack:
+        for layer in self._stack:
             if name in layer:
                 return True
         return False
index 3b8e4485fff9c6de7f3a23c18b64904ac1b824c5..daa2e27ad6c7c0fd255d9605db7a2d0a168e2d18 100644 (file)
@@ -547,7 +547,7 @@ class TemplateStream(object):
 
     def __init__(self, gen):
         self._gen = gen
-        self._next = gen._next
+        self._next = gen.next
         self.buffered = False
 
     def disable_buffering(self):
index e948aa084a290455fd36fcd4ad6ef10be43dcece..7b98a57c19bad359822ca445d3d6658458a7d1b0 100644 (file)
@@ -64,7 +64,7 @@ keywords = set(['and', 'block', 'cycle', 'elif', 'else', 'endblock',
                 'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
                 'endtrans', 'extends', 'filter', 'for', 'if', 'in',
                 'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw',
-                'recursive', 'set', 'trans', 'print', 'call', 'endcall'])
+                'recursive', 'set', 'trans', 'print'])
 
 
 class Failure(object):
index 0bebdc3e3b7adeba9312416f575b3b96b0fc8996..15d04e6840648e62629d7e789465d3d07edd1202 100644 (file)
@@ -62,6 +62,12 @@ class LoaderWrapper(object):
         else:
             self.available = True
 
+    def __getattr__(self, name):
+        """
+        Not found attributes are redirected to the loader
+        """
+        return getattr(self.loader, name)
+
     def get_source(self, name, parent=None):
         """Retrieve the sourcecode of a template."""
         # just ascii chars are allowed as template names
@@ -336,6 +342,12 @@ class PackageLoader(CachedLoaderMixin, BaseLoader):
                         to ``package_name + '/' + package_path``.
                         *New in Jinja 1.1*
     =================== =================================================
+
+    Important note: If you're using an application that is inside of an
+    egg never set `auto_reload` to `True`. The egg resource manager will
+    automatically export files to the file system and touch them so that
+    you not only end up with additional temporary files but also an automatic
+    reload each time you load a template.
     """
 
     def __init__(self, package_name, package_path, use_memcache=False,
@@ -347,11 +359,6 @@ class PackageLoader(CachedLoaderMixin, BaseLoader):
         self.package_path = package_path
         if cache_salt is None:
             cache_salt = package_name + '/' + package_path
-        # if we have an loader we probably retrieved it from an egg
-        # file. In that case don't use the auto_reload!
-        if auto_reload and getattr(__import__(package_name, '', '', ['']),
-                                   '__loader__', None) is not None:
-            auto_reload = False
         CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
                                    cache_folder, auto_reload, cache_salt)
 
index b3e1ba13e60eee172627c850b6fa7b66aca498b2..befcbc5389d14f3e56faa6e0d8fe61aaa3f6c0ed 100644 (file)
@@ -41,7 +41,7 @@ switch_if = StateTest.expect_name('else', 'elif', 'endif')
 end_of_if = StateTest.expect_name('endif')
 end_of_filter = StateTest.expect_name('endfilter')
 end_of_macro = StateTest.expect_name('endmacro')
-end_of_call = StateTest.expect_name('endcall')
+end_of_call = StateTest.expect_name('endcall_')
 end_of_block_tag = StateTest.expect_name('endblock')
 end_of_trans = StateTest.expect_name('endtrans')
 
@@ -54,6 +54,8 @@ class Parser(object):
     """
 
     def __init__(self, environment, source, filename=None):
+        #XXX: with Jinja 1.3 call becomes a keyword. Add it also
+        #     to the lexer.py file.
         self.environment = environment
         if isinstance(source, str):
             source = source.decode(environment.template_charset, 'ignore')
@@ -78,7 +80,7 @@ class Parser(object):
             'filter':       self.handle_filter_directive,
             'print':        self.handle_print_directive,
             'macro':        self.handle_macro_directive,
-            'call':         self.handle_call_directive,
+            'call_':        self.handle_call_directive,
             'block':        self.handle_block_directive,
             'extends':      self.handle_extends_directive,
             'include':      self.handle_include_directive,
index 7b85d7808197a81a55d8157f555d564a547917bc..12a829adf976b82363ae26f540fae294684a6b70 100644 (file)
@@ -423,11 +423,7 @@ class PythonTranslator(Translator):
 
         # handle requirements code
         if requirements:
-            requirement_lines = [
-                'def bootstrap(context):',
-                '    ctx_push = context.push',
-                '    ctx_pop = context.pop'
-            ]
+            requirement_lines = ['def bootstrap(context):']
             has_requirements = False
             for n in requirements:
                 requirement_lines.append(self.handle_node(n))
@@ -452,8 +448,6 @@ class PythonTranslator(Translator):
                 if data:
                     block_lines.extend([
                         'def %s(context):' % func_name,
-                        '    ctx_push = context.push',
-                        '    ctx_pop = context.pop',
                         self.indent(self.nodeinfo(item, True)),
                         data,
                         '    if 0: yield None\n'
@@ -491,9 +485,7 @@ class PythonTranslator(Translator):
             '# Name for disabled debugging\n'
             '__name__ = %r\n\n'
             'def generate(context):\n'
-            '    assert environment is context.environment\n'
-            '    ctx_push = context.push\n'
-            '    ctx_pop = context.pop' % (
+            '    assert environment is context.environment' % (
                 '\n'.join([
                     '%s = environment.%s' % (item, item) for item in
                     ['get_attribute', 'perform_test', 'apply_filters',
@@ -570,14 +562,8 @@ class PythonTranslator(Translator):
         """
         Handle data around nodes.
         """
-        # if we have a ascii only string we go with the
-        # bytestring. otherwise we go with the unicode object
-        try:
-            data = str(node.text)
-        except UnicodeError:
-            data = node.text
         return self.indent(self.nodeinfo(node)) + '\n' +\
-               self.indent('yield %r' % data)
+               self.indent('yield %r' % node.text)
 
     def handle_dynamic_text(self, node):
         """
@@ -620,7 +606,7 @@ class PythonTranslator(Translator):
         buf = []
         write = lambda x: buf.append(self.indent(x))
         write(self.nodeinfo(node))
-        write('ctx_push()')
+        write('context.push()')
 
         # recursive loops
         if node.recursive:
@@ -668,7 +654,7 @@ class PythonTranslator(Translator):
             write('yield item')
             self.indention -= 1
 
-        write('ctx_pop()')
+        write('context.pop()')
         return '\n'.join(buf)
 
     def handle_if_condition(self, node):
@@ -801,7 +787,7 @@ class PythonTranslator(Translator):
         if varargs_init:
             arg_items.append(varargs_init)
 
-        write('ctx_push({%s})' % ',\n          '.join([
+        write('context.push({%s})' % ',\n              '.join([
             idx and self.indent(item) or item for idx, item
             in enumerate(arg_items)
         ]))
@@ -818,7 +804,7 @@ class PythonTranslator(Translator):
         data = self.handle_node(node.body)
         if data:
             buf.append(data)
-        write('ctx_pop()')
+        write('context.pop()')
         write('if 0: yield None')
         self.indention -= 1
         buf.append(self.indent('context[%r] = buffereater(macro)' %
@@ -836,11 +822,11 @@ class PythonTranslator(Translator):
 
         write('def call(**kwargs):')
         self.indention += 1
-        write('ctx_push(kwargs)')
+        write('context.push(kwargs)')
         data = self.handle_node(node.body)
         if data:
             buf.append(data)
-        write('ctx_pop()')
+        write('context.pop()')
         write('if 0: yield None')
         self.indention -= 1
         write('yield ' + self.handle_call_func(node.expr,
@@ -870,12 +856,12 @@ class PythonTranslator(Translator):
         write = lambda x: buf.append(self.indent(x))
         write('def filtered():')
         self.indention += 1
-        write('ctx_push()')
+        write('context.push()')
         write(self.nodeinfo(node.body))
         data = self.handle_node(node.body)
         if data:
             buf.append(data)
-        write('ctx_pop()')
+        write('context.pop()')
         write('if 0: yield None')
         self.indention -= 1
         write('yield %s' % self.filter('buffereater(filtered)()',
@@ -898,13 +884,13 @@ class PythonTranslator(Translator):
         write = lambda x: buf.append(self.indent(x))
 
         write(self.nodeinfo(node))
-        write('ctx_push({\'super\': SuperBlock(%r, blocks, %r, context)})' % (
+        write('context.push({\'super\': SuperBlock(%r, blocks, %r, context)})' % (
             str(node.name),
             level
         ))
         write(self.nodeinfo(node.body))
         buf.append(rv)
-        write('ctx_pop()')
+        write('context.pop()')
         return '\n'.join(buf)
 
     def handle_include(self, node):
index 651aca8b27122e722f84bc7a270568956099f996..e97e30cfceb4c11790afce41289eb32946f15188 100644 (file)
@@ -17,6 +17,40 @@ import py
 from jinja import Environment
 from jinja.parser import Parser
 
+try:
+    # This code adds support for coverage.py (see
+    # http://nedbatchelder.com/code/modules/coverage.html).
+    # It prints a coverage report for the modules specified in all
+    # module globals (of the test modules) named "coverage_modules".
+
+    import coverage, atexit
+
+    IGNORED_MODULES = ['jinja._speedups', 'jinja.defaults',
+                       'jinja.translators']
+
+    def report_coverage():
+        coverage.stop()
+        module_list = [
+            mod for name, mod in sys.modules.copy().iteritems() if
+            getattr(mod, '__file__', None) and
+            name.startswith('jinja.') and
+            name not in IGNORED_MODULES
+        ]
+        module_list.sort()
+        coverage.report(module_list)
+
+    def callback(option, opt_str, value, parser):
+        atexit.register(report_coverage)
+        coverage.erase()
+        coverage.start()
+
+    py.test.config.addoptions('Test options', py.test.config.Option('-C',
+        '--coverage', action='callback', callback=callback,
+        help='Output information about code coverage (slow!)'))
+
+except ImportError:
+    coverage = None
+
 
 class GlobalLoader(object):
 
@@ -45,10 +79,12 @@ class Module(py.test.collect.Module):
         self.env = simple_env
         super(Module, self).__init__(*args, **kwargs)
 
-    def join(self, name):
-        obj = getattr(self.obj, name)
-        if hasattr(obj, 'func_code'):
-            return JinjaTestFunction(name, parent=self)
+    def makeitem(self, name, obj, usefilters=True):
+        if name.startswith('test_'):
+            if hasattr(obj, 'func_code'):
+                return JinjaTestFunction(name, parent=self)
+            elif isinstance(obj, basestring):
+                return JinjaDocTest(name, parent=self)
 
 
 class JinjaTestFunction(py.test.collect.Function):
@@ -60,3 +96,19 @@ class JinjaTestFunction(py.test.collect.Function):
             target(self.parent.env, *args)
         else:
             target(*args)
+
+
+class JinjaDocTest(py.test.collect.Item):
+
+    def run(self):
+        mod = py.std.types.ModuleType(self.name)
+        mod.__doc__ = self.obj
+        self.execute(mod)
+
+    def execute(self, mod):
+        mod.env = self.parent.env
+        mod.MODULE = self.parent.obj
+        failed, tot = py.compat.doctest.testmod(mod, verbose=True)
+        if failed:
+            py.test.fail('doctest %s: %s failed out of %s' % (
+                         self.fspath, failed, tot))
index 50100613e21b9393993de596177abf032bab7343..b1576df39b6ac649145ec1ec6f95f0a40db57ce8 100644 (file)
@@ -13,8 +13,12 @@ import timeit
 import jdebug
 from StringIO import StringIO
 
-from genshi.builder import tag
-from genshi.template import MarkupTemplate
+try:
+    from genshi.builder import tag
+    from genshi.template import MarkupTemplate
+    have_genshi = True
+except ImportError:
+    have_genshi = False
 
 from jinja import Environment
 
@@ -33,7 +37,11 @@ try:
 except ImportError:
     have_kid = False
 
-from Cheetah.Template import Template as CheetahTemplate
+try:
+    from Cheetah.Template import Template as CheetahTemplate
+    have_cheetah = True
+except ImportError:
+    have_cheetah = False
 
 try:
     from mako.template import Template as MakoTemplate
@@ -44,7 +52,8 @@ except ImportError:
 table = [dict(zip('abcdefghij', map(unicode,range(1, 11))))
           for x in range(1000)]
 
-genshi_tmpl = MarkupTemplate("""
+if have_genshi:
+    genshi_tmpl = MarkupTemplate("""
 <table xmlns:py="http://genshi.edgewall.org/">
 <tr py:for="row in table">
 <td py:for="c in row.values()" py:content="c"/>
@@ -78,7 +87,8 @@ jinja_tmpl = Environment().from_string('''
 </table>
 ''')
 
-cheetah_tmpl = CheetahTemplate('''
+if have_cheetah:
+    cheetah_tmpl = CheetahTemplate('''
 <table>
 #for $row in $table
 <tr>
@@ -116,6 +126,8 @@ def test_jinja():
 
 def test_genshi():
     """Genshi Templates"""
+    if not have_genshi:
+        return
     stream = genshi_tmpl.generate(table=table)
     stream.render('html', strip_whitespace=False)
 
@@ -128,6 +140,8 @@ def test_kid():
 
 def test_cheetah():
     """Cheetah Templates"""
+    if not have_cheetah:
+        return
     cheetah_tmpl.respond()
 
 def test_mako():
index e52458c7966bb3aa3e1796ab6fd88404795f22c2..ad1e7b0d82a6dcfb680e59eb202ecbd8bab0bf3a 100644 (file)
@@ -7,6 +7,8 @@
     :license: BSD, see LICENSE for more details.
 """
 
+import time
+import tempfile
 from jinja import Environment, loaders
 from jinja.exceptions import TemplateNotFound
 
@@ -24,6 +26,10 @@ function_loader = loaders.FunctionLoader({'justfunction.html': 'FOO'}.get)
 choice_loader = loaders.ChoiceLoader([dict_loader, package_loader])
 
 
+class FakeLoader(loaders.BaseLoader):
+    local_attr = 42
+
+
 def test_dict_loader():
     env = Environment(loader=dict_loader)
     tmpl = env.get_template('justdict.html')
@@ -84,3 +90,57 @@ def test_function_loader():
         pass
     else:
         raise AssertionError('expected template exception')
+
+
+def test_loader_redirect():
+    env = Environment(loader=FakeLoader())
+    assert env.loader.local_attr == 42
+    assert env.loader.get_source
+    assert env.loader.load
+
+
+class MemcacheTestingLoader(loaders.CachedLoaderMixin, loaders.BaseLoader):
+
+    def __init__(self, enable):
+        loaders.CachedLoaderMixin.__init__(self, enable, 40, None, True, 'foo')
+        self.times = {}
+        self.idx = 0
+
+    def touch(self, name):
+        self.times[name] = time.time()
+
+    def get_source(self, environment, name, parent):
+        self.touch(name)
+        self.idx += 1
+        return 'Template %s (%d)' % (name, self.idx)
+
+    def check_source_changed(self, environment, name):
+        if name in self.times:
+            return self.times[name]
+        return -1
+
+
+memcache_env = Environment(loader=MemcacheTestingLoader(True))
+no_memcache_env = Environment(loader=MemcacheTestingLoader(False))
+
+
+test_memcaching = r'''
+>>> not_caching = MODULE.no_memcache_env.loader
+>>> caching = MODULE.memcache_env.loader
+>>> touch = caching.touch
+
+>>> tmpl1 = not_caching.load('test.html')
+>>> tmpl2 = not_caching.load('test.html')
+>>> tmpl1 == tmpl2
+False
+
+>>> tmpl1 = caching.load('test.html')
+>>> tmpl2 = caching.load('test.html')
+>>> tmpl1 == tmpl2
+True
+
+>>> touch('test.html')
+>>> tmpl2 = caching.load('test.html')
+>>> tmpl1 == tmpl2
+False
+'''
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644 (file)
index 0000000..d9d75c0
--- /dev/null
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+"""
+    unit test for the parser
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: 2007 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from jinja import Environment
+
+NO_VARIABLE_BLOCK = '''\
+{# i'm a freaking comment #}\
+{% if foo %}{% foo %}{% endif %}
+{% for item in seq %}{% item %}{% endfor %}
+{% trans foo %}foo is {% foo %}{% endtrans %}
+{% trans foo %}one foo{% pluralize %}{% foo %} foos{% endtrans %}'''
+
+PHP_SYNTAX = '''\
+<!-- I'm a comment, I'm not interesting -->\
+<? for item in seq -?>
+    <?= item ?>
+<?- endfor ?>'''
+
+ERB_SYNTAX = '''\
+<%# I'm a comment, I'm not interesting %>\
+<% for item in seq -%>
+    <%= item %>
+<%- endfor %>'''
+
+COMMENT_SYNTAX = '''\
+<!--# I'm a comment, I'm not interesting -->\
+<!-- for item in seq --->
+    ${item}
+<!--- endfor -->'''
+
+SMARTY_SYNTAX = '''\
+{* I'm a comment, I'm not interesting *}\
+{for item in seq-}
+    {item}
+{-endfor}'''
+
+
+def test_no_variable_block():
+    env = Environment('{%', '%}', None, None)
+    tmpl = env.from_string(NO_VARIABLE_BLOCK)
+    assert tmpl.render(foo=42, seq=range(2)).splitlines() == [
+        '42',
+        '01',
+        'foo is 42',
+        '42 foos'
+    ]
+
+
+def test_php_syntax():
+    env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->')
+    tmpl = env.from_string(PHP_SYNTAX)
+    assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_erb_syntax():
+    env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>')
+    tmpl = env.from_string(ERB_SYNTAX)
+    assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_comment_syntax():
+    env = Environment('<!--', '-->', '${', '}', '<!--#', '-->')
+    tmpl = env.from_string(COMMENT_SYNTAX)
+    assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_smarty_syntax():
+    env = Environment('{', '}', '{', '}', '{*', '*}')
+    tmpl = env.from_string(SMARTY_SYNTAX)
+    assert tmpl.render(seq=range(5)) == '01234'
diff --git a/tests/test_security.py b/tests/test_security.py
new file mode 100644 (file)
index 0000000..5e0099d
--- /dev/null
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+    unit test for security features
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: 2007 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+class PrivateStuff(object):
+    bar = lambda self: 23
+    foo = lambda self: 42
+    foo.jinja_unsafe_call = True
+
+
+class PublicStuff(object):
+    jinja_allowed_attributes = ['bar']
+    bar = lambda self: 23
+    foo = lambda self: 42
+
+
+test_unsafe = '''
+>>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PrivateStuff())
+u''
+>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PrivateStuff())
+u'23'
+
+>>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PublicStuff())
+u''
+>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PublicStuff())
+u'23'
+
+>>> env.from_string("{{ foo.__class__ }}").render(foo=42)
+u''
+
+>>> env.from_string("{{ foo.func_code }}").render(foo=lambda:None)
+u''
+'''
+
+
+test_restricted = '''
+>>> env.from_string("{% for item.attribute in seq %}...{% endfor %}")
+Traceback (most recent call last):
+    ...
+TemplateSyntaxError: can't assign to expression. (line 1)
+'''
diff --git a/tests/test_streaming.py b/tests/test_streaming.py
new file mode 100644 (file)
index 0000000..709105b
--- /dev/null
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+"""
+    unit test for streaming interface
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: 2007 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+test_basic_streaming = r"""
+>>> tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index "
+...                        "}} - {{ item }}</li>{%- endfor %}</ul>")
+>>> stream = tmpl.stream(seq=range(4))
+>>> stream.next()
+u'<ul>'
+>>> stream.next()
+u'<li>1 - 0</li>'
+>>> stream.next()
+u'<li>2 - 1</li>'
+>>> stream.next()
+u'<li>3 - 2</li>'
+>>> stream.next()
+u'<li>4 - 3</li>'
+>>> stream.next()
+u'</ul>'
+"""
+
+test_buffered_streaming = r"""
+>>> tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index "
+...                        "}} - {{ item }}</li>{%- endfor %}</ul>")
+>>> stream = tmpl.stream(seq=range(4))
+>>> stream.enable_buffering(size=3)
+>>> stream.next()
+u'<ul><li>1 - 0</li><li>2 - 1</li>'
+>>> stream.next()
+u'<li>3 - 2</li><li>4 - 3</li></ul>'
+"""
+
+test_streaming_behavior = r"""
+>>> tmpl = env.from_string("")
+>>> stream = tmpl.stream()
+>>> stream.buffered
+False
+>>> stream.enable_buffering(20)
+>>> stream.buffered
+True
+>>> stream.disable_buffering()
+>>> stream.buffered
+False
+"""