gh-128384: Use a context variable for warnings.catch_warnings (gh-130010)

Make `warnings.catch_warnings()` use a context variable for holding
the warning filtering state if the `sys.flags.context_aware_warnings`
flag is set to true.  This makes using the context manager thread-safe in
multi-threaded programs.

Add the `sys.flags.thread_inherit_context` flag.  If true, starting a new
thread with `threading.Thread` will use a copy of the context
from the caller of `Thread.start()`.

Both these flags are set to true by default for the free-threaded build
and false for the default build.

Move the Python implementation of warnings.py into _py_warnings.py.

Make _contextvars a builtin module.

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
This commit is contained in:
Neil Schemenauer 2025-04-09 16:18:54 -07:00 committed by GitHub
parent e5237541a0
commit d687900f98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1851 additions and 960 deletions

View file

@ -69,6 +69,7 @@ warnings_clear_state(WarningsState *st)
Py_CLEAR(st->filters);
Py_CLEAR(st->once_registry);
Py_CLEAR(st->default_action);
Py_CLEAR(st->context);
}
#ifndef Py_DEBUG
@ -156,6 +157,13 @@ _PyWarnings_InitState(PyInterpreterState *interp)
}
}
if (st->context == NULL) {
st->context = PyContextVar_New("_warnings_context", NULL);
if (st->context == NULL) {
return -1;
}
}
st->filters_version = 0;
return 0;
}
@ -256,6 +264,68 @@ warnings_lock_held(WarningsState *st)
return PyMutex_IsLocked(&st->lock.mutex);
}
static PyObject *
get_warnings_context(PyInterpreterState *interp)
{
WarningsState *st = warnings_get_state(interp);
assert(PyContextVar_CheckExact(st->context));
PyObject *ctx;
if (PyContextVar_Get(st->context, NULL, &ctx) < 0) {
return NULL;
}
if (ctx == NULL) {
Py_RETURN_NONE;
}
return ctx;
}
static PyObject *
get_warnings_context_filters(PyInterpreterState *interp)
{
PyObject *ctx = get_warnings_context(interp);
if (ctx == NULL) {
return NULL;
}
if (ctx == Py_None) {
Py_RETURN_NONE;
}
PyObject *context_filters = PyObject_GetAttr(ctx, &_Py_ID(_filters));
Py_DECREF(ctx);
if (context_filters == NULL) {
return NULL;
}
if (!PyList_Check(context_filters)) {
PyErr_SetString(PyExc_ValueError,
"_filters of warnings._warnings_context must be a list");
Py_DECREF(context_filters);
return NULL;
}
return context_filters;
}
// Returns a borrowed reference to the list.
static PyObject *
get_warnings_filters(PyInterpreterState *interp)
{
WarningsState *st = warnings_get_state(interp);
PyObject *warnings_filters = GET_WARNINGS_ATTR(interp, filters, 0);
if (warnings_filters == NULL) {
if (PyErr_Occurred())
return NULL;
}
else {
Py_SETREF(st->filters, warnings_filters);
}
PyObject *filters = st->filters;
if (filters == NULL || !PyList_Check(filters)) {
PyErr_SetString(PyExc_ValueError,
MODULE_NAME ".filters must be a list");
return NULL;
}
return filters;
}
/*[clinic input]
_acquire_lock as warnings_acquire_lock
@ -349,35 +419,17 @@ get_default_action(PyInterpreterState *interp)
return default_action;
}
/* The item is a new reference. */
static PyObject*
get_filter(PyInterpreterState *interp, PyObject *category,
PyObject *text, Py_ssize_t lineno,
PyObject *module, PyObject **item)
{
WarningsState *st = warnings_get_state(interp);
assert(st != NULL);
assert(warnings_lock_held(st));
PyObject *warnings_filters = GET_WARNINGS_ATTR(interp, filters, 0);
if (warnings_filters == NULL) {
if (PyErr_Occurred())
return NULL;
}
else {
Py_SETREF(st->filters, warnings_filters);
}
PyObject *filters = st->filters;
if (filters == NULL || !PyList_Check(filters)) {
PyErr_SetString(PyExc_ValueError,
MODULE_NAME ".filters must be a list");
return NULL;
}
/* WarningsState.filters could change while we are iterating over it. */
/* Search filters list of match, returns false on error. If no match
* then 'matched_action' is NULL. */
static bool
filter_search(PyInterpreterState *interp, PyObject *category,
PyObject *text, Py_ssize_t lineno,
PyObject *module, char *list_name, PyObject *filters,
PyObject **item, PyObject **matched_action) {
bool result = true;
*matched_action = NULL;
/* Avoid the filters list changing while we iterate over it. */
Py_BEGIN_CRITICAL_SECTION(filters);
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(filters); i++) {
PyObject *tmp_item, *action, *msg, *cat, *mod, *ln_obj;
Py_ssize_t ln;
@ -386,8 +438,9 @@ get_filter(PyInterpreterState *interp, PyObject *category,
tmp_item = PyList_GET_ITEM(filters, i);
if (!PyTuple_Check(tmp_item) || PyTuple_GET_SIZE(tmp_item) != 5) {
PyErr_Format(PyExc_ValueError,
MODULE_NAME ".filters item %zd isn't a 5-tuple", i);
return NULL;
"warnings.%s item %zd isn't a 5-tuple", list_name, i);
result = false;
break;
}
/* Python code: action, msg, cat, mod, ln = item */
@ -403,42 +456,102 @@ get_filter(PyInterpreterState *interp, PyObject *category,
"action must be a string, not '%.200s'",
Py_TYPE(action)->tp_name);
Py_DECREF(tmp_item);
return NULL;
result = false;
break;
}
good_msg = check_matched(interp, msg, text);
if (good_msg == -1) {
Py_DECREF(tmp_item);
return NULL;
result = false;
break;
}
good_mod = check_matched(interp, mod, module);
if (good_mod == -1) {
Py_DECREF(tmp_item);
return NULL;
result = false;
break;
}
is_subclass = PyObject_IsSubclass(category, cat);
if (is_subclass == -1) {
Py_DECREF(tmp_item);
return NULL;
result = false;
break;
}
ln = PyLong_AsSsize_t(ln_obj);
if (ln == -1 && PyErr_Occurred()) {
Py_DECREF(tmp_item);
return NULL;
result = false;
break;
}
if (good_msg && is_subclass && good_mod && (ln == 0 || lineno == ln)) {
*item = tmp_item;
return action;
*matched_action = action;
result = true;
break;
}
Py_DECREF(tmp_item);
}
Py_END_CRITICAL_SECTION();
return result;
}
PyObject *action = get_default_action(interp);
/* The item is a new reference. */
static PyObject*
get_filter(PyInterpreterState *interp, PyObject *category,
PyObject *text, Py_ssize_t lineno,
PyObject *module, PyObject **item)
{
#ifdef Py_DEBUG
WarningsState *st = warnings_get_state(interp);
assert(st != NULL);
assert(warnings_lock_held(st));
#endif
/* check _warning_context _filters list */
PyObject *context_filters = get_warnings_context_filters(interp);
if (context_filters == NULL) {
return NULL;
}
bool use_global_filters = false;
if (context_filters == Py_None) {
use_global_filters = true;
} else {
PyObject *context_action = NULL;
if (!filter_search(interp, category, text, lineno, module, "_warnings_context _filters",
context_filters, item, &context_action)) {
Py_DECREF(context_filters);
return NULL;
}
Py_DECREF(context_filters);
if (context_action != NULL) {
return context_action;
}
}
PyObject *action;
if (use_global_filters) {
/* check warnings.filters list */
PyObject *filters = get_warnings_filters(interp);
if (filters == NULL) {
return NULL;
}
if (!filter_search(interp, category, text, lineno, module, "filters",
filters, item, &action)) {
return NULL;
}
if (action != NULL) {
return action;
}
}
action = get_default_action(interp);
if (action != NULL) {
*item = Py_NewRef(Py_None);
return action;
@ -1540,6 +1653,9 @@ warnings_module_exec(PyObject *module)
if (PyModule_AddObjectRef(module, "_defaultaction", st->default_action) < 0) {
return -1;
}
if (PyModule_AddObjectRef(module, "_warnings_context", st->context) < 0) {
return -1;
}
return 0;
}