8
8
Any ,
9
9
Awaitable ,
10
10
Callable ,
11
- ClassVar ,
12
11
Dict ,
13
12
Generic ,
14
13
List ,
@@ -239,108 +238,94 @@ def use_debug_value(
239
238
logger .debug (f"{ current_hook ().component } { new } " )
240
239
241
240
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 ]:
245
242
"""Return a new context type for use in :func:`use_context`"""
246
243
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
249
259
250
- _Context .__name__ = name or "Context"
251
260
252
- return _Context
261
+ class Context (Protocol [_StateType ]):
262
+ """Returns a :class:`ContextProvider` component"""
253
263
264
+ def __call__ (
265
+ self ,
266
+ * children : Any ,
267
+ value : _StateType = ...,
268
+ key : Key | None = ...,
269
+ ) -> ContextProvider [_StateType ]:
270
+ ...
254
271
255
- def use_context (context_type : type [Context [_StateType ]]) -> _StateType :
272
+
273
+ def use_context (context : Context [_StateType ]) -> _StateType :
256
274
"""Get the current value for the given context type.
257
275
258
276
See the full :ref:`Use Context` docs for more information.
259
277
"""
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
278
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" ])
276
290
277
- context = context_ref . current
291
+ subscribers = provider . _subscribers
278
292
279
293
@use_effect
280
294
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 )
293
297
294
- _UNDEFINED : Any = object ()
298
+ return provider . _value
295
299
296
300
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 ]):
309
302
def __init__ (
310
303
self ,
311
304
* children : Any ,
312
- value : _StateType = _UNDEFINED ,
313
- key : Key | None = None ,
305
+ value : _StateType ,
306
+ key : Key | None ,
307
+ type : Context [_StateType ],
314
308
) -> None :
315
309
self .children = children
316
- self .value : _StateType = self ._default_value if value is _UNDEFINED else value
317
310
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
320
314
321
315
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 )
332
317
return vdom ("" , * self .children )
333
318
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 ( )
339
324
return True
340
325
return False
341
326
342
327
def __repr__ (self ) -> str :
343
- return f"{ type (self ).__name__ } ({ id ( self ) } )"
328
+ return f"{ type (self ).__name__ } ({ self . type } )"
344
329
345
330
346
331
_ActionType = TypeVar ("_ActionType" )
@@ -558,14 +543,14 @@ def _try_to_infer_closure_values(
558
543
559
544
def current_hook () -> LifeCycleHook :
560
545
"""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 :
563
548
msg = "No life cycle hook is active. Are you rendering in a layout?"
564
549
raise RuntimeError (msg )
565
- return hook
550
+ return hook_stack [ - 1 ]
566
551
567
552
568
- _current_hook : ThreadLocal [LifeCycleHook | None ] = ThreadLocal (lambda : None )
553
+ _hook_stack : ThreadLocal [list [ LifeCycleHook ]] = ThreadLocal (list )
569
554
570
555
571
556
EffectType = NewType ("EffectType" , str )
@@ -630,9 +615,8 @@ class LifeCycleHook:
630
615
631
616
hook.affect_component_did_render()
632
617
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.
636
620
hook.affect_layout_did_render()
637
621
638
622
# Typically an event occurs and a new render is scheduled, thus begining
@@ -650,6 +634,7 @@ class LifeCycleHook:
650
634
651
635
__slots__ = (
652
636
"__weakref__" ,
637
+ "_context_providers" ,
653
638
"_current_state_index" ,
654
639
"_event_effects" ,
655
640
"_is_rendering" ,
@@ -666,6 +651,7 @@ def __init__(
666
651
self ,
667
652
schedule_render : Callable [[], None ],
668
653
) -> None :
654
+ self ._context_providers : dict [Context [Any ], ContextProvider [Any ]] = {}
669
655
self ._schedule_render_callback = schedule_render
670
656
self ._schedule_render_later = False
671
657
self ._is_rendering = False
@@ -700,6 +686,14 @@ def add_effect(self, effect_type: EffectType, function: Callable[[], None]) -> N
700
686
"""Trigger a function on the occurance of the given effect type"""
701
687
self ._event_effects [effect_type ].append (function )
702
688
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
+
703
697
def affect_component_will_render (self , component : ComponentType ) -> None :
704
698
"""The component is about to render"""
705
699
self .component = component
@@ -753,13 +747,16 @@ def set_current(self) -> None:
753
747
This method is called by a layout before entering the render method
754
748
of this hook's associated component.
755
749
"""
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 )
757
755
758
756
def unset_current (self ) -> None :
759
757
"""Unset this hook as the active hook in this thread"""
760
758
# 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
763
760
764
761
def _schedule_render (self ) -> None :
765
762
try :
0 commit comments