[svn] added "debugger of awesomeness" :D
authorArmin Ronacher <armin.ronacher@active-4.com>
Fri, 15 Jun 2007 16:03:21 +0000 (18:03 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Fri, 15 Jun 2007 16:03:21 +0000 (18:03 +0200)
--HG--
branch : trunk

CHANGES
docs/src/i18n.txt
jinja/_debugger.c [new file with mode: 0644]
jinja/debugger.py [new file with mode: 0644]
jinja/environment.py
jinja/loaders.py
jinja/translators/python.py
jinja/utils.py
setup.py
tests/runtime/exception.py
tests/test_i18n.py

diff --git a/CHANGES b/CHANGES
index 814baf8f67c410c0d1378a84fa517c78ddf7d8f1..fca9a188db3182a1d2c59327f55d58061efebdcf 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,18 @@
 Jinja Changelog
 ===============
 
+Version 1.2
+-----------
+(codename to be selected, release date unknown)
+
+- environments now have a `translator_factory` parameter that allows
+  to change the translator without subclassing the environment.
+
+- fixed bug in buffet plugin regarding the package loader
+
+- once again improved debugger.
+
+
 Version 1.1
 -----------
 (codename: sinka, released Jun 1, 2007)
index cfad370312caa7dcf2d4a24f5e0a4c46abd5e5c6..91af491609f41d5864f1081866eb9b4f0c33fdfc 100644 (file)
@@ -50,9 +50,41 @@ However. For many web applications this might be a way:
     tmpl = env.get_template('index.html')
     tmpl.render(LANGUAGE='de_DE')
 
-This example assumes that you use gettext and have a gettext
-`Translations` object which is returned by the `get_translator`
-function.
+This example assumes that you use gettext and have a gettext `Translations`
+object which is returned by the `get_translator` function. But you don't
+have to use gettext. The only thing Jinja requires is an object with to
+functions/methods on it that return and accept unicode strings:
+``gettext(string)`` that takes a string, translates and returns it, a
+``ngettext(singular, plural, count)`` function that returns the correct plural
+form for `count` items. Because some languages have no or multiple plural
+forms this is necessary.
+
+
+Translator Factory
+==================
+
+With Jinja 1.2 onwards it's possible to use a translator factory
+instead of an enviornment subclass to create a translator for a context.
+A translator factory is passed a context and has to return a translator.
+Because of the way classes work you can also assign a translator class
+that takes a context object as only argument as factory.
+
+Example:
+
+.. sourcecode:: python
+
+    from jinja import Environment
+    from myapplication import get_translator
+
+    def translator_factory(context):
+        return get_translator(context['LANGUAGE'])
+
+    env = ApplicationEnvironment(translator_factory=translator_factory)
+    tmpl = env.get_template('index.html')
+    tmpl.render(LANGUAGE='de_DE')
+
+This example assumes that the translator returned by `get_translator`
+already has a gettext and ngettext function that returns unicode strings.
 
 
 Collecting Translations
@@ -67,7 +99,7 @@ Example:
 .. sourcecode:: pycon
 
     >>> env.get_translations('index.html')
-    [(1, u'foo', None), (2, u'Foo', None), (3, u'%d Foo', u'%d Foos')]
+    [(1, u'foo', None), (2, u'Foo', None), (3, u'%(count)s Foo', u'%(count)s Foos')]
 
 The first item in the tuple is the linenumer, the second one is the
 singular form and the third is the plural form if given.
diff --git a/jinja/_debugger.c b/jinja/_debugger.c
new file mode 100644 (file)
index 0000000..203cd84
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Jinja Extended Debugger
+ * ~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * this module allows the jinja debugger to set the tb_next flag
+ * on traceback objects. This is required to inject a traceback into
+ * another one.
+ *
+ * :copyright: 2007 by Armin Ronacher.
+ * :license: BSD, see LICENSE for more details.
+ */
+
+#include <Python.h>
+
+
+/**
+ * set the tb_next attribute of a traceback object
+ */
+PyObject*
+tb_set_next(PyObject *self, PyObject *args)
+{
+       PyObject *tb, *next;
+
+       if (!PyArg_ParseTuple(args, "OO", &tb, &next))
+               return NULL;
+       if (!(PyTraceBack_Check(tb) && (PyTraceBack_Check(next) || next == Py_None))) {
+               PyErr_SetString(PyExc_TypeError, "traceback object required.");
+               return NULL;
+       }
+
+       ((PyTracebackObject*)tb)->tb_next = next;
+
+       Py_INCREF(Py_None);
+       return Py_None;
+}
+
+
+static PyMethodDef module_methods[] = {
+       {"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS,
+        "Set the tb_next member of a traceback object."},
+       {NULL, NULL, 0, NULL}           /* Sentinel */
+};
+
+
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+PyMODINIT_FUNC
+init_debugger(void)
+{
+       PyObject *module = Py_InitModule3("_debugger", module_methods, "");
+       if (!module)
+               return;
+}
diff --git a/jinja/debugger.py b/jinja/debugger.py
new file mode 100644 (file)
index 0000000..396c665
--- /dev/null
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.debugger
+    ~~~~~~~~~~~~~~
+
+    The debugger module of awesomeness.
+
+    :copyright: 2007 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+import sys
+from random import randrange
+from opcode import opmap
+from types import CodeType
+
+# if we have extended debugger support we should really use it
+try:
+    from jinja._debugger import *
+    has_extended_debugger = True
+except ImportError:
+    has_extended_debugger = False
+
+# we need the RUNTIME_EXCEPTION_OFFSET to skip the not used frames
+from jinja.utils import RUNTIME_EXCEPTION_OFFSET
+
+
+def fake_template_exception(exc_type, exc_value, traceback, filename, lineno,
+                            source, context_or_env):
+    """
+    Raise an exception "in a template". Return a traceback
+    object. This is used for runtime debugging, not compile time.
+    """
+    # some traceback systems allow to skip frames
+    __traceback_hide__ = True
+
+    # create the namespace which will be the local namespace
+    # of the new frame then. Some debuggers show local variables
+    # so we better inject the context and not the evaluation loop context.
+    from jinja.datastructure import Context
+    if isinstance(context_or_env, Context):
+        env = context_or_env.environment
+        namespace = context_or_env.to_dict()
+    else:
+        env = context_or_env
+        namespace = {}
+
+    # no unicode for filenames
+    if isinstance(filename, unicode):
+        filename = filename.encode('utf-8')
+
+    # generate an jinja unique filename used so that linecache
+    # gets data that doesn't interferes with other modules
+    if filename is None:
+        vfilename = 'jinja://~%d' % randrange(0, 10000)
+        filename = '<string>'
+    else:
+        vfilename = 'jinja://%s' % filename
+
+    # now create the used loaded and update the linecache
+    loader = TracebackLoader(env, source, filename)
+    loader.update_linecache(vfilename)
+    globals = {
+        '__name__':                 vfilename,
+        '__file__':                 vfilename,
+        '__loader__':               loader
+    }
+
+    # use the simple debugger to reraise the exception in the
+    # line where the error originally occoured
+    globals['__exception_to_raise__'] = (exc_type, exc_value)
+    offset = '\n' * (lineno - 1)
+    code = compile(offset + 'raise __exception_to_raise__[0], '
+                            '__exception_to_raise__[1]',
+                   vfilename or '<template>', 'exec')
+    try:
+        exec code in globals, namespace
+    except:
+        exc_info = sys.exc_info()
+
+    # if we have an extended debugger we set the tb_next flag
+    if has_extended_debugger and traceback is not None:
+        tb_set_next(exc_info[2].tb_next, traceback.tb_next)
+
+    # otherwise just return the exc_info from the simple debugger
+    return exc_info
+
+
+def translate_exception(template, context, exc_type, exc_value, traceback):
+    """
+    Translate an exception and return the new traceback.
+    """
+    # depending on the python version we have to skip some frames to
+    # step to get the frame of the current template. The frames before
+    # are the toolchain used to render that thing.
+    for x in xrange(RUNTIME_EXCEPTION_OFFSET):
+        traceback = traceback.tb_next
+
+    # the next thing we do is matching the current error line against the
+    # debugging table to get the correct source line. If we can't find the
+    # filename and line number we return the traceback object unaltered.
+    error_line = traceback.tb_lineno
+    for code_line, tmpl_filename, tmpl_line in template._debug_info[::-1]:
+        if code_line <= error_line:
+            break
+    else:
+        return traceback
+
+    return fake_template_exception(exc_type, exc_value, traceback,
+                                   tmpl_filename, tmpl_line,
+                                   template._source, context)
+
+
+def raise_syntax_error(exception, env, source=None):
+    """
+    This method raises an exception that includes more debugging
+    informations so that debugging works better. Unlike
+    `translate_exception` this method raises the exception with
+    the traceback.
+    """
+    exc_info = fake_template_exception(exception, None, None,
+                                       exception.filename,
+                                       exception.lineno, source, env)
+    raise exc_info[0], exc_info[1], exc_info[2]
+
+
+class TracebackLoader(object):
+    """
+    Fake importer that just returns the source of a template.
+    """
+
+    def __init__(self, environment, source, filename):
+        self.loader = environment.loader
+        self.source = source
+        self.filename = filename
+
+    def update_linecache(self, virtual_filename):
+        """
+        Hacky way to let traceback systems know about the
+        Jinja template sourcecode. Very hackish indeed.
+        """
+        # check for linecache, not every implementation of python
+        # might have such an module.
+        try:
+            from linecache import cache
+        except ImportError:
+            return
+        data = self.get_source(None)
+        cache[virtual_filename] = (
+            len(data),
+            None,
+            data.splitlines(True),
+            virtual_filename
+        )
+
+    def get_source(self, impname):
+        source = ''
+        if self.source is not None:
+            source = self.source
+        elif self.loader is not None:
+            try:
+                source = self.loader.get_source(self.filename)
+            except TemplateNotFound:
+                pass
+        if isinstance(source, unicode):
+            source = source.encode('utf-8')
+        return source
index 034d09287a2f709f0b3c5831fe61e03a1d975c72..38a22bb14a7cb52703bc331ed5584131d87df9ee 100644 (file)
@@ -52,7 +52,8 @@ class Environment(object):
                  tests=None,
                  context_class=Context,
                  undefined_singleton=SilentUndefined,
-                 friendly_traceback=True):
+                 friendly_traceback=True,
+                 translator_factory=None):
         """
         Here the possible initialization parameters:
 
@@ -110,6 +111,10 @@ class Environment(object):
                                   This however can be annoying when debugging
                                   broken functions that are called from the
                                   template. *new in Jinja 1.1*
+        `translator_factory`      A callback function that is called with
+                                  the context as first argument to get the
+                                  translator for the current instance.
+                                  *new in Jinja 1.2*
         ========================= ============================================
 
         All of these variables except those marked with a star (*) are
@@ -150,6 +155,9 @@ class Environment(object):
             self.default_filters.append(('escape', (True,)))
             self.globals['Markup'] = Markup
 
+        # and here the translator factory
+        self.translator_factory = translator_factory
+
         # create lexer
         self.lexer = Lexer(self)
 
@@ -195,7 +203,7 @@ class Environment(object):
             # on syntax errors rewrite the traceback if wanted
             if not self.friendly_traceback:
                 raise
-            from jinja.utils import raise_syntax_error
+            from jinja.debugger import raise_syntax_error
             __traceback_hide__ = True
             raise_syntax_error(e, self, source)
         else:
@@ -237,6 +245,8 @@ class Environment(object):
         ``gettext(string)`` and ``ngettext(singular, plural, n)``. Note
         that both of them have to return unicode!
         """
+        if self.translator_factory is not None:
+            return self.translator_factory(context)
         return FakeTranslator()
 
     def get_translations(self, name):
@@ -258,6 +268,11 @@ class Environment(object):
         """
         Apply a list of filters on the variable.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         cache = context.cache
         for key in filters:
             if key in cache:
@@ -274,6 +289,11 @@ class Environment(object):
         """
         Perform a test on a variable.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         key = (testname, args)
         if key in context.cache:
             func = context.cache[key]
@@ -290,6 +310,11 @@ class Environment(object):
         """
         Get one attribute from an object.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         try:
             return obj[name]
         except (TypeError, KeyError, IndexError, AttributeError):
@@ -316,6 +341,11 @@ class Environment(object):
         Function call helper. Called for all functions that are passed
         any arguments.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         if dyn_args is not None:
             args += tuple(dyn_args)
         if dyn_kwargs is not None:
@@ -332,6 +362,11 @@ class Environment(object):
         Function call without arguments. Because of the smaller signature and
         fewer logic here we have a bit of redundant code.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         if _getattr(f, 'jinja_unsafe_call', False) or \
            _getattr(f, 'alters_data', False):
             return self.undefined_singleton
@@ -345,6 +380,11 @@ class Environment(object):
         evaluator the source generated by the python translator will
         call this function for all variables.
         """
+        # some traceback systems allow to skip frames. but allow
+        # disabling that via -O to not make things slow
+        if __debug__:
+            __traceback_hide__ = True
+
         if value is None:
             return u''
         elif value is self.undefined_singleton:
index 0f0ced7aefa1e1b398ee4489bf9f96525548ba99..aadaad7b86f7b76c1837b04d81f53e4fd244cb31 100644 (file)
@@ -17,7 +17,7 @@ from threading import Lock
 from jinja.parser import Parser
 from jinja.translators.python import PythonTranslator, Template
 from jinja.exceptions import TemplateNotFound, TemplateSyntaxError
-from jinja.utils import CacheDict, raise_syntax_error
+from jinja.utils import CacheDict
 
 
 #: when updating this, update the listing in the jinja package too
@@ -96,6 +96,7 @@ class LoaderWrapper(object):
             if not self.environment.friendly_traceback:
                 raise
             __traceback_hide__ = True
+            from jinja.debugger import raise_syntax_error
             raise_syntax_error(e, self.environment)
 
     def _loader_missing(self, *args, **kwargs):
index bef76198d7cf816c0160a5f06c0d9d010f91af93..907f4fcbd55cd17dffa6d563812148aa948946cf 100644 (file)
@@ -43,8 +43,7 @@ from jinja.parser import Parser
 from jinja.exceptions import TemplateSyntaxError
 from jinja.translators import Translator
 from jinja.datastructure import TemplateStream
-from jinja.utils import set, translate_exception, capture_generator, \
-     RUNTIME_EXCEPTION_OFFSET
+from jinja.utils import set, capture_generator
 
 
 #: regular expression for the debug symbols
@@ -137,16 +136,16 @@ class Template(object):
     def _debug(self, ctx, exc_type, exc_value, traceback):
         """Debugging Helper"""
         # just modify traceback if we have that feature enabled
+        from traceback import print_exception
+        print_exception(exc_type, exc_value, traceback)
+
         if self.environment.friendly_traceback:
-            # debugging system:
-            # on any exception we first skip the internal frames (currently
-            # either one (python2.5) or two (python2.4 and lower)). After that
-            # we call a function that creates a new traceback that is easier
-            # to debug.
-            for _ in xrange(RUNTIME_EXCEPTION_OFFSET):
-                traceback = traceback.tb_next
-            traceback = translate_exception(self, exc_type, exc_value,
-                                            traceback, ctx)
+            # hook the debugger in
+            from jinja.debugger import translate_exception
+            exc_type, exc_value, traceback = translate_exception(
+                self, ctx, exc_type, exc_value, traceback)
+        print_exception(exc_type, exc_value, traceback)
+
         raise exc_type, exc_value, traceback
 
 
index 42eec0df1f08358ce76cef4833374c9347ddd142..eee55061ab2b4965e1b1d96a368669d8de5530e1 100644 (file)
@@ -68,6 +68,13 @@ except NameError:
             rv.reverse()
         return rv
 
+# if we have extended debugger support we should really use it
+try:
+    from jinja._tbtools import *
+    has_extended_debugger = True
+except ImportError:
+    has_extended_debugger = False
+
 #: function types
 callable_types = (FunctionType, MethodType)
 
@@ -326,78 +333,6 @@ def empty_block(context):
     if 0: yield None
 
 
-def fake_template_exception(exception, filename, lineno, source,
-                            context_or_env):
-    """
-    Raise an exception "in a template". Return a traceback
-    object. This is used for runtime debugging, not compile time.
-    """
-    # some traceback systems allow to skip frames
-    __traceback_hide__ = True
-
-    from jinja.datastructure import Context
-    if isinstance(context_or_env, Context):
-        env = context_or_env.environment
-        namespace = context_or_env.to_dict()
-    else:
-        env = context_or_env
-        namespace = {}
-
-    # generate an jinja unique filename used so that linecache
-    # gets data that doesn't interferes with other modules
-    if filename is None:
-        from random import randrange
-        vfilename = 'jinja://~%d' % randrange(0, 10000)
-        filename = '<string>'
-    else:
-        vfilename = 'jinja://%s' % filename
-
-    offset = '\n' * (lineno - 1)
-    code = compile(offset + 'raise __exception_to_raise__',
-                   vfilename or '<template>', 'exec')
-
-    loader = TracebackLoader(env, source, filename)
-    loader.update_linecache(vfilename)
-    globals = {
-        '__name__':                 vfilename,
-        '__file__':                 vfilename,
-        '__loader__':               loader,
-        '__exception_to_raise__':   exception
-    }
-    try:
-        exec code in globals, namespace
-    except:
-        return sys.exc_info()
-
-
-def translate_exception(template, exc_type, exc_value, traceback, context):
-    """
-    Translate an exception and return the new traceback.
-    """
-    error_line = traceback.tb_lineno
-    for code_line, tmpl_filename, tmpl_line in template._debug_info[::-1]:
-        if code_line <= error_line:
-            break
-    else:
-        # no debug symbol found. give up
-        return traceback
-
-    return fake_template_exception(exc_value, tmpl_filename, tmpl_line,
-                                   template._source, context)[2]
-
-
-def raise_syntax_error(exception, env, source=None):
-    """
-    This method raises an exception that includes more debugging
-    informations so that debugging works better. Unlike
-    `translate_exception` this method raises the exception with
-    the traceback.
-    """
-    exc_info = fake_template_exception(exception, exception.filename,
-                                       exception.lineno, source, env)
-    raise exc_info[0], exc_info[1], exc_info[2]
-
-
 def collect_translations(ast):
     """
     Collect all translatable strings for the given ast. The
@@ -414,19 +349,8 @@ def collect_translations(ast):
         elif node.__class__ is CallFunc and \
              node.node.__class__ is Name and \
              node.node.name == '_':
-            if len(node.args) in (1, 3):
-                args = []
-                for arg in node.args:
-                    if not arg.__class__ is Const:
-                        break
-                    args.append(arg.value)
-                else:
-                    if len(args) == 1:
-                        singular = args[0]
-                        plural = None
-                    else:
-                        singular, plural, _ = args
-                    result.append((node.lineno, singular, plural))
+            if len(node.args) == 1 and node.args[0].__class__ is Const:
+                result.append((node.lineno, node.args[0].value, None))
         todo.extend(node.getChildNodes())
     result.sort(lambda a, b: cmp(a[0], b[0]))
     return result
@@ -492,49 +416,6 @@ class DebugHelper(object):
 debug_helper = object.__new__(DebugHelper)
 
 
-class TracebackLoader(object):
-    """
-    Fake importer that just returns the source of a template.
-    """
-
-    def __init__(self, environment, source, filename):
-        self.loader = environment.loader
-        self.source = source
-        self.filename = filename
-
-    def update_linecache(self, virtual_filename):
-        """
-        Hacky way to let traceback systems know about the
-        Jinja template sourcecode. Very hackish indeed.
-        """
-        # check for linecache, not every implementation of python
-        # might have such an module.
-        try:
-            from linecache import cache
-        except ImportError:
-            return
-        data = self.get_source(None)
-        cache[virtual_filename] = (
-            len(data),
-            None,
-            data.splitlines(True),
-            virtual_filename
-        )
-
-    def get_source(self, impname):
-        source = ''
-        if self.source is not None:
-            source = self.source
-        elif self.loader is not None:
-            try:
-                source = self.loader.get_source(self.filename)
-            except TemplateNotFound:
-                pass
-        if isinstance(source, unicode):
-            source = source.encode('utf-8')
-        return source
-
-
 class CacheDict(object):
     """
     A dict like object that stores a limited number of items and forgets
index 456a5b9975fd49c7a7d3be8f93cb8f806c67c866..cae23c7a72fe07b3b64cb4095a81caf264ea33d7 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -78,12 +78,21 @@ setup(
     jinja = jinja.plugin:BuffetPlugin
     ''',
     extras_require = {'plugin': ['setuptools>=0.6a2']},
-    features = {'speedups': Feature(
-        'optional C-speed enhancements',
-        standard = True,
-        ext_modules = [
-            Extension('jinja._speedups', ['jinja/_speedups.c'])
-        ]
-    )},
+    features = {
+        'speedups': Feature(
+            'optional C-speed enhancements',
+            standard = True,
+            ext_modules = [
+                Extension('jinja._speedups', ['jinja/_speedups.c'])
+            ]
+        ),
+        'extended-debugger': Feature(
+            'extended debugger',
+            standard = True,
+            ext_modules = [
+                Extension('jinja._debugger', ['jinja/_debugger.c'])
+            ]
+        )
+    },
     cmdclass = {'build_ext': optional_build_ext}
 )
index 3d2adbdc2a8e5a19bdce762f238df85824ce7f52..60437469830f0a98f008b8df5f480bfadfd0205f 100644 (file)
@@ -32,6 +32,7 @@ e = Environment(loader=DictLoader({
       <li><a href="runtime_error">runtime error</a></li>
       <li><a href="nested_syntax_error">nested syntax error</a></li>
       <li><a href="nested_runtime_error">nested runtime error</a></li>
+      <li><a href="code_runtime_error">runtime error in code</a></li>
       <li><a href="syntax_from_string">a syntax error from string</a></li>
       <li><a href="runtime_from_string">runtime error from a string</a></li>
     </ul>
@@ -53,6 +54,10 @@ e = Environment(loader=DictLoader({
 {% include 'syntax_broken' %}
     ''',
 
+    '/code_runtime_error': u'''
+{{ broken() }}
+''',
+
     'runtime_broken': '''\
 This is an included template
 {% set a = 1 / 0 %}''',
@@ -65,6 +70,10 @@ FAILING_STRING_TEMPLATE = '{{ 1 / 0 }}'
 BROKEN_STRING_TEMPLATE = '{% if foo %}...{% endfor %}'
 
 
+def broken():
+    raise RuntimeError("I'm broken")
+
+
 def test(environ, start_response):
     path = environ.get('PATH_INFO' or '/')
     try:
@@ -78,9 +87,10 @@ def test(environ, start_response):
             start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
             return ['NOT FOUND']
     start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
-    return [tmpl.render().encode('utf-8')]
+    return [tmpl.render(broken=broken).encode('utf-8')]
+
 
 if __name__ == '__main__':
-    from colubrid.debug import DebuggedApplication
+    from werkzeug.debug import DebuggedApplication
     app = DebuggedApplication(test)
     make_server("localhost", 7000, app).serve_forever()
index 040f7baf750cae7003510946933d39dd2a7ed9aa..65f3d2fc9acc37dc939b8937d190997de92ec79e 100644 (file)
@@ -58,6 +58,14 @@ class I18NEnvironment(Environment):
 i18n_env = I18NEnvironment()
 
 
+def test_factory():
+    def factory(context):
+        return SimpleTranslator(context['LANGUAGE'] or 'en')
+    env = Environment(translator_factory=factory)
+    tmpl = env.from_string('{% trans "watch out" %}')
+    assert tmpl.render(LANGUAGE='de') == 'pass auf'
+
+
 def test_get_translations():
     trans = list(i18n_env.get_translations('child.html'))
     assert len(trans) == 1