Skip to content

Commit 3bac8d1

Browse files
committed
wip: warnings local_context
1 parent e61b40e commit 3bac8d1

File tree

1 file changed

+184
-17
lines changed

1 file changed

+184
-17
lines changed

Lib/warnings.py

+184-17
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_unlocked()
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_unlocked()
208316

209317
class _OptionError(Exception):
@@ -372,7 +480,7 @@ def warn_explicit(message, category, filename, lineno,
372480
if registry.get(key):
373481
return
374482
# Search the filters
375-
for item in filters:
483+
for item in _itertools.chain(get_context()._filters, filters):
376484
action, msg, cat, mod, ln = item
377485
if ((msg is None or msg.match(text)) and
378486
issubclass(category, cat) and
@@ -498,17 +606,17 @@ def __enter__(self):
498606
self._module._filters_mutated_unlocked()
499607
self._showwarning = self._module.showwarning
500608
self._showwarnmsg_impl = self._module._showwarnmsg_impl
609+
if self._record:
610+
log = []
611+
self._module._showwarnmsg_impl = log.append
612+
# Reset showwarning() to the default implementation to make sure
613+
# that _showwarnmsg() calls _showwarnmsg_impl()
614+
self._module.showwarning = self._module._showwarning_orig
615+
else:
616+
log = None
501617
if self._filter is not None:
502618
simplefilter(*self._filter)
503-
if self._record:
504-
log = []
505-
self._module._showwarnmsg_impl = log.append
506-
# Reset showwarning() to the default implementation to make sure
507-
# that _showwarnmsg() calls _showwarnmsg_impl()
508-
self._module.showwarning = self._module._showwarning_orig
509-
return log
510-
else:
511-
return None
619+
return log
512620

513621
def __exit__(self, *exc_info):
514622
if not self._entered:
@@ -520,6 +628,64 @@ def __exit__(self, *exc_info):
520628
self._module._showwarnmsg_impl = self._showwarnmsg_impl
521629

522630

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

0 commit comments

Comments
 (0)