From 0a20e79268eafd2c3d3121e912ec8f165946f750 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 25 Mar 2026 17:17:24 -0700 Subject: [PATCH 1/4] Add PyContextVar_GetChanged() method. --- Doc/c-api/contextvars.rst | 24 ++++ Doc/data/refcounts.dat | 6 + Doc/library/contextvars.rst | 35 +++++ Include/cpython/context.h | 20 +++ Include/internal/pycore_context.h | 1 + Lib/test/test_context.py | 220 ++++++++++++++++++++++++++++++ Python/clinic/context.c.h | 48 ++++++- Python/context.c | 161 ++++++++++++++++++++++ 8 files changed, 514 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index b7c6550ff34aac1..1a69888180b9a44 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -182,3 +182,27 @@ Context variable functions: Reset the state of the *var* context variable to that it was in before :c:func:`PyContextVar_Set` that returned the *token* was called. This function returns ``0`` on success and ``-1`` on error. + +.. c:function:: int PyContextVar_GetChanged(PyObject *var, PyObject *default_value, PyObject **value, int *changed) + + Like :c:func:`PyContextVar_Get`, but also reports whether the variable was + changed in the current context scope. This combines a value lookup with a + change check in a single HAMT lookup. + + Returns ``-1`` if an error has occurred during lookup, and ``0`` if no + error occurred, whether or not a value was found. + + On success, *\*value* is set following the same rules as + :c:func:`PyContextVar_Get`. *\*changed* is set to ``1`` if the variable + was changed (via :c:func:`PyContextVar_Set`) in the current context scope + (i.e. within the current :meth:`~contextvars.Context.run` call) with a + value that is a different object than the inherited one. Otherwise + *\*changed* is set to ``0``. If the value was not found, *\*changed* is + always ``0``. + + If the current context was never entered (no :meth:`~contextvars.Context.run` + is active), all existing bindings are considered "changed". + + Except for ``NULL``, the function returns a new reference via *\*value*. + + .. versionadded:: 3.15 diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index 01b064f3e617ff5..f975a31ab2c3bf5 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -400,6 +400,12 @@ PyContextVar_Reset:int::: PyContextVar_Reset:PyObject*:var:0: PyContextVar_Reset:PyObject*:token:-1: +PyContextVar_GetChanged:int::: +PyContextVar_GetChanged:PyObject*:var:0: +PyContextVar_GetChanged:PyObject*:default_value:0: +PyContextVar_GetChanged:PyObject**:value:+1:??? +PyContextVar_GetChanged:int*:changed:: + PyCFunction_New:PyObject*::+1: PyCFunction_New:PyMethodDef*:ml:: PyCFunction_New:PyObject*:self:+1: diff --git a/Doc/library/contextvars.rst b/Doc/library/contextvars.rst index 93d0c0d34bf039d..ac4bfdf3361db39 100644 --- a/Doc/library/contextvars.rst +++ b/Doc/library/contextvars.rst @@ -119,6 +119,41 @@ Context Variables The same *token* cannot be used twice. + .. method:: get_changed([default]) + + Like :meth:`ContextVar.get`, but returns a tuple ``(value, changed)`` + where *changed* indicates whether the variable was changed in the + current context scope. + + A variable is considered "changed" if :meth:`ContextVar.set` has been + called on it within the current :meth:`Context.run` call with a value + that is a different object than the inherited one. Variables inherited + unchanged from a parent context scope are not considered "changed". + If no :meth:`Context.run` is active, all existing bindings are + considered "changed". When the value comes from a default, *changed* + is always ``False``. + + This is useful when a context variable holds a mutable object that + needs to be copied on first access in a new context scope to ensure + modifications are local to that scope:: + + _ctx_var = ContextVar('ctx_var') + + def get_ctx(): + try: + ctx, changed = _ctx_var.get_changed() + except LookupError: + ctx = default_context() + _ctx_var.set(ctx) + return ctx + + if not changed: + ctx = ctx.copy() + _ctx_var.set(ctx) + return ctx + + .. versionadded:: 3.15 + .. class:: Token diff --git a/Include/cpython/context.h b/Include/cpython/context.h index 3a7a4b459c09ad0..c756e4e40d30f8a 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -100,6 +100,26 @@ PyAPI_FUNC(PyObject *) PyContextVar_Set(PyObject *var, PyObject *value); PyAPI_FUNC(int) PyContextVar_Reset(PyObject *var, PyObject *token); +/* Get a value for the variable and check if it was changed. + + Like PyContextVar_Get, but also reports whether the variable was + changed in the current context scope via a single HAMT lookup. + + Returns -1 if an error occurred during lookup. + + Returns 0 if no error occurred. In this case: + + - *value will be set the same as for PyContextVar_Get. + - *changed will be set to 1 if the variable was changed in the + current context scope, 0 otherwise. If the variable was not + found, *changed is always 0. + + '*value' will be a new ref, if not NULL. +*/ +PyAPI_FUNC(int) PyContextVar_GetChanged( + PyObject *var, PyObject *default_value, PyObject **value, int *changed); + + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_context.h b/Include/internal/pycore_context.h index a833f790a621b1d..b8f4e59213d3da3 100644 --- a/Include/internal/pycore_context.h +++ b/Include/internal/pycore_context.h @@ -24,6 +24,7 @@ struct _pycontextobject { PyObject_HEAD PyContext *ctx_prev; PyHamtObject *ctx_vars; + PyHamtObject *ctx_vars_origin; /* snapshot of ctx_vars at Enter time */ PyObject *ctx_weakreflist; int ctx_entered; }; diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index ef20495dcc01ea9..13886bf2827f10b 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -586,6 +586,226 @@ def __eq__(self, other): ctx2.run(var.set, ReentrantHash()) ctx1 == ctx2 + def test_get_changed_outside_run(self): + # Outside any Context.run(), bindings are considered "changed" + v = contextvars.ContextVar('v', default='dflt') + val, changed = v.get_changed() + self.assertEqual(val, 'dflt') + self.assertFalse(changed) # default value, not changed + v.set(42) + val, changed = v.get_changed() + self.assertEqual(val, 42) + self.assertTrue(changed) # set in base context + + def test_get_changed_inherited(self): + # Inherited bindings are not considered "changed" + v = contextvars.ContextVar('v') + v.set('parent') + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertEqual(val, 'parent') + self.assertFalse(changed) + ctx.run(check) + + def test_get_changed_after_set(self): + # After set() inside Context.run(), changed is True + v = contextvars.ContextVar('v') + v.set('parent') + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertFalse(changed) + v.set('child') + val, changed = v.get_changed() + self.assertEqual(val, 'child') + self.assertTrue(changed) + ctx.run(check) + + def test_get_changed_new_var_in_run(self): + # A variable set for the first time inside run() is "changed" + v = contextvars.ContextVar('v') + ctx = contextvars.copy_context() + + def check(): + with self.assertRaises(LookupError): + v.get_changed() + v.set('new') + val, changed = v.get_changed() + self.assertEqual(val, 'new') + self.assertTrue(changed) + ctx.run(check) + + def test_get_changed_not_set_with_default(self): + # A variable not set but with default: changed is False + v = contextvars.ContextVar('v', default='dflt') + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertEqual(val, 'dflt') + self.assertFalse(changed) + ctx.run(check) + + def test_get_changed_not_set_no_default(self): + # A variable that has never been set and has no default + v = contextvars.ContextVar('v') + ctx = contextvars.copy_context() + + def check(): + with self.assertRaises(LookupError): + v.get_changed() + ctx.run(check) + + def test_get_changed_explicit_default_arg(self): + # Passing a default argument to get_changed() + v = contextvars.ContextVar('v') + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed('fallback') + self.assertEqual(val, 'fallback') + self.assertFalse(changed) + ctx.run(check) + + def test_get_changed_set_same_object(self): + # Setting to the exact same object does not count as "changed" + # because the HAMT recognizes the identical key-value pair + obj = object() + v = contextvars.ContextVar('v') + v.set(obj) + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertIs(val, obj) + self.assertFalse(changed) + v.set(obj) # same object + val, changed = v.get_changed() + self.assertIs(val, obj) + self.assertFalse(changed) + ctx.run(check) + + def test_get_changed_set_different_object(self): + # Setting to a different object counts as "changed" + v = contextvars.ContextVar('v') + v.set([1, 2, 3]) + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertFalse(changed) + v.set([1, 2, 3]) # equal value, different object + val, changed = v.get_changed() + self.assertTrue(changed) + ctx.run(check) + + def test_get_changed_after_reset(self): + # After reset(), the variable reverts to its inherited state + v = contextvars.ContextVar('v') + v.set('original') + ctx = contextvars.copy_context() + + def check(): + val, changed = v.get_changed() + self.assertFalse(changed) + tok = v.set('modified') + val, changed = v.get_changed() + self.assertTrue(changed) + v.reset(tok) + val, changed = v.get_changed() + self.assertFalse(changed) + ctx.run(check) + + def test_get_changed_multiple_vars(self): + # Changing one variable does not affect get_changed() for others + v1 = contextvars.ContextVar('v1') + v2 = contextvars.ContextVar('v2') + v1.set('a') + v2.set('b') + ctx = contextvars.copy_context() + + def check(): + _, changed1 = v1.get_changed() + _, changed2 = v2.get_changed() + self.assertFalse(changed1) + self.assertFalse(changed2) + v1.set('a2') + _, changed1 = v1.get_changed() + _, changed2 = v2.get_changed() + self.assertTrue(changed1) + self.assertFalse(changed2) + ctx.run(check) + + def test_get_changed_nested_run(self): + # get_changed() reflects the innermost Context.run() scope + v = contextvars.ContextVar('v') + v.set('root') + ctx1 = contextvars.copy_context() + + def outer(): + _, changed = v.get_changed() + self.assertFalse(changed) + v.set('outer') + _, changed = v.get_changed() + self.assertTrue(changed) + ctx2 = contextvars.copy_context() + + def inner(): + # inherited 'outer' from ctx1, not changed in ctx2 + val, changed = v.get_changed() + self.assertEqual(val, 'outer') + self.assertFalse(changed) + v.set('inner') + val, changed = v.get_changed() + self.assertEqual(val, 'inner') + self.assertTrue(changed) + ctx2.run(inner) + + # after inner run exits, outer's state is restored + _, changed = v.get_changed() + self.assertTrue(changed) + ctx1.run(outer) + + def test_get_changed_with_threads(self): + # get_changed() works correctly in a thread with copied context + import threading + v = contextvars.ContextVar('v') + v.set('parent') + ctx = contextvars.copy_context() + results = {} + + def thread_func(): + val, changed = v.get_changed() + results['inherited'] = changed + results['value'] = val + v.set('thread') + val, changed = v.get_changed() + results['after_set'] = changed + + t = threading.Thread(target=ctx.run, args=(thread_func,)) + t.start() + t.join() + self.assertFalse(results['inherited']) + self.assertEqual(results['value'], 'parent') + self.assertTrue(results['after_set']) + + def test_get_changed_empty_context_run(self): + # Running in a brand new empty context + v = contextvars.ContextVar('v') + ctx = contextvars.Context() + + def check(): + with self.assertRaises(LookupError): + v.get_changed() + v.set('value') + val, changed = v.get_changed() + self.assertEqual(val, 'value') + self.assertTrue(changed) + ctx.run(check) + # HAMT Tests diff --git a/Python/clinic/context.c.h b/Python/clinic/context.c.h index 5ed74e6e6ddb6bf..3fe45c45a64c441 100644 --- a/Python/clinic/context.c.h +++ b/Python/clinic/context.c.h @@ -206,6 +206,52 @@ _contextvars_ContextVar_reset(PyObject *self, PyObject *token) return return_value; } +PyDoc_STRVAR(_contextvars_ContextVar_get_changed__doc__, +"get_changed($self, default=, /)\n" +"--\n" +"\n" +"Return a tuple of (value, changed) for the context variable.\n" +"\n" +"Like ContextVar.get(), but additionally indicates whether the variable was\n" +"changed in the current context scope. *changed* is True if ContextVar.set()\n" +"has been called on the variable within the current Context.run() call with\n" +"a value that is a different object than the inherited one.\n" +"\n" +"If there is no value for the variable in the current context, the method will:\n" +" * return the value of the default argument of the method, if provided; or\n" +" * return the default value for the context variable, if it was created\n" +" with one; or\n" +" * raise a LookupError.\n" +"\n" +"When the value is found via a default, *changed* is always False."); + +#define _CONTEXTVARS_CONTEXTVAR_GET_CHANGED_METHODDEF \ + {"get_changed", _PyCFunction_CAST(_contextvars_ContextVar_get_changed), METH_FASTCALL, _contextvars_ContextVar_get_changed__doc__}, + +static PyObject * +_contextvars_ContextVar_get_changed_impl(PyContextVar *self, + PyObject *default_value); + +static PyObject * +_contextvars_ContextVar_get_changed(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *default_value = NULL; + + if (!_PyArg_CheckPositional("get_changed", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + default_value = args[0]; +skip_optional: + return_value = _contextvars_ContextVar_get_changed_impl((PyContextVar *)self, default_value); + +exit: + return return_value; +} + PyDoc_STRVAR(token_enter__doc__, "__enter__($self, /)\n" "--\n" @@ -256,4 +302,4 @@ token_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=3a04b2fddf24c3e9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=0adcad33b8abcf29 input=a9049054013a1b77]*/ diff --git a/Python/context.c b/Python/context.c index 62b582f271ffe57..1490d60c46e9b20 100644 --- a/Python/context.c +++ b/Python/context.c @@ -209,6 +209,7 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx) } ctx->ctx_prev = (PyContext *)ts->context; /* borrow */ + ctx->ctx_vars_origin = (PyHamtObject *)Py_NewRef(ctx->ctx_vars); ts->context = Py_NewRef(ctx); context_switched(ts); return 0; @@ -248,6 +249,7 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx) Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev); ctx->ctx_prev = NULL; + Py_CLEAR(ctx->ctx_vars_origin); FT_ATOMIC_STORE_INT(ctx->ctx_entered, 0); context_switched(ts); return 0; @@ -410,6 +412,115 @@ PyContextVar_Reset(PyObject *ovar, PyObject *otok) } +/* Check if var's current value (cur_val) differs from the origin snapshot. + ctx must be the current context and cur_val must be the value already + looked up in ctx->ctx_vars. Returns 1 if changed, 0 if not, -1 on error. */ +static int +contextvar_check_changed(PyContext *ctx, PyContextVar *var, PyObject *cur_val) +{ + /* No origin snapshot means this context was never entered via + Context.run(), so all bindings are considered "changed". */ + if (ctx->ctx_vars_origin == NULL) { + return 1; + } + + /* If the HAMT hasn't changed at all, no .set() calls have been made + in this context scope for any variable. */ + if (ctx->ctx_vars == ctx->ctx_vars_origin) { + return 0; + } + + /* Check if this specific variable had a different value (or was + absent) in the origin snapshot. */ + PyObject *orig_val = NULL; + int found_orig = _PyHamt_Find( + ctx->ctx_vars_origin, (PyObject *)var, &orig_val); + if (found_orig < 0) { + return -1; + } + if (found_orig == 0) { + return 1; + } + + return cur_val != orig_val; +} + + +int +PyContextVar_GetChanged(PyObject *ovar, PyObject *def, PyObject **val, + int *changed) +{ + ENSURE_ContextVar(ovar, -1) + PyContextVar *var = (PyContextVar *)ovar; + + *changed = 0; + + PyThreadState *ts = _PyThreadState_GET(); + assert(ts != NULL); + if (ts->context == NULL) { + goto not_found; + } + + PyContext *ctx = (PyContext *)ts->context; + assert(PyContext_CheckExact(ts->context)); + +#ifndef Py_GIL_DISABLED + /* Try the cache first. When we get a cache hit we still need to + check the origin HAMT, but we skip the main HAMT lookup. */ + if (var->var_cached != NULL && + var->var_cached_tsid == ts->id && + var->var_cached_tsver == ts->context_ver) + { + *val = Py_NewRef(var->var_cached); + int res = contextvar_check_changed(ctx, var, var->var_cached); + if (res < 0) { + Py_CLEAR(*val); + return -1; + } + *changed = res; + return 0; + } +#endif + + PyObject *found_val = NULL; + int res = _PyHamt_Find(ctx->ctx_vars, (PyObject *)var, &found_val); + if (res < 0) { + *val = NULL; + return -1; + } + if (res == 1) { + assert(found_val != NULL); +#ifndef Py_GIL_DISABLED + var->var_cached = found_val; /* borrow */ + var->var_cached_tsid = ts->id; + var->var_cached_tsver = ts->context_ver; +#endif + int chg = contextvar_check_changed(ctx, var, found_val); + if (chg < 0) { + *val = NULL; + return -1; + } + *changed = chg; + *val = Py_NewRef(found_val); + return 0; + } + +not_found: + if (def == NULL) { + if (var->var_default != NULL) { + *val = Py_NewRef(var->var_default); + return 0; + } + *val = NULL; + return 0; + } + else { + *val = Py_NewRef(def); + return 0; + } +} + + /////////////////////////// PyContext /*[clinic input] @@ -433,6 +544,7 @@ _context_alloc(void) } ctx->ctx_vars = NULL; + ctx->ctx_vars_origin = NULL; ctx->ctx_prev = NULL; ctx->ctx_entered = 0; ctx->ctx_weakreflist = NULL; @@ -520,6 +632,7 @@ context_tp_clear(PyObject *op) PyContext *self = _PyContext_CAST(op); Py_CLEAR(self->ctx_prev); Py_CLEAR(self->ctx_vars); + Py_CLEAR(self->ctx_vars_origin); return 0; } @@ -529,6 +642,7 @@ context_tp_traverse(PyObject *op, visitproc visit, void *arg) PyContext *self = _PyContext_CAST(op); Py_VISIT(self->ctx_prev); Py_VISIT(self->ctx_vars); + Py_VISIT(self->ctx_vars_origin); return 0; } @@ -1088,6 +1202,52 @@ _contextvars_ContextVar_reset_impl(PyContextVar *self, PyObject *token) } +/*[clinic input] +@permit_long_docstring_body +_contextvars.ContextVar.get_changed + default: object = NULL + / + +Return a tuple of (value, changed) for the context variable. + +Like ContextVar.get(), but additionally indicates whether the variable was +changed in the current context scope. *changed* is True if ContextVar.set() +has been called on the variable within the current Context.run() call with +a value that is a different object than the inherited one. + +If there is no value for the variable in the current context, the method will: + * return the value of the default argument of the method, if provided; or + * return the default value for the context variable, if it was created + with one; or + * raise a LookupError. + +When the value is found via a default, *changed* is always False. +[clinic start generated code]*/ + +static PyObject * +_contextvars_ContextVar_get_changed_impl(PyContextVar *self, + PyObject *default_value) +/*[clinic end generated code: output=2418683613ac96e7 input=2dacfcf7b43f9719]*/ +{ + PyObject *val; + int changed; + if (PyContextVar_GetChanged( + (PyObject *)self, default_value, &val, &changed) < 0) { + return NULL; + } + + if (val == NULL) { + PyErr_SetObject(PyExc_LookupError, (PyObject *)self); + return NULL; + } + + PyObject *changed_obj = changed ? Py_True : Py_False; + PyObject *result = PyTuple_Pack(2, val, changed_obj); + Py_DECREF(val); + return result; +} + + static PyMemberDef PyContextVar_members[] = { {"name", _Py_T_OBJECT, offsetof(PyContextVar, var_name), Py_READONLY}, {NULL} @@ -1097,6 +1257,7 @@ static PyMethodDef PyContextVar_methods[] = { _CONTEXTVARS_CONTEXTVAR_GET_METHODDEF _CONTEXTVARS_CONTEXTVAR_SET_METHODDEF _CONTEXTVARS_CONTEXTVAR_RESET_METHODDEF + _CONTEXTVARS_CONTEXTVAR_GET_CHANGED_METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, {NULL, NULL} From b33469eb37f0746916353b539b36c2a03ea14eca Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 25 Mar 2026 23:15:58 -0700 Subject: [PATCH 2/4] Use ContextVar.get_changed() for decimal context. Ensure that decimal.getcontext() returns a per-task copy of the decimal.Context() so that mutations are isolated between async tasks and threads using sys.flags.thread_inherit_context. --- Lib/_pydecimal.py | 10 +++- Lib/test/test_decimal.py | 53 +++++++++++++++++++ ...-03-26-10-27-07.gh-issue-141148._XpYnI.rst | 4 ++ Modules/_decimal/_decimal.c | 20 +++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index ef889ea0cc834c1..902ca17e07483ca 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -361,11 +361,19 @@ def getcontext(): New contexts are copies of DefaultContext. """ try: - return _current_context_var.get() + context, changed = _current_context_var.get_changed() except LookupError: context = Context() _current_context_var.set(context) return context + if not changed: + # The context value was inherited from another task/thread. Because + # the Context() instance is mutable, copy it to ensure that if it is + # changed, those changes are isolated from other tasks/threads. + context = context.copy() + _current_context_var.set(context) + return context + def setcontext(context): """Set this thread's context to context.""" diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index fe8c8ce12da0bfa..dc365a7cd8228d7 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1770,6 +1770,59 @@ def test_threading(self): DefaultContext.Emax = save_emax DefaultContext.Emin = save_emin + @threading_helper.requires_working_threading() + def test_inherited_context_isolation(self): + # Test that when threads inherit contextvars (e.g. via + # sys.flags.thread_inherit_context), each thread gets its own + # copy of the decimal context so mutations don't leak between + # threads. Also verifies correct behavior with asyncio tasks. + Decimal = self.decimal.Decimal + getcontext = self.decimal.getcontext + setcontext = self.decimal.setcontext + Context = self.decimal.Context + Underflow = self.decimal.Underflow + + # Set up parent context with specific precision + parent_ctx = getcontext() + parent_ctx.prec = 20 + + barrier = threading.Barrier(2, timeout=2) + results = {} + + def child(name, prec_delta): + barrier.wait() + ctx = getcontext() + # Each child should see a context with the parent's precision + results[name + '_initial_prec'] = ctx.prec + results[name + '_ctx_id'] = id(ctx) + # Mutate this thread's context + ctx.prec += prec_delta + results[name + '_modified_prec'] = ctx.prec + + # Spawn threads that inherit the parent's contextvars. + t1 = threading.Thread(target=child, args=('t1', 5), + context=contextvars.copy_context()) + t2 = threading.Thread(target=child, args=('t2', 10), + context=contextvars.copy_context()) + t1.start() + t2.start() + t1.join() + t2.join() + + # Each thread should have started with the parent's precision + self.assertEqual(results['t1_initial_prec'], 20) + self.assertEqual(results['t2_initial_prec'], 20) + + # Each thread should have its own context (different id) + self.assertNotEqual(results['t1_ctx_id'], results['t2_ctx_id']) + + # Mutations should be independent + self.assertEqual(results['t1_modified_prec'], 25) + self.assertEqual(results['t2_modified_prec'], 30) + + # Parent context should be unaffected + self.assertEqual(getcontext().prec, 20) + @requires_cdecimal class CThreadingTest(ThreadingTest, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst b/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst new file mode 100644 index 000000000000000..8cc474526b8c4d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst @@ -0,0 +1,4 @@ +Ensure that :func:`decimal.getcontext` returns a per-task copy of the +:class:`decimal.Context` so that mutations are isolated between asyncio +tasks and threads using :data:`sys.flags.thread_inherit_context`. Added +:meth:`contextvars.ContextVar.get_changed` to support this. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index b47014c4e7466dd..b361d61deb4020a 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -1914,9 +1914,9 @@ PyDec_SetCurrentContext(PyObject *self, PyObject *v) } #else static PyObject * -init_current_context(decimal_state *state) +init_current_context(decimal_state *state, PyObject *prev_context) { - PyObject *tl_context = context_copy(state, state->default_context_template); + PyObject *tl_context = context_copy(state, prev_context); if (tl_context == NULL) { return NULL; } @@ -1936,15 +1936,25 @@ static inline PyObject * current_context(decimal_state *state) { PyObject *tl_context; - if (PyContextVar_Get(state->current_context_var, NULL, &tl_context) < 0) { + int changed; + if (PyContextVar_GetChanged(state->current_context_var, NULL, &tl_context, + &changed) < 0) { return NULL; } if (tl_context != NULL) { - return tl_context; + if (!changed) { + /* inherited context object from another thread for async task */ + PyObject *new_context = init_current_context(state, tl_context); + Py_DECREF(tl_context); + return new_context; + } + else { + return tl_context; + } } - return init_current_context(state); + return init_current_context(state, state->default_context_template); } /* ctxobj := borrowed reference to the current context */ From 74482e303587929b75143a6d4283a83073c88ff5 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 26 Mar 2026 11:49:20 -0700 Subject: [PATCH 3/4] Mark unit test that needs working threads. --- Lib/test/test_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index 13886bf2827f10b..56e6fd3591f4eda 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -769,6 +769,7 @@ def inner(): self.assertTrue(changed) ctx1.run(outer) + @threading_helper.requires_working_threading() def test_get_changed_with_threads(self): # get_changed() works correctly in a thread with copied context import threading From c2dbdca27fb97831bee4c23d3e9e857588d76b5e Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 17 Jun 2026 16:44:36 -0700 Subject: [PATCH 4/4] Remove public PyContextVar_GetChanged() API. Use a private API instead (ctx._get_changed() is the Python-level API). --- Doc/c-api/contextvars.rst | 24 ----- Doc/data/refcounts.dat | 6 -- Doc/library/contextvars.rst | 35 ------- Include/cpython/context.h | 20 ---- Include/internal/pycore_context.h | 19 ++++ Lib/_pydecimal.py | 2 +- Lib/test/test_context.py | 96 +++++++++---------- ...-03-26-10-27-07.gh-issue-141148._XpYnI.rst | 3 +- Modules/_decimal/_decimal.c | 5 +- Python/clinic/context.c.h | 20 ++-- Python/context.c | 16 ++-- 11 files changed, 90 insertions(+), 156 deletions(-) diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index 1a69888180b9a44..b7c6550ff34aac1 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -182,27 +182,3 @@ Context variable functions: Reset the state of the *var* context variable to that it was in before :c:func:`PyContextVar_Set` that returned the *token* was called. This function returns ``0`` on success and ``-1`` on error. - -.. c:function:: int PyContextVar_GetChanged(PyObject *var, PyObject *default_value, PyObject **value, int *changed) - - Like :c:func:`PyContextVar_Get`, but also reports whether the variable was - changed in the current context scope. This combines a value lookup with a - change check in a single HAMT lookup. - - Returns ``-1`` if an error has occurred during lookup, and ``0`` if no - error occurred, whether or not a value was found. - - On success, *\*value* is set following the same rules as - :c:func:`PyContextVar_Get`. *\*changed* is set to ``1`` if the variable - was changed (via :c:func:`PyContextVar_Set`) in the current context scope - (i.e. within the current :meth:`~contextvars.Context.run` call) with a - value that is a different object than the inherited one. Otherwise - *\*changed* is set to ``0``. If the value was not found, *\*changed* is - always ``0``. - - If the current context was never entered (no :meth:`~contextvars.Context.run` - is active), all existing bindings are considered "changed". - - Except for ``NULL``, the function returns a new reference via *\*value*. - - .. versionadded:: 3.15 diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index f975a31ab2c3bf5..01b064f3e617ff5 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -400,12 +400,6 @@ PyContextVar_Reset:int::: PyContextVar_Reset:PyObject*:var:0: PyContextVar_Reset:PyObject*:token:-1: -PyContextVar_GetChanged:int::: -PyContextVar_GetChanged:PyObject*:var:0: -PyContextVar_GetChanged:PyObject*:default_value:0: -PyContextVar_GetChanged:PyObject**:value:+1:??? -PyContextVar_GetChanged:int*:changed:: - PyCFunction_New:PyObject*::+1: PyCFunction_New:PyMethodDef*:ml:: PyCFunction_New:PyObject*:self:+1: diff --git a/Doc/library/contextvars.rst b/Doc/library/contextvars.rst index ac4bfdf3361db39..93d0c0d34bf039d 100644 --- a/Doc/library/contextvars.rst +++ b/Doc/library/contextvars.rst @@ -119,41 +119,6 @@ Context Variables The same *token* cannot be used twice. - .. method:: get_changed([default]) - - Like :meth:`ContextVar.get`, but returns a tuple ``(value, changed)`` - where *changed* indicates whether the variable was changed in the - current context scope. - - A variable is considered "changed" if :meth:`ContextVar.set` has been - called on it within the current :meth:`Context.run` call with a value - that is a different object than the inherited one. Variables inherited - unchanged from a parent context scope are not considered "changed". - If no :meth:`Context.run` is active, all existing bindings are - considered "changed". When the value comes from a default, *changed* - is always ``False``. - - This is useful when a context variable holds a mutable object that - needs to be copied on first access in a new context scope to ensure - modifications are local to that scope:: - - _ctx_var = ContextVar('ctx_var') - - def get_ctx(): - try: - ctx, changed = _ctx_var.get_changed() - except LookupError: - ctx = default_context() - _ctx_var.set(ctx) - return ctx - - if not changed: - ctx = ctx.copy() - _ctx_var.set(ctx) - return ctx - - .. versionadded:: 3.15 - .. class:: Token diff --git a/Include/cpython/context.h b/Include/cpython/context.h index c756e4e40d30f8a..3a7a4b459c09ad0 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -100,26 +100,6 @@ PyAPI_FUNC(PyObject *) PyContextVar_Set(PyObject *var, PyObject *value); PyAPI_FUNC(int) PyContextVar_Reset(PyObject *var, PyObject *token); -/* Get a value for the variable and check if it was changed. - - Like PyContextVar_Get, but also reports whether the variable was - changed in the current context scope via a single HAMT lookup. - - Returns -1 if an error occurred during lookup. - - Returns 0 if no error occurred. In this case: - - - *value will be set the same as for PyContextVar_Get. - - *changed will be set to 1 if the variable was changed in the - current context scope, 0 otherwise. If the variable was not - found, *changed is always 0. - - '*value' will be a new ref, if not NULL. -*/ -PyAPI_FUNC(int) PyContextVar_GetChanged( - PyObject *var, PyObject *default_value, PyObject **value, int *changed); - - #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_context.h b/Include/internal/pycore_context.h index b8f4e59213d3da3..8d76a9adf4a21c0 100644 --- a/Include/internal/pycore_context.h +++ b/Include/internal/pycore_context.h @@ -59,5 +59,24 @@ PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void); PyAPI_FUNC(int) _PyContext_Enter(PyThreadState *ts, PyObject *octx); PyAPI_FUNC(int) _PyContext_Exit(PyThreadState *ts, PyObject *octx); +/* Get a value for the variable and check if it was changed. + + Like PyContextVar_Get, but also reports whether the variable was + changed in the current context scope via a single HAMT lookup. + + Returns -1 if an error occurred during lookup. + + Returns 0 if no error occurred. In this case: + + - *value will be set the same as for PyContextVar_Get. + - *changed will be set to 1 if the variable was changed in the + current context scope, 0 otherwise. If the variable was not + found, *changed is always 0. + + '*value' will be a new ref, if not NULL. +*/ +PyAPI_FUNC(int) _PyContextVar_GetChanged( + PyObject *var, PyObject *default_value, PyObject **value, int *changed); + #endif /* !Py_INTERNAL_CONTEXT_H */ diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 902ca17e07483ca..26b76740fb34d04 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -361,7 +361,7 @@ def getcontext(): New contexts are copies of DefaultContext. """ try: - context, changed = _current_context_var.get_changed() + context, changed = _current_context_var._get_changed() except LookupError: context = Context() _current_context_var.set(context) diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index 56e6fd3591f4eda..181a4fd3f4b7c14 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -586,91 +586,91 @@ def __eq__(self, other): ctx2.run(var.set, ReentrantHash()) ctx1 == ctx2 - def test_get_changed_outside_run(self): + def test__get_changed_outside_run(self): # Outside any Context.run(), bindings are considered "changed" v = contextvars.ContextVar('v', default='dflt') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'dflt') self.assertFalse(changed) # default value, not changed v.set(42) - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 42) self.assertTrue(changed) # set in base context - def test_get_changed_inherited(self): + def test__get_changed_inherited(self): # Inherited bindings are not considered "changed" v = contextvars.ContextVar('v') v.set('parent') ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'parent') self.assertFalse(changed) ctx.run(check) - def test_get_changed_after_set(self): + def test__get_changed_after_set(self): # After set() inside Context.run(), changed is True v = contextvars.ContextVar('v') v.set('parent') ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertFalse(changed) v.set('child') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'child') self.assertTrue(changed) ctx.run(check) - def test_get_changed_new_var_in_run(self): + def test__get_changed_new_var_in_run(self): # A variable set for the first time inside run() is "changed" v = contextvars.ContextVar('v') ctx = contextvars.copy_context() def check(): with self.assertRaises(LookupError): - v.get_changed() + v._get_changed() v.set('new') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'new') self.assertTrue(changed) ctx.run(check) - def test_get_changed_not_set_with_default(self): + def test__get_changed_not_set_with_default(self): # A variable not set but with default: changed is False v = contextvars.ContextVar('v', default='dflt') ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'dflt') self.assertFalse(changed) ctx.run(check) - def test_get_changed_not_set_no_default(self): + def test__get_changed_not_set_no_default(self): # A variable that has never been set and has no default v = contextvars.ContextVar('v') ctx = contextvars.copy_context() def check(): with self.assertRaises(LookupError): - v.get_changed() + v._get_changed() ctx.run(check) - def test_get_changed_explicit_default_arg(self): - # Passing a default argument to get_changed() + def test__get_changed_explicit_default_arg(self): + # Passing a default argument to _get_changed() v = contextvars.ContextVar('v') ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed('fallback') + val, changed = v._get_changed('fallback') self.assertEqual(val, 'fallback') self.assertFalse(changed) ctx.run(check) - def test_get_changed_set_same_object(self): + def test__get_changed_set_same_object(self): # Setting to the exact same object does not count as "changed" # because the HAMT recognizes the identical key-value pair obj = object() @@ -679,48 +679,48 @@ def test_get_changed_set_same_object(self): ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertIs(val, obj) self.assertFalse(changed) v.set(obj) # same object - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertIs(val, obj) self.assertFalse(changed) ctx.run(check) - def test_get_changed_set_different_object(self): + def test__get_changed_set_different_object(self): # Setting to a different object counts as "changed" v = contextvars.ContextVar('v') v.set([1, 2, 3]) ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertFalse(changed) v.set([1, 2, 3]) # equal value, different object - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertTrue(changed) ctx.run(check) - def test_get_changed_after_reset(self): + def test__get_changed_after_reset(self): # After reset(), the variable reverts to its inherited state v = contextvars.ContextVar('v') v.set('original') ctx = contextvars.copy_context() def check(): - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertFalse(changed) tok = v.set('modified') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertTrue(changed) v.reset(tok) - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertFalse(changed) ctx.run(check) - def test_get_changed_multiple_vars(self): - # Changing one variable does not affect get_changed() for others + def test__get_changed_multiple_vars(self): + # Changing one variable does not affect _get_changed() for others v1 = contextvars.ContextVar('v1') v2 = contextvars.ContextVar('v2') v1.set('a') @@ -728,50 +728,50 @@ def test_get_changed_multiple_vars(self): ctx = contextvars.copy_context() def check(): - _, changed1 = v1.get_changed() - _, changed2 = v2.get_changed() + _, changed1 = v1._get_changed() + _, changed2 = v2._get_changed() self.assertFalse(changed1) self.assertFalse(changed2) v1.set('a2') - _, changed1 = v1.get_changed() - _, changed2 = v2.get_changed() + _, changed1 = v1._get_changed() + _, changed2 = v2._get_changed() self.assertTrue(changed1) self.assertFalse(changed2) ctx.run(check) - def test_get_changed_nested_run(self): - # get_changed() reflects the innermost Context.run() scope + def test__get_changed_nested_run(self): + # _get_changed() reflects the innermost Context.run() scope v = contextvars.ContextVar('v') v.set('root') ctx1 = contextvars.copy_context() def outer(): - _, changed = v.get_changed() + _, changed = v._get_changed() self.assertFalse(changed) v.set('outer') - _, changed = v.get_changed() + _, changed = v._get_changed() self.assertTrue(changed) ctx2 = contextvars.copy_context() def inner(): # inherited 'outer' from ctx1, not changed in ctx2 - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'outer') self.assertFalse(changed) v.set('inner') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'inner') self.assertTrue(changed) ctx2.run(inner) # after inner run exits, outer's state is restored - _, changed = v.get_changed() + _, changed = v._get_changed() self.assertTrue(changed) ctx1.run(outer) @threading_helper.requires_working_threading() - def test_get_changed_with_threads(self): - # get_changed() works correctly in a thread with copied context + def test__get_changed_with_threads(self): + # _get_changed() works correctly in a thread with copied context import threading v = contextvars.ContextVar('v') v.set('parent') @@ -779,11 +779,11 @@ def test_get_changed_with_threads(self): results = {} def thread_func(): - val, changed = v.get_changed() + val, changed = v._get_changed() results['inherited'] = changed results['value'] = val v.set('thread') - val, changed = v.get_changed() + val, changed = v._get_changed() results['after_set'] = changed t = threading.Thread(target=ctx.run, args=(thread_func,)) @@ -793,16 +793,16 @@ def thread_func(): self.assertEqual(results['value'], 'parent') self.assertTrue(results['after_set']) - def test_get_changed_empty_context_run(self): + def test__get_changed_empty_context_run(self): # Running in a brand new empty context v = contextvars.ContextVar('v') ctx = contextvars.Context() def check(): with self.assertRaises(LookupError): - v.get_changed() + v._get_changed() v.set('value') - val, changed = v.get_changed() + val, changed = v._get_changed() self.assertEqual(val, 'value') self.assertTrue(changed) ctx.run(check) diff --git a/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst b/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst index 8cc474526b8c4d6..8589be19132a745 100644 --- a/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst +++ b/Misc/NEWS.d/next/Library/2026-03-26-10-27-07.gh-issue-141148._XpYnI.rst @@ -1,4 +1,3 @@ Ensure that :func:`decimal.getcontext` returns a per-task copy of the :class:`decimal.Context` so that mutations are isolated between asyncio -tasks and threads using :data:`sys.flags.thread_inherit_context`. Added -:meth:`contextvars.ContextVar.get_changed` to support this. +tasks and threads using :data:`sys.flags.thread_inherit_context`. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index b361d61deb4020a..f96c3a6ae0745f6 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -30,6 +30,7 @@ #endif #include +#include "pycore_context.h" // _PyContextVar_GetChanged() #include "pycore_object.h" // _PyObject_VisitType() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_typeobject.h" @@ -1937,8 +1938,8 @@ current_context(decimal_state *state) { PyObject *tl_context; int changed; - if (PyContextVar_GetChanged(state->current_context_var, NULL, &tl_context, - &changed) < 0) { + if (_PyContextVar_GetChanged(state->current_context_var, NULL, &tl_context, + &changed) < 0) { return NULL; } diff --git a/Python/clinic/context.c.h b/Python/clinic/context.c.h index 3fe45c45a64c441..4125d7a609e3529 100644 --- a/Python/clinic/context.c.h +++ b/Python/clinic/context.c.h @@ -206,8 +206,8 @@ _contextvars_ContextVar_reset(PyObject *self, PyObject *token) return return_value; } -PyDoc_STRVAR(_contextvars_ContextVar_get_changed__doc__, -"get_changed($self, default=, /)\n" +PyDoc_STRVAR(_contextvars_ContextVar__get_changed__doc__, +"_get_changed($self, default=, /)\n" "--\n" "\n" "Return a tuple of (value, changed) for the context variable.\n" @@ -225,20 +225,20 @@ PyDoc_STRVAR(_contextvars_ContextVar_get_changed__doc__, "\n" "When the value is found via a default, *changed* is always False."); -#define _CONTEXTVARS_CONTEXTVAR_GET_CHANGED_METHODDEF \ - {"get_changed", _PyCFunction_CAST(_contextvars_ContextVar_get_changed), METH_FASTCALL, _contextvars_ContextVar_get_changed__doc__}, +#define _CONTEXTVARS_CONTEXTVAR__GET_CHANGED_METHODDEF \ + {"_get_changed", _PyCFunction_CAST(_contextvars_ContextVar__get_changed), METH_FASTCALL, _contextvars_ContextVar__get_changed__doc__}, static PyObject * -_contextvars_ContextVar_get_changed_impl(PyContextVar *self, - PyObject *default_value); +_contextvars_ContextVar__get_changed_impl(PyContextVar *self, + PyObject *default_value); static PyObject * -_contextvars_ContextVar_get_changed(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +_contextvars_ContextVar__get_changed(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { PyObject *return_value = NULL; PyObject *default_value = NULL; - if (!_PyArg_CheckPositional("get_changed", nargs, 0, 1)) { + if (!_PyArg_CheckPositional("_get_changed", nargs, 0, 1)) { goto exit; } if (nargs < 1) { @@ -246,7 +246,7 @@ _contextvars_ContextVar_get_changed(PyObject *self, PyObject *const *args, Py_ss } default_value = args[0]; skip_optional: - return_value = _contextvars_ContextVar_get_changed_impl((PyContextVar *)self, default_value); + return_value = _contextvars_ContextVar__get_changed_impl((PyContextVar *)self, default_value); exit: return return_value; @@ -302,4 +302,4 @@ token_exit(PyObject *self, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=0adcad33b8abcf29 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=cc072c96b14bdbd3 input=a9049054013a1b77]*/ diff --git a/Python/context.c b/Python/context.c index 1490d60c46e9b20..12a07d9083f6456 100644 --- a/Python/context.c +++ b/Python/context.c @@ -447,8 +447,8 @@ contextvar_check_changed(PyContext *ctx, PyContextVar *var, PyObject *cur_val) int -PyContextVar_GetChanged(PyObject *ovar, PyObject *def, PyObject **val, - int *changed) +_PyContextVar_GetChanged(PyObject *ovar, PyObject *def, PyObject **val, + int *changed) { ENSURE_ContextVar(ovar, -1) PyContextVar *var = (PyContextVar *)ovar; @@ -1204,7 +1204,7 @@ _contextvars_ContextVar_reset_impl(PyContextVar *self, PyObject *token) /*[clinic input] @permit_long_docstring_body -_contextvars.ContextVar.get_changed +_contextvars.ContextVar._get_changed default: object = NULL / @@ -1225,13 +1225,13 @@ When the value is found via a default, *changed* is always False. [clinic start generated code]*/ static PyObject * -_contextvars_ContextVar_get_changed_impl(PyContextVar *self, - PyObject *default_value) -/*[clinic end generated code: output=2418683613ac96e7 input=2dacfcf7b43f9719]*/ +_contextvars_ContextVar__get_changed_impl(PyContextVar *self, + PyObject *default_value) +/*[clinic end generated code: output=16b72be2c79429e9 input=aa6c784a3846a840]*/ { PyObject *val; int changed; - if (PyContextVar_GetChanged( + if (_PyContextVar_GetChanged( (PyObject *)self, default_value, &val, &changed) < 0) { return NULL; } @@ -1257,7 +1257,7 @@ static PyMethodDef PyContextVar_methods[] = { _CONTEXTVARS_CONTEXTVAR_GET_METHODDEF _CONTEXTVARS_CONTEXTVAR_SET_METHODDEF _CONTEXTVARS_CONTEXTVAR_RESET_METHODDEF - _CONTEXTVARS_CONTEXTVAR_GET_CHANGED_METHODDEF + _CONTEXTVARS_CONTEXTVAR__GET_CHANGED_METHODDEF {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")}, {NULL, NULL}