1
1
from __future__ import annotations
2
2
3
3
import logging
4
- from asyncio import gather
5
- from collections .abc import Awaitable
6
- from typing import Any , Callable , TypeVar
4
+ from asyncio import Event , Task , create_task , gather
5
+ from typing import Any , Callable , Protocol , TypeVar
7
6
8
7
from anyio import Semaphore
9
8
12
11
13
12
T = TypeVar ("T" )
14
13
14
+
15
+ class EffectFunc (Protocol ):
16
+ async def __call__ (self , stop : Event ) -> None :
17
+ ...
18
+
19
+
15
20
logger = logging .getLogger (__name__ )
16
21
17
22
_HOOK_STATE : ThreadLocal [list [LifeCycleHook ]] = ThreadLocal (list )
@@ -95,8 +100,9 @@ async def stop_effect():
95
100
"__weakref__" ,
96
101
"_context_providers" ,
97
102
"_current_state_index" ,
98
- "_effect_cleanups" ,
99
- "_effect_startups" ,
103
+ "_effect_funcs" ,
104
+ "_effect_tasks" ,
105
+ "_effect_stops" ,
100
106
"_render_access" ,
101
107
"_rendered_atleast_once" ,
102
108
"_schedule_render_callback" ,
@@ -117,8 +123,9 @@ def __init__(
117
123
self ._rendered_atleast_once = False
118
124
self ._current_state_index = 0
119
125
self ._state : tuple [Any , ...] = ()
120
- self ._effect_startups : list [Callable [[], Awaitable [None ]]] = []
121
- self ._effect_cleanups : list [Callable [[], Awaitable [None ]]] = []
126
+ self ._effect_funcs : list [EffectFunc ] = []
127
+ self ._effect_tasks : list [Task [None ]] = []
128
+ self ._effect_stops : list [Event ] = []
122
129
self ._render_access = Semaphore (1 ) # ensure only one render at a time
123
130
124
131
def schedule_render (self ) -> None :
@@ -138,19 +145,15 @@ def use_state(self, function: Callable[[], T]) -> T:
138
145
self ._current_state_index += 1
139
146
return result
140
147
141
- def add_effect (
142
- self ,
143
- start_effect : Callable [[], Awaitable [None ]],
144
- clean_effect : Callable [[], Awaitable [None ]],
145
- ) -> None :
148
+ def add_effect (self , effect_func : EffectFunc ) -> None :
146
149
"""Add an effect to this hook
147
150
148
- Effects are started when the component is done renderig and cleaned up when the
149
- component is removed from the layout. Any other actions (e.g. re-running the
150
- effect if a dependency changes) are the responsibility of the effect itself.
151
+ A task to run the effect is created when the component is done rendering.
152
+ When the component will be unmounted, the event passed to the effect is
153
+ triggered and the task is awaited. The effect should eventually halt after
154
+ the event is triggered.
151
155
"""
152
- self ._effect_startups .append (start_effect )
153
- self ._effect_cleanups .append (clean_effect )
156
+ self ._effect_funcs .append (effect_func )
154
157
155
158
def set_context_provider (self , provider : ContextProviderType [Any ]) -> None :
156
159
self ._context_providers [provider .type ] = provider
@@ -176,24 +179,25 @@ async def affect_component_did_render(self) -> None:
176
179
177
180
async def affect_layout_did_render (self ) -> None :
178
181
"""The layout completed a render"""
179
- try :
180
- await gather (* [start () for start in self ._effect_startups ])
181
- except Exception :
182
- logger .exception ("Error during effect startup" )
183
- finally :
184
- self ._effect_startups .clear ()
185
- if self ._schedule_render_later :
186
- self ._schedule_render ()
187
- self ._schedule_render_later = False
182
+ stop = Event ()
183
+ self ._effect_stops .append (stop )
184
+ self ._effect_tasks .extend (create_task (e (stop )) for e in self ._effect_funcs )
185
+ self ._effect_funcs .clear ()
186
+ if self ._schedule_render_later :
187
+ self ._schedule_render ()
188
+ self ._schedule_render_later = False
188
189
189
190
async def affect_component_will_unmount (self ) -> None :
190
191
"""The component is about to be removed from the layout"""
192
+ for stop in self ._effect_stops :
193
+ stop .set ()
194
+ self ._effect_stops .clear ()
191
195
try :
192
- await gather (* [ clean () for clean in self ._effect_cleanups ] )
196
+ await gather (* self ._effect_tasks )
193
197
except Exception :
194
- logger .exception ("Error during effect cleanup " )
198
+ logger .exception ("Error in effect" )
195
199
finally :
196
- self ._effect_cleanups .clear ()
200
+ self ._effect_tasks .clear ()
197
201
198
202
def set_current (self ) -> None :
199
203
"""Set this hook as the active hook in this thread
0 commit comments