88 Any ,
99 Awaitable ,
1010 Callable ,
11- ClassVar ,
1211 Dict ,
1312 Generic ,
1413 List ,
2120 cast ,
2221 overload ,
2322)
23+ from warnings import warn
2424
2525from typing_extensions import Protocol
2626
2727from idom .config import IDOM_DEBUG_MODE
2828from idom .utils import Ref
2929
30+ from ._f_back import f_module_name
3031from ._thread_local import ThreadLocal
3132from .types import ComponentType , Key , VdomDict
3233from .vdom import vdom
@@ -240,107 +241,94 @@ def use_debug_value(
240241
241242
242243def create_context (
243- default_value : _StateType , name : str | None = None
244- ) -> type [ Context [_StateType ] ]:
244+ default_value : _StateType , name : str = "Context"
245+ ) -> Context [_StateType ]:
245246 """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+
246254
247- class _Context (Context [_StateType ]):
248- _default_value = default_value
255+ _UNDEFINED = object ()
249256
250- _Context .__name__ = name or "Context"
251257
252- return _Context
258+ class Context (Generic [_StateType ]):
259+ """Returns a :class:`ContextProvider` component"""
253260
261+ def __init__ (self , name : str , default_value : _StateType ) -> None :
262+ self .name = name
263+ self .default_value = default_value
254264
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 :
256286 """Get the current value for the given context type.
257287
258288 See the full :ref:`Use Context` docs for more information.
259289 """
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
275290 hook = current_hook ()
276291
277- context = context_ref .current
292+ provider = hook .get_context_provider (context )
293+ if provider is None :
294+ return context .default_value
278295
279296 @use_effect
280297 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 )
293300
294- _UNDEFINED : Any = object ()
301+ return provider . value
295302
296303
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 ]):
309305 def __init__ (
310306 self ,
311307 * children : Any ,
312- value : _StateType = _UNDEFINED ,
313- key : Key | None = None ,
308+ value : _StateType ,
309+ key : Key | None ,
310+ type : Context [_StateType ],
314311 ) -> None :
315312 self .children = children
316- self .value : _StateType = self . _default_value if value is _UNDEFINED else value
313+ self .value = value
317314 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
320317
321318 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 )
332320 return vdom ("" , * self .children )
333321
334- def should_render (self , new : Context [_StateType ]) -> bool :
322+ def should_render (self , new : ContextProvider [_StateType ]) -> bool :
335323 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 ( )
339327 return True
340328 return False
341329
342330 def __repr__ (self ) -> str :
343- return f"{ type (self ).__name__ } ({ id ( self ) } )"
331+ return f"{ type (self ).__name__ } ({ self . type . name !r } )"
344332
345333
346334_ActionType = TypeVar ("_ActionType" )
@@ -558,14 +546,14 @@ def _try_to_infer_closure_values(
558546
559547def current_hook () -> LifeCycleHook :
560548 """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 :
563551 msg = "No life cycle hook is active. Are you rendering in a layout?"
564552 raise RuntimeError (msg )
565- return hook
553+ return hook_stack [ - 1 ]
566554
567555
568- _current_hook : ThreadLocal [LifeCycleHook | None ] = ThreadLocal (lambda : None )
556+ _hook_stack : ThreadLocal [list [ LifeCycleHook ]] = ThreadLocal (list )
569557
570558
571559EffectType = NewType ("EffectType" , str )
@@ -630,9 +618,8 @@ class LifeCycleHook:
630618
631619 hook.affect_component_did_render()
632620
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.
636623 hook.affect_layout_did_render()
637624
638625 # Typically an event occurs and a new render is scheduled, thus begining
@@ -650,6 +637,7 @@ class LifeCycleHook:
650637
651638 __slots__ = (
652639 "__weakref__" ,
640+ "_context_providers" ,
653641 "_current_state_index" ,
654642 "_event_effects" ,
655643 "_is_rendering" ,
@@ -666,6 +654,7 @@ def __init__(
666654 self ,
667655 schedule_render : Callable [[], None ],
668656 ) -> None :
657+ self ._context_providers : dict [Context [Any ], ContextProvider [Any ]] = {}
669658 self ._schedule_render_callback = schedule_render
670659 self ._schedule_render_later = False
671660 self ._is_rendering = False
@@ -700,6 +689,14 @@ def add_effect(self, effect_type: EffectType, function: Callable[[], None]) -> N
700689 """Trigger a function on the occurance of the given effect type"""
701690 self ._event_effects [effect_type ].append (function )
702691
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+
703700 def affect_component_will_render (self , component : ComponentType ) -> None :
704701 """The component is about to render"""
705702 self .component = component
@@ -753,13 +750,16 @@ def set_current(self) -> None:
753750 This method is called by a layout before entering the render method
754751 of this hook's associated component.
755752 """
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 )
757758
758759 def unset_current (self ) -> None :
759760 """Unset this hook as the active hook in this thread"""
760761 # 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
763763
764764 def _schedule_render (self ) -> None :
765765 try :
0 commit comments