From ccf284bd2fdd400588aa9d9df3ca929917f4a162 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 May 2007 16:44:26 +0200 Subject: [PATCH] [svn] added many new tests to jinja --HG-- branch : trunk --- CHANGES | 3 +- Makefile | 3 ++ docs/generate.py | 2 +- docs/src/altsyntax.txt | 7 +++- docs/src/designerdoc.txt | 11 +++--- docs/src/devrecipies.txt | 11 +++--- docs/src/objects.txt | 24 ++++++++++++ jinja/_native.py | 39 +++++++++++++------ jinja/datastructure.py | 2 +- jinja/lexer.py | 2 +- jinja/loaders.py | 17 ++++++--- jinja/parser.py | 6 ++- jinja/translators/python.py | 40 +++++++------------ tests/conftest.py | 60 +++++++++++++++++++++++++++-- tests/runtime/bigtable.py | 24 +++++++++--- tests/test_loaders.py | 60 +++++++++++++++++++++++++++++ tests/test_parser.py | 76 +++++++++++++++++++++++++++++++++++++ tests/test_security.py | 47 +++++++++++++++++++++++ tests/test_streaming.py | 51 +++++++++++++++++++++++++ 19 files changed, 414 insertions(+), 71 deletions(-) create mode 100644 tests/test_parser.py create mode 100644 tests/test_security.py create mode 100644 tests/test_streaming.py diff --git a/CHANGES b/CHANGES index 0c0fd47..bda176c 100644 --- 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. diff --git a/Makefile b/Makefile index ca294d9..8f40d47 100644 --- 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) diff --git a/docs/generate.py b/docs/generate.py index 8acf5b7..f8e8238 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -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') diff --git a/docs/src/altsyntax.txt b/docs/src/altsyntax.txt index 088445c..9fb3b64 100644 --- a/docs/src/altsyntax.txt +++ b/docs/src/altsyntax.txt @@ -46,7 +46,7 @@ An example template then looks like this: @@ -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. diff --git a/docs/src/designerdoc.txt b/docs/src/designerdoc.txt index ba5de71..d48dde2 100644 --- a/docs/src/designerdoc.txt +++ b/docs/src/designerdoc.txt @@ -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 ============= diff --git a/docs/src/devrecipies.txt b/docs/src/devrecipies.txt index ae70226..7f5b6c0 100644 --- a/docs/src/devrecipies.txt +++ b/docs/src/devrecipies.txt @@ -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`. diff --git a/docs/src/objects.txt b/docs/src/objects.txt index b1f86e4..7b0641f 100644 --- a/docs/src/objects.txt +++ b/docs/src/objects.txt @@ -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 '

%s

%s
' % (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 =============== diff --git a/jinja/_native.py b/jinja/_native.py index ab7d1d3..9fa723f 100644 --- a/jinja/_native.py +++ b/jinja/_native.py @@ -14,6 +14,20 @@ :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 diff --git a/jinja/datastructure.py b/jinja/datastructure.py index 3b8e448..daa2e27 100644 --- a/jinja/datastructure.py +++ b/jinja/datastructure.py @@ -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): diff --git a/jinja/lexer.py b/jinja/lexer.py index e948aa0..7b98a57 100644 --- a/jinja/lexer.py +++ b/jinja/lexer.py @@ -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): diff --git a/jinja/loaders.py b/jinja/loaders.py index 0bebdc3..15d04e6 100644 --- a/jinja/loaders.py +++ b/jinja/loaders.py @@ -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) diff --git a/jinja/parser.py b/jinja/parser.py index b3e1ba1..befcbc5 100644 --- a/jinja/parser.py +++ b/jinja/parser.py @@ -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, diff --git a/jinja/translators/python.py b/jinja/translators/python.py index 7b85d78..12a829a 100644 --- a/jinja/translators/python.py +++ b/jinja/translators/python.py @@ -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): diff --git a/tests/conftest.py b/tests/conftest.py index 651aca8..e97e30c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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)) diff --git a/tests/runtime/bigtable.py b/tests/runtime/bigtable.py index 5010061..b1576df 100644 --- a/tests/runtime/bigtable.py +++ b/tests/runtime/bigtable.py @@ -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("""
@@ -78,7 +87,8 @@ jinja_tmpl = Environment().from_string('''
''') -cheetah_tmpl = CheetahTemplate(''' +if have_cheetah: + cheetah_tmpl = CheetahTemplate(''' #for $row in $table @@ -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(): diff --git a/tests/test_loaders.py b/tests/test_loaders.py index e52458c..ad1e7b0 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -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 index 0000000..d9d75c0 --- /dev/null +++ b/tests/test_parser.py @@ -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 = '''\ +\ + + +''' + +ERB_SYNTAX = '''\ +<%# I'm a comment, I'm not interesting %>\ +<% for item in seq -%> + <%= item %> +<%- endfor %>''' + +COMMENT_SYNTAX = '''\ +\ + + ${item} +''' + +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 index 0000000..5e0099d --- /dev/null +++ b/tests/test_security.py @@ -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 index 0000000..709105b --- /dev/null +++ b/tests/test_streaming.py @@ -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("") +>>> stream = tmpl.stream(seq=range(4)) +>>> stream.next() +u'' +""" + +test_buffered_streaming = r""" +>>> tmpl = env.from_string("") +>>> stream = tmpl.stream(seq=range(4)) +>>> stream.enable_buffering(size=3) +>>> stream.next() +u'' +""" + +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 +""" -- 2.26.2