44import contextvars
55import functools
66import gc
7+ import os
78import random
89import time
910import unittest
11+ import warnings
1012import weakref
1113from test import support
12- from test .support import threading_helper
14+ from test .support import script_helper , threading_helper
1315
1416try :
1517 from _testinternalcapi import hamt
1618except ImportError :
1719 hamt = None
1820
1921
22+ THREAD_INHERIT_CONTEXT_WARNING = (
23+ not support .Py_GIL_DISABLED
24+ and not sys .flags .thread_inherit_context
25+ and "thread_inherit_context" not in sys ._xoptions
26+ and (sys .flags .ignore_environment
27+ # An empty value is treated as unset, as in Python/initconfig.c.
28+ or not os .environ .get ("PYTHON_THREAD_INHERIT_CONTEXT" ))
29+ )
30+
31+
2032def isolated_context (func ):
2133 """Needed to make reftracking test mode work."""
2234 @functools .wraps (func )
@@ -400,12 +412,12 @@ def test_context_thread_inherit(self):
400412
401413 cvar = contextvars .ContextVar ('cvar' )
402414
415+ results = []
416+
403417 def run_context_none ():
404- if sys .flags .thread_inherit_context :
405- expected = 1
406- else :
407- expected = None
408- self .assertEqual (cvar .get (None ), expected )
418+ with warnings .catch_warnings (record = True ) as caught :
419+ warnings .simplefilter ("always" , DeprecationWarning )
420+ results .append ((cvar .get (None ), len (caught )))
409421
410422 # By default, context is inherited based on the
411423 # sys.flags.thread_inherit_context option.
@@ -420,6 +432,12 @@ def run_context_none():
420432 thread .start ()
421433 thread .join ()
422434
435+ if sys .flags .thread_inherit_context :
436+ self .assertEqual (results , [(1 , 0 ), (1 , 0 )])
437+ else :
438+ warning_count = int (THREAD_INHERIT_CONTEXT_WARNING )
439+ self .assertEqual (results , [(None , warning_count )] * 2 )
440+
423441 # An explicit Context value can also be passed
424442 custom_ctx = contextvars .Context ()
425443 custom_var = None
@@ -447,6 +465,220 @@ def run_empty():
447465 thread .start ()
448466 thread .join ()
449467
468+ @isolated_context
469+ @threading_helper .requires_working_threading ()
470+ @unittest .skipUnless (THREAD_INHERIT_CONTEXT_WARNING ,
471+ "requires the warning-enabled GIL-build default" )
472+ def test_context_thread_inherit_warning (self ):
473+ import threading
474+
475+ call_default = contextvars .ContextVar ("call_default" )
476+ var_default = contextvars .ContextVar ("var_default" , default = "variable" )
477+ missing = contextvars .ContextVar ("missing" )
478+ other = contextvars .ContextVar ("other" )
479+ for var in (call_default , var_default , missing , other ):
480+ var .set ("starter" )
481+
482+ results = []
483+
484+ def target ():
485+ with warnings .catch_warnings (record = True ) as caught :
486+ warnings .simplefilter ("always" , DeprecationWarning )
487+ # Explicit nested contexts and copies of the child context do
488+ # not retain the compatibility metadata.
489+ values = [
490+ contextvars .Context ().run (call_default .get , "nested" ),
491+ contextvars .copy_context ().run (call_default .get , "copy" ),
492+ call_default .get ("argument" ),
493+ var_default .get (),
494+ ]
495+ try :
496+ missing .get ()
497+ except LookupError :
498+ values .append ("LookupError" )
499+ # The snapshot is detached after the first mismatch, including
500+ # for repeated lookups and other variables.
501+ values .extend ((call_default .get ("again" ), other .get (None )))
502+ results .append ((values , caught ))
503+
504+ thread = threading .Thread (target = target )
505+ thread .start ()
506+ thread .join ()
507+
508+ values , caught = results [0 ]
509+ self .assertEqual (values , [
510+ "nested" , "copy" , "argument" , "variable" , "LookupError" ,
511+ "again" , None ,
512+ ])
513+ self .assertEqual (len (caught ), 1 )
514+ self .assertIs (caught [0 ].category , DeprecationWarning )
515+ message = str (caught [0 ].message )
516+ self .assertIn (
517+ "threads will inherit context by default" , message )
518+
519+ @isolated_context
520+ @threading_helper .requires_working_threading ()
521+ @unittest .skipIf (sys .flags .thread_inherit_context ,
522+ "requires the non-inheriting thread default" )
523+ def test_context_thread_inherit_warning_not_emitted (self ):
524+ import threading
525+
526+ unbound = contextvars .ContextVar ("unbound" )
527+ bound = contextvars .ContextVar ("bound" )
528+ bound .set ("starter" )
529+ results = []
530+
531+ def no_starter_binding ():
532+ with warnings .catch_warnings (record = True ) as caught :
533+ warnings .simplefilter ("always" , DeprecationWarning )
534+ results .append ((unbound .get (None ), len (caught )))
535+
536+ def child_binding ():
537+ with warnings .catch_warnings (record = True ) as caught :
538+ warnings .simplefilter ("always" , DeprecationWarning )
539+ bound .set ("child" )
540+ results .append ((bound .get (), len (caught )))
541+
542+ def explicit_empty ():
543+ with warnings .catch_warnings (record = True ) as caught :
544+ warnings .simplefilter ("always" , DeprecationWarning )
545+ results .append ((bound .get (None ), len (caught )))
546+
547+ threads = [
548+ threading .Thread (target = no_starter_binding ),
549+ threading .Thread (target = child_binding ),
550+ threading .Thread (target = explicit_empty ,
551+ context = contextvars .Context ()),
552+ ]
553+ for thread in threads :
554+ thread .start ()
555+ thread .join ()
556+
557+ self .assertEqual (results , [(None , 0 ), ("child" , 0 ), (None , 0 )])
558+
559+ @threading_helper .requires_working_threading ()
560+ def test_context_thread_inherit_warning_explicitly_disabled (self ):
561+ code = """
562+ import contextvars
563+ import threading
564+ import warnings
565+
566+ var = contextvars.ContextVar('var')
567+ var.set('starter')
568+ result = []
569+
570+ def target():
571+ with warnings.catch_warnings(record=True) as caught:
572+ warnings.simplefilter('always', DeprecationWarning)
573+ result.append((var.get(None), len(caught)))
574+
575+ thread = threading.Thread(target=target)
576+ thread.start()
577+ thread.join()
578+ assert result == [(None, 0)], result
579+ """
580+ script_helper .assert_python_ok (
581+ "-X" , "thread_inherit_context=0" , "-c" , code )
582+ script_helper .assert_python_ok (
583+ "-c" , code , PYTHON_THREAD_INHERIT_CONTEXT = "0" )
584+
585+ @isolated_context
586+ @threading_helper .requires_working_threading ()
587+ @unittest .skipIf (sys .flags .thread_inherit_context ,
588+ "requires the non-inheriting thread default" )
589+ def test_context_thread_warning_snapshot_at_start (self ):
590+ import threading
591+
592+ var = contextvars .ContextVar ("var" )
593+ # Keep another variable bound so the starter context is non-empty at
594+ # start() and a snapshot is retained.
595+ keep = contextvars .ContextVar ("keep" )
596+ keep .set ("starter" )
597+ token = var .set ("during construction" )
598+ results = []
599+
600+ def target ():
601+ with warnings .catch_warnings (record = True ) as caught :
602+ warnings .simplefilter ("always" , DeprecationWarning )
603+ results .append ((var .get (None ), len (caught )))
604+
605+ thread = threading .Thread (target = target )
606+ var .reset (token )
607+ thread .start ()
608+ thread .join ()
609+
610+ self .assertEqual (results , [(None , 0 )])
611+
612+ @isolated_context
613+ @threading_helper .requires_working_threading ()
614+ @unittest .skipUnless (THREAD_INHERIT_CONTEXT_WARNING ,
615+ "requires the warning-enabled GIL-build default" )
616+ def test_context_thread_inherit_warning_as_error (self ):
617+ import threading
618+
619+ var = contextvars .ContextVar ("var" )
620+ var .set ("starter" )
621+ results = []
622+
623+ def target ():
624+ with warnings .catch_warnings ():
625+ warnings .simplefilter ("error" , DeprecationWarning )
626+ try :
627+ var .get ()
628+ except DeprecationWarning as exc :
629+ results .append (str (exc ))
630+
631+ thread = threading .Thread (target = target )
632+ thread .start ()
633+ thread .join ()
634+
635+ self .assertEqual (len (results ), 1 )
636+ self .assertIn (
637+ "threads will inherit context by default" ,
638+ results [0 ])
639+
640+ @threading_helper .requires_working_threading ()
641+ @unittest .skipIf (support .Py_GIL_DISABLED ,
642+ "the free-threaded default inherits context" )
643+ def test_context_thread_inherit_warning_context_aware (self ):
644+ code = """
645+ import contextvars
646+ import threading
647+ import warnings
648+
649+ var = contextvars.ContextVar('var')
650+ var.set('starter')
651+ result = []
652+
653+ def target():
654+ try:
655+ var.get()
656+ except DeprecationWarning:
657+ result.append('warning')
658+
659+ with warnings.catch_warnings():
660+ warnings.simplefilter('error', DeprecationWarning)
661+ thread = threading.Thread(target=target)
662+ thread.start()
663+ thread.join()
664+
665+ assert result == ['warning'], result
666+ """
667+ # An empty value is treated as unset; this shields the subprocess from
668+ # any PYTHON_THREAD_INHERIT_CONTEXT in the test environment.
669+ script_helper .assert_python_ok (
670+ "-W" , "error::DeprecationWarning" ,
671+ "-X" , "context_aware_warnings=1" , "-c" , code ,
672+ PYTHON_THREAD_INHERIT_CONTEXT = "" )
673+
674+ def test_private_thread_start_context (self ):
675+ import _contextvars
676+
677+ self .assertFalse (hasattr (contextvars , "_thread_start_context" ))
678+ ctx = _contextvars ._thread_start_context ()
679+ self .assertIs (type (ctx ), contextvars .Context )
680+ self .assertEqual (ctx .run (lambda : "result" ), "result" )
681+
450682 def test_token_contextmanager_with_default (self ):
451683 ctx = contextvars .Context ()
452684 c = contextvars .ContextVar ('c' , default = 42 )
0 commit comments