From 5e4933d8e0c9c5424872ed789ad999229c00bbb0 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 24 Jul 2007 00:30:47 +0000 Subject: Handle unicode/str more nicely/properly. Better error if you pass a non-dict in --- docs/index.txt | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++ tempita/__init__.py | 36 ++++++++--- 2 files changed, 209 insertions(+), 9 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index def94a8..f19e8d5 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -1,6 +1,8 @@ Tempita +++++++ +.. contents:: + :author: Ian Bicking Status & License @@ -13,3 +15,183 @@ seek to take over the templating world, or adopt many new features. I just wanted a small templating language for cases when ``%`` and ``string.Template`` weren't enough. +Why Another Templating Language +=============================== + +Surely the world has enough templating languages? So why did I write +another. + +I initially used `Cheetah `_ as the +templating language for `Paste Script +`_, but this caused quite a few +problems. People frequently had problems installing Cheetah because +it includes a C extension. Also, the errors and invocation can be a +little confusing. This might be okay for something that used +Cheetah's features extensively, except that the templating was a very +minor feature of the system, and many people didn't even understand or +care about where templating came into the system. + +At the same time, I was starting to create reusable WSGI components +that had some templating in them. Not a lot of templating, but enough +that ``string.Template`` had become too complicated -- I need if +statements and loops. + +Given this, I started looking around for a very small templating +language, and I didn't like anything I found. Many of them seemed +awkward or like toys that were more about the novelty of the +implementation than the utility of the language. + +So one night when I felt like coding but didn't feel like working on +anything I was already working on, I wrote this. It was first called +``paste.util.template``, but I decided it deserved a life of its own, +hence Tempita. + +The Interface +============= + +The interface is intended to look a lot like ``string.Template``. You +can create a template object like:: + + >>> import tempita + >>> tmpl = tempita.Template("""Hello {{name}}""") + >>> tmpl.substitute(name='Bob') + 'Hello Bob' + +Or if you want to skip the class:: + + >>> tempita.sub("Hello {{name}}", name='Alice') + 'Hello Alice' + +Note that the language allows arbitrary Python to be executed, so +your templates must be trusted. + +You can give a name to your template, which is handy when there is an +error (the name will be displayed):: + + >>> tmpl = tempita.Template('Hi {{name}}', name='tmpl') + >>> tmpl.substitute() + Traceback (most recent call last): + ... + NameError: name 'name' is not defined at line 1 column 6 in file tmpl + +You can also give a namespace to use by default, which +``.substitute(...)`` will augment:: + + >>> tmpl = tempita.Template( + ... 'Hi {{upper(name)}}', + ... namespace=dict(upper=lambda s: s.upper())) + >>> tmpl.substitute(name='Joe') + 'Hi JOE' + +Lastly, you can give a dictionary-like object as the argument to +``.substitute``, like:: + + >>> name = 'Jane' + >>> tmpl.substitute(locals()) + 'Hi JANE' + +There's also an `HTMLTemplate`_ class that is more appropriate for +templates that produce HTML. + +Unicode +------- + +Tempita tries to handle unicode gracefully, for some value of +"graceful". ``Template`` objects have a ``default_encoding`` +attribute. It will try to use that encoding whenever ``unicode`` and +``str`` objects are mixed in the template. E.g.:: + + >>> tmpl = tempita.Template(u'Hi {{name}}') + >>> tmpl.substitute(name='Jos\xc3\xa9') + u'Hi Jos\xe9' + >>> tmpl = tempita.Template('Hi {{name}}') + >>> tmpl.substitute(name=u'Jos\xe9') + 'Hi Jos\xc3\xa9' + +The Language +============ + +The language is fairly simple; all the constructs look like +``{{stuff}}``. + +To insert a variable or expression, use ``{{expression}}``. You can't +use ``}}`` in your expression, but if it comes up just use ``} }`` +(put a space between them). You can pass your expression through +*filters* with ``{{expression | filter}}``, for instance +``{{expression | repr}}``. This is entirely equivalent to +``{{repr(expression)}}``. But it might look nicer to some people; I +took it from Django because I liked it. There's a shared namespace, +so ``repr`` is just an object in the namespace. + +If you want to have ``{{`` or ``}}`` in your template, you must use +the built-in variables like ``{{start_braces}}`` and +``{{end_braces}}``. There's no escape character. + +You can do an if statement with:: + + {{if condition}} + true stuff + {{elif other_condition}} + other stuff + {{else}} + final stuff + {{endif}} + +Some of the blank lines will be removed when, as in this case, they +only contain a single directive. A trailing ``:`` is optional. + +Loops should be unsurprising:: + + {{for a, b in items}} + {{a}} = {{b | repr}} + {{endfor}} + +For anything more complicated, you can use blocks of Python code, +like:: + + {{py:x = 1}} + + {{py: + lots of code + }} + +The first form allows statements, like an assignment or raising an +exception. The second form is for multiple lines. If you have +multiple lines, then ``{{py:`` must be on a line of its own and the +code can't be indented (except for normal indenting in ``def x():`` +etc). + +These can't output any values, but they can calculate values and +define functions. So you can do something like:: + + {{py: + def pad(s): + return s + ' '*(20-len(s)) + }} + {{for name, value in kw.items()}} + {{s | pad}} {{value | repr}} + {{endfor}} + +The last construct is for setting defaults in your template, like:: + + {{default width = 100}} + +You can use this so that the ``width`` variable will always have a +value in your template (the number ``100``). If someone calls +``tmpl.substitute(width=200)`` then this will have no effect; only if +the variable is undefined will this default matter. + +As a last detail ``{{# comments...}}`` doesn't do anything at all, +because it is a comment. + +Still To Do +=========== + +Currently nested structures in ``for`` loop assignments don't work, +like ``for (a, b), c in x``. They should. + +There's no way to handle exceptions, except in your ``py:`` code. I'm +not sure what there should be. + +Probably I should try to dedent ``py:`` code. + diff --git a/tempita/__init__.py b/tempita/__init__.py index 1e42b5a..7f24a70 100644 --- a/tempita/__init__.py +++ b/tempita/__init__.py @@ -1,10 +1,9 @@ """ A small templating language -This implements a small templating language for use internally in -Paste and Paste Script. This language implements if/elif/else, -for/continue/break, expressions, and blocks of Python code. The -syntax is:: +This implements a small templating language. This language implements +if/elif/else, for/continue/break, expressions, and blocks of Python +code. The syntax is:: {{any expression (function calls etc)}} {{any expression | filter}} @@ -106,7 +105,11 @@ class Template(object): "You can only give positional *or* keyword arguments") if len(args) > 1: raise TypeError( - "You can only give on positional argument") + "You can only give one positional argument") + if not hasattr(args[0], 'items'): + raise TypeError( + "If you pass in a single argument, you must pass in a dictionary-like object (with a .items() method); you gave %r" + % (args[0],)) kw = args[0] ns = self.default_namespace.copy() ns.update(self.namespace) @@ -230,7 +233,14 @@ class Template(object): except UnicodeDecodeError: value = str(value) else: - value = str(value) + if not isinstance(value, basestring): + if hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + if (isinstance(value, unicode) + and self.default_encoding): + value = value.encode(self.default_encoding) except: exc_info = sys.exc_info() e = exc_info[1] @@ -238,13 +248,21 @@ class Template(object): raise exc_info[0], e, exc_info[2] else: if self._unicode and isinstance(value, str): - if not self.decode_encoding: + if not self.default_encoding: raise UnicodeDecodeError( 'Cannot decode str value %r into unicode ' '(no default_encoding provided)' % value) - value = value.decode(self.default_encoding) + try: + value = value.decode(self.default_encoding) + except UnicodeDecodeError, e: + raise UnicodeDecodeError( + e.encoding, + e.object, + e.start, + e.end, + e.reason + ' in string %r' % value) elif not self._unicode and isinstance(value, unicode): - if not self.decode_encoding: + if not self.default_encoding: raise UnicodeEncodeError( 'Cannot encode unicode value %r into str ' '(no default_encoding provided)' % value) -- cgit v1.2.1