memo

2017-02-10

Jinja2 の async support を試した

Jinja2 にバージョン 2.9 で async support が入ったとのこと。

  • Environment 作成時に引数 enable_async=True を渡すことで有効になる

  • テンプレート中の関数呼び出しが、 (結果が coroutine であれば?) 自動で await されるようになる

  • Template.render_async() を呼ぶことで、テンプレートをレンダリングする coroutine が得られる

ということで動かしてみる:

import asyncio                                             
import jinja2                                             


# テキトウに非同期な関数を用意する
async def do_something(value: int) -> int:
    await asyncio.sleep(1)
    return value + 1 


async def main():         
    # `enable_async=True` を渡して Environment 作成
    env = jinja2.Environment(enable_async=True)       
    env.globals.update({                                                       
        'do_something': do_something,                                          
    })
    tmpl = env.from_string('''
    {#- テンプレート中では、非同期関数は普通の関数と同じように呼び出せる #}
    result: {{ do_something(1) }}                  
    ''')                                       

    # .render_async() で coroutine が得られる
    coro = tmpl.render_async()                         
    print(repr(coro))
    # coroutine を await することでレンダリング結果が手に入る
    txt = await coro                                                       
    print(txt)                   


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())          

実行結果:

<coroutine object Template.render_async at 0x7ff2038688e0>

    result: 2

さて、ドキュメントには asyncio がどうのこうのと書いてあるが、 これは Template.render() などの同期的なメソッドが呼ばれた際に、 内部で asyncio の event loop を取得して coroutine を実行し、結果を得ているという話っぽい。

なので、 .render_async() などを呼んでいる分には、 asyncio 以外の event loop でも使えそう。

ということで twisted と組み合わせて動かしてみる:

import jinja2

from twisted.internet import defer


def sleep(delay: int) -> defer.Deferred:
    d = defer.Deferred()

    from twisted.internet import reactor
    reactor.callLater(delay, d.callback, None)

    return d


# テキトウに非同期な関数を用意する
async def do_something(value: int) -> int:
    await sleep(1)
    return value + 1


# Deferred を返す版も試しておく
def do_something_deferred(value: int) -> defer.Deferred:
    d = sleep(1)
    d.addCallback(lambda _: value + 1)
    return d


async def main():
    # `enable_async=True` を渡して Environment 作成
    env = jinja2.Environment(enable_async=True)
    env.globals.update({
        'do_something': do_something,
        'do_something_deferred': do_something_deferred,
    })
    tmpl = env.from_string('''
    {#- テンプレート中では、非同期関数は普通の関数と同じように呼び出せる #}
    result: {{ do_something(1) }}
    {#- Deferred を返す関数も同様に、普通に呼び出せる #}
    result from deferred: {{ do_something_deferred(1) }}
    ''')

    # .render_async() で coroutine が得られる
    coro = tmpl.render_async()
    print(repr(coro))
    # coroutine を await することでレンダリング結果が手に入る
    txt = await coro
    print(txt)


if __name__ == '__main__':
    from twisted.internet import task
    task.react(lambda _: defer.ensureDeferred(main()))

実行結果:

<coroutine object Template.render_async at 0x7f16bbea5ca8>

    result: 2
    result from deferred: 2