[3.13] gh-124872: Refine contextvars documentation (GH-124773) (#125233)
[3.13] gh-124872: Refine contextvars documentation
* Add definitions for "context", "current context", and "context
management protocol".
* Update related definitions to be consistent with the new
definitions.
* Restructure the documentation for the `contextvars.Context` class
to prepare for adding context manager support, and for consistency
with the definitions.
* Use `testcode` and `testoutput` to test the `Context.run` example.
(cherry-picked from commit 99400930ac1d4e5e10a5ae30f8202d8bc2661e39)
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
diff --git a/Doc/glossary.rst b/Doc/glossary.rst
index 3c931a7..5c56c41 100644
--- a/Doc/glossary.rst
+++ b/Doc/glossary.rst
@@ -260,19 +260,33 @@
advanced mathematical feature. If you're not aware of a need for them,
it's almost certain you can safely ignore them.
+ context
+ This term has different meanings depending on where and how it is used.
+ Some common meanings:
+
+ * The temporary state or environment established by a :term:`context
+ manager` via a :keyword:`with` statement.
+ * The collection of keyvalue bindings associated with a particular
+ :class:`contextvars.Context` object and accessed via
+ :class:`~contextvars.ContextVar` objects. Also see :term:`context
+ variable`.
+ * A :class:`contextvars.Context` object. Also see :term:`current
+ context`.
+
+ context management protocol
+ The :meth:`~object.__enter__` and :meth:`~object.__exit__` methods called
+ by the :keyword:`with` statement. See :pep:`343`.
+
context manager
- An object which controls the environment seen in a :keyword:`with`
- statement by defining :meth:`~object.__enter__` and :meth:`~object.__exit__` methods.
- See :pep:`343`.
+ An object which implements the :term:`context management protocol` and
+ controls the environment seen in a :keyword:`with` statement. See
+ :pep:`343`.
context variable
- A variable which can have different values depending on its context.
- This is similar to Thread-Local Storage in which each execution
- thread may have a different value for a variable. However, with context
- variables, there may be several contexts in one execution thread and the
- main usage for context variables is to keep track of variables in
+ A variable whose value depends on which context is the :term:`current
+ context`. Values are accessed via :class:`contextvars.ContextVar`
+ objects. Context variables are primarily used to isolate state between
concurrent asynchronous tasks.
- See :mod:`contextvars`.
contiguous
.. index:: C-contiguous, Fortran contiguous
@@ -306,6 +320,14 @@
is used when necessary to distinguish this implementation from others
such as Jython or IronPython.
+ current context
+ The :term:`context` (:class:`contextvars.Context` object) that is
+ currently used by :class:`~contextvars.ContextVar` objects to access (get
+ or set) the values of :term:`context variables <context variable>`. Each
+ thread has its own current context. Frameworks for executing asynchronous
+ tasks (see :mod:`asyncio`) associate each task with a context which
+ becomes the current context whenever the task starts or resumes execution.
+
decorator
A function returning another function, usually applied as a function
transformation using the ``@wrapper`` syntax. Common examples for
diff --git a/Doc/library/contextvars.rst b/Doc/library/contextvars.rst
index 2a79dfe..2b1fb9f 100644
--- a/Doc/library/contextvars.rst
+++ b/Doc/library/contextvars.rst
@@ -144,51 +144,89 @@
To get a copy of the current context use the
:func:`~contextvars.copy_context` function.
- Every thread will have a different top-level :class:`~contextvars.Context`
- object. This means that a :class:`ContextVar` object behaves in a similar
- fashion to :func:`threading.local` when values are assigned in different
- threads.
+ Each thread has its own effective stack of :class:`!Context` objects. The
+ :term:`current context` is the :class:`!Context` object at the top of the
+ current thread's stack. All :class:`!Context` objects in the stacks are
+ considered to be *entered*.
+
+ *Entering* a context, which can be done by calling its :meth:`~Context.run`
+ method, makes the context the current context by pushing it onto the top of
+ the current thread's context stack.
+
+ *Exiting* from the current context, which can be done by returning from the
+ callback passed to the :meth:`~Context.run` method, restores the current
+ context to what it was before the context was entered by popping the context
+ off the top of the context stack.
+
+ Since each thread has its own context stack, :class:`ContextVar` objects
+ behave in a similar fashion to :func:`threading.local` when values are
+ assigned in different threads.
+
+ Attempting to enter an already entered context, including contexts entered in
+ other threads, raises a :exc:`RuntimeError`.
+
+ After exiting a context, it can later be re-entered (from any thread).
+
+ Any changes to :class:`ContextVar` values via the :meth:`ContextVar.set`
+ method are recorded in the current context. The :meth:`ContextVar.get`
+ method returns the value associated with the current context. Exiting a
+ context effectively reverts any changes made to context variables while the
+ context was entered (if needed, the values can be restored by re-entering the
+ context).
Context implements the :class:`collections.abc.Mapping` interface.
.. method:: run(callable, *args, **kwargs)
- Execute ``callable(*args, **kwargs)`` code in the context object
- the *run* method is called on. Return the result of the execution
- or propagate an exception if one occurred.
+ Enters the Context, executes ``callable(*args, **kwargs)``, then exits the
+ Context. Returns *callable*'s return value, or propagates an exception if
+ one occurred.
- Any changes to any context variables that *callable* makes will
- be contained in the context object::
+ Example:
- var = ContextVar('var')
- var.set('spam')
+ .. testcode::
- def main():
- # 'var' was set to 'spam' before
- # calling 'copy_context()' and 'ctx.run(main)', so:
- # var.get() == ctx[var] == 'spam'
+ import contextvars
- var.set('ham')
+ var = contextvars.ContextVar('var')
+ var.set('spam')
+ print(var.get()) # 'spam'
- # Now, after setting 'var' to 'ham':
- # var.get() == ctx[var] == 'ham'
+ ctx = contextvars.copy_context()
- ctx = copy_context()
+ def main():
+ # 'var' was set to 'spam' before
+ # calling 'copy_context()' and 'ctx.run(main)', so:
+ print(var.get()) # 'spam'
+ print(ctx[var]) # 'spam'
- # Any changes that the 'main' function makes to 'var'
- # will be contained in 'ctx'.
- ctx.run(main)
+ var.set('ham')
- # The 'main()' function was run in the 'ctx' context,
- # so changes to 'var' are contained in it:
- # ctx[var] == 'ham'
+ # Now, after setting 'var' to 'ham':
+ print(var.get()) # 'ham'
+ print(ctx[var]) # 'ham'
- # However, outside of 'ctx', 'var' is still set to 'spam':
- # var.get() == 'spam'
+ # Any changes that the 'main' function makes to 'var'
+ # will be contained in 'ctx'.
+ ctx.run(main)
- The method raises a :exc:`RuntimeError` when called on the same
- context object from more than one OS thread, or when called
- recursively.
+ # The 'main()' function was run in the 'ctx' context,
+ # so changes to 'var' are contained in it:
+ print(ctx[var]) # 'ham'
+
+ # However, outside of 'ctx', 'var' is still set to 'spam':
+ print(var.get()) # 'spam'
+
+ .. testoutput::
+ :hide:
+
+ spam
+ spam
+ spam
+ ham
+ ham
+ ham
+ spam
.. method:: copy()
diff --git a/Misc/NEWS.d/next/Documentation/2024-10-10-02-56-24.gh-issue-124872.0mDDOq.rst b/Misc/NEWS.d/next/Documentation/2024-10-10-02-56-24.gh-issue-124872.0mDDOq.rst
new file mode 100644
index 0000000..69a5c76
--- /dev/null
+++ b/Misc/NEWS.d/next/Documentation/2024-10-10-02-56-24.gh-issue-124872.0mDDOq.rst
@@ -0,0 +1,3 @@
+Added definitions for :term:`context`, :term:`current context`, and
+:term:`context management protocol`, updated related definitions to be
+consistent, and expanded the documentation for :class:`contextvars.Context`.