Skip to content

Commit 5478d7f

Browse files
committed
Handle generator and awaitable responses across adapters
1 parent 07aa21a commit 5478d7f

File tree

5 files changed

+62
-10
lines changed

5 files changed

+62
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.idea
33
.zed
44
__pycache__
5+
uv.lock

src/datastar_py/django.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,18 @@ def datastar_response(
5454
@wraps(func)
5555
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
5656
r = func(*args, **kwargs)
57+
58+
# Collect async iterables so we don't hand StreamingHttpResponse an async generator
59+
if hasattr(r, "__aiter__"):
60+
events = [event async for event in r]
61+
return DatastarResponse(events)
62+
63+
if hasattr(r, "__iter__") and not isinstance(r, (str, bytes)):
64+
return DatastarResponse(r)
65+
5766
if isinstance(r, Awaitable):
5867
return DatastarResponse(await r)
68+
5969
return DatastarResponse(r)
6070

6171
return wrapper

src/datastar_py/litestar.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,28 @@ def __init__(
6464

6565
def datastar_response(
6666
func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents],
67-
) -> Callable[P, Awaitable[DatastarResponse]]:
67+
) -> Callable[P, DatastarResponse]:
6868
"""A decorator which wraps a function result in DatastarResponse.
6969
7070
Can be used on a sync or async function or generator function.
7171
"""
7272

7373
@wraps(func)
74-
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
74+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
7575
r = func(*args, **kwargs)
76+
77+
if hasattr(r, "__aiter__"):
78+
return DatastarResponse(r)
79+
80+
if hasattr(r, "__iter__") and not isinstance(r, (str, bytes)):
81+
return DatastarResponse(r)
82+
7683
if isinstance(r, Awaitable):
77-
return DatastarResponse(await r)
84+
async def await_and_yield():
85+
yield await r
86+
87+
return DatastarResponse(await_and_yield())
88+
7889
return DatastarResponse(r)
7990

8091
wrapper.__annotations__["return"] = DatastarResponse

src/datastar_py/quart.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections.abc import Awaitable, Mapping
44
from functools import wraps
5-
from inspect import isasyncgen, isasyncgenfunction, isgenerator
5+
from inspect import isasyncgen, isgenerator
66
from typing import Any, Callable, ParamSpec
77

88
from quart import Response, copy_current_request_context, request, stream_with_context
@@ -51,9 +51,23 @@ def datastar_response(
5151

5252
@wraps(func)
5353
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
54-
if isasyncgenfunction(func):
55-
return DatastarResponse(stream_with_context(func)(*args, **kwargs))
56-
return DatastarResponse(await copy_current_request_context(func)(*args, **kwargs))
54+
# Preserve request context for whatever we return
55+
bound_func = copy_current_request_context(func)
56+
r = bound_func(*args, **kwargs)
57+
58+
if hasattr(r, "__aiter__"):
59+
return DatastarResponse(stream_with_context(r))
60+
61+
if hasattr(r, "__iter__") and not isinstance(r, (str, bytes)):
62+
return DatastarResponse(stream_with_context(r))
63+
64+
if isinstance(r, Awaitable):
65+
async def await_and_yield():
66+
yield await r
67+
68+
return DatastarResponse(stream_with_context(await_and_yield()))
69+
70+
return DatastarResponse(r)
5771

5872
wrapper.__annotations__["return"] = DatastarResponse
5973
return wrapper

src/datastar_py/starlette.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,33 @@ def __init__(
5454

5555
def datastar_response(
5656
func: Callable[P, Awaitable[DatastarEvents] | DatastarEvents],
57-
) -> Callable[P, Awaitable[DatastarResponse]]:
57+
) -> Callable[P, DatastarResponse]:
5858
"""A decorator which wraps a function result in DatastarResponse.
5959
6060
Can be used on a sync or async function or generator function.
6161
"""
6262

6363
@wraps(func)
64-
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
64+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> DatastarResponse:
6565
r = func(*args, **kwargs)
66+
67+
# Check for async generator/iterator first (most specific case)
68+
if hasattr(r, "__aiter__"):
69+
return DatastarResponse(r)
70+
71+
# Check for sync generator/iterator (before Awaitable to avoid false positives)
72+
if hasattr(r, "__iter__") and not isinstance(r, (str, bytes)):
73+
return DatastarResponse(r)
74+
75+
# Check for coroutines/tasks (but NOT async generators, already handled above)
6676
if isinstance(r, Awaitable):
67-
return DatastarResponse(await r)
77+
# Wrap awaitable in an async generator that yields the result
78+
async def await_and_yield():
79+
yield await r
80+
81+
return DatastarResponse(await_and_yield())
82+
83+
# Default case: single value or unknown type
6884
return DatastarResponse(r)
6985

7086
wrapper.__annotations__["return"] = DatastarResponse

0 commit comments

Comments
 (0)