From fd31049f62b962181e0413b59bed9529b9df7b2b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 May 2008 00:16:51 +0200 Subject: [PATCH] all calls are proxied by context.call now so that we can inject environment and context as first arguments. This slows calls down a bit but is a lot more user friendly. Added first draft of FAQ --HG-- branch : trunk --- docs/faq.rst | 123 +++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/templates.rst | 15 ++++++ jinja2/compiler.py | 37 +++++-------- jinja2/nodes.py | 10 ++-- jinja2/parser.py | 1 + jinja2/runtime.py | 19 ++++--- jinja2/sandbox.py | 4 +- jinja2/utils.py | 6 +-- tests/test_syntax.py | 12 +++-- 10 files changed, 180 insertions(+), 48 deletions(-) create mode 100644 docs/faq.rst diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..4b4fab4 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,123 @@ +Frequently Asked Questions +========================== + +This page answers some of the often asked questions about Jinja. + +.. highlight:: html+jinja + + +Why is it called Jinja? +----------------------- + +The name Jinja was chosen because it's the name of a Japanese temple and +temple and template share a similar pronunciation. It is not named after +the capital city of Uganda. + +How fast is it? +--------------- + +We really hate benchmarks especially since they don't reflect much. The +performance of a template depends on many factors and you would have to +benchmark different engines in different situations. The benchmarks from the +testsuite show that Jinja2 has a similar performance to `Mako`_ and is more +than 20 times faster than Django's template engine or Genshi. These numbers +should be taken with tons of salt! + +.. _Mako: http://www.makotemplates.org/ + +How Compatible is Jinja2 with Django? +------------------------------------- + +The default syntax of Jinja2 matches Django syntax in many ways. However +this similarity doesn't mean that you can use a Django template unmodified +in Jinja2. For example filter arguments use a function call syntax rather +than a colon to separate filter name and arguments. Additionally the +extension interface in Jinja is fundamentally different from the Django one +which means that your custom tags won't work any longer. + +Generally speaking you will use much less custom extensions as the Jinja +template system allows you to use a certain subset of Python expressions +which can replace most Django extensions. For example instead of using +something like this:: + + {% load comments %} + {% get_latest_comments 10 as latest_comments %} + {% for comment in latest_comments %} + ... + {% endfor %} + +You will most likely provide an object with attributes to retrieve +comments from the database:: + + {% for comment in models.comments.latest(10) %} + ... + {% endfor %} + +Or directly provide the model for quick testing:: + + {% for comment in Comment.objects.order_by('-pub_date')[:10] %} + ... + {% endfor %} + +Please keep in mind that even though you may put such things into templates +it still isn't a good idea. Queries should go into the view code and now +the template! + +Isn't it a terrible idea to put Logic into Templates? +----------------------------------------------------- + +Without a doubt you should try to remove as much logic from templates as +possible. But templates without any logic mean that you have to do all +the processing in the code which is boring and stupid. A template engine +that does that is shipped with Python and called `string.Template`. Comes +without loops and if conditions and is by far the fastest template engine +you can get for Python. + +So some amount of logic is required in templates to keep everyone happy. +And Jinja leaves it pretty much to you how much logic you want to put into +templates. There are some restrictions in what you can do and what not. + +Jinja2 neither allows you to put arbitrary Python code into templates nor +does it allow all Python expressions. The operators are limited to the +most common ones and more advanced expressions such as list comprehensions +and generator expressions are not supported. This keeps the template engine +easier to maintain and templates more readable. + +Why is Autoescaping not the Default? +------------------------------------ + +There are multiple reasons why automatic escaping is not the default mode +and also not the recommended one. While automatic escaping of variables +means that you will less likely have an XSS problem it also causes a huge +amount of extra processing in the template engine which can cause serious +performance problems. As Python doesn't provide a way to mark strings as +unsafe Jinja has to hack around that limitation by providing a custom +string class (the :class:`Markup` string) that safely interacts with safe +and unsafe strings. + +With explicit escaping however the template engine doesn't have to perform +any safety checks on variables. Also a human knows not to escape integers +or strings that may never contain characters one has to escape or already +HTML markup. For example when iterating over a list over a table of +integers and floats for a table of statistics the template designer can +omit the escaping because he knows that integers or floats don't contain +any unsafe parameters. + +Additionally Jinja2 is a general purpose template engine and not only used +for HTML/XML generation. For example you may generate LaTeX, emails, +CSS, JavaScript, or configuration files. + +Why is the Context immutable? +----------------------------- + +When writing a :func:`contextfunction` or something similar you may have +noticed that the context tries to stop you from modifying it. If you have +managed to modify the context by using an internal context API you may +have noticed that changes in the context don't seem to be visible in the +template. The reason for this is that Jinja uses the context only as +primary data source for template variables for performance reasons. + +If you want to modify the context write a function that returns a variable +instead that one can assign to a variable by using set:: + + {% set comments = get_latest_comments() %} diff --git a/docs/index.rst b/docs/index.rst index 43f3680..27bee23 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ fast and secure. switching tricks + faq changelog If you can't find the information you're looking for, have a look at the diff --git a/docs/templates.rst b/docs/templates.rst index a95b874..1576142 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -326,6 +326,21 @@ This gives back the results of the parent block:: {% endblock %} +Named Block End-Tags +~~~~~~~~~~~~~~~~~~~~ + +Jinja2 allows you to put the name of the block after the end tag for better +readability:: + + {% block sidebar %} + {% block inner_sidebar %} + ... + {% endblock inner_sidebar %} + {% endblock sidebar %} + +However the name after the `endblock` word must match the block name. + + HTML Escaping ------------- diff --git a/jinja2/compiler.py b/jinja2/compiler.py index e43c362..83afc34 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -438,21 +438,13 @@ class CodeGenerator(NodeVisitor): self._write_debug_info = node.lineno self._last_line = node.lineno - def signature(self, node, frame, have_comma=True, extra_kwargs=None): + def signature(self, node, frame, extra_kwargs=None): """Writes a function call to the stream for the current node. - Per default it will write a leading comma but this can be - disabled by setting have_comma to False. The extra keyword + A leading comma is added automatically. The extra keyword arguments may not include python keywords otherwise a syntax error could occour. The extra keyword arguments should be given as python dict. """ - have_comma = have_comma and [True] or [] - def touch_comma(): - if have_comma: - self.write(', ') - else: - have_comma.append(True) - # if any of the given keyword arguments is a python keyword # we have to make sure that no invalid call is created. kwarg_workaround = False @@ -462,28 +454,25 @@ class CodeGenerator(NodeVisitor): break for arg in node.args: - touch_comma() + self.write(', ') self.visit(arg, frame) if not kwarg_workaround: for kwarg in node.kwargs: - touch_comma() + self.write(', ') self.visit(kwarg, frame) if extra_kwargs is not None: for key, value in extra_kwargs.iteritems(): - touch_comma() - self.write('%s=%s' % (key, value)) + self.write(', %s=%s' % (key, value)) if node.dyn_args: - touch_comma() - self.write('*') + self.write(', *') self.visit(node.dyn_args, frame) if kwarg_workaround: - touch_comma() if node.dyn_kwargs is not None: - self.write('**dict({') + self.write(', **dict({') else: - self.write('**{') + self.write(', **{') for kwarg in node.kwargs: self.write('%r: ' % kwarg.key) self.visit(kwarg.value, frame) @@ -499,8 +488,7 @@ class CodeGenerator(NodeVisitor): self.write('}') elif node.dyn_kwargs is not None: - touch_comma() - self.write('**') + self.write(', **') self.visit(node.dyn_kwargs, frame) def pull_locals(self, frame): @@ -1353,11 +1341,12 @@ class CodeGenerator(NodeVisitor): def visit_Call(self, node, frame, forward_caller=False): if self.environment.sandboxed: - self.write('environment.call(') + self.write('environment.call(context, ') + else: + self.write('context.call(') self.visit(node.node, frame) - self.write(self.environment.sandboxed and ', ' or '(') extra_kwargs = forward_caller and {'caller': 'caller'} or None - self.signature(node, frame, False, extra_kwargs) + self.signature(node, frame, extra_kwargs) self.write(')') def visit_Keyword(self, node, frame): diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 568220f..9eb5460 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -14,7 +14,6 @@ """ import operator from copy import copy -from types import FunctionType from itertools import chain, izip from collections import deque from jinja2.utils import Markup @@ -550,11 +549,10 @@ class Call(Expr): # don't evaluate context functions args = [x.as_const() for x in self.args] - if type(obj) is FunctionType: - if getattr(obj, 'contextfunction', False): - raise Impossible() - elif obj.environmentfunction: - args.insert(0, self.environment) + if getattr(obj, 'contextfunction', False): + raise Impossible() + elif getattr(obj, 'environmentfunction', False): + args.insert(0, self.environment) kwargs = dict(x.as_const() for x in self.kwargs) if self.dyn_args is not None: diff --git a/jinja2/parser.py b/jinja2/parser.py index 86ee570..8ca1bd2 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -150,6 +150,7 @@ class Parser(object): node = nodes.Block(lineno=self.stream.next().lineno) node.name = self.stream.expect('name').value node.body = self.parse_statements(('name:endblock',), drop_needle=True) + self.stream.skip_if('name:' + node.name) return node def parse_extends(self): diff --git a/jinja2/runtime.py b/jinja2/runtime.py index fb72ed4..1325b17 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -66,14 +66,6 @@ class Context(object): self.exported_vars = set() self.name = name - # bind functions to the context of environment if required - for name, obj in parent.iteritems(): - if type(obj) is FunctionType: - if getattr(obj, 'contextfunction', 0): - vars[name] = partial(obj, self) - elif getattr(obj, 'environmentfunction', 0): - vars[name] = partial(obj, environment) - # create the initial mapping of blocks. Whenever template inheritance # takes place the runtime will update this mapping with the new blocks # from the template. @@ -122,6 +114,17 @@ class Context(object): """ return dict(self.parent, **self.vars) + def call(__self, __obj, *args, **kwargs): + """Called by the template code to inject the current context + or environment as first arguments. Then forwards the call to + the object with the arguments and keyword arguments. + """ + if getattr(__obj, 'contextfunction', 0): + args = (__self,) + args + elif getattr(__obj, 'environmentfunction', 0): + args = (__self.environment,) + args + return __obj(*args, **kwargs) + def _all(meth): proxy = lambda self: getattr(self.get_all(), meth)() proxy.__doc__ = getattr(dict, meth).__doc__ diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py index c558c73..b0de8e7 100644 --- a/jinja2/sandbox.py +++ b/jinja2/sandbox.py @@ -190,13 +190,13 @@ class SandboxedEnvironment(Environment): ), name=argument, exc=SecurityError) return self.undefined(obj=obj, name=argument) - def call(__self, __obj, *args, **kwargs): + def call(__self, __context, __obj, *args, **kwargs): """Call an object from sandboxed code.""" # the double prefixes are to avoid double keyword argument # errors when proxying the call. if not __self.is_safe_callable(__obj): raise SecurityError('%r is not safely callable' % (__obj,)) - return __obj(*args, **kwargs) + return __context.call(__obj, *args, **kwargs) class ImmutableSandboxedEnvironment(SandboxedEnvironment): diff --git a/jinja2/utils.py b/jinja2/utils.py index 2a671d0..258961f 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -64,8 +64,8 @@ except TypeError, _error: def contextfunction(f): """This decorator can be used to mark a callable as context callable. A - context callable is passed the active context as first argument if it - was directly stored in the context. + context callable is passed the active context as first argument when + called from the template. """ f.contextfunction = True return f @@ -74,7 +74,7 @@ def contextfunction(f): def environmentfunction(f): """This decorator can be used to mark a callable as environment callable. A environment callable is passed the current environment as first argument - if it was directly stored in the context. + when called from the template. """ f.environmentfunction = True return f diff --git a/tests/test_syntax.py b/tests/test_syntax.py index b76fe5f..717e165 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -6,6 +6,7 @@ :copyright: 2007 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from py.test import raises from jinja2 import Environment, DictLoader from jinja2.exceptions import TemplateSyntaxError @@ -143,11 +144,7 @@ def test_function_calls(env): ] for should_fail, sig in tests: if should_fail: - try: - print env.from_string('{{ foo(%s) }}' % sig) - except TemplateSyntaxError: - continue - assert False, 'expected syntax error' + raises(TemplateSyntaxError, env.from_string, '{{ foo(%s) }}' % sig) else: env.from_string('foo(%s)' % sig) @@ -161,3 +158,8 @@ def test_tuple_expr(env): def test_trailing_comma(env): tmpl = env.from_string(TRAILINGCOMMA) assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}' + + +def test_block_end_name(env): + env.from_string('{% block foo %}...{% endblock foo %}') + raises(TemplateSyntaxError, env.from_string, '{% block x %}{% endblock y %}') -- 2.26.2