From: Armin Ronacher Date: Wed, 28 Dec 2016 11:40:42 +0000 (+0100) Subject: Initial support for async rendering X-Git-Tag: 2.9~81 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b11056d7808c270ed974079d8ccea2ab3fb80e76;p=thirdparty%2Fjinja.git Initial support for async rendering --- diff --git a/jinja2/__init__.py b/jinja2/__init__.py index e68c2856..cefd0d6b 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -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 index 00000000..eaa6ea94 --- /dev/null +++ b/jinja2/asyncsupport.py @@ -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() diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 9c745bd8..1cc4aa8c 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -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 = {} diff --git a/jinja2/environment.py b/jinja2/environment.py index 100a0a43..3b3dc4cc 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -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) diff --git a/jinja2/utils.py b/jinja2/utils.py index da22e789..12ae68c7 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -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 index 00000000..a96eaa44 --- /dev/null +++ b/tests/test_async.py @@ -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]'