Opened 3 weeks ago

Closed 3 weeks ago

Last modified 3 weeks ago

#36872 closed New feature (wontfix)

Django's template engine cannot handle asynchronous methods

Reported by: Ricardo Robles Owned by: Ricardo Robles
Component: Template system Version: 6.0
Severity: Normal Keywords: Template, Async
Cc: Carlton Gibson Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description (last modified by Ricardo Robles)

Currently, the Django template is not designed to execute asynchronous methods; it is only designed to execute synchronous methods.

Here's an example of how to reproduce the error:

from django.template import engines

django_engine = engines['django']

class Example:
    def sync_method(self):
        return "Synchronous Method Result"
    async def async_method(self):
        return "Asynchronous Method Result"

html_string = """
<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
</head>
<body>
    <h1>sync: {{ example.sync_method }}!</h1>
    <p>async: {{ example.async_method }}</p>
</body>
</html>
"""

template = django_engine.from_string(html_string)
rendered_html = template.render({'example': Example()})

print(rendered_html)

This will return this error:

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
</head>
<body>
    <h1>sync: Synchronous Method Result!</h1>
    <p>async: &lt;coroutine object Example.async_method at 0x7bdeeb9aa980&gt;</p>
</body>
</html>

I had thought that a solution to this error might be to modify the resolve method of the FilterExpression class
https://github.com/django/django/blob/main/django/template/base.py#L785

If we add this:

class FilterExpression:
    ...

    def resolve(self, context, ignore_failures=False):
        if self.is_var:
            try:
                obj = self.var.resolve(context)

                # My proposal begins
                if asyncio.iscoroutine(obj):
                    obj = async_to_sync(lambda: obj)()
                # My proposal ends

            except VariableDoesNotExist:
                ...

Now it renders correctly:

<!DOCTYPE html>
<html>
<head>
    <title>Example</title>
</head>
<body>
    <h1>sync: Synchronous Method Result!</h1>
    <p>async: Asynchronous Method Result</p>
</body>
</html>

I use Django ASGI a lot at work, and there are many features like this that would be very useful. I look forward to your feedback.

Attachments (2)

proposal.diff (2.6 KB ) - added by Ricardo Robles 3 weeks ago.
proposal_2.diff (3.4 KB ) - added by Ricardo Robles 3 weeks ago.

Download all attachments as: .zip

Change History (17)

comment:1 by ar3ph, 3 weeks ago

in reply to:  1 comment:2 by Ricardo Robles, 3 weeks ago

Replying to Zachary W:

Could be related. There is an async_to_sync function in asgiref: https://github.com/django/asgiref/blob/2b28409ab83b3e4cf6fed9019403b71f8d7d1c51/asgiref/sync.py#L585

You're correct, the most elegant and simple way to do it would be:

    def resolve(self, context, ignore_failures=False):
        if self.is_var:
            try:
                obj = self.var.resolve(context)
                if asyncio.iscoroutine(obj):
                    obj = async_to_sync(lambda: obj)()
                ...

comment:3 by Ricardo Robles, 3 weeks ago

Description: modified (diff)

comment:4 by itzmehaksharma89-cell, 3 weeks ago

Owner: set to itzmehaksharma89-cell
Status: newassigned

by Ricardo Robles, 3 weeks ago

Attachment: proposal.diff added

comment:5 by Ricardo Robles, 3 weeks ago

Owner: changed from itzmehaksharma89-cell to Ricardo Robles

comment:6 by Jacob Walls, 3 weeks ago

Triage Stage: UnreviewedAccepted
UI/UX: unset

Thanks, I think it makes sense to have this. The provided patch is non-invasive, and I don't think anyone wants the current behavior with the memory address of the coroutine rendered into the template (and RuntimeWarning at shutdown for a coroutine not awaited).

PR

comment:7 by Jacob Walls, 3 weeks ago

Has patch: set

comment:8 by Jacob Walls, 3 weeks ago

Needs documentation: set
Type: BugNew feature

I think this will need a sliver of documentation. For instance where we say:

If the resulting value is callable, it is called with no arguments.

We could augment it with:

If the resulting value is a coroutine, it is awaited via async_to_sync().

or similar.

Although this is sort of a bug since elsewhere the docs say "variables are evaluated", I think we should let folks know we added this with a release note and versionchanged annotation.

by Ricardo Robles, 3 weeks ago

Attachment: proposal_2.diff added

in reply to:  8 comment:9 by Ricardo Robles, 3 weeks ago

Replying to Jacob Walls:

I think this will need a sliver of documentation. For instance where we say:

If the resulting value is callable, it is called with no arguments.

We could augment it with:

If the resulting value is a coroutine, it is awaited via async_to_sync().

or similar.

Although this is sort of a bug since elsewhere the docs say "variables are evaluated", I think we should let folks know we added this with a release note and versionchanged annotation.

Good afternoon,

Okay, I've just updated the documentation in both the diff and the PR to include this information.

comment:10 by Jacob Walls, 3 weeks ago

Patch needs improvement: set

comment:11 by Natalia Bidart, 3 weeks ago

Cc: Carlton Gibson added

comment:12 by Carlton Gibson, 3 weeks ago

As small as the change is here, I can't help but thinking it makes little sense. Async functions just aren't supported in the DTL... and what purpose do they serve? Adding this auto-wrap in async_to_sync is a road to where exactly? Nowhere. It's a very short cul-de-sac.

I don't think anyone wants the current behavior with the memory address of the coroutine rendered into the template (and RuntimeWarning at shutdown for a coroutine not awaited).

If this is a genuine problem (which I'm not sure it is: "Just don't do that" is often enough) raising an error here would be better. It's not making a misleading promise that we won't later keep.

🤷

comment:13 by Natalia Bidart, 3 weeks ago

Thanks all for your comments and views! I agree with Carlton in that there is no async support nowhere in the DTL so adding this specific bit feels disconnected and strange.

comment:14 by Jacob Walls, 3 weeks ago

Resolution: wontfix
Status: assignedclosed

I'm convinced. Thanks for helping me keep the bigger picture in mind. Ricardo, I hope that makes some sense about why we're not acting on this.

in reply to:  12 comment:15 by Ricardo Robles, 3 weeks ago

Replying to Carlton Gibson:

As small as the change is here, I can't help but thinking it makes little sense. Async functions just aren't supported in the DTL... and what purpose do they serve? Adding this auto-wrap in async_to_sync is a road to where exactly? Nowhere. It's a very short cul-de-sac.

I don't think anyone wants the current behavior with the memory address of the coroutine rendered into the template (and RuntimeWarning at shutdown for a coroutine not awaited).

If this is a genuine problem (which I'm not sure it is: "Just don't do that" is often enough) raising an error here would be better. It's not making a misleading promise that we won't later keep.

🤷

Good morning, the inability to use asynchronous methods in DTL is a real problem.
There are increasingly more asynchronous libraries that don't require additional code to convert them to synchronous functionality for use in a template.

For example, Django 6.0 introduced AsyncPaginator, which cannot be used in DTL. Why did you add AsyncPaginator if you're not going to allow its use in DTL?

Note: See TracTickets for help on using tickets.
Back to Top