Skip to content

Commit f180876

Browse files
committed
Use kwarg, ContextVar(thread_inheritable=True).
A keyword, rather than class method, is the more usual way to do this and we can still do feature detection by checking `hasattr(ContextVar, 'thread_inheritable')`. This also allows us to expose the `thread_inheritable` property of the ContextVar instance (althogh it's hard to see the use of that).
1 parent c29fef5 commit f180876

10 files changed

Lines changed: 130 additions & 204 deletions

File tree

Doc/howto/free-threading-python.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,9 @@ is set to true by default which causes threads created with
153153
:class:`~contextvars.Context()` of the caller of
154154
:meth:`~threading.Thread.start`. In the default GIL-enabled build, the flag
155155
defaults to false, so threads start with a context containing only the caller's
156-
current bindings for context variables created by
157-
:meth:`~contextvars.ContextVar.thread_inheritable`.
156+
current bindings for context variables
157+
created with ``thread_inheritable=True`` (see
158+
:class:`~contextvars.ContextVar`).
158159

159160

160161
Warning filters

Doc/library/contextvars.rst

Lines changed: 75 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ See also :pep:`567` for additional details.
2424
Context Variables
2525
-----------------
2626

27-
.. class:: ContextVar(name, [*, default])
27+
.. class:: ContextVar(name, [*, default, thread_inheritable=False])
2828

2929
This class is used to declare a new Context Variable, e.g.::
3030

@@ -37,6 +37,71 @@ Context Variables
3737
:meth:`ContextVar.get` when no value for the variable is found
3838
in the current context.
3939

40+
If the optional keyword-only *thread_inheritable* parameter is true,
41+
the variable's binding is inherited by new :class:`threading.Thread`
42+
instances. When :meth:`threading.Thread.start` would otherwise start
43+
the thread with an empty context (that is, when
44+
:data:`sys.flags.thread_inherit_context` is false and no explicit
45+
:class:`Context` was supplied), the new thread's context is initialized
46+
with the caller's current bindings for all thread-inheritable
47+
variables. This allows individual context variables to opt in to
48+
thread inheritance on a per-variable basis.
49+
50+
These are ordinary bindings in the new thread's context: they are
51+
visible to :func:`copy_context`, may be changed independently with
52+
:meth:`ContextVar.set`, and are in turn inherited by threads started
53+
from the new thread. Threads started with an explicitly supplied
54+
context are unaffected, as are threads started by means other than
55+
:meth:`threading.Thread.start`, such as
56+
:func:`_thread.start_new_thread` or the C API.
57+
58+
When :data:`sys.flags.thread_inherit_context` is true, a
59+
thread-inheritable variable behaves identically to an ordinary one, so
60+
it is safe to use ``thread_inheritable=True`` unconditionally.
61+
62+
Libraries that also support Python versions without this parameter
63+
must choose how to degrade on those versions. Whether the parameter
64+
is supported can be detected by checking for the
65+
:attr:`~ContextVar.thread_inheritable` attribute on the
66+
:class:`!ContextVar` class. For state with a safe default in a new
67+
thread (the behavior that :class:`threading.local` based state has
68+
always had), fall back to an ordinary context variable::
69+
70+
if hasattr(ContextVar, "thread_inheritable"):
71+
var = ContextVar("var", thread_inheritable=True)
72+
else:
73+
var = ContextVar("var")
74+
75+
With this fallback, on older versions new threads see the variable's
76+
default rather than the starting thread's binding, unless the
77+
application enables :option:`-X thread_inherit_context <-X>`.
78+
79+
For state that was previously a module-level global, reverting to the
80+
default in new threads may break existing threaded code, since such
81+
code has always seen the current global value. Those libraries can
82+
instead pick the best semantics each interpreter supports::
83+
84+
if hasattr(ContextVar, "thread_inheritable"):
85+
# Context-local and inherited by new threads.
86+
def _new_var(name, **kwargs):
87+
return ContextVar(name, thread_inheritable=True, **kwargs)
88+
elif getattr(sys.flags, "thread_inherit_context", False):
89+
# Threads inherit the whole context, so an ordinary
90+
# context variable is inherited too.
91+
_new_var = ContextVar
92+
else:
93+
# Keep the library's historical global semantics.
94+
_new_var = _GlobalVar
95+
96+
Here ``_GlobalVar`` is a class provided by the library that stores a
97+
single process-wide value. To be substitutable for
98+
:class:`ContextVar` it must accept the same constructor arguments
99+
(*name* positional, keyword-only *default*), distinguish a missing
100+
*default* from ``default=None``, and provide ``get()``, ``set()``
101+
returning a token, and ``reset()``. This case gives up task-local
102+
isolation: concurrent tasks and threads share one value, exactly as
103+
they did before the library adopted context variables.
104+
40105
**Important:** Context Variables should be created at the top module
41106
level and never in closures. :class:`Context` objects hold strong
42107
references to context variables which prevents context variables
@@ -45,78 +110,22 @@ Context Variables
45110
:class:`!ContextVar`\s are :ref:`generic <generics>` over the type of
46111
their contained value.
47112

48-
.. classmethod:: ContextVar.thread_inheritable(name, [*, default])
49-
50-
Return a new context variable whose binding is inherited by new
51-
:class:`threading.Thread` instances. When
52-
:meth:`threading.Thread.start` would otherwise start the thread with
53-
an empty context (that is, when
54-
:data:`sys.flags.thread_inherit_context` is false and no explicit
55-
:class:`Context` was supplied), the new thread's context is initialized
56-
with the caller's current bindings for all thread-inheritable variables.
57-
This allows individual context variables to opt in to thread inheritance
58-
on a per-variable basis.
59-
60-
These are ordinary bindings in the new thread's context: they are
61-
visible to :func:`copy_context`, may be changed independently with
62-
:meth:`ContextVar.set`, and are in turn inherited by threads started
63-
from the new thread. Threads started with an explicitly supplied
64-
context are unaffected, as are threads started by means other than
65-
:meth:`threading.Thread.start`, such as
66-
:func:`_thread.start_new_thread` or the C API.
67-
68-
The *name* and *default* parameters have the same meaning as for the
69-
:class:`ContextVar` constructor. When
70-
:data:`sys.flags.thread_inherit_context` is true, a variable created
71-
with this method behaves identically to one created with
72-
:class:`ContextVar`, so it is safe to use unconditionally.
73-
74-
Libraries that also support Python versions without this method must
75-
choose how to degrade on those versions. For state with a safe
76-
default in a new thread (the behavior that :class:`threading.local`
77-
based state has always had), fall back to an ordinary context
78-
variable::
79-
80-
_new_var = getattr(ContextVar, "thread_inheritable", ContextVar)
81-
var = _new_var("var")
82-
83-
With this fallback, on older versions new threads see the variable's
84-
default rather than the starting thread's binding, unless the
85-
application enables :option:`-X thread_inherit_context <-X>`.
86-
87-
For state that was previously a module-level global, reverting to the
88-
default in new threads may break existing threaded code, since such
89-
code has always seen the current global value. Those libraries can
90-
instead pick the best semantics each interpreter supports::
91-
92-
if hasattr(ContextVar, "thread_inheritable"):
93-
# Context-local and inherited by new threads.
94-
_new_var = ContextVar.thread_inheritable
95-
elif getattr(sys.flags, "thread_inherit_context", False):
96-
# Threads inherit the whole context, so an ordinary
97-
# context variable is inherited too.
98-
_new_var = ContextVar
99-
else:
100-
# Keep the library's historical global semantics.
101-
_new_var = _GlobalVar
102-
103-
Here ``_GlobalVar`` is a class provided by the library that stores a
104-
single process-wide value. To be substitutable for
105-
:class:`ContextVar` it must accept the same constructor arguments
106-
(*name* positional, keyword-only *default*), distinguish a missing
107-
*default* from ``default=None``, and provide ``get()``, ``set()``
108-
returning a token, and ``reset()``. This case gives up task-local
109-
isolation: concurrent tasks and threads share one value, exactly as
110-
they did before the library adopted context variables.
111-
112-
.. versionadded:: 3.16
113+
.. versionchanged:: 3.16
114+
Added the *thread_inheritable* parameter.
113115

114116
.. attribute:: ContextVar.name
115117

116118
The name of the variable. This is a read-only property.
117119

118120
.. versionadded:: 3.7.1
119121

122+
.. attribute:: ContextVar.thread_inheritable
123+
124+
``True`` if the variable was created with ``thread_inheritable=True``.
125+
This is a read-only property.
126+
127+
.. versionadded:: 3.16
128+
120129
.. method:: get([default])
121130

122131
Return a value for the context variable for the current context.

Doc/library/threading.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,8 @@ since it is impossible to detect the termination of alien threads.
541541
the flag is true, threads will start with a copy of the context of the
542542
caller of :meth:`~Thread.start`. If false, they will start with a
543543
context containing only the caller's current bindings for context variables
544-
created by :meth:`~contextvars.ContextVar.thread_inheritable`. To explicitly
544+
created with ``thread_inheritable=True`` (see
545+
:class:`~contextvars.ContextVar`). To explicitly
545546
start with an empty context, pass a new instance of
546547
:class:`~contextvars.Context()`. To explicitly start with a copy of the
547548
current context, pass the value from :func:`~contextvars.copy_context`. The

Doc/using/cmdline.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,8 @@ Miscellaneous options
678678
to, by default, use a copy of the context of the caller of
679679
``Thread.start()`` when starting. Otherwise, threads start with a context
680680
containing only the caller's current bindings for context variables
681-
created by :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, the
681+
created with ``thread_inheritable=True`` (see
682+
:class:`~contextvars.ContextVar`). If unset, the
682683
value of this option defaults to ``1`` on free-threaded builds and to ``0``
683684
otherwise. See also :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`.
684685

@@ -1370,8 +1371,9 @@ conflict.
13701371
If this variable is set to ``1`` then :class:`~threading.Thread` will,
13711372
by default, use a copy of the context of the caller of ``Thread.start()``
13721373
when starting. Otherwise, new threads start with a context containing only
1373-
the caller's current bindings for context variables created by
1374-
:meth:`~contextvars.ContextVar.thread_inheritable`. If unset, this variable
1374+
the caller's current bindings for context variables
1375+
created with ``thread_inheritable=True`` (see
1376+
:class:`~contextvars.ContextVar`). If unset, this variable
13751377
defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See also
13761378
:option:`-X thread_inherit_context<-X>`.
13771379

Include/internal/pycore_context.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ struct _pycontextobject {
2727
PyObject *ctx_weakreflist;
2828
int ctx_entered;
2929
// Redundant subset of ctx_vars holding only the bindings of
30-
// thread-inheritable context variables (see
31-
// ContextVar.thread_inheritable()). Used to efficiently create the
32-
// starting context of a new thread.
30+
// thread-inheritable context variables (see the ContextVar
31+
// "thread_inheritable" keyword argument). Used to efficiently create
32+
// the starting context of a new thread.
3333
PyHamtObject *ctx_thread_inheritable_vars;
3434
};
3535

Lib/test/test_context.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,32 @@ def test_context_var_new_1(self):
4040
with self.assertRaises(AttributeError):
4141
c.name = 'bbb'
4242

43-
inheritable = contextvars.ContextVar.thread_inheritable('inheritable')
43+
self.assertFalse(c.thread_inheritable)
44+
with self.assertRaises(AttributeError):
45+
c.thread_inheritable = True
46+
47+
# Allows feature detection with hasattr().
48+
self.assertTrue(hasattr(contextvars.ContextVar, 'thread_inheritable'))
49+
50+
inheritable = contextvars.ContextVar(
51+
'inheritable', thread_inheritable=True)
4452
self.assertIs(type(inheritable), contextvars.ContextVar)
4553
self.assertEqual(inheritable.name, 'inheritable')
54+
self.assertTrue(inheritable.thread_inheritable)
55+
56+
explicit_false = contextvars.ContextVar(
57+
'explicit_false', thread_inheritable=False)
58+
self.assertFalse(explicit_false.thread_inheritable)
4659

47-
inheritable_with_default = (
48-
contextvars.ContextVar.thread_inheritable(
49-
'inheritable_with_default', default=42,
50-
)
60+
inheritable_with_default = contextvars.ContextVar(
61+
'inheritable_with_default', default=42, thread_inheritable=True,
5162
)
5263
self.assertEqual(inheritable_with_default.get(), 42)
5364

5465
with self.assertRaisesRegex(TypeError, 'must be a str'):
55-
contextvars.ContextVar.thread_inheritable(1)
66+
contextvars.ContextVar(1, thread_inheritable=True)
5667
with self.assertRaises(TypeError):
57-
contextvars.ContextVar.thread_inheritable('var', None)
68+
contextvars.ContextVar('var', None)
5869
with self.assertRaises(TypeError):
5970
contextvars.ContextVar('var', inherit=True)
6071

@@ -65,7 +76,7 @@ class Value:
6576
pass
6677

6778
def make_cycle():
68-
var = contextvars.ContextVar.thread_inheritable('var')
79+
var = contextvars.ContextVar('var', thread_inheritable=True)
6980
ctx = contextvars.Context()
7081
value = Value()
7182
value_ref = weakref.ref(value)
@@ -641,7 +652,7 @@ def test_thread_inheritance(self):
641652
import threading
642653
from contextvars import ContextVar, copy_context
643654
644-
inh = ContextVar.thread_inheritable('inh', default='default')
655+
inh = ContextVar('inh', default='default', thread_inheritable=True)
645656
plain = ContextVar('plain')
646657
inh.set('inherited')
647658
plain.set('not inherited')
@@ -673,7 +684,7 @@ def test_inheritance_captured_at_start_and_context_copy(self):
673684
import threading
674685
from contextvars import Context, ContextVar
675686
676-
inh = ContextVar.thread_inheritable('inh')
687+
inh = ContextVar('inh', thread_inheritable=True)
677688
values = []
678689
679690
# The binding is captured by start(), not by Thread().
@@ -704,7 +715,7 @@ def test_thread_start_retry_recaptures_context(self):
704715
import threading
705716
from contextvars import ContextVar
706717
707-
inh = ContextVar.thread_inheritable('inh')
718+
inh = ContextVar('inh', thread_inheritable=True)
708719
inh.set('first attempt')
709720
values = []
710721
t = threading.Thread(target=lambda: values.append(inh.get()))
@@ -736,7 +747,7 @@ def test_thread_set_and_reset(self):
736747
import threading
737748
from contextvars import ContextVar
738749
739-
inh = ContextVar.thread_inheritable('inh')
750+
inh = ContextVar('inh', thread_inheritable=True)
740751
inh.set('inherited')
741752
742753
def child():
@@ -757,7 +768,7 @@ def test_thread_inheritance_transitive(self):
757768
import threading
758769
from contextvars import ContextVar
759770
760-
inh = ContextVar.thread_inheritable('inh')
771+
inh = ContextVar('inh', thread_inheritable=True)
761772
inh.set('inherited')
762773
763774
def grandchild():
@@ -780,8 +791,8 @@ def test_thread_inheritance_unset_or_deleted(self):
780791
import threading
781792
from contextvars import ContextVar
782793
783-
unset = ContextVar.thread_inheritable('unset', default='default')
784-
deleted = ContextVar.thread_inheritable('deleted')
794+
unset = ContextVar('unset', default='default', thread_inheritable=True)
795+
deleted = ContextVar('deleted', thread_inheritable=True)
785796
token = deleted.set('inherited')
786797
deleted.reset(token)
787798
@@ -804,7 +815,7 @@ def test_thread_explicit_context(self):
804815
import threading
805816
from contextvars import ContextVar, Context
806817
807-
inh = ContextVar.thread_inheritable('inh', default='default')
818+
inh = ContextVar('inh', default='default', thread_inheritable=True)
808819
inh.set('inherited')
809820
810821
def child():
@@ -821,7 +832,7 @@ def test_thread_inheritance_asyncio(self):
821832
import threading
822833
from contextvars import ContextVar
823834
824-
inh = ContextVar.thread_inheritable('inh')
835+
inh = ContextVar('inh', thread_inheritable=True)
825836
plain = ContextVar('plain')
826837
inh.set('inherited')
827838
plain.set('not inherited')
@@ -866,7 +877,7 @@ def test_thread_inherit_context_flag_true(self):
866877
import threading
867878
from contextvars import ContextVar
868879
869-
inh = ContextVar.thread_inheritable('inh')
880+
inh = ContextVar('inh', thread_inheritable=True)
870881
plain = ContextVar('plain')
871882
inh.set('inherited')
872883
plain.set('also inherited')

Lib/threading.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,8 +1136,8 @@ def start(self):
11361136
self._context = _contextvars.copy_context()
11371137
else:
11381138
# Start with a context containing only the bindings of
1139-
# thread-inheritable context variables (see
1140-
# ContextVar.thread_inheritable); usually empty.
1139+
# thread-inheritable context variables (created with
1140+
# ContextVar(name, thread_inheritable=True)); usually empty.
11411141
self._context = _contextvars._thread_start_context()
11421142

11431143
try:
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
Add :meth:`contextvars.ContextVar.thread_inheritable`, an alternative
2-
constructor for context variables whose bindings are inherited by new
3-
:class:`threading.Thread` instances even when
1+
Add a *thread_inheritable* keyword argument to
2+
:class:`contextvars.ContextVar` for creating context variables whose
3+
bindings are inherited by new :class:`threading.Thread` instances even when
44
:data:`sys.flags.thread_inherit_context` is false. This lets libraries opt
55
individual context variables in to thread inheritance without requiring the
66
application to enable the flag process-wide.

0 commit comments

Comments
 (0)