gh-129889: Support context manager protocol by contextvars.Token (#129888)

This commit is contained in:
Andrew Svetlov 2025-02-12 12:32:58 +01:00 committed by GitHub
parent e1b38ea82e
commit 469d2e416c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 223 additions and 2 deletions

View file

@ -101,6 +101,21 @@ Context Variables
the value of the variable to what it was before the corresponding
*set*.
The token supports :ref:`context manager protocol <context-managers>`
to restore the corresponding context variable value at the exit from
:keyword:`with` block::
var = ContextVar('var', default='default value')
with var.set('new value'):
assert var.get() == 'new value'
assert var.get() == 'default value'
.. versionadded:: next
Added support for usage as a context manager.
.. attribute:: Token.var
A read-only property. Points to the :class:`ContextVar` object

View file

@ -1,4 +1,3 @@
****************************
What's new in Python 3.14
****************************
@ -362,6 +361,13 @@ concurrent.futures
supplying a *mp_context* to :class:`concurrent.futures.ProcessPoolExecutor`.
(Contributed by Gregory P. Smith in :gh:`84559`.)
contextvars
-----------
* Support context manager protocol by :class:`contextvars.Token`.
(Contributed by Andrew Svetlov in :gh:`129889`.)
ctypes
------

View file

@ -383,6 +383,115 @@ class ContextTest(unittest.TestCase):
tp.shutdown()
self.assertEqual(results, list(range(10)))
def test_token_contextmanager_with_default(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
with c.set(36):
self.assertEqual(c.get(), 36)
self.assertEqual(c.get(), 42)
ctx.run(fun)
def test_token_contextmanager_without_default(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c')
def fun():
with c.set(36):
self.assertEqual(c.get(), 36)
with self.assertRaisesRegex(LookupError, "<ContextVar name='c'"):
c.get()
ctx.run(fun)
def test_token_contextmanager_on_exception(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
with c.set(36):
self.assertEqual(c.get(), 36)
raise ValueError("custom exception")
self.assertEqual(c.get(), 42)
with self.assertRaisesRegex(ValueError, "custom exception"):
ctx.run(fun)
def test_token_contextmanager_reentrant(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
token = c.set(36)
with self.assertRaisesRegex(
RuntimeError,
"<Token .+ has already been used once"
):
with token:
with token:
self.assertEqual(c.get(), 36)
self.assertEqual(c.get(), 42)
ctx.run(fun)
def test_token_contextmanager_multiple_c_set(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
with c.set(36):
self.assertEqual(c.get(), 36)
c.set(24)
self.assertEqual(c.get(), 24)
c.set(12)
self.assertEqual(c.get(), 12)
self.assertEqual(c.get(), 42)
ctx.run(fun)
def test_token_contextmanager_with_explicit_reset_the_same_token(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
with self.assertRaisesRegex(
RuntimeError,
"<Token .+ has already been used once"
):
with c.set(36) as token:
self.assertEqual(c.get(), 36)
c.reset(token)
self.assertEqual(c.get(), 42)
self.assertEqual(c.get(), 42)
ctx.run(fun)
def test_token_contextmanager_with_explicit_reset_another_token(self):
ctx = contextvars.Context()
c = contextvars.ContextVar('c', default=42)
def fun():
with c.set(36):
self.assertEqual(c.get(), 36)
token = c.set(24)
self.assertEqual(c.get(), 24)
c.reset(token)
self.assertEqual(c.get(), 36)
self.assertEqual(c.get(), 42)
ctx.run(fun)
# HAMT Tests

View file

@ -0,0 +1,2 @@
Support context manager protocol by :class:`contextvars.Token`. Patch by
Andrew Svetlov.

View file

@ -179,4 +179,55 @@ PyDoc_STRVAR(_contextvars_ContextVar_reset__doc__,
#define _CONTEXTVARS_CONTEXTVAR_RESET_METHODDEF \
{"reset", (PyCFunction)_contextvars_ContextVar_reset, METH_O, _contextvars_ContextVar_reset__doc__},
/*[clinic end generated code: output=444567eaf0df25e0 input=a9049054013a1b77]*/
PyDoc_STRVAR(token_enter__doc__,
"__enter__($self, /)\n"
"--\n"
"\n"
"Enter into Token context manager.");
#define TOKEN_ENTER_METHODDEF \
{"__enter__", (PyCFunction)token_enter, METH_NOARGS, token_enter__doc__},
static PyObject *
token_enter_impl(PyContextToken *self);
static PyObject *
token_enter(PyObject *self, PyObject *Py_UNUSED(ignored))
{
return token_enter_impl((PyContextToken *)self);
}
PyDoc_STRVAR(token_exit__doc__,
"__exit__($self, type, val, tb, /)\n"
"--\n"
"\n"
"Exit from Token context manager, restore the linked ContextVar.");
#define TOKEN_EXIT_METHODDEF \
{"__exit__", _PyCFunction_CAST(token_exit), METH_FASTCALL, token_exit__doc__},
static PyObject *
token_exit_impl(PyContextToken *self, PyObject *type, PyObject *val,
PyObject *tb);
static PyObject *
token_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
PyObject *type;
PyObject *val;
PyObject *tb;
if (!_PyArg_CheckPositional("__exit__", nargs, 3, 3)) {
goto exit;
}
type = args[0];
val = args[1];
tb = args[2];
return_value = token_exit_impl((PyContextToken *)self, type, val, tb);
exit:
return return_value;
}
/*[clinic end generated code: output=01987cdbf68a951a input=a9049054013a1b77]*/

View file

@ -1231,9 +1231,47 @@ static PyGetSetDef PyContextTokenType_getsetlist[] = {
{NULL}
};
/*[clinic input]
_contextvars.Token.__enter__ as token_enter
Enter into Token context manager.
[clinic start generated code]*/
static PyObject *
token_enter_impl(PyContextToken *self)
/*[clinic end generated code: output=9af4d2054e93fb75 input=41a3d6c4195fd47a]*/
{
return Py_NewRef(self);
}
/*[clinic input]
_contextvars.Token.__exit__ as token_exit
type: object
val: object
tb: object
/
Exit from Token context manager, restore the linked ContextVar.
[clinic start generated code]*/
static PyObject *
token_exit_impl(PyContextToken *self, PyObject *type, PyObject *val,
PyObject *tb)
/*[clinic end generated code: output=3e6a1c95d3da703a input=7f117445f0ccd92e]*/
{
int ret = PyContextVar_Reset((PyObject *)self->tok_var, (PyObject *)self);
if (ret < 0) {
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef PyContextTokenType_methods[] = {
{"__class_getitem__", Py_GenericAlias,
METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
TOKEN_ENTER_METHODDEF
TOKEN_EXIT_METHODDEF
{NULL}
};