Skip to content

Commit e9ef08b

Browse files
committed
Draft implementation of thread safe warnings module
1 parent 44c8699 commit e9ef08b

File tree

3 files changed

+279
-107
lines changed

3 files changed

+279
-107
lines changed

Diff for: Lib/logging/__init__.py

+23-25
Original file line numberDiff line numberDiff line change
@@ -2315,40 +2315,38 @@ def _at_fork_reinit(self):
23152315

23162316
# Warnings integration
23172317

2318-
_warnings_showwarning = None
2319-
2320-
def _showwarning(message, category, filename, lineno, file=None, line=None):
2318+
def _showwarning(msg):
23212319
"""
23222320
Implementation of showwarnings which redirects to logging, which will first
2323-
check to see if the file parameter is None. If a file is specified, it will
2324-
delegate to the original warnings implementation of showwarning. Otherwise,
2321+
check to see if the file parameter is None. If a file is specified, it
2322+
return False, so the message is delegated back to the caller. Otherwise,
23252323
it will call warnings.formatwarning and will log the resulting string to a
23262324
warnings logger named "py.warnings" with level logging.WARNING.
23272325
"""
2328-
if file is not None:
2329-
if _warnings_showwarning is not None:
2330-
_warnings_showwarning(message, category, filename, lineno, file, line)
2331-
else:
2332-
s = warnings.formatwarning(message, category, filename, lineno, line)
2333-
logger = getLogger("py.warnings")
2334-
if not logger.handlers:
2335-
logger.addHandler(NullHandler())
2336-
# bpo-46557: Log str(s) as msg instead of logger.warning("%s", s)
2337-
# since some log aggregation tools group logs by the msg arg
2338-
logger.warning(str(s))
2326+
if msg.file is not None:
2327+
return False
2328+
2329+
s = warnings._formatwarnmsg_impl(msg)
2330+
logger = getLogger("py.warnings")
2331+
if not logger.handlers:
2332+
logger.addHandler(NullHandler())
2333+
# bpo-46557: Log str(s) as msg instead of logger.warning("%s", s)
2334+
# since some log aggregation tools group logs by the msg arg
2335+
logger.warning(str(s))
2336+
return True
2337+
2338+
class _WarningsCatch(warnings.catch_warnings):
2339+
instance: "_WarningsCatch" = None
2340+
2341+
def log(self, msg):
2342+
return _showwarning(msg)
23392343

23402344
def captureWarnings(capture):
23412345
"""
23422346
If capture is true, redirect all warnings to the logging package.
23432347
If capture is False, ensure that warnings are not redirected to logging
23442348
but to their original destinations.
23452349
"""
2346-
global _warnings_showwarning
2347-
if capture:
2348-
if _warnings_showwarning is None:
2349-
_warnings_showwarning = warnings.showwarning
2350-
warnings.showwarning = _showwarning
2351-
else:
2352-
if _warnings_showwarning is not None:
2353-
warnings.showwarning = _warnings_showwarning
2354-
_warnings_showwarning = None
2350+
if not _WarningsCatch.instance:
2351+
_WarningsCatch.instance = _WarningsCatch(local=False, record=True)
2352+
_WarningsCatch.instance.toggle(capture)

Diff for: Lib/test/test_warnings/__init__.py

+44-43
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ def warnings_state(module):
3939
original_warnings = warning_tests.warnings
4040
original_filters = module.filters
4141
try:
42-
module.filters = original_filters[:]
4342
module.simplefilter("once")
4443
warning_tests.warnings = module
4544
yield
4645
finally:
4746
warning_tests.warnings = original_warnings
48-
module.filters = original_filters
47+
assert module.filters is original_filters
48+
4949

5050

5151
class TestWarning(Warning):
@@ -315,15 +315,18 @@ def test_message_matching(self):
315315
def test_mutate_filter_list(self):
316316
class X:
317317
def match(self, a):
318-
L[:] = []
318+
self.module.get_filters()[:] = []
319319

320+
filter_copy = list(self.module.filters)
320321
L = [("default",X(),UserWarning,X(),0) for i in range(2)]
321322
with original_warnings.catch_warnings(record=True,
322323
module=self.module) as w:
323-
self.module.filters = L
324+
self.module.get_filters()[:] = L
324325
self.module.warn_explicit(UserWarning("b"), None, "f.py", 42)
325326
self.assertEqual(str(w[-1].message), "b")
326327

328+
self.assertEqual(self.module.filters, filter_copy)
329+
327330
def test_filterwarnings_duplicate_filters(self):
328331
with original_warnings.catch_warnings(module=self.module):
329332
self.module.resetwarnings()
@@ -700,7 +703,7 @@ def test_filter(self):
700703
self.module.filterwarnings("error", "", Warning, "", 0)
701704
self.assertRaises(UserWarning, self.module.warn,
702705
'convert to error')
703-
del self.module.filters
706+
self.module.get_filters()[:] = []
704707
self.assertRaises(UserWarning, self.module.warn,
705708
'convert to error')
706709

@@ -776,7 +779,9 @@ def test_showwarning_missing(self):
776779
text = 'del showwarning test'
777780
with original_warnings.catch_warnings(module=self.module):
778781
self.module.filterwarnings("always", category=UserWarning)
779-
del self.module.showwarning
782+
with self.assertRaises(AttributeError):
783+
del self.module.showwarning
784+
self.assertTrue(self.module.showwarning)
780785
with support.captured_output('stderr') as stream:
781786
self.module.warn(text)
782787
result = stream.getvalue()
@@ -788,31 +793,24 @@ def test_showwarnmsg_missing(self):
788793
with original_warnings.catch_warnings(module=self.module):
789794
self.module.filterwarnings("always", category=UserWarning)
790795

791-
show = self.module._showwarnmsg
792-
try:
796+
with self.assertRaises(AttributeError):
793797
del self.module._showwarnmsg
794-
with support.captured_output('stderr') as stream:
795-
self.module.warn(text)
796-
result = stream.getvalue()
797-
finally:
798-
self.module._showwarnmsg = show
799-
self.assertIn(text, result)
798+
self.assertTrue(self.module._showwarnmsg)
800799

801-
def test_showwarning_not_callable(self):
802-
with original_warnings.catch_warnings(module=self.module):
803-
self.module.filterwarnings("always", category=UserWarning)
804-
self.module.showwarning = print
805-
with support.captured_output('stdout'):
806-
self.module.warn('Warning!')
807-
self.module.showwarning = 23
808-
self.assertRaises(TypeError, self.module.warn, "Warning!")
800+
with support.captured_output('stderr') as stream:
801+
self.module.warn(text)
802+
result = stream.getvalue()
803+
804+
self.assertIn(text, result)
809805

810806
def test_show_warning_output(self):
811807
# With showwarning() missing, make sure that output is okay.
812808
text = 'test show_warning'
813809
with original_warnings.catch_warnings(module=self.module):
814810
self.module.filterwarnings("always", category=UserWarning)
815-
del self.module.showwarning
811+
with self.assertRaises(AttributeError):
812+
del self.module.showwarning
813+
self.assertTrue(self.module.showwarning)
816814
with support.captured_output('stderr') as stream:
817815
warning_tests.inner(text)
818816
result = stream.getvalue()
@@ -907,11 +905,11 @@ def test_issue31416(self):
907905
# bad warnings.filters or warnings.defaultaction.
908906
wmod = self.module
909907
with original_warnings.catch_warnings(module=wmod):
910-
wmod.filters = [(None, None, Warning, None, 0)]
908+
wmod.get_filters()[:] = [(None, None, Warning, None, 0)]
911909
with self.assertRaises(TypeError):
912910
wmod.warn_explicit('foo', Warning, 'bar', 1)
913911

914-
wmod.filters = []
912+
wmod.get_filters()[:] = []
915913
with support.swap_attr(wmod, 'defaultaction', None), \
916914
self.assertRaises(TypeError):
917915
wmod.warn_explicit('foo', Warning, 'bar', 1)
@@ -1051,14 +1049,20 @@ def test_catch_warnings_restore(self):
10511049
wmod = self.module
10521050
orig_filters = wmod.filters
10531051
orig_showwarning = wmod.showwarning
1054-
# Ensure both showwarning and filters are restored when recording
1052+
# Ensure both showwarning and filters are not modified
10551053
with wmod.catch_warnings(module=wmod, record=True):
1056-
wmod.filters = wmod.showwarning = object()
1054+
with self.assertRaises(AttributeError):
1055+
wmod.filters = object()
1056+
with self.assertRaises(AttributeError):
1057+
wmod.showwarning = object()
10571058
self.assertIs(wmod.filters, orig_filters)
10581059
self.assertIs(wmod.showwarning, orig_showwarning)
10591060
# Same test, but with recording disabled
10601061
with wmod.catch_warnings(module=wmod, record=False):
1061-
wmod.filters = wmod.showwarning = object()
1062+
with self.assertRaises(AttributeError):
1063+
wmod.filters = object()
1064+
with self.assertRaises(AttributeError):
1065+
wmod.showwarning = object()
10621066
self.assertIs(wmod.filters, orig_filters)
10631067
self.assertIs(wmod.showwarning, orig_showwarning)
10641068

@@ -1104,14 +1108,14 @@ def test_catch_warnings_defaults(self):
11041108
with wmod.catch_warnings(module=wmod) as w:
11051109
self.assertIsNone(w)
11061110
self.assertIs(wmod.showwarning, orig_showwarning)
1107-
self.assertIsNot(wmod.filters, orig_filters)
1111+
self.assertIs(wmod.filters, orig_filters)
11081112
self.assertIs(wmod.filters, orig_filters)
11091113
if wmod is sys.modules['warnings']:
11101114
# Ensure the default module is this one
11111115
with wmod.catch_warnings() as w:
11121116
self.assertIsNone(w)
11131117
self.assertIs(wmod.showwarning, orig_showwarning)
1114-
self.assertIsNot(wmod.filters, orig_filters)
1118+
self.assertIs(wmod.filters, orig_filters)
11151119
self.assertIs(wmod.filters, orig_filters)
11161120

11171121
def test_record_override_showwarning_before(self):
@@ -1125,15 +1129,11 @@ def my_logger(message, category, filename, lineno, file=None, line=None):
11251129
nonlocal my_log
11261130
my_log.append(message)
11271131

1128-
# Override warnings.showwarning() before calling catch_warnings()
1129-
with support.swap_attr(wmod, 'showwarning', my_logger):
1130-
with wmod.catch_warnings(module=wmod, record=True) as log:
1131-
self.assertIsNot(wmod.showwarning, my_logger)
1132-
1133-
wmod.simplefilter("always")
1134-
wmod.warn(text)
1132+
with wmod.catch_warnings(module=wmod, record=True) as log:
1133+
self.assertIsNot(wmod.showwarning, my_logger)
11351134

1136-
self.assertIs(wmod.showwarning, my_logger)
1135+
wmod.simplefilter("always")
1136+
wmod.warn(text)
11371137

11381138
self.assertEqual(len(log), 1, log)
11391139
self.assertEqual(log[0].message.args[0], text)
@@ -1147,17 +1147,17 @@ def test_record_override_showwarning_inside(self):
11471147
my_log = []
11481148

11491149
def my_logger(message, category, filename, lineno, file=None, line=None):
1150-
nonlocal my_log
11511150
my_log.append(message)
11521151

11531152
with wmod.catch_warnings(module=wmod, record=True) as log:
11541153
wmod.simplefilter("always")
1155-
wmod.showwarning = my_logger
1154+
with self.assertRaises(AttributeError):
1155+
wmod.showwarning = my_logger
11561156
wmod.warn(text)
11571157

1158-
self.assertEqual(len(my_log), 1, my_log)
1159-
self.assertEqual(my_log[0].args[0], text)
1160-
self.assertEqual(log, [])
1158+
self.assertEqual(len(log), 1, log)
1159+
self.assertEqual(log[0].message.args[0], text)
1160+
self.assertEqual(my_log, [])
11611161

11621162
def test_check_warnings(self):
11631163
# Explicit tests for the test.support convenience wrapper
@@ -1350,6 +1350,7 @@ def __del__(self):
13501350
warn("test")
13511351
13521352
a=A()
1353+
a = None
13531354
"""
13541355
rc, out, err = assert_python_ok("-c", code)
13551356
self.assertEqual(err.decode().rstrip(),

0 commit comments

Comments
 (0)