Skip to content

Commit fbd6289

Browse files
committed
Use contextvar for catch_warnings().
1 parent da96aa8 commit fbd6289

File tree

7 files changed

+281
-84
lines changed

7 files changed

+281
-84
lines changed

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ struct _Py_global_strings {
265265
STRUCT_FOR_ID(_type_)
266266
STRUCT_FOR_ID(_uninitialized_submodules)
267267
STRUCT_FOR_ID(_warn_unawaited_coroutine)
268+
STRUCT_FOR_ID(_warnings_context)
268269
STRUCT_FOR_ID(_xoptions)
269270
STRUCT_FOR_ID(abs_tol)
270271
STRUCT_FOR_ID(access)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_warnings/__init__.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,14 @@ def warnings_state(module):
4343
except NameError:
4444
pass
4545
original_warnings = warning_tests.warnings
46-
original_filters = module.filters
46+
saved_context, context = module._new_context()
4747
try:
48-
module.filters = original_filters[:]
4948
module.simplefilter("once")
5049
warning_tests.warnings = module
5150
yield
5251
finally:
5352
warning_tests.warnings = original_warnings
54-
module.filters = original_filters
53+
module._set_context(saved_context)
5554

5655

5756
class TestWarning(Warning):
@@ -336,15 +335,15 @@ def test_filterwarnings_duplicate_filters(self):
336335
with original_warnings.catch_warnings(module=self.module):
337336
self.module.resetwarnings()
338337
self.module.filterwarnings("error", category=UserWarning)
339-
self.assertEqual(len(self.module.filters), 1)
338+
self.assertEqual(len(self.module._get_filters()), 1)
340339
self.module.filterwarnings("ignore", category=UserWarning)
341340
self.module.filterwarnings("error", category=UserWarning)
342341
self.assertEqual(
343-
len(self.module.filters), 2,
342+
len(self.module._get_filters()), 2,
344343
"filterwarnings inserted duplicate filter"
345344
)
346345
self.assertEqual(
347-
self.module.filters[0][0], "error",
346+
self.module._get_filters()[0][0], "error",
348347
"filterwarnings did not promote filter to "
349348
"the beginning of list"
350349
)
@@ -353,15 +352,15 @@ def test_simplefilter_duplicate_filters(self):
353352
with original_warnings.catch_warnings(module=self.module):
354353
self.module.resetwarnings()
355354
self.module.simplefilter("error", category=UserWarning)
356-
self.assertEqual(len(self.module.filters), 1)
355+
self.assertEqual(len(self.module._get_filters()), 1)
357356
self.module.simplefilter("ignore", category=UserWarning)
358357
self.module.simplefilter("error", category=UserWarning)
359358
self.assertEqual(
360-
len(self.module.filters), 2,
359+
len(self.module._get_filters()), 2,
361360
"simplefilter inserted duplicate filter"
362361
)
363362
self.assertEqual(
364-
self.module.filters[0][0], "error",
363+
self.module._get_filters()[0][0], "error",
365364
"simplefilter did not promote filter to the beginning of list"
366365
)
367366

@@ -373,7 +372,7 @@ def test_append_duplicate(self):
373372
self.module.simplefilter("error", append=True)
374373
self.module.simplefilter("ignore", append=True)
375374
self.module.warn("test_append_duplicate", category=UserWarning)
376-
self.assertEqual(len(self.module.filters), 2,
375+
self.assertEqual(len(self.module._get_filters()), 2,
377376
"simplefilter inserted duplicate filter"
378377
)
379378
self.assertEqual(len(w), 0,
@@ -1049,11 +1048,11 @@ def test_issue31416(self):
10491048
# bad warnings.filters or warnings.defaultaction.
10501049
wmod = self.module
10511050
with original_warnings.catch_warnings(module=wmod):
1052-
wmod.filters = [(None, None, Warning, None, 0)]
1051+
wmod._get_filters()[:] = [(None, None, Warning, None, 0)]
10531052
with self.assertRaises(TypeError):
10541053
wmod.warn_explicit('foo', Warning, 'bar', 1)
10551054

1056-
wmod.filters = []
1055+
wmod._get_filters()[:] = []
10571056
with support.swap_attr(wmod, 'defaultaction', None), \
10581057
self.assertRaises(TypeError):
10591058
wmod.warn_explicit('foo', Warning, 'bar', 1)
@@ -1191,17 +1190,17 @@ class CatchWarningTests(BaseTest):
11911190

11921191
def test_catch_warnings_restore(self):
11931192
wmod = self.module
1194-
orig_filters = wmod.filters
1193+
orig_filters = wmod._get_filters()
11951194
orig_showwarning = wmod.showwarning
11961195
# Ensure both showwarning and filters are restored when recording
11971196
with wmod.catch_warnings(module=wmod, record=True):
1198-
wmod.filters = wmod.showwarning = object()
1199-
self.assertIs(wmod.filters, orig_filters)
1197+
wmod.get_context()._filters = wmod.showwarning = object()
1198+
self.assertIs(wmod._get_filters(), orig_filters)
12001199
self.assertIs(wmod.showwarning, orig_showwarning)
12011200
# Same test, but with recording disabled
12021201
with wmod.catch_warnings(module=wmod, record=False):
1203-
wmod.filters = wmod.showwarning = object()
1204-
self.assertIs(wmod.filters, orig_filters)
1202+
wmod.get_context()._filters = wmod.showwarning = object()
1203+
self.assertIs(wmod._get_filters(), orig_filters)
12051204
self.assertIs(wmod.showwarning, orig_showwarning)
12061205

12071206
def test_catch_warnings_recording(self):
@@ -1240,21 +1239,21 @@ def test_catch_warnings_reentry_guard(self):
12401239

12411240
def test_catch_warnings_defaults(self):
12421241
wmod = self.module
1243-
orig_filters = wmod.filters
1242+
orig_filters = wmod._get_filters()
12441243
orig_showwarning = wmod.showwarning
12451244
# Ensure default behaviour is not to record warnings
12461245
with wmod.catch_warnings(module=wmod) as w:
12471246
self.assertIsNone(w)
12481247
self.assertIs(wmod.showwarning, orig_showwarning)
1249-
self.assertIsNot(wmod.filters, orig_filters)
1250-
self.assertIs(wmod.filters, orig_filters)
1248+
self.assertIsNot(wmod._get_filters(), orig_filters)
1249+
self.assertIs(wmod._get_filters(), orig_filters)
12511250
if wmod is sys.modules['warnings']:
12521251
# Ensure the default module is this one
12531252
with wmod.catch_warnings() as w:
12541253
self.assertIsNone(w)
12551254
self.assertIs(wmod.showwarning, orig_showwarning)
1256-
self.assertIsNot(wmod.filters, orig_filters)
1257-
self.assertIs(wmod.filters, orig_filters)
1255+
self.assertIsNot(wmod._get_filters(), orig_filters)
1256+
self.assertIs(wmod._get_filters(), orig_filters)
12581257

12591258
def test_record_override_showwarning_before(self):
12601259
# Issue #28835: If warnings.showwarning() was overridden, make sure
@@ -1406,7 +1405,7 @@ def test_default_filter_configuration(self):
14061405
code = "import sys; sys.modules.pop('warnings', None); sys.modules['_warnings'] = None; "
14071406
else:
14081407
code = ""
1409-
code += "import warnings; [print(f) for f in warnings.filters]"
1408+
code += "import warnings; [print(f) for f in warnings._get_filters()]"
14101409

14111410
rc, stdout, stderr = assert_python_ok("-c", code, __isolated=True)
14121411
stdout_lines = [line.strip() for line in stdout.splitlines()]
@@ -1821,6 +1820,7 @@ async def coro(self):
18211820
self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
18221821
self.assertTrue(inspect.iscoroutinefunction(Cls.coro))
18231822

1823+
18241824
def setUpModule():
18251825
py_warnings.onceregistry.clear()
18261826
c_warnings.onceregistry.clear()

Lib/warnings.py

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,73 @@
11
"""Python part of the warnings subsystem."""
22

33
import sys
4+
import itertools as _itertools
5+
import contextvars as _contextvars
46

57

68
__all__ = ["warn", "warn_explicit", "showwarning",
79
"formatwarning", "filterwarnings", "simplefilter",
810
"resetwarnings", "catch_warnings", "deprecated"]
911

12+
class _Context:
13+
def __init__(self, filters):
14+
self._filters = filters
15+
self.log = None # if set to a list, logging is enabled
16+
17+
def copy(self):
18+
context = _Context(self._filters[:])
19+
if self.log is not None:
20+
context.log = self.log
21+
return context
22+
23+
def _record_warning(self, msg):
24+
self.log.append(msg)
25+
26+
27+
class _GlobalContext(_Context):
28+
def __init__(self):
29+
self.log = None
30+
31+
@property
32+
def _filters(self):
33+
# Since there is quite a lot of code that assigns to
34+
# warnings.filters, this needs to return the current value of
35+
# the module global.
36+
try:
37+
return filters
38+
except NameError:
39+
# 'filters' global was deleted. Do we need to actually handle this case?
40+
return []
41+
42+
_global_context = _GlobalContext()
43+
_warnings_context = _contextvars.ContextVar('warnings_context')
44+
45+
def get_context():
46+
try:
47+
context = _warnings_context.get()
48+
except LookupError:
49+
context = _global_context
50+
_warnings_context.set(context)
51+
return context
52+
53+
54+
def _set_context(context):
55+
_warnings_context.set(context)
56+
57+
58+
def _new_context():
59+
old_context = get_context()
60+
new_context = old_context.copy()
61+
_set_context(new_context)
62+
return old_context, new_context
63+
64+
65+
def _get_filters():
66+
"""Return the current list of filters. This is a non-public API used by
67+
the unit tests."""
68+
return get_context()._filters
69+
70+
1071
def showwarning(message, category, filename, lineno, file=None, line=None):
1172
"""Hook to write a warning to a file; replace if you like."""
1273
msg = WarningMessage(message, category, filename, lineno, file, line)
@@ -18,6 +79,10 @@ def formatwarning(message, category, filename, lineno, line=None):
1879
return _formatwarnmsg_impl(msg)
1980

2081
def _showwarnmsg_impl(msg):
82+
context = get_context()
83+
if context.log is not None:
84+
context._record_warning(msg)
85+
return
2186
file = msg.file
2287
if file is None:
2388
file = sys.stderr
@@ -129,7 +194,7 @@ def _formatwarnmsg(msg):
129194
return _formatwarnmsg_impl(msg)
130195

131196
def filterwarnings(action, message="", category=Warning, module="", lineno=0,
132-
append=False):
197+
append=False, *, context=None):
133198
"""Insert an entry into the list of warnings filters (at the front).
134199
135200
'action' -- one of "error", "ignore", "always", "all", "default", "module",
@@ -165,9 +230,11 @@ def filterwarnings(action, message="", category=Warning, module="", lineno=0,
165230
else:
166231
module = None
167232

168-
_add_filter(action, message, category, module, lineno, append=append)
233+
_add_filter(action, message, category, module, lineno, append=append,
234+
context=context)
169235

170-
def simplefilter(action, category=Warning, lineno=0, append=False):
236+
def simplefilter(action, category=Warning, lineno=0, append=False, *,
237+
context=None):
171238
"""Insert a simple entry into the list of warnings filters (at the front).
172239
173240
A simple filter matches all modules and messages.
@@ -183,16 +250,20 @@ def simplefilter(action, category=Warning, lineno=0, append=False):
183250
raise TypeError("lineno must be an int")
184251
if lineno < 0:
185252
raise ValueError("lineno must be an int >= 0")
186-
_add_filter(action, None, category, None, lineno, append=append)
253+
_add_filter(action, None, category, None, lineno, append=append,
254+
context=context)
187255

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

194-
def _add_filter(*item, append):
262+
def _add_filter(*item, append, context=None):
195263
with _lock:
264+
if context is None:
265+
context = get_context()
266+
filters = context._filters
196267
if not append:
197268
# Remove possible duplicate filters, so new one will be placed
198269
# in correct place. If append=True and duplicate exists, do nothing.
@@ -206,10 +277,12 @@ def _add_filter(*item, append):
206277
filters.append(item)
207278
_filters_mutated_lock_held()
208279

209-
def resetwarnings():
280+
def resetwarnings(*, context=None):
210281
"""Clear the list of warning filters, so that no filters are active."""
211282
with _lock:
212-
filters[:] = []
283+
if context is None:
284+
context = get_context()
285+
del context._filters[:]
213286
_filters_mutated_lock_held()
214287

215288
class _OptionError(Exception):
@@ -378,7 +451,7 @@ def warn_explicit(message, category, filename, lineno,
378451
if registry.get(key):
379452
return
380453
# Search the filters
381-
for item in filters:
454+
for item in get_context()._filters:
382455
action, msg, cat, mod, ln = item
383456
if ((msg is None or msg.match(text)) and
384457
issubclass(category, cat) and
@@ -499,31 +572,28 @@ def __enter__(self):
499572
raise RuntimeError("Cannot enter %r twice" % self)
500573
self._entered = True
501574
with _lock:
502-
self._filters = self._module.filters
503-
self._module.filters = self._filters[:]
504-
self._module._filters_mutated_lock_held()
575+
self._saved_context, context = self._module._new_context()
505576
self._showwarning = self._module.showwarning
506577
self._showwarnmsg_impl = self._module._showwarnmsg_impl
578+
if self._record:
579+
context.log = log = []
580+
# Reset showwarning() to the default implementation to make sure
581+
# that _showwarnmsg() calls _showwarnmsg_impl()
582+
self._module.showwarning = self._module._showwarning_orig
583+
else:
584+
log = None
507585
if self._filter is not None:
508-
simplefilter(*self._filter)
509-
if self._record:
510-
log = []
511-
self._module._showwarnmsg_impl = log.append
512-
# Reset showwarning() to the default implementation to make sure
513-
# that _showwarnmsg() calls _showwarnmsg_impl()
514-
self._module.showwarning = self._module._showwarning_orig
515-
return log
516-
else:
517-
return None
586+
self._module.simplefilter(*self._filter, context=context)
587+
return log
518588

519589
def __exit__(self, *exc_info):
520590
if not self._entered:
521591
raise RuntimeError("Cannot exit %r without entering first" % self)
522592
with _lock:
523-
self._module.filters = self._filters
524-
self._module._filters_mutated_lock_held()
593+
self._module._warnings_context.set(self._saved_context)
525594
self._module.showwarning = self._showwarning
526595
self._module._showwarnmsg_impl = self._showwarnmsg_impl
596+
self._module._filters_mutated_lock_held()
527597

528598

529599
class deprecated:
@@ -762,3 +832,9 @@ def _filters_mutated_lock_held():
762832
simplefilter("ignore", category=ResourceWarning, append=1)
763833

764834
del _warnings_defaults
835+
836+
#def __getattr__(name):
837+
# if name == "filters":
838+
# warn('Accessing warnings.filters is likely not thread-safe.', DeprecationWarning, stacklevel=2)
839+
# return get_context()._filters
840+
# raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

0 commit comments

Comments
 (0)