]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Initial support for async rendering
authorArmin Ronacher <armin.ronacher@active-4.com>
Wed, 28 Dec 2016 11:40:42 +0000 (12:40 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Wed, 28 Dec 2016 11:40:42 +0000 (12:40 +0100)
jinja2/__init__.py
jinja2/asyncsupport.py [new file with mode: 0644]
jinja2/compiler.py
jinja2/environment.py
jinja2/utils.py
tests/test_async.py [new file with mode: 0644]

index e68c28562be3afed64f17d16e56217fe80233f3c..cefd0d6b716d0e20a9e8e14c811e1dd514b25ae8 100644 (file)
@@ -68,3 +68,14 @@ __all__ = [
     'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
     'evalcontextfilter', 'evalcontextfunction', 'make_logging_undefined',
 ]
+
+
+def _patch_async():
+    from jinja2.utils import have_async_gen
+    if have_async_gen:
+        from jinja2.asyncsupport import patch_all
+        patch_all()
+
+
+_patch_async()
+del _patch_async
diff --git a/jinja2/asyncsupport.py b/jinja2/asyncsupport.py
new file mode 100644 (file)
index 0000000..eaa6ea9
--- /dev/null
@@ -0,0 +1,43 @@
+import sys
+import asyncio
+
+from jinja2.utils import concat
+
+
+async def render_async(self, *args, **kwargs):
+    if not self.environment._async:
+        raise RuntimeError('The environment was not created with async mode '
+                           'enabled.')
+
+    vars = dict(*args, **kwargs)
+    ctx = self.new_context(vars)
+    rv = []
+    async def collect():
+        async for event in self.root_render_func(ctx):
+            rv.append(event)
+
+    try:
+        await collect()
+        return concat(rv)
+    except Exception:
+        exc_info = sys.exc_info()
+    return self.environment.handle_exception(exc_info, True)
+
+
+def wrap_render_func(original_render):
+    def render(self, *args, **kwargs):
+        if not self.environment._async:
+            return original_render(self, *args, **kwargs)
+        loop = asyncio.get_event_loop()
+        return loop.run_until_complete(self.render_async(self, *args, **kwargs))
+    return render
+
+
+def patch_template():
+    from jinja2 import Template
+    Template.render_async = render_async
+    Template.render = wrap_render_func(Template.render)
+
+
+def patch_all():
+    patch_template()
index 9c745bd8148731a0858e0d430ec472e80aa1e1b1..1cc4aa8c1770d55760033138b6852ac7320bd163 100644 (file)
@@ -47,13 +47,6 @@ try:
 except SyntaxError:
     pass
 
-# does this python version support async for in and async generators?
-try:
-    exec('async def _():\n async for _ in ():\n  yield _')
-    have_async_gen = True
-except SyntaxError:
-    have_async_gen = False
-
 
 # does if 0: dummy(x) get us x into the scope?
 def unoptimize_before_dead_code():
@@ -655,6 +648,11 @@ class CodeGenerator(NodeVisitor):
             #   a = 42; b = lambda: a; del a
             self.writeline(' = '.join(to_delete) + ' = missing')
 
+    def func(self, name):
+        if self.environment._async:
+            return 'async def %s' % name
+        return 'def %s' % name
+
     def function_scoping(self, node, frame, children=None,
                          find_special=True):
         """In Jinja a few statements require the help of anonymous
@@ -739,7 +737,7 @@ class CodeGenerator(NodeVisitor):
         # and assigned.
         if 'loop' in frame.identifiers.declared:
             args = args + ['l_loop=l_loop']
-        self.writeline('def macro(%s):' % ', '.join(args), node)
+        self.writeline('%s(%s):' % (self.func('macro'), ', '.join(args)), node)
         self.indent()
         self.buffer(frame)
         self.pull_locals(frame)
@@ -814,7 +812,7 @@ class CodeGenerator(NodeVisitor):
         self.writeline('name = %r' % self.name)
 
         # generate the root render function.
-        self.writeline('def root(context%s):' % envenv, extra=1)
+        self.writeline('%s(context%s):' % (self.func('root'), envenv), extra=1)
 
         # process the root
         frame = Frame(eval_ctx)
@@ -849,7 +847,7 @@ class CodeGenerator(NodeVisitor):
             block_frame = Frame(eval_ctx)
             block_frame.inspect(block.body)
             block_frame.block = name
-            self.writeline('def block_%s(context%s):' % (name, envenv),
+            self.writeline('%s(context%s):' % (self.func('block_' + name), envenv),
                            block, 1)
             self.indent()
             undeclared = find_undeclared(block.body, ('self', 'super'))
@@ -1079,7 +1077,8 @@ class CodeGenerator(NodeVisitor):
 
         # otherwise we set up a buffer and add a function def
         else:
-            self.writeline('def loop(reciter, loop_render_func, depth=0):', node)
+            self.writeline('%s(reciter, loop_render_func, depth=0):' %
+                           self.func('loop'), node)
             self.indent()
             self.buffer(loop_frame)
             aliases = {}
index 100a0a437f8f474ea2cd1c22757a8c727a3f501b..3b3dc4cc72bed8d6470b8bcd5275fb91bd7db029 100644 (file)
@@ -28,7 +28,7 @@ from jinja2.runtime import Undefined, new_context, Context
 from jinja2.exceptions import TemplateSyntaxError, TemplateNotFound, \
      TemplatesNotFound, TemplateRuntimeError
 from jinja2.utils import import_string, LRUCache, Markup, missing, \
-     concat, consume, internalcode
+     concat, consume, internalcode, have_async_gen
 from jinja2._compat import imap, ifilter, string_types, iteritems, \
      text_type, reraise, implements_iterator, implements_to_string, \
      encode_filename, PY2, PYPY
@@ -321,6 +321,7 @@ class Environment(object):
         self.extensions = load_extensions(self, extensions)
 
         self.enable_async = enable_async
+        self._async = self.enable_async and have_async_gen
 
         _environment_sanity_check(self)
 
index da22e789ab7a01bb3485677bc61b772b5e3ad08b..12ae68c7fe752c63b03b8c273356f5808240876a 100644 (file)
@@ -527,5 +527,13 @@ class Joiner(object):
         return self.sep
 
 
+# does this python version support async for in and async generators?
+try:
+    exec('async def _():\n async for _ in ():\n  yield _')
+    have_async_gen = True
+except SyntaxError:
+    have_async_gen = False
+
+
 # Imported here because that's where it was in the past
 from markupsafe import Markup, escape, soft_unicode
diff --git a/tests/test_async.py b/tests/test_async.py
new file mode 100644 (file)
index 0000000..a96eaa4
--- /dev/null
@@ -0,0 +1,21 @@
+import pytest
+import asyncio
+
+from jinja2 import Template
+from jinja2.utils import have_async_gen
+
+
+def run(func):
+    loop = asyncio.get_event_loop()
+    return loop.run_until_complete(func())
+
+
+@pytest.mark.skipif(not have_async_gen, reason='No async generators')
+def test_basic_async():
+    t = Template('{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}',
+                 enable_async=True)
+    async def func():
+        return await t.render_async()
+
+    rv = run(func)
+    assert rv == '[1][2][3]'