88 Any ,
99 Awaitable ,
1010 Callable ,
11- ClassVar ,
1211 Dict ,
1312 Generic ,
1413 List ,
@@ -239,108 +238,94 @@ def use_debug_value(
239238 logger .debug (f"{ current_hook ().component } { new } " )
240239
241240
242- def create_context (
243- default_value : _StateType , name : str | None = None
244- ) -> type [Context [_StateType ]]:
241+ def create_context (default_value : _StateType ) -> Context [_StateType ]:
245242 """Return a new context type for use in :func:`use_context`"""
246243
247- class _Context (Context [_StateType ]):
248- _default_value = default_value
244+ def context (
245+ * children : Any ,
246+ value : _StateType = default_value ,
247+ key : Key | None = None ,
248+ ) -> ContextProvider [_StateType ]:
249+ return ContextProvider (
250+ * children ,
251+ value = value ,
252+ key = key ,
253+ type = context ,
254+ )
255+
256+ context .__qualname__ = "context"
257+
258+ return context
249259
250- _Context .__name__ = name or "Context"
251260
252- return _Context
261+ class Context (Protocol [_StateType ]):
262+ """Returns a :class:`ContextProvider` component"""
253263
264+ def __call__ (
265+ self ,
266+ * children : Any ,
267+ value : _StateType = ...,
268+ key : Key | None = ...,
269+ ) -> ContextProvider [_StateType ]:
270+ ...
254271
255- def use_context (context_type : type [Context [_StateType ]]) -> _StateType :
272+
273+ def use_context (context : Context [_StateType ]) -> _StateType :
256274 """Get the current value for the given context type.
257275
258276 See the full :ref:`Use Context` docs for more information.
259277 """
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
275278 hook = current_hook ()
279+ provider = hook .get_context_provider (context )
280+
281+ if provider is None :
282+ # force type checker to realize this is just a normal function
283+ assert isinstance (context , FunctionType ), f"{ context } is not a Context"
284+ # __kwdefault__ can be None if no kwarg only parameters exist
285+ assert context .__kwdefaults__ is not None , f"{ context } has no 'value' kwarg"
286+ # lastly check that 'value' kwarg exists
287+ assert "value" in context .__kwdefaults__ , f"{ context } has no 'value' kwarg"
288+ # then we can safely access the context's default value
289+ return cast (_StateType , context .__kwdefaults__ ["value" ])
276290
277- context = context_ref . current
291+ subscribers = provider . _subscribers
278292
279293 @use_effect
280294 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-
295+ subscribers .add (hook )
296+ return lambda : subscribers .remove (hook )
293297
294- _UNDEFINED : Any = object ()
298+ return provider . _value
295299
296300
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-
301+ class ContextProvider (Generic [_StateType ]):
309302 def __init__ (
310303 self ,
311304 * children : Any ,
312- value : _StateType = _UNDEFINED ,
313- key : Key | None = None ,
305+ value : _StateType ,
306+ key : Key | None ,
307+ type : Context [_StateType ],
314308 ) -> None :
315309 self .children = children
316- self .value : _StateType = self ._default_value if value is _UNDEFINED else value
317310 self .key = key
318- self .subscribers : set [Callable [[Context [_StateType ]], None ]] = set ()
319- self .type = self .__class__
311+ self .type = type
312+ self ._subscribers : set [LifeCycleHook ] = set ()
313+ self ._value = value
320314
321315 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-
316+ current_hook ().set_context_provider (self )
332317 return vdom ("" , * self .children )
333318
334- def should_render (self , new : Context [_StateType ]) -> bool :
335- if self .value is not new .value :
336- new . subscribers . update ( self .subscribers )
337- for set_context in self . subscribers :
338- set_context ( new )
319+ def should_render (self , new : ContextProvider [_StateType ]) -> bool :
320+ if self ._value is not new ._value :
321+ for hook in self ._subscribers :
322+ hook . set_context_provider ( new )
323+ hook . schedule_render ( )
339324 return True
340325 return False
341326
342327 def __repr__ (self ) -> str :
343- return f"{ type (self ).__name__ } ({ id ( self ) } )"
328+ return f"{ type (self ).__name__ } ({ self . type } )"
344329
345330
346331_ActionType = TypeVar ("_ActionType" )
@@ -558,14 +543,14 @@ def _try_to_infer_closure_values(
558543
559544def current_hook () -> LifeCycleHook :
560545 """Get the current :class:`LifeCycleHook`"""
561- hook = _current_hook .get ()
562- if hook is None :
546+ hook_stack = _hook_stack .get ()
547+ if not hook_stack :
563548 msg = "No life cycle hook is active. Are you rendering in a layout?"
564549 raise RuntimeError (msg )
565- return hook
550+ return hook_stack [ - 1 ]
566551
567552
568- _current_hook : ThreadLocal [LifeCycleHook | None ] = ThreadLocal (lambda : None )
553+ _hook_stack : ThreadLocal [list [ LifeCycleHook ]] = ThreadLocal (list )
569554
570555
571556EffectType = NewType ("EffectType" , str )
@@ -630,9 +615,8 @@ class LifeCycleHook:
630615
631616 hook.affect_component_did_render()
632617
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.
618+ # This should only be called after the full set of changes associated with a
619+ # given render have been completed.
636620 hook.affect_layout_did_render()
637621
638622 # Typically an event occurs and a new render is scheduled, thus begining
@@ -650,6 +634,7 @@ class LifeCycleHook:
650634
651635 __slots__ = (
652636 "__weakref__" ,
637+ "_context_providers" ,
653638 "_current_state_index" ,
654639 "_event_effects" ,
655640 "_is_rendering" ,
@@ -666,6 +651,7 @@ def __init__(
666651 self ,
667652 schedule_render : Callable [[], None ],
668653 ) -> None :
654+ self ._context_providers : dict [Context [Any ], ContextProvider [Any ]] = {}
669655 self ._schedule_render_callback = schedule_render
670656 self ._schedule_render_later = False
671657 self ._is_rendering = False
@@ -700,6 +686,14 @@ def add_effect(self, effect_type: EffectType, function: Callable[[], None]) -> N
700686 """Trigger a function on the occurance of the given effect type"""
701687 self ._event_effects [effect_type ].append (function )
702688
689+ def set_context_provider (self , provider : ContextProvider [Any ]) -> None :
690+ self ._context_providers [provider .type ] = provider
691+
692+ def get_context_provider (
693+ self , context : Context [_StateType ]
694+ ) -> ContextProvider [_StateType ] | None :
695+ return self ._context_providers .get (context )
696+
703697 def affect_component_will_render (self , component : ComponentType ) -> None :
704698 """The component is about to render"""
705699 self .component = component
@@ -753,13 +747,16 @@ def set_current(self) -> None:
753747 This method is called by a layout before entering the render method
754748 of this hook's associated component.
755749 """
756- _current_hook .set (self )
750+ hook_stack = _hook_stack .get ()
751+ if hook_stack :
752+ parent = hook_stack [- 1 ]
753+ self ._context_providers .update (parent ._context_providers )
754+ hook_stack .append (self )
757755
758756 def unset_current (self ) -> None :
759757 """Unset this hook as the active hook in this thread"""
760758 # this assertion should never fail - primarilly useful for debug
761- assert _current_hook .get () is self
762- _current_hook .set (None )
759+ assert _hook_stack .get ().pop () is self
763760
764761 def _schedule_render (self ) -> None :
765762 try :
0 commit comments