From 99400930ac1d4e5e10a5ae30f8202d8bc2661e39 Mon Sep 17 00:00:00 2001
From: Richard Hansen <rhansen@rhansen.org>
Date: Wed, 9 Oct 2024 19:44:03 -0400
Subject: gh-124872: Refine contextvars documentation (#124773)

* 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.
  * Expand the documentation for the `Py_CONTEXT_EVENT_ENTER` and
    `Py_CONTEXT_EVENT_EXIT` events to clarify and to prepare for
    planned changes.
---
 Doc/c-api/contextvars.rst                          | 20 +++--
 Doc/glossary.rst                                   | 40 +++++++--
 Doc/library/contextvars.rst                        | 98 +++++++++++++++-------
 Include/cpython/context.h                          | 23 +++--
 .../2024-09-29-18-14-52.gh-issue-119333.7tinr0.rst |  3 +
 5 files changed, 132 insertions(+), 52 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Documentation/2024-09-29-18-14-52.gh-issue-119333.7tinr0.rst

diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst
index 0de135b..59e74ba 100644
--- a/Doc/c-api/contextvars.rst
+++ b/Doc/c-api/contextvars.rst
@@ -122,18 +122,24 @@ Context object management functions:
 .. c:type:: PyContextEvent
 
    Enumeration of possible context object watcher events:
-   - ``Py_CONTEXT_EVENT_ENTER``
-   - ``Py_CONTEXT_EVENT_EXIT``
+
+   - ``Py_CONTEXT_EVENT_ENTER``: A context has been entered, causing the
+     :term:`current context` to switch to it.  The object passed to the watch
+     callback is the now-current :class:`contextvars.Context` object.  Each
+     enter event will eventually have a corresponding exit event for the same
+     context object after any subsequently entered contexts have themselves been
+     exited.
+   - ``Py_CONTEXT_EVENT_EXIT``: A context is about to be exited, which will
+     cause the :term:`current context` to switch back to what it was before the
+     context was entered.  The object passed to the watch callback is the
+     still-current :class:`contextvars.Context` object.
 
    .. versionadded:: 3.14
 
 .. c:type:: int (*PyContext_WatchCallback)(PyContextEvent event, PyContext* ctx)
 
-   Type of a context object watcher callback function.
-   If *event* is ``Py_CONTEXT_EVENT_ENTER``, then the callback is invoked
-   after *ctx* has been set as the current context for the current thread.
-   Otherwise, the callback is invoked before the deactivation of *ctx* as the current context
-   and the restoration of the previous contex object for the current thread.
+   Context object watcher callback function.  The object passed to the callback
+   is event-specific; see :c:type:`PyContextEvent` for details.
 
    If the callback returns with an exception set, it must return ``-1``; this
    exception will be printed as an unraisable exception using
diff --git a/Doc/glossary.rst b/Doc/glossary.rst
index cb7e0a2b..1d40773 100644
--- a/Doc/glossary.rst
+++ b/Doc/glossary.rst
@@ -265,19 +265,33 @@ Glossary
       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 key­value 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
@@ -311,6 +325,14 @@ Glossary
       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 @@ Manual Context Management
    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.
+
+      Example:
+
+      .. testcode::
+
+         import contextvars
 
-      Any changes to any context variables that *callable* makes will
-      be contained in the context object::
+         var = contextvars.ContextVar('var')
+         var.set('spam')
+         print(var.get())  # 'spam'
 
-        var = ContextVar('var')
-        var.set('spam')
+         ctx = contextvars.copy_context()
 
-        def main():
-            # 'var' was set to 'spam' before
-            # calling 'copy_context()' and 'ctx.run(main)', so:
-            # var.get() == ctx[var] == 'spam'
+         def main():
+             # 'var' was set to 'spam' before
+             # calling 'copy_context()' and 'ctx.run(main)', so:
+             print(var.get())  # 'spam'
+             print(ctx[var])  # 'spam'
 
-            var.set('ham')
+             var.set('ham')
 
-            # Now, after setting 'var' to 'ham':
-            # var.get() == ctx[var] == 'ham'
+             # Now, after setting 'var' to 'ham':
+             print(var.get())  # 'ham'
+             print(ctx[var])  # 'ham'
 
-        ctx = copy_context()
+         # Any changes that the 'main' function makes to 'var'
+         # will be contained in 'ctx'.
+         ctx.run(main)
 
-        # Any changes that the 'main' function makes to 'var'
-        # will be contained in 'ctx'.
-        ctx.run(main)
+         # The 'main()' function was run in the 'ctx' context,
+         # so changes to 'var' are contained in it:
+         print(ctx[var])  # 'ham'
 
-        # The 'main()' function was run in the 'ctx' context,
-        # so changes to 'var' are contained in it:
-        # ctx[var] == 'ham'
+         # However, outside of 'ctx', 'var' is still set to 'spam':
+         print(var.get())  # 'spam'
 
-        # However, outside of 'ctx', 'var' is still set to 'spam':
-        # var.get() == 'spam'
+      .. testoutput::
+         :hide:
 
-      The method raises a :exc:`RuntimeError` when called on the same
-      context object from more than one OS thread, or when called
-      recursively.
+         spam
+         spam
+         spam
+         ham
+         ham
+         ham
+         spam
 
    .. method:: copy()
 
diff --git a/Include/cpython/context.h b/Include/cpython/context.h
index ec72966..d722b4d 100644
--- a/Include/cpython/context.h
+++ b/Include/cpython/context.h
@@ -28,15 +28,26 @@ PyAPI_FUNC(int) PyContext_Enter(PyObject *);
 PyAPI_FUNC(int) PyContext_Exit(PyObject *);
 
 typedef enum {
-   Py_CONTEXT_EVENT_ENTER,
-   Py_CONTEXT_EVENT_EXIT,
+    /*
+     * A context has been entered, causing the "current context" to switch to
+     * it.  The object passed to the watch callback is the now-current
+     * contextvars.Context object.  Each enter event will eventually have a
+     * corresponding exit event for the same context object after any
+     * subsequently entered contexts have themselves been exited.
+     */
+    Py_CONTEXT_EVENT_ENTER,
+    /*
+     * A context is about to be exited, which will cause the "current context"
+     * to switch back to what it was before the context was entered.  The
+     * object passed to the watch callback is the still-current
+     * contextvars.Context object.
+     */
+    Py_CONTEXT_EVENT_EXIT,
 } PyContextEvent;
 
 /*
- * Callback to be invoked when a context object is entered or exited.
- *
- * The callback is invoked with the event and a reference to
- * the context after its entered and before its exited.
+ * Context object watcher callback function.  The object passed to the callback
+ * is event-specific; see PyContextEvent for details.
  *
  * if the callback returns with an exception set, it must return -1. Otherwise
  * it should return 0
diff --git a/Misc/NEWS.d/next/Documentation/2024-09-29-18-14-52.gh-issue-119333.7tinr0.rst b/Misc/NEWS.d/next/Documentation/2024-09-29-18-14-52.gh-issue-119333.7tinr0.rst
new file mode 100644
index 0000000..69a5c76
--- /dev/null
+++ b/Misc/NEWS.d/next/Documentation/2024-09-29-18-14-52.gh-issue-119333.7tinr0.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`.
-- 
cgit v0.12