Skip to content
2 changes: 1 addition & 1 deletion Include/internal/pycore_warnings.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct _warnings_runtime_state {
PyObject *filters; /* List */
PyObject *once_registry; /* Dict */
PyObject *default_action; /* String */
PyMutex mutex;
_PyRecursiveMutex lock;
long filters_version;
};

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ def test_late_resource_warning(self):
self.assertTrue(err.startswith(expected), ascii(err))


class DeprecatedTests(unittest.TestCase):
class DeprecatedTests(PyPublicAPITests):
def test_dunder_deprecated(self):
@deprecated("A will go away soon")
class A:
Expand Down
170 changes: 100 additions & 70 deletions Lib/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,24 +185,32 @@ def simplefilter(action, category=Warning, lineno=0, append=False):
raise ValueError("lineno must be an int >= 0")
_add_filter(action, None, category, None, lineno, append=append)

def _filters_mutated():
# Even though this function is not part of the public API, it's used by
# a fair amount of user code.
with _lock:
_filters_mutated_lock_held()

def _add_filter(*item, append):
# Remove possible duplicate filters, so new one will be placed
# in correct place. If append=True and duplicate exists, do nothing.
if not append:
try:
filters.remove(item)
except ValueError:
pass
filters.insert(0, item)
else:
if item not in filters:
filters.append(item)
_filters_mutated()
with _lock:
if not append:
# Remove possible duplicate filters, so new one will be placed
# in correct place. If append=True and duplicate exists, do nothing.
try:
filters.remove(item)
except ValueError:
pass
filters.insert(0, item)
else:
if item not in filters:
filters.append(item)
_filters_mutated_lock_held()

def resetwarnings():
"""Clear the list of warning filters, so that no filters are active."""
filters[:] = []
_filters_mutated()
with _lock:
filters[:] = []
_filters_mutated_lock_held()

class _OptionError(Exception):
"""Exception used by option processing helpers."""
Expand Down Expand Up @@ -353,64 +361,66 @@ def warn_explicit(message, category, filename, lineno,
module = filename or "<unknown>"
if module[-3:].lower() == ".py":
module = module[:-3] # XXX What about leading pathname?
if registry is None:
registry = {}
if registry.get('version', 0) != _filters_version:
registry.clear()
registry['version'] = _filters_version
if isinstance(message, Warning):
text = str(message)
category = message.__class__
else:
text = message
message = category(message)
key = (text, category, lineno)
# Quick test for common case
if registry.get(key):
return
# Search the filters
for item in filters:
action, msg, cat, mod, ln = item
if ((msg is None or msg.match(text)) and
issubclass(category, cat) and
(mod is None or mod.match(module)) and
(ln == 0 or lineno == ln)):
break
else:
action = defaultaction
# Early exit actions
if action == "ignore":
return
with _lock:
if registry is None:
registry = {}
if registry.get('version', 0) != _filters_version:
registry.clear()
registry['version'] = _filters_version
# Quick test for common case
if registry.get(key):
return
# Search the filters
for item in filters:
action, msg, cat, mod, ln = item
if ((msg is None or msg.match(text)) and
issubclass(category, cat) and
(mod is None or mod.match(module)) and
(ln == 0 or lineno == ln)):
break
else:
action = defaultaction
# Early exit actions
if action == "ignore":
return

if action == "error":
raise message
# Other actions
if action == "once":
registry[key] = 1
oncekey = (text, category)
if onceregistry.get(oncekey):
return
onceregistry[oncekey] = 1
elif action in {"always", "all"}:
pass
elif action == "module":
registry[key] = 1
altkey = (text, category, 0)
if registry.get(altkey):
return
registry[altkey] = 1
elif action == "default":
registry[key] = 1
else:
# Unrecognized actions are errors
raise RuntimeError(
"Unrecognized action (%r) in warnings.filters:\n %s" %
(action, item))

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

if action == "error":
raise message
# Other actions
if action == "once":
registry[key] = 1
oncekey = (text, category)
if onceregistry.get(oncekey):
return
onceregistry[oncekey] = 1
elif action in {"always", "all"}:
pass
elif action == "module":
registry[key] = 1
altkey = (text, category, 0)
if registry.get(altkey):
return
registry[altkey] = 1
elif action == "default":
registry[key] = 1
else:
# Unrecognized actions are errors
raise RuntimeError(
"Unrecognized action (%r) in warnings.filters:\n %s" %
(action, item))
# Print message and context
msg = WarningMessage(message, category, filename, lineno, source)
_showwarnmsg(msg)
Expand Down Expand Up @@ -488,11 +498,12 @@ def __enter__(self):
if self._entered:
raise RuntimeError("Cannot enter %r twice" % self)
self._entered = True
self._filters = self._module.filters
self._module.filters = self._filters[:]
self._module._filters_mutated()
self._showwarning = self._module.showwarning
self._showwarnmsg_impl = self._module._showwarnmsg_impl
with _lock:
self._filters = self._module.filters
self._module.filters = self._filters[:]
self._module._filters_mutated_lock_held()
self._showwarning = self._module.showwarning
self._showwarnmsg_impl = self._module._showwarnmsg_impl
if self._filter is not None:
simplefilter(*self._filter)
if self._record:
Expand All @@ -508,10 +519,11 @@ def __enter__(self):
def __exit__(self, *exc_info):
if not self._entered:
raise RuntimeError("Cannot exit %r without entering first" % self)
self._module.filters = self._filters
self._module._filters_mutated()
self._module.showwarning = self._showwarning
self._module._showwarnmsg_impl = self._showwarnmsg_impl
with _lock:
self._module.filters = self._filters
self._module._filters_mutated_lock_held()
self._module.showwarning = self._showwarning
self._module._showwarnmsg_impl = self._showwarnmsg_impl


class deprecated:
Expand Down Expand Up @@ -701,18 +713,36 @@ def extract():
# If either if the compiled regexs are None, match anything.
try:
from _warnings import (filters, _defaultaction, _onceregistry,
warn, warn_explicit, _filters_mutated)
warn, warn_explicit,
_filters_mutated_lock_held,
_acquire_lock, _release_lock,
)
defaultaction = _defaultaction
onceregistry = _onceregistry
_warnings_defaults = True

class _Lock:
def __enter__(self):
_acquire_lock()
return self

def __exit__(self, *args):
_release_lock()

_lock = _Lock()

except ImportError:
filters = []
defaultaction = "default"
onceregistry = {}

import _thread

_lock = _thread.RLock()

_filters_version = 1

def _filters_mutated():
def _filters_mutated_lock_held():
global _filters_version
_filters_version += 1

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add locking to :mod:`warnings` to avoid some data races when free-threading
is used. Change ``_warnings_runtime_state.mutex`` to be a recursive mutex
and expose it to :mod:`warnings`, via the :func:`!_acquire_lock` and
:func:`!_release_lock` functions. The lock is held when ``filters`` and
``_filters_version`` are updated.
Loading
Loading