Skip to content

Commit bb44fe0

Browse files
committed
Issue python#22389: Add contextlib.redirect_stderr().
1 parent ae553eb commit bb44fe0

5 files changed

Lines changed: 85 additions & 34 deletions

File tree

‎Doc/library/contextlib.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ Functions and classes provided:
172172
.. versionadded:: 3.4
173173

174174

175+
.. function:: redirect_stderr(new_target)
176+
177+
Similar to :func:`~contextlib.redirect_stdout` but redirecting
178+
:data:`sys.stderr` to another file or file-like object.
179+
180+
This context manager is :ref:`reentrant <reentrant-cms>`.
181+
182+
.. versionadded:: 3.5
183+
184+
175185
.. class:: ContextDecorator()
176186

177187
A base class that enables a context manager to also be used as a decorator.

‎Doc/whatsnew/3.5.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ compileall
148148
can now do parallel bytecode compilation.
149149
(Contributed by Claudiu Popa in :issue:`16104`.)
150150

151+
contextlib
152+
----------
153+
154+
* The new :func:`contextlib.redirect_stderr` context manager(similar to
155+
:func:`contextlib.redirect_stdout`) makes it easier for utility scripts to
156+
handle inflexible APIs that write their output to :data:`sys.stderr` and
157+
don't provide any options to redirect it.
158+
(Contributed by Berker Peksag in :issue:`22389`.)
159+
151160
doctest
152161
-------
153162

‎Lib/contextlib.py‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from functools import wraps
66

77
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
8-
"redirect_stdout", "suppress"]
8+
"redirect_stdout", "redirect_stderr", "suppress"]
99

1010

1111
class ContextDecorator(object):
@@ -151,8 +151,27 @@ def __enter__(self):
151151
def __exit__(self, *exc_info):
152152
self.thing.close()
153153

154-
class redirect_stdout:
155-
"""Context manager for temporarily redirecting stdout to another file
154+
155+
class _RedirectStream:
156+
157+
_stream = None
158+
159+
def __init__(self, new_target):
160+
self._new_target = new_target
161+
# We use a list of old targets to make this CM re-entrant
162+
self._old_targets = []
163+
164+
def __enter__(self):
165+
self._old_targets.append(getattr(sys, self._stream))
166+
setattr(sys, self._stream, self._new_target)
167+
return self._new_target
168+
169+
def __exit__(self, exctype, excinst, exctb):
170+
setattr(sys, self._stream, self._old_targets.pop())
171+
172+
173+
class redirect_stdout(_RedirectStream):
174+
"""Context manager for temporarily redirecting stdout to another file.
156175
157176
# How to send help() to stderr
158177
with redirect_stdout(sys.stderr):
@@ -164,18 +183,13 @@ class redirect_stdout:
164183
help(pow)
165184
"""
166185

167-
def __init__(self, new_target):
168-
self._new_target = new_target
169-
# We use a list of old targets to make this CM re-entrant
170-
self._old_targets = []
186+
_stream = "stdout"
171187

172-
def __enter__(self):
173-
self._old_targets.append(sys.stdout)
174-
sys.stdout = self._new_target
175-
return self._new_target
176188

177-
def __exit__(self, exctype, excinst, exctb):
178-
sys.stdout = self._old_targets.pop()
189+
class redirect_stderr(_RedirectStream):
190+
"""Context manager for temporarily redirecting stderr to another file."""
191+
192+
_stream = "stderr"
179193

180194

181195
class suppress:

‎Lib/test/test_contextlib.py‎

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -718,60 +718,76 @@ class Example(object): pass
718718
stack.push(cm)
719719
self.assertIs(stack._exit_callbacks[-1], cm)
720720

721-
class TestRedirectStdout(unittest.TestCase):
721+
722+
class TestRedirectStream:
723+
724+
redirect_stream = None
725+
orig_stream = None
722726

723727
@support.requires_docstrings
724728
def test_instance_docs(self):
725729
# Issue 19330: ensure context manager instances have good docstrings
726-
cm_docstring = redirect_stdout.__doc__
727-
obj = redirect_stdout(None)
730+
cm_docstring = self.redirect_stream.__doc__
731+
obj = self.redirect_stream(None)
728732
self.assertEqual(obj.__doc__, cm_docstring)
729733

730734
def test_no_redirect_in_init(self):
731-
orig_stdout = sys.stdout
732-
redirect_stdout(None)
733-
self.assertIs(sys.stdout, orig_stdout)
735+
orig_stdout = getattr(sys, self.orig_stream)
736+
self.redirect_stream(None)
737+
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
734738

735739
def test_redirect_to_string_io(self):
736740
f = io.StringIO()
737741
msg = "Consider an API like help(), which prints directly to stdout"
738-
orig_stdout = sys.stdout
739-
with redirect_stdout(f):
740-
print(msg)
741-
self.assertIs(sys.stdout, orig_stdout)
742+
orig_stdout = getattr(sys, self.orig_stream)
743+
with self.redirect_stream(f):
744+
print(msg, file=getattr(sys, self.orig_stream))
745+
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
742746
s = f.getvalue().strip()
743747
self.assertEqual(s, msg)
744748

745749
def test_enter_result_is_target(self):
746750
f = io.StringIO()
747-
with redirect_stdout(f) as enter_result:
751+
with self.redirect_stream(f) as enter_result:
748752
self.assertIs(enter_result, f)
749753

750754
def test_cm_is_reusable(self):
751755
f = io.StringIO()
752-
write_to_f = redirect_stdout(f)
753-
orig_stdout = sys.stdout
756+
write_to_f = self.redirect_stream(f)
757+
orig_stdout = getattr(sys, self.orig_stream)
754758
with write_to_f:
755-
print("Hello", end=" ")
759+
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
756760
with write_to_f:
757-
print("World!")
758-
self.assertIs(sys.stdout, orig_stdout)
761+
print("World!", file=getattr(sys, self.orig_stream))
762+
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
759763
s = f.getvalue()
760764
self.assertEqual(s, "Hello World!\n")
761765

762766
def test_cm_is_reentrant(self):
763767
f = io.StringIO()
764-
write_to_f = redirect_stdout(f)
765-
orig_stdout = sys.stdout
768+
write_to_f = self.redirect_stream(f)
769+
orig_stdout = getattr(sys, self.orig_stream)
766770
with write_to_f:
767-
print("Hello", end=" ")
771+
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
768772
with write_to_f:
769-
print("World!")
770-
self.assertIs(sys.stdout, orig_stdout)
773+
print("World!", file=getattr(sys, self.orig_stream))
774+
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
771775
s = f.getvalue()
772776
self.assertEqual(s, "Hello World!\n")
773777

774778

779+
class TestRedirectStdout(TestRedirectStream, unittest.TestCase):
780+
781+
redirect_stream = redirect_stdout
782+
orig_stream = "stdout"
783+
784+
785+
class TestRedirectStderr(TestRedirectStream, unittest.TestCase):
786+
787+
redirect_stream = redirect_stderr
788+
orig_stream = "stderr"
789+
790+
775791
class TestSuppress(unittest.TestCase):
776792

777793
@support.requires_docstrings

‎Misc/NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ Core and Builtins
191191
Library
192192
-------
193193

194+
- Issue #22389: Add contextlib.redirect_stderr().
195+
194196
- Issue #21356: Make ssl.RAND_egd() optional to support LibreSSL. The
195197
availability of the function is checked during the compilation. Patch written
196198
by Bernard Spil.

0 commit comments

Comments
 (0)