Skip to content

Commit 3952bb0

Browse files
committed
wip: warnings local_context
1 parent b0c3994 commit 3952bb0

File tree

1 file changed

+184
-17
lines changed

1 file changed

+184
-17
lines changed

Lib/warnings.py

Lines changed: 184 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,112 @@
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+
return context
20+
21+
def _record_warning(self, msg):
22+
self.log.append(msg)
23+
24+
def filterwarnings(
25+
self,
26+
action,
27+
message="",
28+
category=Warning,
29+
module="",
30+
lineno=0,
31+
append=False,
32+
):
33+
filterwarnings(
34+
action,
35+
message=message,
36+
category=category,
37+
module=module,
38+
lineno=lineno,
39+
append=append,
40+
context=self,
41+
)
42+
43+
def simplefilter(self, action, category=Warning, lineno=0, append=False):
44+
simplefilter(
45+
action,
46+
category=category,
47+
lineno=lineno,
48+
append=append,
49+
context=self,
50+
)
51+
52+
def resetwarnings(self):
53+
resetwarnings(context=self)
54+
55+
def catch_warnings(
56+
self,
57+
*,
58+
record=False,
59+
action=None,
60+
category=Warning,
61+
lineno=0,
62+
append=False,
63+
):
64+
# For easier backwards compatibility.
65+
return _CatchManager(
66+
record=record,
67+
action=action,
68+
category=category,
69+
lineno=lineno,
70+
append=append,
71+
)
72+
73+
74+
class _GlobalContext(_Context):
75+
def __init__(self):
76+
self.log = None
77+
78+
@property
79+
def _filters(self):
80+
# Since there is quite a lot of code that assigns to
81+
# warnings.filters, this needs to return the current value of
82+
# the module global.
83+
return filters
84+
85+
86+
_global_context = _GlobalContext()
87+
88+
_warnings_context = _contextvars.ContextVar('warnings_context')
89+
90+
def get_context():
91+
try:
92+
return _warnings_context.get()
93+
except LookupError:
94+
context = _Context([])
95+
_warnings_context.set(context)
96+
return context
97+
98+
99+
def _set_context(context):
100+
_warnings_context.set(context)
101+
102+
103+
def _new_context():
104+
old_context = get_context()
105+
new_context = old_context.copy()
106+
_set_context(new_context)
107+
return old_context, new_context
108+
109+
10110
def showwarning(message, category, filename, lineno, file=None, line=None):
11111
"""Hook to write a warning to a file; replace if you like."""
12112
msg = WarningMessage(message, category, filename, lineno, file, line)
@@ -18,6 +118,10 @@ def formatwarning(message, category, filename, lineno, line=None):
18118
return _formatwarnmsg_impl(msg)
19119

20120
def _showwarnmsg_impl(msg):
121+
context = get_context()
122+
if context.log is not None:
123+
context._record_warning(msg)
124+
return
21125
file = msg.file
22126
if file is None:
23127
file = sys.stderr
@@ -129,7 +233,7 @@ def _formatwarnmsg(msg):
129233
return _formatwarnmsg_impl(msg)
130234

131235
def filterwarnings(action, message="", category=Warning, module="", lineno=0,
132-
append=False):
236+
append=False, *, context=_global_context):
133237
"""Insert an entry into the list of warnings filters (at the front).
134238
135239
'action' -- one of "error", "ignore", "always", "all", "default", "module",
@@ -165,9 +269,11 @@ def filterwarnings(action, message="", category=Warning, module="", lineno=0,
165269
else:
166270
module = None
167271

168-
_add_filter(action, message, category, module, lineno, append=append)
272+
_add_filter(action, message, category, module, lineno, append=append,
273+
context=context)
169274

170-
def simplefilter(action, category=Warning, lineno=0, append=False):
275+
def simplefilter(action, category=Warning, lineno=0, append=False, *,
276+
context=_global_context):
171277
"""Insert a simple entry into the list of warnings filters (at the front).
172278
173279
A simple filter matches all modules and messages.
@@ -183,10 +289,12 @@ def simplefilter(action, category=Warning, lineno=0, append=False):
183289
raise TypeError("lineno must be an int")
184290
if lineno < 0:
185291
raise ValueError("lineno must be an int >= 0")
186-
_add_filter(action, None, category, None, lineno, append=append)
292+
_add_filter(action, None, category, None, lineno, append=append,
293+
context=context)
187294

188-
def _add_filter(*item, append):
295+
def _add_filter(*item, append, context=_global_context):
189296
with _lock:
297+
filters = context._filters
190298
if not append:
191299
# Remove possible duplicate filters, so new one will be placed
192300
# in correct place. If append=True and duplicate exists, do nothing.
@@ -200,10 +308,10 @@ def _add_filter(*item, append):
200308
filters.append(item)
201309
_filters_mutated()
202310

203-
def resetwarnings():
311+
def resetwarnings(*, context=_global_context):
204312
"""Clear the list of warning filters, so that no filters are active."""
205313
with _lock:
206-
filters[:] = []
314+
context._filters[:] = []
207315
_filters_mutated()
208316

209317
class _OptionError(Exception):
@@ -371,7 +479,7 @@ def warn_explicit(message, category, filename, lineno,
371479
if registry.get(key):
372480
return
373481
# Search the filters
374-
for item in filters:
482+
for item in _itertools.chain(get_context()._filters, filters):
375483
action, msg, cat, mod, ln = item
376484
if ((msg is None or msg.match(text)) and
377485
issubclass(category, cat) and
@@ -496,17 +604,17 @@ def __enter__(self):
496604
self._module._filters_mutated()
497605
self._showwarning = self._module.showwarning
498606
self._showwarnmsg_impl = self._module._showwarnmsg_impl
607+
if self._record:
608+
log = []
609+
self._module._showwarnmsg_impl = log.append
610+
# Reset showwarning() to the default implementation to make sure
611+
# that _showwarnmsg() calls _showwarnmsg_impl()
612+
self._module.showwarning = self._module._showwarning_orig
613+
else:
614+
log = None
499615
if self._filter is not None:
500616
simplefilter(*self._filter)
501-
if self._record:
502-
log = []
503-
self._module._showwarnmsg_impl = log.append
504-
# Reset showwarning() to the default implementation to make sure
505-
# that _showwarnmsg() calls _showwarnmsg_impl()
506-
self._module.showwarning = self._module._showwarning_orig
507-
return log
508-
else:
509-
return None
617+
return log
510618

511619
def __exit__(self, *exc_info):
512620
if not self._entered:
@@ -518,6 +626,64 @@ def __exit__(self, *exc_info):
518626
self._module._showwarnmsg_impl = self._showwarnmsg_impl
519627

520628

629+
class local_context:
630+
"""A context manager that copies and restores the warnings filter upon
631+
exiting the context. This uses a context variable so that the filter
632+
changes are thread local and work as expected with asynchronous task
633+
switching.
634+
635+
The 'record' argument specifies whether warnings should be captured rather
636+
than being emitted by warnings.showwarning(). When capture is enabled, the
637+
list of warnings is available as get_context().log.
638+
"""
639+
def __init__(self, *, record=False):
640+
self._record = record
641+
self._entered = False
642+
643+
def __enter__(self):
644+
if self._entered:
645+
raise RuntimeError("Cannot enter %r twice" % self)
646+
self._entered = True
647+
self._saved_context, context = _new_context()
648+
if self._record:
649+
context.log = []
650+
_filters_mutated()
651+
return context
652+
653+
def __exit__(self, *exc_info):
654+
if not self._entered:
655+
raise RuntimeError("Cannot exit %r without entering first" % self)
656+
_warnings_context.set(self._saved_context)
657+
_filters_mutated()
658+
659+
660+
class _CatchManager(local_context):
661+
"""Context manager used by get_context().catch_warnings()."""
662+
def __init__(
663+
self,
664+
*,
665+
record=False,
666+
action=None,
667+
category=Warning,
668+
lineno=0,
669+
append=False,
670+
):
671+
super().__init__(record=record)
672+
if action is None:
673+
self._filter = None
674+
else:
675+
self._filter = (action, category, lineno, append)
676+
677+
def __enter__(self):
678+
context = super().__enter__()
679+
if self._filter is not None:
680+
context.simplefilter(*self._filter)
681+
return context.log
682+
683+
def __exit__(self, *exc_info):
684+
context = super().__exit__(*exc_info)
685+
686+
521687
class deprecated:
522688
"""Indicate that a class, function or overload is deprecated.
523689
@@ -704,6 +870,7 @@ def extract():
704870
# - a line number for the line being warning, or 0 to mean any line
705871
# If either if the compiled regexs are None, match anything.
706872
try:
873+
raise ImportError # FIXME: temporary, until _warnings is updated
707874
from _warnings import (filters, _defaultaction, _onceregistry,
708875
warn, warn_explicit, _filters_mutated,
709876
_acquire_lock, _release_lock,

0 commit comments

Comments
 (0)