From: Armin Ronacher Date: Sun, 20 Apr 2008 11:11:43 +0000 (+0200) Subject: added first code for parser extensions and moved some code in speedups around X-Git-Tag: 2.0rc1~144 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=f59bac20dff640f8b4156b1a4e4331ea32cbd9af;p=jinja2.git added first code for parser extensions and moved some code in speedups around --HG-- branch : trunk --- diff --git a/examples/translate.py b/examples/translate.py index 9043a21..0169c88 100644 --- a/examples/translate.py +++ b/examples/translate.py @@ -1,7 +1,6 @@ from jinja2 import Environment -print Environment().from_string("""\ -{{ foo.bar }} +print Environment(parser_extensions=['jinja2.i18n.trans']).from_string("""\ {% trans %}Hello {{ user }}!{% endtrans %} {% trans count=users|count %}{{ count }} user{% pluralize %}{{ count }} users{% endtrans %} """).render(user="someone") diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 9099a21..12c147e 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -60,4 +60,5 @@ from jinja2.environment import Environment from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \ DictLoader from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined +from jinja2.filters import environmentfilter, contextfilter from jinja2.utils import Markup, escape diff --git a/jinja2/_speedups.c b/jinja2/_speedups.c index 36a1404..10ae6f6 100644 --- a/jinja2/_speedups.c +++ b/jinja2/_speedups.c @@ -46,19 +46,19 @@ escape_unicode(PyUnicodeObject *in) switch (*inp++) { case '&': len += 5; - erepl++; + ++erepl; break; case '"': len += 6; - erepl++; + ++erepl; break; case '<': case '>': len += 4; - erepl++; + ++erepl; break; default: - len++; + ++len; } /* Do we need to escape anything at all? */ @@ -84,22 +84,22 @@ escape_unicode(PyUnicodeObject *in) case '&': Py_UNICODE_COPY(outp, amp, 5); outp += 5; - repl++; + ++repl; break; case '"': Py_UNICODE_COPY(outp, qt, 6); outp += 6; - repl++; + ++repl; break; case '<': Py_UNICODE_COPY(outp, lt, 4); outp += 4; - repl++; + ++repl; break; case '>': Py_UNICODE_COPY(outp, gt, 4); outp += 4; - repl++; + ++repl; break; default: *outp++ = *inp; @@ -112,17 +112,25 @@ escape_unicode(PyUnicodeObject *in) static PyObject* -escape(PyObject *self, PyObject *args) +soft_unicode(PyObject *self, PyObject *s) { - PyObject *text = NULL, *s = NULL, *rv = NULL; - if (!PyArg_UnpackTuple(args, "escape", 1, 1, &text)) - return NULL; + if (!PyUnicode_Check(s)) + return PyObject_Unicode(s); + Py_INCREF(s); + return s; +} + + +static PyObject* +escape(PyObject *self, PyObject *text) +{ + PyObject *s = NULL, *rv = NULL; /* we don't have to escape integers, bools or floats */ if (PyInt_CheckExact(text) || PyLong_CheckExact(text) || PyFloat_CheckExact(text) || PyBool_Check(text) || text == Py_None) { - args = PyTuple_New(1); + PyObject *args = PyTuple_New(1); if (!args) { Py_DECREF(s); return NULL; @@ -152,15 +160,7 @@ escape(PyObject *self, PyObject *args) s = escape_unicode((PyUnicodeObject*)text); /* convert the unicode string into a markup object. */ - args = PyTuple_New(1); - if (!args) { - Py_DECREF(s); - return NULL; - } - PyTuple_SET_ITEM(args, 0, (PyObject*)s); - rv = PyObject_CallObject(markup, args); - Py_DECREF(args); - return rv; + return PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL); } @@ -192,11 +192,15 @@ tb_set_next(PyObject *self, PyObject *args) static PyMethodDef module_methods[] = { - {"escape", (PyCFunction)escape, METH_VARARGS, + {"escape", (PyCFunction)escape, METH_O, "escape(s) -> string\n\n" "Convert the characters &, <, >, and \" in string s to HTML-safe\n" "sequences. Use this if you need to display text that might contain\n" "such characters in HTML."}, + {"soft_unicode", (PyCFunction)soft_unicode, METH_O, + "soft_unicode(object) -> string\n\n" + "Make a string unicode if it isn't already. That way a markup\n" + "string is not converted back to unicode."}, {"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS, "Set the tb_next member of a traceback object."}, {NULL, NULL, 0, NULL} /* Sentinel */ diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 871728f..f5e0b48 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -280,6 +280,12 @@ class CodeGenerator(NodeVisitor): # the current indentation self._indentation = 0 + def get_visitor(self, node): + """Custom nodes have their own compilation method.""" + if isinstance(node, nodes.CustomStmt): + return node.compile + return NodeVisitor.get_visitor(self, node) + def temporary_identifier(self): """Get a new unique identifier.""" self._last_identifier += 1 diff --git a/jinja2/datastructure.py b/jinja2/datastructure.py index 6b684c4..9dab02b 100644 --- a/jinja2/datastructure.py +++ b/jinja2/datastructure.py @@ -13,9 +13,6 @@ from collections import deque from jinja2.exceptions import TemplateSyntaxError, TemplateRuntimeError -_missing = object() - - class Token(tuple): """ Token class. @@ -32,7 +29,27 @@ class Token(tuple): return self.type elif self.type in reverse_operators: return reverse_operators[self.type] - return self.value + return '%s:%s' % (self.type, self.value) + + def test(self, expr): + """Test a token against a token expression. This can either be a + token type or 'token_type:token_value'. This can only test against + string values! + """ + # here we do a regular string equality check as test_many is usually + # passed an iterable of not interned strings. + if self.type == expr: + return True + elif ':' in expr: + return expr.split(':', 1) == [self.type, self.value] + return False + + def test_many(self, iterable): + """Test against multiple token expressions.""" + for expr in iterable: + if self.test(expr): + return True + return False def __repr__(self): return 'Token(%r, %r, %r)' % ( @@ -127,17 +144,11 @@ class TokenStream(object): self.current = Token(self.current.lineno, 'eof', '') self._next = None - def expect(self, token_type, token_value=_missing): + def expect(self, expr): """Expect a given token type and return it""" - if self.current.type is not token_type: + if not self.current.test(expr): raise TemplateSyntaxError("expected token %r, got %r" % - (token_type, self.current.type), - self.current.lineno, - self.filename) - elif token_value is not _missing and \ - self.current.value != token_value: - raise TemplateSyntaxError("expected %r, got %r" % - (token_value, self.current.value), + (expr, self.current), self.current.lineno, self.filename) try: diff --git a/jinja2/environment.py b/jinja2/environment.py index 35b01e7..004ef74 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -10,11 +10,12 @@ """ import sys from jinja2.lexer import Lexer -from jinja2.parser import Parser +from jinja2.parser import Parser, ParserExtension from jinja2.optimizer import optimize from jinja2.compiler import generate from jinja2.runtime import Undefined from jinja2.debug import translate_exception +from jinja2.utils import import_string from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE @@ -43,6 +44,7 @@ class Environment(object): optimized=True, undefined=Undefined, loader=None, + parser_extensions=(), finalize=unicode): """Here the possible initialization parameters: @@ -68,6 +70,10 @@ class Environment(object): `undefined` a subclass of `Undefined` that is used to represent undefined variables. `loader` the loader which should be used. + `parser_extensions` List of parser extensions to use. + `finalize` A callable that finalizes the variable. Per + default this is `unicode`, other useful + builtin finalizers are `escape`. ========================= ============================================ """ @@ -87,6 +93,14 @@ class Environment(object): self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.trim_blocks = trim_blocks + self.parser_extensions = {} + for extension in parser_extensions: + if isinstance(extension, basestring): + extension = import_string(extension) + self.parser_extensions[extension.tag] = extension + + + # runtime information self.undefined = undefined self.optimized = optimized self.finalize = finalize diff --git a/jinja2/filters.py b/jinja2/filters.py index 2fc1b30..d48ac94 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -44,6 +44,13 @@ def environmentfilter(f): return f +def do_forceescape(value): + """Enforce HTML escaping. This will probably double escape variables.""" + if hasattr(value, '__html__'): + value = value.__html__() + return escape(unicode(value)) + + def do_replace(s, old, new, count=None): """Return a copy of the value with all occurrences of a substring replaced with a new one. The first argument is the substring @@ -383,6 +390,7 @@ def do_int(value, default=0): try: return int(value) except (TypeError, ValueError): + # this quirk is necessary so that "42.23"|int gives 42. try: return int(float(value)) except (TypeError, ValueError): @@ -423,7 +431,9 @@ def do_trim(value): def do_striptags(value): """Strip SGML/XML tags and replace adjacent whitespace by one space. """ - return ' '.join(_striptags_re.sub('', value).split()) + if hasattr(value, '__html__'): + value = value.__html__() + return u' '.join(_striptags_re.sub('', value).split()) def do_slice(value, slices, fill_with=None): @@ -571,7 +581,7 @@ FILTERS = { 'lower': do_lower, 'escape': escape, 'e': escape, - 'xmlattr': do_xmlattr, + 'forceescape': do_forceescape, 'capitalize': do_capitalize, 'title': do_title, 'default': do_default, diff --git a/jinja2/i18n.py b/jinja2/i18n.py index b08ccf3..026750b 100644 --- a/jinja2/i18n.py +++ b/jinja2/i18n.py @@ -8,14 +8,82 @@ :copyright: Copyright 2008 by Armin Ronacher. :license: BSD. """ +from collections import deque from jinja2 import nodes -from jinja2.parser import _statement_end_tokens +from jinja2.environment import Environment +from jinja2.parser import ParserExtension, statement_end_tokens from jinja2.exceptions import TemplateAssertionError +# the only real useful gettext functions for a Jinja template. Note +# that ugettext must be assigned to gettext as Jinja doesn't support +# non unicode strings. +GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') + + +def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS): + """Extract localizable strings from the given template node. + + For every string found this function yields a ``(lineno, function, + message)`` tuple, where: + + * ``lineno`` is the number of the line on which the string was found, + * ``function`` is the name of the ``gettext`` function used (if the + string was extracted from embedded Python code), and + * ``message`` is the string itself (a ``unicode`` object, or a tuple + of ``unicode`` objects for functions with multiple string arguments). + """ + for call in node.find_all(nodes.Call): + if not isinstance(node.node, nodes.Name) or \ + node.node.name not in gettext_functions: + continue + + strings = [] + for arg in node.args: + if isinstance(arg, nodes.Const) and \ + isinstance(arg.value, basestring): + strings.append(arg) + else: + strings.append(None) + + if len(strings) == 1: + strings = strings[0] + else: + strings = tuple(strings) + yield node.lineno, node.node.name, strings + + +def babel_extract(fileobj, keywords, comment_tags, options): + """Babel extraction method for Jinja templates. + + :param fileobj: the file-like object the messages should be extracted from + :param keywords: a list of keywords (i.e. function names) that should be + recognized as translation functions + :param comment_tags: a list of translator tags to search for and include + in the results. (Unused) + :param options: a dictionary of additional options (optional) + :return: an iterator over ``(lineno, funcname, message, comments)`` tuples. + (comments will be empty currently) + """ + encoding = options.get('encoding', 'utf-8') + environment = Environment( + options.get('block_start_string', '{%'), + options.get('block_end_string', '%}'), + options.get('variable_start_string', '{{'), + options.get('variable_end_string', '}}'), + options.get('comment_start_string', '{#'), + options.get('comment_end_string', '#}'), + options.get('line_statement_prefix') or None, + options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true') + ) + node = environment.parse(fileobj.read().decode(encoding)) + for lineno, func, message in extract_from_ast(node, keywords): + yield lineno, func, message, [] + + def parse_trans(parser): """Parse a translatable tag.""" - lineno = parser.stream.expect('trans').lineno + lineno = parser.stream.next().lineno # skip colon for python compatibility if parser.stream.current.type is 'colon': @@ -57,7 +125,7 @@ def parse_trans(parser): plural_expr = nodes.Name(singular_names[0], 'load') # if we have a pluralize block, we parse that too - if parser.stream.current.type is 'pluralize': + if parser.stream.current.test('name:pluralize'): have_plural = True parser.stream.next() if parser.stream.current.type is not 'block_end': @@ -114,9 +182,9 @@ def _parse_block(parser, allow_pluralize): parser.stream.expect('variable_end') elif parser.stream.current.type is 'block_begin': parser.stream.next() - if parser.stream.current.type is 'endtrans': + if parser.stream.current.test('name:endtrans'): break - elif parser.stream.current.type is 'pluralize': + elif parser.stream.current.test('name:pluralize'): if allow_pluralize: break raise TemplateSyntaxError('a translatable section can have ' @@ -134,7 +202,9 @@ def _parse_block(parser, allow_pluralize): def _make_node(singular, plural, variables, plural_expr): - """Generates a useful node from the data provided.""" + """Generates a useful node from the data provided. The nodes won't have + a line number information so the caller must set it via `set_lineno` later. + """ # singular only: if plural_expr is None: gettext = nodes.Name('gettext', 'load') @@ -154,3 +224,6 @@ def _make_node(singular, plural, variables, plural_expr): if variables: node = nodes.Mod(node, variables) return nodes.Output([node]) + + +trans = ParserExtension('trans', parse_trans) diff --git a/jinja2/lexer.py b/jinja2/lexer.py index 931d7c1..364d3f5 100644 --- a/jinja2/lexer.py +++ b/jinja2/lexer.py @@ -38,9 +38,9 @@ float_re = re.compile(r'\d+\.\d+') # set of used keywords keywords = set(['and', 'block', 'elif', 'else', 'endblock', 'print', 'endfilter', 'endfor', 'endif', 'endmacro', 'endraw', - 'endtrans', 'extends', 'filter', 'for', 'if', 'in', - 'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw', - 'recursive', 'set', 'trans', 'call', 'endcall']) + 'extends', 'filter', 'for', 'if', 'in', + 'include', 'is', 'macro', 'not', 'or', 'raw', + 'recursive', 'set', 'call', 'endcall']) # bind operators to token types operators = { diff --git a/jinja2/nodes.py b/jinja2/nodes.py index c579395..8c58959 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -63,7 +63,19 @@ class NodeType(type): storage.extend(d.get(attr, ())) assert len(storage) == len(set(storage)) d[attr] = tuple(storage) - return type.__new__(cls, name, bases, d) + rv = type.__new__(cls, name, bases, d) + + # unless the node is a subclass of `CustomNode` it may not + # be defined in any other module than the jinja2.nodes module. + # the reason for this is that the we don't want users to take + # advantage of the fact that the parser is using the node name + # only as callback name for non custom nodes. This could lead + # to broken code in the future and is disallowed because of this. + if rv.__module__ != 'jinja2.nodes' and not \ + isinstance(rv, CustomStmt): + raise TypeError('non builtin node %r is not a subclass of ' + 'CustomStmt.' % node.__class__.__name__) + return rv class Node(object): @@ -193,6 +205,15 @@ class Helper(Node): """Nodes that exist in a specific context only.""" +class CustomStmt(Stmt): + """Custom statements must extend this node.""" + + def compile(self, compiler): + """The compiler calls this method to get the python sourcecode + for the statement. + """ + + class Template(Node): """Node that represents a template.""" fields = ('body',) diff --git a/jinja2/parser.py b/jinja2/parser.py index 8b82a4d..4204c6a 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -8,14 +8,15 @@ :copyright: 2008 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from operator import itemgetter from jinja2 import nodes from jinja2.exceptions import TemplateSyntaxError _statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print', - 'macro', 'include', 'trans']) + 'macro', 'include']) _compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq', 'in']) -_statement_end_tokens = set(['elif', 'else', 'endblock', 'endfilter', +statement_end_tokens = set(['elif', 'else', 'endblock', 'endfilter', 'endfor', 'endif', 'endmacro', 'variable_end', 'in', 'recursive', 'endcall', 'block_end']) @@ -34,12 +35,13 @@ class Parser(object): self.filename = filename self.closed = False self.stream = environment.lexer.tokenize(source, filename) + self.extensions = environment.parser_extensions def end_statement(self): """Make sure that the statement ends properly.""" if self.stream.current.type is 'semicolon': self.stream.next() - elif self.stream.current.type not in _statement_end_tokens: + elif self.stream.current.type not in statement_end_tokens: raise TemplateSyntaxError('ambigous end of statement', self.stream.current.lineno, self.filename) @@ -53,6 +55,10 @@ class Parser(object): return self.parse_call_block() elif token_type is 'filter': return self.parse_filter_block() + elif token_type is 'name': + ext = self.extensions.get(self.stream.current.value) + if ext is not None: + return ext(self) lineno = self.stream.current expr = self.parse_tuple() if self.stream.current.type == 'assign': @@ -75,10 +81,9 @@ class Parser(object): return nodes.Assign(target, expr, lineno=lineno) def parse_statements(self, end_tokens, drop_needle=False): - """ - Parse multiple statements into a list until one of the end tokens - is reached. This is used to parse the body of statements as it - also parses template data if appropriate. + """Parse multiple statements into a list until one of the end tokens + is reached. This is used to parse the body of statements as it also + parses template data if appropriate. """ # the first token may be a colon for python compatibility if self.stream.current.type is 'colon': @@ -89,7 +94,7 @@ class Parser(object): result = self.subparse(end_tokens) else: result = [] - while self.stream.current.type not in end_tokens: + while not self.stream.current.test_many(end_tokens): if self.stream.current.type is 'block_end': self.stream.next() result.extend(self.subparse(end_tokens)) @@ -227,20 +232,13 @@ class Parser(object): def parse_print(self): node = nodes.Output(lineno=self.stream.expect('print').lineno) node.nodes = [] - while self.stream.current.type not in _statement_end_tokens: + while self.stream.current.type not in statement_end_tokens: if node.nodes: self.stream.expect('comma') node.nodes.append(self.parse_expression()) self.end_statement() return node - def parse_trans(self): - """Parse a translatable section.""" - # lazily imported because we don't want the i18n overhead - # if it's not used. (Even though the overhead is low) - from jinja2.i18n import parse_trans - return parse_trans(self) - def parse_expression(self, no_condexpr=False): """Parse an expression.""" if no_condexpr: @@ -442,7 +440,7 @@ class Parser(object): while 1: if args: self.stream.expect('comma') - if self.stream.current.type in _statement_end_tokens: + if self.stream.current.type in statement_end_tokens: break args.append(parse()) if self.stream.current.type is not 'comma': @@ -662,7 +660,7 @@ class Parser(object): elif token.type is 'variable_begin': self.stream.next() want_comma = False - while self.stream.current.type not in _statement_end_tokens: + while not self.stream.current.test_many(statement_end_tokens): if want_comma: self.stream.expect('comma') add_data(self.parse_expression()) @@ -672,7 +670,7 @@ class Parser(object): flush_data() self.stream.next() if end_tokens is not None and \ - self.stream.current.type in end_tokens: + self.stream.current.test_many(end_tokens): return body while self.stream.current.type is not 'block_end': body.append(self.parse_statement()) @@ -688,3 +686,20 @@ class Parser(object): result = nodes.Template(self.subparse(), lineno=1) result.set_environment(self.environment) return result + + +class ParserExtension(tuple): + """Instances of this class store parser extensions.""" + __slots__ = () + + def __new__(cls, tag, parse_func): + return tuple.__new__(cls, (tag, parse_func)) + + def __call__(self, parser): + return self.parse_func(parser) + + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, self.tag) + + tag = property(itemgetter(0)) + parse_func = property(itemgetter(1)) diff --git a/jinja2/runtime.py b/jinja2/runtime.py index e0630cc..d7de80e 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -108,14 +108,8 @@ class IncludedTemplate(object): self._rendered_body = u''.join(gen) self._context = context.get_exported() - def __getitem__(self, name): - return self._context[name] - - def __unicode__(self): - return self._rendered_body - - def __html__(self): - return self._rendered_body + __getitem__ = lambda x, n: x._context[n] + __html__ = __unicode__ = lambda x: x._rendered_body def __repr__(self): return '<%s %r>' % ( diff --git a/jinja2/tests.py b/jinja2/tests.py index ab7992d..d62ffeb 100644 --- a/jinja2/tests.py +++ b/jinja2/tests.py @@ -26,6 +26,11 @@ def test_even(value): return value % 2 == 0 +def test_divisibleby(value, num): + """Check if a variable is divisible by a number.""" + return value % num == 0 + + def test_defined(value): """Return true if the variable is defined: @@ -78,7 +83,7 @@ def test_sameas(value, other): .. sourcecode:: jinja - {% if foo.attribute is sameas(false) %} + {% if foo.attribute is sameas false %} the foo attribute really is the `False` singleton {% endif %} """ @@ -88,6 +93,7 @@ def test_sameas(value, other): TESTS = { 'odd': test_odd, 'even': test_even, + 'divisibleby': test_divisibleby, 'defined': test_defined, 'lower': test_lower, 'upper': test_upper, diff --git a/jinja2/utils.py b/jinja2/utils.py index 2c857f4..195a942 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -26,13 +26,30 @@ _punctuation_re = re.compile( _simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') -def soft_unicode(s): - """Make a string unicode if it isn't already. That way a markup - string is not converted back to unicode. +def import_string(import_name, silent=False): + """Imports an object based on a string. This use useful if you want to + use import paths as endpoints or something similar. An import path can + be specified either in dotted notation (``xml.sax.saxutils.escape``) + or with a colon as object delimiter (``xml.sax.saxutils:escape``). + + If the `silent` is True the return value will be `None` if the import + fails. + + :return: imported object """ - if not isinstance(s, unicode): - s = unicode(s) - return s + try: + if ':' in import_name: + module, obj = import_name.split(':', 1) + elif '.' in import_name: + items = import_name.split('.') + module = '.'.join(items[:-1]) + obj = items[-1] + else: + return __import__(import_name) + return getattr(__import__(module, None, None, [obj]), obj) + except (ImportError, AttributeError): + if not silent: + raise def pformat(obj, verbose=False): @@ -100,7 +117,6 @@ class Markup(unicode): happen. If you want to use autoescaping in Jinja just set the finalizer of the environment to `escape`. """ - __slots__ = () def __html__(self): @@ -137,15 +153,19 @@ class Markup(unicode): def join(self, seq): return self.__class__(unicode.join(self, imap(escape, seq))) + join.__doc__ = unicode.join.__doc__ def split(self, *args, **kwargs): return map(self.__class__, unicode.split(self, *args, **kwargs)) + split.__doc__ = unicode.split.__doc__ def rsplit(self, *args, **kwargs): return map(self.__class__, unicode.rsplit(self, *args, **kwargs)) + rsplit.__doc__ = unicode.rsplit.__doc__ def splitlines(self, *args, **kwargs): return map(self.__class__, unicode.splitlines(self, *args, **kwargs)) + splitlines.__doc__ = unicode.splitlines.__doc__ def make_wrapper(name): orig = getattr(unicode, name) @@ -296,7 +316,7 @@ class LRUCache(object): # we have to import it down here as the speedups module imports the # markup type which is define above. try: - from jinja2._speedups import escape + from jinja2._speedups import escape, soft_unicode except ImportError: def escape(obj): """Convert the characters &, <, >, and " in string s to HTML-safe @@ -311,3 +331,11 @@ except ImportError: .replace('<', '<') .replace('"', '"') ) + + def soft_unicode(s): + """Make a string unicode if it isn't already. That way a markup + string is not converted back to unicode. + """ + if not isinstance(s, unicode): + s = unicode(s) + return s diff --git a/setup.py b/setup.py index a292d5b..a12d998 100644 --- a/setup.py +++ b/setup.py @@ -108,7 +108,7 @@ setup( # in form of html and txt files it's a better idea to extract the files zip_safe=False, classifiers=[ - 'Development Status :: 1 - Alpha', + 'Development Status :: 4 Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', @@ -131,5 +131,9 @@ setup( ] ) }, - extras_require={'plugin': ['setuptools>=0.6a2']} + extras_require={'i18n': ['Babel>=0.8']} + entry_points=""" + [babel.extractors] + jinja2 = jinja.i18n:babel_extract[i18n] + """ )