added first code for parser extensions and moved some code in speedups around
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 20 Apr 2008 11:11:43 +0000 (13:11 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 20 Apr 2008 11:11:43 +0000 (13:11 +0200)
--HG--
branch : trunk

15 files changed:
examples/translate.py
jinja2/__init__.py
jinja2/_speedups.c
jinja2/compiler.py
jinja2/datastructure.py
jinja2/environment.py
jinja2/filters.py
jinja2/i18n.py
jinja2/lexer.py
jinja2/nodes.py
jinja2/parser.py
jinja2/runtime.py
jinja2/tests.py
jinja2/utils.py
setup.py

index 9043a2112540fd64de78ae406844995d38693e0b..0169c88fb727e524f9e3c0211c6ebd09c528d54e 100644 (file)
@@ -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")
index 9099a21219ca01c7d5aad66735e2c200b7f681e1..12c147e7794709b6d9a0d711eea0f920934bff28 100644 (file)
@@ -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
index 36a1404de312a50f6a3bf4cc39dc97c6a386355e..10ae6f692cd8d43b2f9789b1b2d52d5bb26542a0 100644 (file)
@@ -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 */
index 871728f48d1a82b2b0d844ed111c60ed29c2f575..f5e0b4886a7d90a05d60892e1bf2c98b4fa11c71 100644 (file)
@@ -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
index 6b684c4428ce56c3934066c9c88e3304be7e7d15..9dab02b83099d4abe710ceb695359f9c09e7b673 100644 (file)
@@ -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:
index 35b01e7af324529b4df10f507be926d9d1d8397f..004ef74888c47c2a2d4b43dbdde648a9c2f30773 100644 (file)
 """
 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
index 2fc1b306324f849c39d4600447927d51de4fe157..d48ac940dd15ce9a7f240ddf6b394a4b9a858c8e 100644 (file)
@@ -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,
index b08ccf30c1613ace16e1bc26ddbc36259caae996..026750bc98b0d6c84b514dd21a5ce59a03c99a7d 100644 (file)
@@ -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)
index 931d7c1a0f10100a723dbd632b6510ea706d30d6..364d3f5e1cc5de6f73044de9a0437d93fb44a7e5 100644 (file)
@@ -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 = {
index c57939594ae39f3b6d3b14e9702b09aaa628007f..8c58959ed09f43bf5b8eb79d179841949ebfbb41 100644 (file)
@@ -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',)
index 8b82a4d077752cd726816917dcc7164b1bbf2213..4204c6aae9418a1f534479959e8556aeab6b1007 100644 (file)
@@ -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))
index e0630cccdd89452a04be2636f896dcb684ce0c24..d7de80e05aea6501062c1bb962c8537074bbc837 100644 (file)
@@ -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>' % (
index ab7992db273d32d273fe03df4343ffe7c92bb38d..d62ffeb223a6cea2fbff3e3838fec9897c2dfd65 100644 (file)
@@ -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,
index 2c857f4a8d087b67c91fa5081c49d235f4665c70..195a9420c27f79dc74afad9a354dad7b355095bc 100644 (file)
@@ -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('<', '&lt;')
             .replace('"', '&quot;')
         )
+
+    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
index a292d5bcec7f7ea97b1dfb888d114c19fcb2b3e3..a12d9986f65a20c21599246a6283d65204d395ae 100644 (file)
--- 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]
+    """
 )