@@ -586,6 +586,226 @@ def __eq__(self, other):
586586 ctx2 .run (var .set , ReentrantHash ())
587587 ctx1 == ctx2
588588
589+ def test_get_changed_outside_run (self ):
590+ # Outside any Context.run(), bindings are considered "changed"
591+ v = contextvars .ContextVar ('v' , default = 'dflt' )
592+ val , changed = v .get_changed ()
593+ self .assertEqual (val , 'dflt' )
594+ self .assertFalse (changed ) # default value, not changed
595+ v .set (42 )
596+ val , changed = v .get_changed ()
597+ self .assertEqual (val , 42 )
598+ self .assertTrue (changed ) # set in base context
599+
600+ def test_get_changed_inherited (self ):
601+ # Inherited bindings are not considered "changed"
602+ v = contextvars .ContextVar ('v' )
603+ v .set ('parent' )
604+ ctx = contextvars .copy_context ()
605+
606+ def check ():
607+ val , changed = v .get_changed ()
608+ self .assertEqual (val , 'parent' )
609+ self .assertFalse (changed )
610+ ctx .run (check )
611+
612+ def test_get_changed_after_set (self ):
613+ # After set() inside Context.run(), changed is True
614+ v = contextvars .ContextVar ('v' )
615+ v .set ('parent' )
616+ ctx = contextvars .copy_context ()
617+
618+ def check ():
619+ val , changed = v .get_changed ()
620+ self .assertFalse (changed )
621+ v .set ('child' )
622+ val , changed = v .get_changed ()
623+ self .assertEqual (val , 'child' )
624+ self .assertTrue (changed )
625+ ctx .run (check )
626+
627+ def test_get_changed_new_var_in_run (self ):
628+ # A variable set for the first time inside run() is "changed"
629+ v = contextvars .ContextVar ('v' )
630+ ctx = contextvars .copy_context ()
631+
632+ def check ():
633+ with self .assertRaises (LookupError ):
634+ v .get_changed ()
635+ v .set ('new' )
636+ val , changed = v .get_changed ()
637+ self .assertEqual (val , 'new' )
638+ self .assertTrue (changed )
639+ ctx .run (check )
640+
641+ def test_get_changed_not_set_with_default (self ):
642+ # A variable not set but with default: changed is False
643+ v = contextvars .ContextVar ('v' , default = 'dflt' )
644+ ctx = contextvars .copy_context ()
645+
646+ def check ():
647+ val , changed = v .get_changed ()
648+ self .assertEqual (val , 'dflt' )
649+ self .assertFalse (changed )
650+ ctx .run (check )
651+
652+ def test_get_changed_not_set_no_default (self ):
653+ # A variable that has never been set and has no default
654+ v = contextvars .ContextVar ('v' )
655+ ctx = contextvars .copy_context ()
656+
657+ def check ():
658+ with self .assertRaises (LookupError ):
659+ v .get_changed ()
660+ ctx .run (check )
661+
662+ def test_get_changed_explicit_default_arg (self ):
663+ # Passing a default argument to get_changed()
664+ v = contextvars .ContextVar ('v' )
665+ ctx = contextvars .copy_context ()
666+
667+ def check ():
668+ val , changed = v .get_changed ('fallback' )
669+ self .assertEqual (val , 'fallback' )
670+ self .assertFalse (changed )
671+ ctx .run (check )
672+
673+ def test_get_changed_set_same_object (self ):
674+ # Setting to the exact same object does not count as "changed"
675+ # because the HAMT recognizes the identical key-value pair
676+ obj = object ()
677+ v = contextvars .ContextVar ('v' )
678+ v .set (obj )
679+ ctx = contextvars .copy_context ()
680+
681+ def check ():
682+ val , changed = v .get_changed ()
683+ self .assertIs (val , obj )
684+ self .assertFalse (changed )
685+ v .set (obj ) # same object
686+ val , changed = v .get_changed ()
687+ self .assertIs (val , obj )
688+ self .assertFalse (changed )
689+ ctx .run (check )
690+
691+ def test_get_changed_set_different_object (self ):
692+ # Setting to a different object counts as "changed"
693+ v = contextvars .ContextVar ('v' )
694+ v .set ([1 , 2 , 3 ])
695+ ctx = contextvars .copy_context ()
696+
697+ def check ():
698+ val , changed = v .get_changed ()
699+ self .assertFalse (changed )
700+ v .set ([1 , 2 , 3 ]) # equal value, different object
701+ val , changed = v .get_changed ()
702+ self .assertTrue (changed )
703+ ctx .run (check )
704+
705+ def test_get_changed_after_reset (self ):
706+ # After reset(), the variable reverts to its inherited state
707+ v = contextvars .ContextVar ('v' )
708+ v .set ('original' )
709+ ctx = contextvars .copy_context ()
710+
711+ def check ():
712+ val , changed = v .get_changed ()
713+ self .assertFalse (changed )
714+ tok = v .set ('modified' )
715+ val , changed = v .get_changed ()
716+ self .assertTrue (changed )
717+ v .reset (tok )
718+ val , changed = v .get_changed ()
719+ self .assertFalse (changed )
720+ ctx .run (check )
721+
722+ def test_get_changed_multiple_vars (self ):
723+ # Changing one variable does not affect get_changed() for others
724+ v1 = contextvars .ContextVar ('v1' )
725+ v2 = contextvars .ContextVar ('v2' )
726+ v1 .set ('a' )
727+ v2 .set ('b' )
728+ ctx = contextvars .copy_context ()
729+
730+ def check ():
731+ _ , changed1 = v1 .get_changed ()
732+ _ , changed2 = v2 .get_changed ()
733+ self .assertFalse (changed1 )
734+ self .assertFalse (changed2 )
735+ v1 .set ('a2' )
736+ _ , changed1 = v1 .get_changed ()
737+ _ , changed2 = v2 .get_changed ()
738+ self .assertTrue (changed1 )
739+ self .assertFalse (changed2 )
740+ ctx .run (check )
741+
742+ def test_get_changed_nested_run (self ):
743+ # get_changed() reflects the innermost Context.run() scope
744+ v = contextvars .ContextVar ('v' )
745+ v .set ('root' )
746+ ctx1 = contextvars .copy_context ()
747+
748+ def outer ():
749+ _ , changed = v .get_changed ()
750+ self .assertFalse (changed )
751+ v .set ('outer' )
752+ _ , changed = v .get_changed ()
753+ self .assertTrue (changed )
754+ ctx2 = contextvars .copy_context ()
755+
756+ def inner ():
757+ # inherited 'outer' from ctx1, not changed in ctx2
758+ val , changed = v .get_changed ()
759+ self .assertEqual (val , 'outer' )
760+ self .assertFalse (changed )
761+ v .set ('inner' )
762+ val , changed = v .get_changed ()
763+ self .assertEqual (val , 'inner' )
764+ self .assertTrue (changed )
765+ ctx2 .run (inner )
766+
767+ # after inner run exits, outer's state is restored
768+ _ , changed = v .get_changed ()
769+ self .assertTrue (changed )
770+ ctx1 .run (outer )
771+
772+ def test_get_changed_with_threads (self ):
773+ # get_changed() works correctly in a thread with copied context
774+ import threading
775+ v = contextvars .ContextVar ('v' )
776+ v .set ('parent' )
777+ ctx = contextvars .copy_context ()
778+ results = {}
779+
780+ def thread_func ():
781+ val , changed = v .get_changed ()
782+ results ['inherited' ] = changed
783+ results ['value' ] = val
784+ v .set ('thread' )
785+ val , changed = v .get_changed ()
786+ results ['after_set' ] = changed
787+
788+ t = threading .Thread (target = ctx .run , args = (thread_func ,))
789+ t .start ()
790+ t .join ()
791+ self .assertFalse (results ['inherited' ])
792+ self .assertEqual (results ['value' ], 'parent' )
793+ self .assertTrue (results ['after_set' ])
794+
795+ def test_get_changed_empty_context_run (self ):
796+ # Running in a brand new empty context
797+ v = contextvars .ContextVar ('v' )
798+ ctx = contextvars .Context ()
799+
800+ def check ():
801+ with self .assertRaises (LookupError ):
802+ v .get_changed ()
803+ v .set ('value' )
804+ val , changed = v .get_changed ()
805+ self .assertEqual (val , 'value' )
806+ self .assertTrue (changed )
807+ ctx .run (check )
808+
589809
590810# HAMT Tests
591811
0 commit comments