diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -129,22 +129,23 @@ Functions and classes provided: pass This context manager is :ref:`reentrant `. .. versionadded:: 3.4 .. function:: redirect_stdout(new_target) + redirect_stderr(new_target) - Context manager for temporarily redirecting :data:`sys.stdout` to - another file or file-like object. + Context managers for temporarily redirecting :data:`sys.stdout` or + :data:`sys.stderr` to another file or file-like object. This tool adds flexibility to existing functions or classes whose output - is hardwired to stdout. + is hardwired to stdout or stderr. For example, the output of :func:`help` normally is sent to *sys.stdout*. You can capture that output in a string by redirecting the output to a :class:`io.StringIO` object:: f = io.StringIO() with redirect_stdout(f): help(pow) @@ -166,16 +167,18 @@ Functions and classes provided: context manager is not suitable for use in library code and most threaded applications. It also has no effect on the output of subprocesses. However, it is still a useful approach for many utility scripts. This context manager is :ref:`reusable but not reentrant `. .. versionadded:: 3.4 + .. versionadded:: 3.5 + :func:`~contextlib.redirect_stderr` was added. .. class:: ContextDecorator() A base class that enables a context manager to also be used as a decorator. Context managers inheriting from ``ContextDecorator`` have to implement ``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional exception handling even when used as a decorator. diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -143,16 +143,25 @@ code compileall ---------- * :func:`compileall.compile_dir` and :mod:`compileall`'s command-line interface can now do parallel bytecode compilation. (Contributed by Claudiu Popa in :issue:`16104`). +contextlib +---------- + +* The new :func:`contextlib.redirect_stderr` context manager(similar to + :func:`contextlib.redirect_stdout`) makes it easier for utility scripts to + handle inflexible APIs that write their output to :data:`sys.stderr` and + don't provide any options to redirect it. (Contributed by Berker Peksag in + :issue:`22389`.) + doctest ------- * :func:`doctest.DocTestSuite` returns an empty :class:`unittest.TestSuite` if *module* contains no docstrings instead of raising :exc:`ValueError` (contributed by Glenn Jones in :issue:`15916`). glob diff --git a/Lib/contextlib.py b/Lib/contextlib.py --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -1,16 +1,16 @@ """Utilities for with-statement contexts. See PEP 343.""" import sys from collections import deque from functools import wraps __all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", - "redirect_stdout", "suppress"] + "redirect_stdout", "redirect_stderr", "suppress"] class ContextDecorator(object): "A base class or mixin that enables context managers to work as decorators." def _recreate_cm(self): """Return a recreated instance of self. @@ -146,41 +146,55 @@ class closing(object): """ def __init__(self, thing): self.thing = thing def __enter__(self): return self.thing def __exit__(self, *exc_info): self.thing.close() -class redirect_stdout: - """Context manager for temporarily redirecting stdout to another file + +class _RedirectStream: + + _stream = None + + def __init__(self, new_target): + self._new_target = new_target + # We use a list of old targets to make this CM re-entrant + self._old_targets = [] + + def __enter__(self): + self._old_targets.append(getattr(sys, self._stream)) + setattr(sys, self._stream, self._new_target) + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + setattr(sys, self._stream, self._old_targets.pop()) + + +class redirect_stdout(_RedirectStream): + """Context manager for temporarily redirecting stdout to another file. # How to send help() to stderr with redirect_stdout(sys.stderr): help(dir) # How to write help() to a file with open('help.txt', 'w') as f: with redirect_stdout(f): help(pow) """ - def __init__(self, new_target): - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] + _stream = "stdout" - def __enter__(self): - self._old_targets.append(sys.stdout) - sys.stdout = self._new_target - return self._new_target - def __exit__(self, exctype, excinst, exctb): - sys.stdout = self._old_targets.pop() +class redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + + _stream = "stderr" class suppress: """Context manager to suppress specified exceptions After the exception is suppressed, execution proceeds with the next statement following the with statement. diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -713,70 +713,86 @@ class TestExitStack(unittest.TestCase): class Example(object): pass cm = Example() cm.__exit__ = object() stack = ExitStack() self.assertRaises(AttributeError, stack.enter_context, cm) stack.push(cm) self.assertIs(stack._exit_callbacks[-1], cm) -class TestRedirectStdout(unittest.TestCase): + +class TestRedirectStream: + + redirect_stream = None + orig_stream = None @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings - cm_docstring = redirect_stdout.__doc__ - obj = redirect_stdout(None) + cm_docstring = self.redirect_stream.__doc__ + obj = self.redirect_stream(None) self.assertEqual(obj.__doc__, cm_docstring) def test_no_redirect_in_init(self): - orig_stdout = sys.stdout - redirect_stdout(None) - self.assertIs(sys.stdout, orig_stdout) + orig_stdout = getattr(sys, self.orig_stream) + self.redirect_stream(None) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) def test_redirect_to_string_io(self): f = io.StringIO() msg = "Consider an API like help(), which prints directly to stdout" - orig_stdout = sys.stdout - with redirect_stdout(f): - print(msg) - self.assertIs(sys.stdout, orig_stdout) + orig_stdout = getattr(sys, self.orig_stream) + with self.redirect_stream(f): + print(msg, file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) s = f.getvalue().strip() self.assertEqual(s, msg) def test_enter_result_is_target(self): f = io.StringIO() - with redirect_stdout(f) as enter_result: + with self.redirect_stream(f) as enter_result: self.assertIs(enter_result, f) def test_cm_is_reusable(self): f = io.StringIO() - write_to_f = redirect_stdout(f) - orig_stdout = sys.stdout + write_to_f = self.redirect_stream(f) + orig_stdout = getattr(sys, self.orig_stream) with write_to_f: - print("Hello", end=" ") + print("Hello", end=" ", file=getattr(sys, self.orig_stream)) with write_to_f: - print("World!") - self.assertIs(sys.stdout, orig_stdout) + print("World!", file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) s = f.getvalue() self.assertEqual(s, "Hello World!\n") def test_cm_is_reentrant(self): f = io.StringIO() - write_to_f = redirect_stdout(f) - orig_stdout = sys.stdout + write_to_f = self.redirect_stream(f) + orig_stdout = getattr(sys, self.orig_stream) with write_to_f: - print("Hello", end=" ") + print("Hello", end=" ", file=getattr(sys, self.orig_stream)) with write_to_f: - print("World!") - self.assertIs(sys.stdout, orig_stdout) + print("World!", file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) s = f.getvalue() self.assertEqual(s, "Hello World!\n") +class TestRedirectStdout(TestRedirectStream, unittest.TestCase): + + redirect_stream = redirect_stdout + orig_stream = "stdout" + + +class TestRedirectStderr(TestRedirectStream, unittest.TestCase): + + redirect_stream = redirect_stderr + orig_stream = "stderr" + + class TestSuppress(unittest.TestCase): @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = suppress.__doc__ obj = suppress() self.assertEqual(obj.__doc__, cm_docstring)