11from __future__ import annotations
22
33import asyncio
4- from collections .abc import Awaitable , Sequence
4+ from collections .abc import Coroutine , Sequence
5+ from dataclasses import dataclass
56from logging import getLogger
67from types import FunctionType
78from typing import (
89 TYPE_CHECKING ,
910 Any ,
1011 Callable ,
1112 Generic ,
12- NewType ,
1313 Protocol ,
1414 TypeVar ,
1515 cast ,
1616 overload ,
1717)
18+ from weakref import WeakSet
1819
1920from typing_extensions import TypeAlias
2021
@@ -96,30 +97,30 @@ def dispatch(new: _Type | Callable[[_Type], _Type]) -> None:
9697
9798_EffectCleanFunc : TypeAlias = "Callable[[], None]"
9899_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"
101102
102103
103104@overload
104105def use_effect (
105106 function : None = None ,
106107 dependencies : Sequence [Any ] | ellipsis | None = ...,
107- ) -> Callable [[_EffectApplyFunc ], None ]:
108+ ) -> Callable [[_EffectFunc ], None ]:
108109 ...
109110
110111
111112@overload
112113def use_effect (
113- function : _EffectApplyFunc ,
114+ function : _EffectFunc ,
114115 dependencies : Sequence [Any ] | ellipsis | None = ...,
115116) -> None :
116117 ...
117118
118119
119120def use_effect (
120- function : _EffectApplyFunc | None = None ,
121+ function : _EffectFunc | None = None ,
121122 dependencies : Sequence [Any ] | ellipsis | None = ...,
122- ) -> Callable [[_EffectApplyFunc ], None ] | None :
123+ ) -> Callable [[_EffectFunc ], None ] | None :
123124 """See the full :ref:`Use Effect` docs for details
124125
125126 Parameters:
@@ -135,37 +136,25 @@ def use_effect(
135136 If not function is provided, a decorator. Otherwise ``None``.
136137 """
137138 hook = current_hook ()
138-
139139 dependencies = _try_to_infer_closure_values (function , dependencies )
140140 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 )
151142
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 )
157145
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
159151
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
163156
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 ))
169158
170159 if function is not None :
171160 add_effect (function )
@@ -174,6 +163,19 @@ def effect() -> None:
174163 return add_effect
175164
176165
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+
177179def use_debug_value (
178180 message : Any | Callable [[], Any ],
179181 dependencies : Sequence [Any ] | ellipsis | None = ...,
@@ -507,19 +509,6 @@ def current_hook() -> LifeCycleHook:
507509_hook_stack : ThreadLocal [list [LifeCycleHook ]] = ThreadLocal (list )
508510
509511
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-
523512class LifeCycleHook :
524513 """Defines the life cycle of a layout component.
525514
@@ -590,7 +579,8 @@ class LifeCycleHook:
590579 "__weakref__" ,
591580 "_context_providers" ,
592581 "_current_state_index" ,
593- "_event_effects" ,
582+ "_effect_funcs" ,
583+ "_effect_infos" ,
594584 "_is_rendering" ,
595585 "_rendered_atleast_once" ,
596586 "_schedule_render_callback" ,
@@ -612,11 +602,8 @@ def __init__(
612602 self ._rendered_atleast_once = False
613603 self ._current_state_index = 0
614604 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 ()
620607
621608 def schedule_render (self ) -> None :
622609 if self ._is_rendering :
@@ -635,9 +622,9 @@ def use_state(self, function: Callable[[], _Type]) -> _Type:
635622 self ._current_state_index += 1
636623 return result
637624
638- def add_effect (self , effect_type : EffectType , function : Callable [[], None ] ) -> None :
625+ def add_effect (self , start_effect : _EffectStarter ) -> None :
639626 """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 )
641628
642629 def set_context_provider (self , provider : ContextProvider [Any ]) -> None :
643630 self ._context_providers [provider .type ] = provider
@@ -647,52 +634,40 @@ def get_context_provider(
647634 ) -> ContextProvider [_Type ] | None :
648635 return self ._context_providers .get (context )
649636
650- def affect_component_will_render (self , component : ComponentType ) -> None :
637+ async def affect_component_will_render (self , component : ComponentType ) -> None :
651638 """The component is about to render"""
652639 self .component = component
653-
654640 self ._is_rendering = True
655- self ._event_effects [ COMPONENT_WILL_UNMOUNT_EFFECT ]. clear ()
641+ self .set_current ()
656642
657- def affect_component_did_render (self ) -> None :
643+ async def affect_component_did_render (self ) -> None :
658644 """The component completed a render"""
645+ self .unset_current ()
659646 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-
669647 self ._is_rendering = False
670648 self ._rendered_atleast_once = True
671649 self ._current_state_index = 0
672650
673- def affect_layout_did_render (self ) -> None :
651+ async def affect_layout_did_render (self ) -> None :
674652 """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 ()
682657
683658 if self ._schedule_render_later :
684659 self ._schedule_render ()
685660 self ._schedule_render_later = False
686661
687- def affect_component_will_unmount (self ) -> None :
662+ async def affect_component_will_unmount (self ) -> None :
688663 """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 ()
696671
697672 def set_current (self ) -> None :
698673 """Set this hook as the active hook in this thread
@@ -720,6 +695,15 @@ def _schedule_render(self) -> None:
720695 )
721696
722697
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+
723707def strictly_equal (x : Any , y : Any ) -> bool :
724708 """Check if two values are identical or, for a limited set or types, equal.
725709
0 commit comments