Skip to content

Commit 1f8b441

Browse files
committed
wip: warnings local_context
1 parent 1d7ded0 commit 1f8b441

File tree

1 file changed

+182
-9
lines changed

1 file changed

+182
-9
lines changed

Lib/warnings.py

Lines changed: 182 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,113 @@
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+
91+
def get_context():
92+
try:
93+
return _warnings_context.get()
94+
except LookupError:
95+
context = _Context([])
96+
_warnings_context.set(context)
97+
return context
98+
99+
100+
def _set_context(context):
101+
_warnings_context.set(context)
102+
103+
104+
def _new_context():
105+
old_context = get_context()
106+
new_context = old_context.copy()
107+
_set_context(new_context)
108+
return old_context, new_context
109+
110+
10111
def showwarning(message, category, filename, lineno, file=None, line=None):
11112
"""Hook to write a warning to a file; replace if you like."""
12113
msg = WarningMessage(message, category, filename, lineno, file, line)
@@ -18,6 +119,10 @@ def formatwarning(message, category, filename, lineno, line=None):
18119
return _formatwarnmsg_impl(msg)
19120

20121
def _showwarnmsg_impl(msg):
122+
context = get_context()
123+
if context.log is not None:
124+
context._record_warning(msg)
125+
return
21126
file = msg.file
22127
if file is None:
23128
file = sys.stderr
@@ -129,7 +234,7 @@ def _formatwarnmsg(msg):
129234
return _formatwarnmsg_impl(msg)
130235

131236
def filterwarnings(action, message="", category=Warning, module="", lineno=0,
132-
append=False):
237+
append=False, *, context=_global_context):
133238
"""Insert an entry into the list of warnings filters (at the front).
134239
135240
'action' -- one of "error", "ignore", "always", "all", "default", "module",
@@ -165,9 +270,11 @@ def filterwarnings(action, message="", category=Warning, module="", lineno=0,
165270
else:
166271
module = None
167272

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

170-
def simplefilter(action, category=Warning, lineno=0, append=False):
276+
def simplefilter(action, category=Warning, lineno=0, append=False, *,
277+
context=_global_context):
171278
"""Insert a simple entry into the list of warnings filters (at the front).
172279
173280
A simple filter matches all modules and messages.
@@ -183,10 +290,12 @@ def simplefilter(action, category=Warning, lineno=0, append=False):
183290
raise TypeError("lineno must be an int")
184291
if lineno < 0:
185292
raise ValueError("lineno must be an int >= 0")
186-
_add_filter(action, None, category, None, lineno, append=append)
293+
_add_filter(action, None, category, None, lineno, append=append,
294+
context=context)
187295

188-
def _add_filter(*item, append):
296+
def _add_filter(*item, append, context=_global_context):
189297
with _lock:
298+
filters = context._filters
190299
if not append:
191300
# Remove possible duplicate filters, so new one will be placed
192301
# in correct place. If append=True and duplicate exists, do nothing.
@@ -200,10 +309,10 @@ def _add_filter(*item, append):
200309
filters.append(item)
201310
_filters_mutated()
202311

203-
def resetwarnings():
312+
def resetwarnings(*, context=_global_context):
204313
"""Clear the list of warning filters, so that no filters are active."""
205314
with _lock:
206-
filters[:] = []
315+
context._filters[:] = []
207316
_filters_mutated()
208317

209318
class _OptionError(Exception):
@@ -347,7 +456,7 @@ def warn(message, category=None, stacklevel=1, source=None,
347456
warn_explicit(message, category, filename, lineno, module, registry,
348457
globals, source)
349458

350-
def warn_explicit(message, category, filename, lineno,
459+
def _warn_explicit_impl(message, category, filename, lineno,
351460
module=None, registry=None, module_globals=None,
352461
source=None):
353462
lineno = int(lineno)
@@ -371,7 +480,7 @@ def warn_explicit(message, category, filename, lineno,
371480
if registry.get(key):
372481
return
373482
# Search the filters
374-
for item in filters:
483+
for item in _itertools.chain(get_context()._filters, filters):
375484
action, msg, cat, mod, ln = item
376485
if ((msg is None or msg.match(text)) and
377486
issubclass(category, cat) and
@@ -418,6 +527,11 @@ def warn_explicit(message, category, filename, lineno,
418527
_showwarnmsg(msg)
419528

420529

530+
def warn_explicit(*args, **kwargs):
531+
with _lock:
532+
return _warn_explicit_impl(*args, **kwargs)
533+
534+
421535
class WarningMessage(object):
422536

423537
_WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
@@ -518,6 +632,64 @@ def __exit__(self, *exc_info):
518632
self._module._showwarnmsg_impl = self._showwarnmsg_impl
519633

520634

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

0 commit comments

Comments
 (0)