Commit 0901cbc9 authored by Nick Coghlan's avatar Nick Coghlan

Close #19403: make contextlib.redirect_stdout reentrant

parent c4c3c6b1
...@@ -651,22 +651,33 @@ managers can not only be used in multiple :keyword:`with` statements, ...@@ -651,22 +651,33 @@ managers can not only be used in multiple :keyword:`with` statements,
but may also be used *inside* a :keyword:`with` statement that is already but may also be used *inside* a :keyword:`with` statement that is already
using the same context manager. using the same context manager.
:class:`threading.RLock` is an example of a reentrant context manager, as is :class:`threading.RLock` is an example of a reentrant context manager, as are
:func:`suppress`. Here's a toy example of reentrant use (real world :func:`suppress` and :func:`redirect_stdout`. Here's a very simple example of
examples of reentrancy are more likely to occur with objects like recursive reentrant use::
locks and are likely to be far more complicated than this example)::
>>> from contextlib import redirect_stdout
>>> from contextlib import suppress >>> from io import StringIO
>>> ignore_raised_exception = suppress(ZeroDivisionError) >>> stream = StringIO()
>>> with ignore_raised_exception: >>> write_to_stream = redirect_stdout(stream)
... with ignore_raised_exception: >>> with write_to_stream:
... 1/0 ... print("This is written to the stream rather than stdout")
... print("This line runs") ... with write_to_stream:
... 1/0 ... print("This is also written to the stream")
... print("This is skipped")
... ...
This line runs >>> print("This is written directly to stdout")
>>> # The second exception is also suppressed This is written directly to stdout
>>> print(stream.getvalue())
This is written to the stream rather than stdout
This is also written to the stream
Real world examples of reentrancy are more likely to involve multiple
functions calling each other and hence be far more complicated than this
example.
Note also that being reentrant is *not* the same thing as being thread safe.
:func:`redirect_stdout`, for example, is definitely not thread safe, as it
makes a global modification to the system state by binding :data:`sys.stdout`
to a different stream.
.. _reusable-cms: .. _reusable-cms:
...@@ -681,32 +692,58 @@ reusable). These context managers support being used multiple times, but ...@@ -681,32 +692,58 @@ reusable). These context managers support being used multiple times, but
will fail (or otherwise not work correctly) if the specific context manager will fail (or otherwise not work correctly) if the specific context manager
instance has already been used in a containing with statement. instance has already been used in a containing with statement.
An example of a reusable context manager is :func:`redirect_stdout`:: :class:`threading.Lock` is an example of a reusable, but not reentrant,
context manager (for a reentrant lock, it is necessary to use
:class:`threading.RLock` instead).
>>> from contextlib import redirect_stdout Another example of a reusable, but not reentrant, context manager is
>>> from io import StringIO :class:`ExitStack`, as it invokes *all* currently registered callbacks
>>> f = StringIO() when leaving any with statement, regardless of where those callbacks
>>> collect_output = redirect_stdout(f) were added::
>>> with collect_output:
... print("Collected") >>> from contextlib import ExitStack
>>> stack = ExitStack()
>>> with stack:
... stack.callback(print, "Callback: from first context")
... print("Leaving first context")
... ...
>>> print("Not collected") Leaving first context
Not collected Callback: from first context
>>> with collect_output: >>> with stack:
... print("Also collected") ... stack.callback(print, "Callback: from second context")
... print("Leaving second context")
... ...
>>> print(f.getvalue()) Leaving second context
Collected Callback: from second context
Also collected >>> with stack:
... stack.callback(print, "Callback: from outer context")
However, this context manager is not reentrant, so attempting to reuse it ... with stack:
within a containing with statement fails: ... stack.callback(print, "Callback: from inner context")
... print("Leaving inner context")
>>> with collect_output: ... print("Leaving outer context")
... # Nested reuse is not permitted
... with collect_output:
... pass
... ...
Traceback (most recent call last): Leaving inner context
... Callback: from inner context
RuntimeError: Cannot reenter <...> Callback: from outer context
Leaving outer context
As the output from the example shows, reusing a single stack object across
multiple with statements works correctly, but attempting to nest them
will cause the stack to be cleared at the end of the innermost with
statement, which is unlikely to be desirable behaviour.
Using separate :class:`ExitStack` instances instead of reusing a single
instance avoids that problem::
>>> from contextlib import ExitStack
>>> with ExitStack() as outer_stack:
... outer_stack.callback(print, "Callback: from outer context")
... with ExitStack() as inner_stack:
... inner_stack.callback(print, "Callback: from inner context")
... print("Leaving inner context")
... print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Leaving outer context
Callback: from outer context
...@@ -166,20 +166,16 @@ class redirect_stdout: ...@@ -166,20 +166,16 @@ class redirect_stdout:
def __init__(self, new_target): def __init__(self, new_target):
self._new_target = new_target self._new_target = new_target
self._old_target = self._sentinel = object() # We use a list of old targets to make this CM re-entrant
self._old_targets = []
def __enter__(self): def __enter__(self):
if self._old_target is not self._sentinel: self._old_targets.append(sys.stdout)
raise RuntimeError("Cannot reenter {!r}".format(self))
self._old_target = sys.stdout
sys.stdout = self._new_target sys.stdout = self._new_target
return self._new_target return self._new_target
def __exit__(self, exctype, excinst, exctb): def __exit__(self, exctype, excinst, exctb):
restore_stdout = self._old_target sys.stdout = self._old_targets.pop()
self._old_target = self._sentinel
sys.stdout = restore_stdout
class suppress: class suppress:
......
...@@ -666,11 +666,18 @@ class TestRedirectStdout(unittest.TestCase): ...@@ -666,11 +666,18 @@ class TestRedirectStdout(unittest.TestCase):
obj = redirect_stdout(None) obj = redirect_stdout(None)
self.assertEqual(obj.__doc__, cm_docstring) 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)
def test_redirect_to_string_io(self): def test_redirect_to_string_io(self):
f = io.StringIO() f = io.StringIO()
msg = "Consider an API like help(), which prints directly to stdout" msg = "Consider an API like help(), which prints directly to stdout"
orig_stdout = sys.stdout
with redirect_stdout(f): with redirect_stdout(f):
print(msg) print(msg)
self.assertIs(sys.stdout, orig_stdout)
s = f.getvalue().strip() s = f.getvalue().strip()
self.assertEqual(s, msg) self.assertEqual(s, msg)
...@@ -682,23 +689,26 @@ class TestRedirectStdout(unittest.TestCase): ...@@ -682,23 +689,26 @@ class TestRedirectStdout(unittest.TestCase):
def test_cm_is_reusable(self): def test_cm_is_reusable(self):
f = io.StringIO() f = io.StringIO()
write_to_f = redirect_stdout(f) write_to_f = redirect_stdout(f)
orig_stdout = sys.stdout
with write_to_f: with write_to_f:
print("Hello", end=" ") print("Hello", end=" ")
with write_to_f: with write_to_f:
print("World!") print("World!")
self.assertIs(sys.stdout, orig_stdout)
s = f.getvalue() s = f.getvalue()
self.assertEqual(s, "Hello World!\n") self.assertEqual(s, "Hello World!\n")
# If this is ever made reentrant, update the reusable-but-not-reentrant def test_cm_is_reentrant(self):
# example at the end of the contextlib docs accordingly.
def test_nested_reentry_fails(self):
f = io.StringIO() f = io.StringIO()
write_to_f = redirect_stdout(f) write_to_f = redirect_stdout(f)
with self.assertRaisesRegex(RuntimeError, "Cannot reenter"): orig_stdout = sys.stdout
with write_to_f:
print("Hello", end=" ")
with write_to_f: with write_to_f:
print("Hello", end=" ") print("World!")
with write_to_f: self.assertIs(sys.stdout, orig_stdout)
print("World!") s = f.getvalue()
self.assertEqual(s, "Hello World!\n")
class TestSuppress(unittest.TestCase): class TestSuppress(unittest.TestCase):
......
...@@ -31,6 +31,8 @@ Core and Builtins ...@@ -31,6 +31,8 @@ Core and Builtins
Library Library
------- -------
- Issue #19403: contextlib.redirect_stdout is now reentrant
- Issue #19286: Directories in ``package_data`` are no longer added to - Issue #19286: Directories in ``package_data`` are no longer added to
the filelist, preventing failure outlined in the ticket. the filelist, preventing failure outlined in the ticket.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment