Skip to content

Commit 941f9bb

Browse files
BABTUNAclaude
andauthored
fix(app): pass Reflex app instance to lifespan task app parameter (#6358)
* Pass Reflex app instance to lifespan tasks # Conflicts: # tests/units/app_mixins/test_lifespan.py * test(lifespan): cover starlette_app injection paths # Conflicts: # tests/units/app_mixins/test_lifespan.py * style(lifespan): apply ruff formatting for pre-commit * refactor(test): use LifespanMixin directly instead of DummyApp subclass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9fffe53 commit 941f9bb

3 files changed

Lines changed: 78 additions & 4 deletions

File tree

docs/utility_methods/lifespan_tasks.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ async def long_running_task(foo, bar):
4040
To register a lifespan task, use `app.register_lifespan_task(coro_func, **kwargs)`.
4141
Any keyword arguments specified during registration will be passed to the task.
4242

43-
If the task accepts the special argument, `app`, it will be passed the `Starlette`
44-
application instance.
43+
If the task accepts the special argument, `app`, it will be passed the Reflex app
44+
instance (`rx.App`/`LifespanMixin`).
45+
46+
If the task accepts the special argument, `starlette_app`, it will be passed the
47+
underlying `Starlette` application instance.
4548

4649
```python
4750
app = rx.App()

reflex/app_mixins/lifespan.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get_lifespan_tasks(self) -> tuple[asyncio.Task | Callable, ...]:
8787
return tuple(self._lifespan_tasks)
8888

8989
@contextlib.asynccontextmanager
90-
async def _run_lifespan_tasks(self, app: Starlette):
90+
async def _run_lifespan_tasks(self, starlette_app: Starlette):
9191
self._lifespan_tasks_started = True
9292
running_tasks = []
9393
try:
@@ -100,7 +100,9 @@ async def _run_lifespan_tasks(self, app: Starlette):
100100
else:
101101
signature = inspect.signature(task)
102102
if "app" in signature.parameters:
103-
task = functools.partial(task, app=app)
103+
task = functools.partial(task, app=self)
104+
if "starlette_app" in signature.parameters:
105+
task = functools.partial(task, starlette_app=starlette_app)
104106
t_ = task()
105107
if isinstance(t_, contextlib._AsyncGeneratorContextManager):
106108
await stack.enter_async_context(t_)

tests/units/app_mixins/test_lifespan.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pytest
99
from reflex_base.utils.exceptions import InvalidLifespanTaskTypeError
10+
from starlette.applications import Starlette
1011

1112
from reflex.app_mixins.lifespan import LifespanMixin
1213

@@ -38,6 +39,7 @@ def check_for_updates(timeout: int) -> int:
3839
assert registered_task() == 10
3940

4041

42+
@pytest.mark.asyncio
4143
async def test_register_lifespan_task_rejects_kwargs_for_asyncio_task():
4244
"""Registering kwargs against an asyncio.Task raises a clear error."""
4345
mixin = LifespanMixin()
@@ -53,3 +55,70 @@ async def test_register_lifespan_task_rejects_kwargs_for_asyncio_task():
5355
task.cancel()
5456
with contextlib.suppress(asyncio.CancelledError):
5557
await task
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_lifespan_task_app_param_receives_reflex_app_instance():
62+
"""Lifespan tasks should receive the Reflex app instance, not Starlette."""
63+
mixin = LifespanMixin()
64+
received: dict[str, object] = {}
65+
66+
def lifespan_task(app):
67+
"""Record the app argument injected by the lifespan runner."""
68+
received["app"] = app
69+
70+
mixin.register_lifespan_task(lifespan_task)
71+
72+
async with mixin._run_lifespan_tasks(Starlette()):
73+
await asyncio.sleep(0)
74+
75+
assert received["app"] is mixin
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_lifespan_task_starlette_app_param_receives_starlette_instance():
80+
"""Lifespan tasks should receive the Starlette app when requested."""
81+
mixin = LifespanMixin()
82+
received: dict[str, object] = {}
83+
starlette_app = Starlette()
84+
85+
def lifespan_task(starlette_app):
86+
"""Record the Starlette app argument injected by the lifespan runner.
87+
88+
Args:
89+
starlette_app: Starlette app object injected by the lifespan runner.
90+
"""
91+
received["starlette_app"] = starlette_app
92+
93+
mixin.register_lifespan_task(lifespan_task)
94+
95+
async with mixin._run_lifespan_tasks(starlette_app):
96+
await asyncio.sleep(0)
97+
98+
assert received["starlette_app"] is starlette_app
99+
100+
101+
@pytest.mark.asyncio
102+
async def test_lifespan_task_both_app_and_starlette_app_params_are_injected():
103+
"""Lifespan tasks should receive both app and starlette_app when declared."""
104+
mixin = LifespanMixin()
105+
received: dict[str, object] = {}
106+
starlette_app = Starlette()
107+
108+
def lifespan_task(app, starlette_app):
109+
"""Record both injected app objects from the lifespan runner.
110+
111+
Args:
112+
app: Reflex app object injected by the lifespan runner.
113+
starlette_app: Starlette app object injected by the lifespan runner.
114+
"""
115+
received["app"] = app
116+
received["starlette_app"] = starlette_app
117+
118+
mixin.register_lifespan_task(lifespan_task)
119+
120+
async with mixin._run_lifespan_tasks(starlette_app):
121+
await asyncio.sleep(0)
122+
123+
assert received["app"] is mixin
124+
assert received["starlette_app"] is starlette_app

0 commit comments

Comments
 (0)