1
1
from __future__ import annotations
2
2
3
3
import asyncio
4
- from collections .abc import Awaitable , Sequence
4
+ from collections .abc import Coroutine , Sequence
5
+ from dataclasses import dataclass
5
6
from logging import getLogger
6
7
from types import FunctionType
7
8
from typing import (
8
9
TYPE_CHECKING ,
9
10
Any ,
10
11
Callable ,
11
12
Generic ,
12
- NewType ,
13
13
Protocol ,
14
14
TypeVar ,
15
15
cast ,
16
16
overload ,
17
17
)
18
+ from weakref import WeakSet
18
19
19
20
from typing_extensions import TypeAlias
20
21
@@ -96,30 +97,30 @@ def dispatch(new: _Type | Callable[[_Type], _Type]) -> None:
96
97
97
98
_EffectCleanFunc : TypeAlias = "Callable[[], None]"
98
99
_SyncEffectFunc : TypeAlias = "Callable[[], _EffectCleanFunc | None]"
99
- _AsyncEffectFunc : TypeAlias = "Callable[[], Awaitable[_EffectCleanFunc | None]]"
100
- _EffectApplyFunc : TypeAlias = "_SyncEffectFunc | _AsyncEffectFunc"
100
+ _AsyncEffectFunc : TypeAlias = "Callable[[asyncio.Event ], Coroutine[None, None, None]]"
101
+ _EffectFunc : TypeAlias = "_SyncEffectFunc | _AsyncEffectFunc"
101
102
102
103
103
104
@overload
104
105
def use_effect (
105
106
function : None = None ,
106
107
dependencies : Sequence [Any ] | ellipsis | None = ...,
107
- ) -> Callable [[_EffectApplyFunc ], None ]:
108
+ ) -> Callable [[_EffectFunc ], None ]:
108
109
...
109
110
110
111
111
112
@overload
112
113
def use_effect (
113
- function : _EffectApplyFunc ,
114
+ function : _EffectFunc ,
114
115
dependencies : Sequence [Any ] | ellipsis | None = ...,
115
116
) -> None :
116
117
...
117
118
118
119
119
120
def use_effect (
120
- function : _EffectApplyFunc | None = None ,
121
+ function : _EffectFunc | None = None ,
121
122
dependencies : Sequence [Any ] | ellipsis | None = ...,
122
- ) -> Callable [[_EffectApplyFunc ], None ] | None :
123
+ ) -> Callable [[_EffectFunc ], None ] | None :
123
124
"""See the full :ref:`Use Effect` docs for details
124
125
125
126
Parameters:
@@ -135,37 +136,25 @@ def use_effect(
135
136
If not function is provided, a decorator. Otherwise ``None``.
136
137
"""
137
138
hook = current_hook ()
138
-
139
139
dependencies = _try_to_infer_closure_values (function , dependencies )
140
140
memoize = use_memo (dependencies = dependencies )
141
- last_clean_callback : Ref [_EffectCleanFunc | None ] = use_ref (None )
142
-
143
- def add_effect (function : _EffectApplyFunc ) -> None :
144
- if not asyncio .iscoroutinefunction (function ):
145
- sync_function = cast (_SyncEffectFunc , function )
146
- else :
147
- async_function = cast (_AsyncEffectFunc , function )
148
-
149
- def sync_function () -> _EffectCleanFunc | None :
150
- future = asyncio .ensure_future (async_function ())
141
+ effect_info : Ref [_EffectInfo | None ] = use_ref (None )
151
142
152
- def clean_future () -> None :
153
- if not future .cancel ():
154
- clean = future .result ()
155
- if clean is not None :
156
- clean ()
143
+ def add_effect (function : _EffectFunc ) -> None :
144
+ effect = _cast_async_effect (function )
157
145
158
- return clean_future
146
+ async def create_effect_task () -> _EffectInfo :
147
+ if effect_info .current is not None :
148
+ last_effect_info = effect_info .current
149
+ last_effect_info .stop .set ()
150
+ await last_effect_info .task
159
151
160
- def effect () -> None :
161
- if last_clean_callback .current is not None :
162
- last_clean_callback .current ()
152
+ stop = asyncio .Event ()
153
+ info = _EffectInfo (asyncio .create_task (effect (stop )), stop )
154
+ effect_info .current = info
155
+ return info
163
156
164
- clean = last_clean_callback .current = sync_function ()
165
- if clean is not None :
166
- hook .add_effect (COMPONENT_WILL_UNMOUNT_EFFECT , clean )
167
-
168
- return memoize (lambda : hook .add_effect (LAYOUT_DID_RENDER_EFFECT , effect ))
157
+ return memoize (lambda : hook .add_effect (create_effect_task ))
169
158
170
159
if function is not None :
171
160
add_effect (function )
@@ -174,6 +163,19 @@ def effect() -> None:
174
163
return add_effect
175
164
176
165
166
+ def _cast_async_effect (function : Callable [..., Any ]) -> _AsyncEffectFunc :
167
+ if asyncio .iscoroutinefunction (function ):
168
+ return function
169
+
170
+ async def wrapper (stop : asyncio .Event ) -> None :
171
+ cleanup = function ()
172
+ await stop .wait ()
173
+ if cleanup is not None :
174
+ cleanup ()
175
+
176
+ return wrapper
177
+
178
+
177
179
def use_debug_value (
178
180
message : Any | Callable [[], Any ],
179
181
dependencies : Sequence [Any ] | ellipsis | None = ...,
@@ -507,19 +509,6 @@ def current_hook() -> LifeCycleHook:
507
509
_hook_stack : ThreadLocal [list [LifeCycleHook ]] = ThreadLocal (list )
508
510
509
511
510
- EffectType = NewType ("EffectType" , str )
511
- """Used in :meth:`LifeCycleHook.add_effect` to indicate what effect should be saved"""
512
-
513
- COMPONENT_DID_RENDER_EFFECT = EffectType ("COMPONENT_DID_RENDER" )
514
- """An effect that will be triggered each time a component renders"""
515
-
516
- LAYOUT_DID_RENDER_EFFECT = EffectType ("LAYOUT_DID_RENDER" )
517
- """An effect that will be triggered each time a layout renders"""
518
-
519
- COMPONENT_WILL_UNMOUNT_EFFECT = EffectType ("COMPONENT_WILL_UNMOUNT" )
520
- """An effect that will be triggered just before the component is unmounted"""
521
-
522
-
523
512
class LifeCycleHook :
524
513
"""Defines the life cycle of a layout component.
525
514
@@ -590,7 +579,8 @@ class LifeCycleHook:
590
579
"__weakref__" ,
591
580
"_context_providers" ,
592
581
"_current_state_index" ,
593
- "_event_effects" ,
582
+ "_effect_funcs" ,
583
+ "_effect_infos" ,
594
584
"_is_rendering" ,
595
585
"_rendered_atleast_once" ,
596
586
"_schedule_render_callback" ,
@@ -612,11 +602,8 @@ def __init__(
612
602
self ._rendered_atleast_once = False
613
603
self ._current_state_index = 0
614
604
self ._state : tuple [Any , ...] = ()
615
- self ._event_effects : dict [EffectType , list [Callable [[], None ]]] = {
616
- COMPONENT_DID_RENDER_EFFECT : [],
617
- LAYOUT_DID_RENDER_EFFECT : [],
618
- COMPONENT_WILL_UNMOUNT_EFFECT : [],
619
- }
605
+ self ._effect_funcs : list [_EffectStarter ] = []
606
+ self ._effect_infos : WeakSet [_EffectInfo ] = WeakSet ()
620
607
621
608
def schedule_render (self ) -> None :
622
609
if self ._is_rendering :
@@ -635,9 +622,9 @@ def use_state(self, function: Callable[[], _Type]) -> _Type:
635
622
self ._current_state_index += 1
636
623
return result
637
624
638
- def add_effect (self , effect_type : EffectType , function : Callable [[], None ] ) -> None :
625
+ def add_effect (self , start_effect : _EffectStarter ) -> None :
639
626
"""Trigger a function on the occurrence of the given effect type"""
640
- self ._event_effects [ effect_type ] .append (function )
627
+ self ._effect_funcs .append (start_effect )
641
628
642
629
def set_context_provider (self , provider : ContextProvider [Any ]) -> None :
643
630
self ._context_providers [provider .type ] = provider
@@ -647,52 +634,40 @@ def get_context_provider(
647
634
) -> ContextProvider [_Type ] | None :
648
635
return self ._context_providers .get (context )
649
636
650
- def affect_component_will_render (self , component : ComponentType ) -> None :
637
+ async def affect_component_will_render (self , component : ComponentType ) -> None :
651
638
"""The component is about to render"""
652
639
self .component = component
653
-
654
640
self ._is_rendering = True
655
- self ._event_effects [ COMPONENT_WILL_UNMOUNT_EFFECT ]. clear ()
641
+ self .set_current ()
656
642
657
- def affect_component_did_render (self ) -> None :
643
+ async def affect_component_did_render (self ) -> None :
658
644
"""The component completed a render"""
645
+ self .unset_current ()
659
646
del self .component
660
-
661
- component_did_render_effects = self ._event_effects [COMPONENT_DID_RENDER_EFFECT ]
662
- for effect in component_did_render_effects :
663
- try :
664
- effect ()
665
- except Exception :
666
- logger .exception (f"Component post-render effect { effect } failed" )
667
- component_did_render_effects .clear ()
668
-
669
647
self ._is_rendering = False
670
648
self ._rendered_atleast_once = True
671
649
self ._current_state_index = 0
672
650
673
- def affect_layout_did_render (self ) -> None :
651
+ async def affect_layout_did_render (self ) -> None :
674
652
"""The layout completed a render"""
675
- layout_did_render_effects = self ._event_effects [LAYOUT_DID_RENDER_EFFECT ]
676
- for effect in layout_did_render_effects :
677
- try :
678
- effect ()
679
- except Exception :
680
- logger .exception (f"Layout post-render effect { effect } failed" )
681
- layout_did_render_effects .clear ()
653
+ for start_effect in self ._effect_funcs :
654
+ effect_info = await start_effect ()
655
+ self ._effect_infos .add (effect_info )
656
+ self ._effect_funcs .clear ()
682
657
683
658
if self ._schedule_render_later :
684
659
self ._schedule_render ()
685
660
self ._schedule_render_later = False
686
661
687
- def affect_component_will_unmount (self ) -> None :
662
+ async def affect_component_will_unmount (self ) -> None :
688
663
"""The component is about to be removed from the layout"""
689
- will_unmount_effects = self ._event_effects [ COMPONENT_WILL_UNMOUNT_EFFECT ]
690
- for effect in will_unmount_effects :
691
- try :
692
- effect ( )
693
- except Exception :
694
- logger .exception (f"Pre-unmount effect { effect } failed " )
695
- will_unmount_effects .clear ()
664
+ for infos in self ._effect_infos :
665
+ infos . stop . set ()
666
+ try :
667
+ await asyncio . gather ( * [ i . task for i in self . _effect_infos ] )
668
+ except Exception :
669
+ logger .exception ("Error during effect cancellation " )
670
+ self . _effect_infos .clear ()
696
671
697
672
def set_current (self ) -> None :
698
673
"""Set this hook as the active hook in this thread
@@ -720,6 +695,15 @@ def _schedule_render(self) -> None:
720
695
)
721
696
722
697
698
+ _EffectStarter : TypeAlias = "Callable[[], Coroutine[None, None, _EffectInfo]]"
699
+
700
+
701
+ @dataclass (frozen = True )
702
+ class _EffectInfo :
703
+ task : asyncio .Task [None ]
704
+ stop : asyncio .Event
705
+
706
+
723
707
def strictly_equal (x : Any , y : Any ) -> bool :
724
708
"""Check if two values are identical or, for a limited set or types, equal.
725
709
0 commit comments