Skip to content

Commit a5ec58d

Browse files
committed
Add locking to warnings.py.
Expose the mutex from _warnings.c and hold it when mutating the filters list.
1 parent 30efede commit a5ec58d

File tree

4 files changed

+202
-85
lines changed

4 files changed

+202
-85
lines changed

Lib/test/test_warnings/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,7 +1521,7 @@ def test_late_resource_warning(self):
15211521
self.assertTrue(err.startswith(expected), ascii(err))
15221522

15231523

1524-
class DeprecatedTests(unittest.TestCase):
1524+
class DeprecatedTests(PyPublicAPITests):
15251525
def test_dunder_deprecated(self):
15261526
@deprecated("A will go away soon")
15271527
class A:

Lib/warnings.py

Lines changed: 106 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -186,23 +186,25 @@ def simplefilter(action, category=Warning, lineno=0, append=False):
186186
_add_filter(action, None, category, None, lineno, append=append)
187187

188188
def _add_filter(*item, append):
189-
# Remove possible duplicate filters, so new one will be placed
190-
# in correct place. If append=True and duplicate exists, do nothing.
191-
if not append:
192-
try:
193-
filters.remove(item)
194-
except ValueError:
195-
pass
196-
filters.insert(0, item)
197-
else:
198-
if item not in filters:
199-
filters.append(item)
200-
_filters_mutated()
189+
with _lock:
190+
if not append:
191+
# Remove possible duplicate filters, so new one will be placed
192+
# in correct place. If append=True and duplicate exists, do nothing.
193+
try:
194+
filters.remove(item)
195+
except ValueError:
196+
pass
197+
filters.insert(0, item)
198+
else:
199+
if item not in filters:
200+
filters.append(item)
201+
_filters_mutated_unlocked()
201202

202203
def resetwarnings():
203204
"""Clear the list of warning filters, so that no filters are active."""
204-
filters[:] = []
205-
_filters_mutated()
205+
with _lock:
206+
filters[:] = []
207+
_filters_mutated_unlocked()
206208

207209
class _OptionError(Exception):
208210
"""Exception used by option processing helpers."""
@@ -353,64 +355,66 @@ def warn_explicit(message, category, filename, lineno,
353355
module = filename or "<unknown>"
354356
if module[-3:].lower() == ".py":
355357
module = module[:-3] # XXX What about leading pathname?
356-
if registry is None:
357-
registry = {}
358-
if registry.get('version', 0) != _filters_version:
359-
registry.clear()
360-
registry['version'] = _filters_version
361358
if isinstance(message, Warning):
362359
text = str(message)
363360
category = message.__class__
364361
else:
365362
text = message
366363
message = category(message)
367364
key = (text, category, lineno)
368-
# Quick test for common case
369-
if registry.get(key):
370-
return
371-
# Search the filters
372-
for item in filters:
373-
action, msg, cat, mod, ln = item
374-
if ((msg is None or msg.match(text)) and
375-
issubclass(category, cat) and
376-
(mod is None or mod.match(module)) and
377-
(ln == 0 or lineno == ln)):
378-
break
379-
else:
380-
action = defaultaction
381-
# Early exit actions
382-
if action == "ignore":
383-
return
365+
with _lock:
366+
if registry is None:
367+
registry = {}
368+
if registry.get('version', 0) != _filters_version:
369+
registry.clear()
370+
registry['version'] = _filters_version
371+
# Quick test for common case
372+
if registry.get(key):
373+
return
374+
# Search the filters
375+
for item in filters:
376+
action, msg, cat, mod, ln = item
377+
if ((msg is None or msg.match(text)) and
378+
issubclass(category, cat) and
379+
(mod is None or mod.match(module)) and
380+
(ln == 0 or lineno == ln)):
381+
break
382+
else:
383+
action = defaultaction
384+
# Early exit actions
385+
if action == "ignore":
386+
return
387+
388+
if action == "error":
389+
raise message
390+
# Other actions
391+
if action == "once":
392+
registry[key] = 1
393+
oncekey = (text, category)
394+
if onceregistry.get(oncekey):
395+
return
396+
onceregistry[oncekey] = 1
397+
elif action in {"always", "all"}:
398+
pass
399+
elif action == "module":
400+
registry[key] = 1
401+
altkey = (text, category, 0)
402+
if registry.get(altkey):
403+
return
404+
registry[altkey] = 1
405+
elif action == "default":
406+
registry[key] = 1
407+
else:
408+
# Unrecognized actions are errors
409+
raise RuntimeError(
410+
"Unrecognized action (%r) in warnings.filters:\n %s" %
411+
(action, item))
384412

385413
# Prime the linecache for formatting, in case the
386414
# "file" is actually in a zipfile or something.
387415
import linecache
388416
linecache.getlines(filename, module_globals)
389417

390-
if action == "error":
391-
raise message
392-
# Other actions
393-
if action == "once":
394-
registry[key] = 1
395-
oncekey = (text, category)
396-
if onceregistry.get(oncekey):
397-
return
398-
onceregistry[oncekey] = 1
399-
elif action in {"always", "all"}:
400-
pass
401-
elif action == "module":
402-
registry[key] = 1
403-
altkey = (text, category, 0)
404-
if registry.get(altkey):
405-
return
406-
registry[altkey] = 1
407-
elif action == "default":
408-
registry[key] = 1
409-
else:
410-
# Unrecognized actions are errors
411-
raise RuntimeError(
412-
"Unrecognized action (%r) in warnings.filters:\n %s" %
413-
(action, item))
414418
# Print message and context
415419
msg = WarningMessage(message, category, filename, lineno, source)
416420
_showwarnmsg(msg)
@@ -488,11 +492,12 @@ def __enter__(self):
488492
if self._entered:
489493
raise RuntimeError("Cannot enter %r twice" % self)
490494
self._entered = True
491-
self._filters = self._module.filters
492-
self._module.filters = self._filters[:]
493-
self._module._filters_mutated()
494-
self._showwarning = self._module.showwarning
495-
self._showwarnmsg_impl = self._module._showwarnmsg_impl
495+
with _lock:
496+
self._filters = self._module.filters
497+
self._module.filters = self._filters[:]
498+
self._module._filters_mutated_unlocked()
499+
self._showwarning = self._module.showwarning
500+
self._showwarnmsg_impl = self._module._showwarnmsg_impl
496501
if self._filter is not None:
497502
simplefilter(*self._filter)
498503
if self._record:
@@ -508,10 +513,11 @@ def __enter__(self):
508513
def __exit__(self, *exc_info):
509514
if not self._entered:
510515
raise RuntimeError("Cannot exit %r without entering first" % self)
511-
self._module.filters = self._filters
512-
self._module._filters_mutated()
513-
self._module.showwarning = self._showwarning
514-
self._module._showwarnmsg_impl = self._showwarnmsg_impl
516+
with _lock:
517+
self._module.filters = self._filters
518+
self._module._filters_mutated_unlocked()
519+
self._module.showwarning = self._showwarning
520+
self._module._showwarnmsg_impl = self._showwarnmsg_impl
515521

516522

517523
class deprecated:
@@ -701,17 +707,48 @@ def extract():
701707
# If either if the compiled regexs are None, match anything.
702708
try:
703709
from _warnings import (filters, _defaultaction, _onceregistry,
704-
warn, warn_explicit, _filters_mutated)
710+
warn, warn_explicit,
711+
_filters_mutated_unlocked,
712+
_acquire_lock, _release_lock,
713+
)
705714
defaultaction = _defaultaction
706715
onceregistry = _onceregistry
707716
_warnings_defaults = True
717+
718+
class _Lock:
719+
def __enter__(self):
720+
_acquire_lock()
721+
return self
722+
723+
def __exit__(self, *args):
724+
_release_lock()
725+
726+
_lock = _Lock()
727+
728+
def _filters_mutated():
729+
# Even though this function is part of the public API, it's used
730+
# by a fair amount of user code.
731+
with _lock:
732+
_filters_mutated_unlocked()
733+
708734
except ImportError:
709735
filters = []
710736
defaultaction = "default"
711737
onceregistry = {}
712738

739+
import _thread
740+
741+
# Note that this is a non-reentrant lock, matching what's used by
742+
# _acquire_lock() and _release_lock(). Care must be taken to
743+
# not deadlock.
744+
_lock = _thread.LockType()
745+
713746
_filters_version = 1
714747

748+
def _filters_mutated_unlocked():
749+
global _filters_version
750+
_filters_version += 1
751+
715752
def _filters_mutated():
716753
global _filters_version
717754
_filters_version += 1

Python/_warnings.c

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,47 @@ get_warnings_attr(PyInterpreterState *interp, PyObject *attr, int try_import)
232232
return obj;
233233
}
234234

235+
/*[clinic input]
236+
_acquire_lock as warnings_acquire_lock
237+
238+
[clinic start generated code]*/
239+
240+
static PyObject *
241+
warnings_acquire_lock_impl(PyObject *module)
242+
/*[clinic end generated code: output=594313457d1bf8e1 input=46ec20e55acca52f]*/
243+
{
244+
PyInterpreterState *interp = get_current_interp();
245+
if (interp == NULL) {
246+
return NULL;
247+
}
248+
249+
WarningsState *st = warnings_get_state(interp);
250+
assert(st != NULL);
251+
252+
_PyMutex_Lock(&st->mutex);
253+
Py_RETURN_NONE;
254+
}
255+
256+
/*[clinic input]
257+
_release_lock as warnings_release_lock
258+
259+
[clinic start generated code]*/
260+
261+
static PyObject *
262+
warnings_release_lock_impl(PyObject *module)
263+
/*[clinic end generated code: output=d73d5a8789396750 input=ea01bb77870c5693]*/
264+
{
265+
PyInterpreterState *interp = get_current_interp();
266+
if (interp == NULL) {
267+
return NULL;
268+
}
269+
270+
WarningsState *st = warnings_get_state(interp);
271+
assert(st != NULL);
272+
273+
_PyMutex_Unlock(&st->mutex);
274+
Py_RETURN_NONE;
275+
}
235276

236277
static PyObject *
237278
get_once_registry(PyInterpreterState *interp)
@@ -1165,13 +1206,13 @@ warnings_warn_explicit_impl(PyObject *module, PyObject *message,
11651206
}
11661207

11671208
/*[clinic input]
1168-
_filters_mutated as warnings_filters_mutated
1209+
_filters_mutated_unlocked as warnings_filters_mutated_unlocked
11691210
11701211
[clinic start generated code]*/
11711212

11721213
static PyObject *
1173-
warnings_filters_mutated_impl(PyObject *module)
1174-
/*[clinic end generated code: output=8ce517abd12b88f4 input=35ecbf08ee2491b2]*/
1214+
warnings_filters_mutated_unlocked_impl(PyObject *module)
1215+
/*[clinic end generated code: output=05ce1f43c187b8cc input=c0488aa2a6f0f661]*/
11751216
{
11761217
PyInterpreterState *interp = get_current_interp();
11771218
if (interp == NULL) {
@@ -1181,14 +1222,17 @@ warnings_filters_mutated_impl(PyObject *module)
11811222
WarningsState *st = warnings_get_state(interp);
11821223
assert(st != NULL);
11831224

1184-
Py_BEGIN_CRITICAL_SECTION_MUT(&st->mutex);
1225+
// Note that the lock must be held by the caller.
1226+
if (!PyMutex_IsLocked(&st->mutex)) {
1227+
PyErr_SetString(PyExc_RuntimeError, "warnings mutex is not held");
1228+
return NULL;
1229+
}
1230+
11851231
st->filters_version++;
1186-
Py_END_CRITICAL_SECTION();
11871232

11881233
Py_RETURN_NONE;
11891234
}
11901235

1191-
11921236
/* Function to issue a warning message; may raise an exception. */
11931237

11941238
static int
@@ -1464,7 +1508,9 @@ _PyErr_WarnUnawaitedCoroutine(PyObject *coro)
14641508
static PyMethodDef warnings_functions[] = {
14651509
WARNINGS_WARN_METHODDEF
14661510
WARNINGS_WARN_EXPLICIT_METHODDEF
1467-
WARNINGS_FILTERS_MUTATED_METHODDEF
1511+
WARNINGS_FILTERS_MUTATED_UNLOCKED_METHODDEF
1512+
WARNINGS_ACQUIRE_LOCK_METHODDEF
1513+
WARNINGS_RELEASE_LOCK_METHODDEF
14681514
/* XXX(brett.cannon): add showwarning? */
14691515
/* XXX(brett.cannon): Reasonable to add formatwarning? */
14701516
{NULL, NULL} /* sentinel */

0 commit comments

Comments
 (0)