8
8
Any ,
9
9
Awaitable ,
10
10
Callable ,
11
- ClassVar ,
12
11
Dict ,
13
12
Generic ,
14
13
List ,
21
20
cast ,
22
21
overload ,
23
22
)
23
+ from warnings import warn
24
24
25
25
from typing_extensions import Protocol
26
26
27
27
from idom .config import IDOM_DEBUG_MODE
28
28
from idom .utils import Ref
29
29
30
+ from ._f_back import f_module_name
30
31
from ._thread_local import ThreadLocal
31
32
from .types import ComponentType , Key , VdomDict
32
33
from .vdom import vdom
@@ -240,107 +241,94 @@ def use_debug_value(
240
241
241
242
242
243
def create_context (
243
- default_value : _StateType , name : str | None = None
244
- ) -> type [ Context [_StateType ] ]:
244
+ default_value : _StateType , name : str = "Context"
245
+ ) -> Context [_StateType ]:
245
246
"""Return a new context type for use in :func:`use_context`"""
247
+ warn (
248
+ "The 'create_context' function is deprecated. "
249
+ "Use the 'Context' class instead. "
250
+ "This function will be removed in a future release."
251
+ )
252
+ return Context (name , default_value )
253
+
246
254
247
- class _Context (Context [_StateType ]):
248
- _default_value = default_value
255
+ _UNDEFINED = object ()
249
256
250
- _Context .__name__ = name or "Context"
251
257
252
- return _Context
258
+ class Context (Generic [_StateType ]):
259
+ """Returns a :class:`ContextProvider` component"""
253
260
261
+ def __init__ (self , name : str , default_value : _StateType ) -> None :
262
+ self .name = name
263
+ self .default_value = default_value
254
264
255
- def use_context (context_type : type [Context [_StateType ]]) -> _StateType :
265
+ def __call__ (
266
+ self ,
267
+ * children : Any ,
268
+ value : _StateType = _UNDEFINED ,
269
+ key : Key | None = None ,
270
+ ) -> (
271
+ # users don't need to see that this is a ContextProvider
272
+ ComponentType
273
+ ):
274
+ return ContextProvider (
275
+ * children ,
276
+ value = self .default_value if value is _UNDEFINED else value ,
277
+ key = key ,
278
+ type = self ,
279
+ )
280
+
281
+ def __repr__ (self ):
282
+ return f"{ type (self ).__name__ } ({ self .name !r} )"
283
+
284
+
285
+ def use_context (context : Context [_StateType ]) -> _StateType :
256
286
"""Get the current value for the given context type.
257
287
258
288
See the full :ref:`Use Context` docs for more information.
259
289
"""
260
- # We have to use a Ref here since, if initially context_type._current is None, and
261
- # then on a subsequent render it is present, we need to be able to dynamically adopt
262
- # that newly present current context. When we update it though, we don't need to
263
- # schedule a new render since we're already rending right now. Thus we can't do this
264
- # with use_state() since we'd incur an extra render when calling set_state.
265
- context_ref : Ref [Context [_StateType ] | None ] = use_ref (None )
266
-
267
- if context_ref .current is None :
268
- provided_context = context_type ._current .get ()
269
- if provided_context is None :
270
- # Cast required because of: https://github.com/python/mypy/issues/5144
271
- return cast (_StateType , context_type ._default_value )
272
- context_ref .current = provided_context
273
-
274
- # We need the hook now so that we can schedule an update when
275
290
hook = current_hook ()
276
291
277
- context = context_ref .current
292
+ provider = hook .get_context_provider (context )
293
+ if provider is None :
294
+ return context .default_value
278
295
279
296
@use_effect
280
297
def subscribe_to_context_change () -> Callable [[], None ]:
281
- def set_context (new : Context [_StateType ]) -> None :
282
- # We don't need to check if `new is not context_ref.current` because we only
283
- # trigger this callback when the value of a context, and thus the context
284
- # itself changes. Therefore we can always schedule a render.
285
- context_ref .current = new
286
- hook .schedule_render ()
287
-
288
- context .subscribers .add (set_context )
289
- return lambda : context .subscribers .remove (set_context )
290
-
291
- return context .value
292
-
298
+ provider .subscribers .add (hook )
299
+ return lambda : provider .subscribers .remove (hook )
293
300
294
- _UNDEFINED : Any = object ()
301
+ return provider . value
295
302
296
303
297
- class Context (Generic [_StateType ]):
298
-
299
- # This should be _StateType instead of Any, but it can't due to this limitation:
300
- # https://github.com/python/mypy/issues/5144
301
- _default_value : ClassVar [Any ]
302
-
303
- _current : ClassVar [ThreadLocal [Context [Any ] | None ]]
304
-
305
- def __init_subclass__ (cls ) -> None :
306
- # every context type tracks which of its instances are currently in use
307
- cls ._current = ThreadLocal (lambda : None )
308
-
304
+ class ContextProvider (Generic [_StateType ]):
309
305
def __init__ (
310
306
self ,
311
307
* children : Any ,
312
- value : _StateType = _UNDEFINED ,
313
- key : Key | None = None ,
308
+ value : _StateType ,
309
+ key : Key | None ,
310
+ type : Context [_StateType ],
314
311
) -> None :
315
312
self .children = children
316
- self .value : _StateType = self . _default_value if value is _UNDEFINED else value
313
+ self .value = value
317
314
self .key = key
318
- self .subscribers : set [Callable [[ Context [ _StateType ]], None ] ] = set ()
319
- self .type = self . __class__
315
+ self .subscribers : set [LifeCycleHook ] = set ()
316
+ self .type = type
320
317
321
318
def render (self ) -> VdomDict :
322
- current_ctx = self .__class__ ._current
323
-
324
- prior_ctx = current_ctx .get ()
325
- current_ctx .set (self )
326
-
327
- def reset_ctx () -> None :
328
- current_ctx .set (prior_ctx )
329
-
330
- current_hook ().add_effect (COMPONENT_DID_RENDER_EFFECT , reset_ctx )
331
-
319
+ current_hook ().set_context_provider (self )
332
320
return vdom ("" , * self .children )
333
321
334
- def should_render (self , new : Context [_StateType ]) -> bool :
322
+ def should_render (self , new : ContextProvider [_StateType ]) -> bool :
335
323
if self .value is not new .value :
336
- new . subscribers . update ( self .subscribers )
337
- for set_context in self . subscribers :
338
- set_context ( new )
324
+ for hook in self .subscribers :
325
+ hook . set_context_provider ( new )
326
+ hook . schedule_render ( )
339
327
return True
340
328
return False
341
329
342
330
def __repr__ (self ) -> str :
343
- return f"{ type (self ).__name__ } ({ id ( self ) } )"
331
+ return f"{ type (self ).__name__ } ({ self . type . name !r } )"
344
332
345
333
346
334
_ActionType = TypeVar ("_ActionType" )
@@ -558,14 +546,14 @@ def _try_to_infer_closure_values(
558
546
559
547
def current_hook () -> LifeCycleHook :
560
548
"""Get the current :class:`LifeCycleHook`"""
561
- hook = _current_hook .get ()
562
- if hook is None :
549
+ hook_stack = _hook_stack .get ()
550
+ if not hook_stack :
563
551
msg = "No life cycle hook is active. Are you rendering in a layout?"
564
552
raise RuntimeError (msg )
565
- return hook
553
+ return hook_stack [ - 1 ]
566
554
567
555
568
- _current_hook : ThreadLocal [LifeCycleHook | None ] = ThreadLocal (lambda : None )
556
+ _hook_stack : ThreadLocal [list [ LifeCycleHook ]] = ThreadLocal (list )
569
557
570
558
571
559
EffectType = NewType ("EffectType" , str )
@@ -630,9 +618,8 @@ class LifeCycleHook:
630
618
631
619
hook.affect_component_did_render()
632
620
633
- # This should only be called after any child components yielded by
634
- # component_instance.render() have also been rendered because effects of
635
- # this type must run after the full set of changes have been resolved.
621
+ # This should only be called after the full set of changes associated with a
622
+ # given render have been completed.
636
623
hook.affect_layout_did_render()
637
624
638
625
# Typically an event occurs and a new render is scheduled, thus begining
@@ -650,6 +637,7 @@ class LifeCycleHook:
650
637
651
638
__slots__ = (
652
639
"__weakref__" ,
640
+ "_context_providers" ,
653
641
"_current_state_index" ,
654
642
"_event_effects" ,
655
643
"_is_rendering" ,
@@ -666,6 +654,7 @@ def __init__(
666
654
self ,
667
655
schedule_render : Callable [[], None ],
668
656
) -> None :
657
+ self ._context_providers : dict [Context [Any ], ContextProvider [Any ]] = {}
669
658
self ._schedule_render_callback = schedule_render
670
659
self ._schedule_render_later = False
671
660
self ._is_rendering = False
@@ -700,6 +689,14 @@ def add_effect(self, effect_type: EffectType, function: Callable[[], None]) -> N
700
689
"""Trigger a function on the occurance of the given effect type"""
701
690
self ._event_effects [effect_type ].append (function )
702
691
692
+ def set_context_provider (self , provider : ContextProvider [Any ]) -> None :
693
+ self ._context_providers [provider .type ] = provider
694
+
695
+ def get_context_provider (
696
+ self , context : Context [_StateType ]
697
+ ) -> ContextProvider [_StateType ] | None :
698
+ return self ._context_providers .get (context )
699
+
703
700
def affect_component_will_render (self , component : ComponentType ) -> None :
704
701
"""The component is about to render"""
705
702
self .component = component
@@ -753,13 +750,16 @@ def set_current(self) -> None:
753
750
This method is called by a layout before entering the render method
754
751
of this hook's associated component.
755
752
"""
756
- _current_hook .set (self )
753
+ hook_stack = _hook_stack .get ()
754
+ if hook_stack :
755
+ parent = hook_stack [- 1 ]
756
+ self ._context_providers .update (parent ._context_providers )
757
+ hook_stack .append (self )
757
758
758
759
def unset_current (self ) -> None :
759
760
"""Unset this hook as the active hook in this thread"""
760
761
# this assertion should never fail - primarilly useful for debug
761
- assert _current_hook .get () is self
762
- _current_hook .set (None )
762
+ assert _hook_stack .get ().pop () is self
763
763
764
764
def _schedule_render (self ) -> None :
765
765
try :
0 commit comments