pallets / jinja

A very fast and expressive template engine.
https://jinja.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
10.28k stars 1.61k forks source link

no way to explicitly shutdown async generators #1953

Closed graingert closed 4 months ago

graingert commented 6 months ago

given:

import sys
import trio

from jinja2 import Template, select_autoescape

class MyModel:
    class objects:
        @staticmethod
        async def all():
            while True:
                yield "hello"

template = """
<html>
  <head></head>
  <body>
    {% for m in model.objects.all() %}
        {{ m }}
        {% break %}
    {% endfor %}
  </body>
</html>
"""

async def amain():
    return await Template(
        source=template, enable_async=True, extensions=["jinja2.ext.loopcontrols"]
    ).render_async(model=MyModel)

def main():
    trio.run(amain)

if __name__ == "__main__":
    sys.exit(main())

this results in:

 python -Wall demo_jinja_asyncgens.py
<template>:19: ResourceWarning: Async generator 'jinja2.async_utils.auto_aiter' was garbage collected before it had been exhausted. Surround its use in 'async with aclosing(...):' to ensure that it gets cleaned up as soon as you're done using it.
ResourceWarning: Enable tracemalloc to get the object allocation traceback
/home/graingert/.virtualenvs/demo-jinja-asyncgens/lib/python3.12/site-packages/trio/_core/_asyncgens.py:204: ResourceWarning: Async generator '__main__.MyModel.objects.all' was garbage collected before it had been exhausted. Surround its use in 'async with aclosing(...):' to ensure that it gets cleaned up as soon as you're done using it.
  await agen.aclose()
ResourceWarning: Enable tracemalloc to get the object allocation traceback

but there's no way to wrap generators with async with aclosing(...): in jinja2

Environment:

graingert commented 6 months ago

I'd like to see a cmgr/closing block so I can do:

<html>
  <head></head>
  <body>
    {% closing model.objects.all() as agen %}
        {% for m in agen %}
            {{ m }}
            {% break %}
        {% endfor %}
    {% endclosing %}
  </body>
</html>

but with all the changes in https://github.com/pallets/jinja/pull/1960 it's possible pass in an aclosing function to context:

import sys
import trio
import contextlib

from jinja2 import Template, select_autoescape

class MyModel:
    class objects:
        @staticmethod
        async def all():
            while True:
                yield "hello"

template = """
<html>
  <head></head>
  <body>
    {% for m in aclosing(model.objects.all()) %}
        {{ m }}
        {% break %}
    {% endfor %}
  </body>
</html>
"""

async def amain():
    async with contextlib.AsyncExitStack() as stack:

        def aclosing(agen):
            stack.push_async_callback(agen.aclose)
            return agen

        return await Template(
            source=template, enable_async=True, extensions=["jinja2.ext.loopcontrols"]
        ).render_async(model=MyModel, aclosing=aclosing)

def main():
    trio.run(amain)

if __name__ == "__main__":
    sys.exit(main())