Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8e113b4

Browse files
committed
Close #19403: make contextlib.redirect_stdout reentrant
1 parent 4e641df commit 8e113b4

4 files changed

Lines changed: 100 additions & 55 deletions

File tree

Doc/library/contextlib.rst

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -651,22 +651,33 @@ managers can not only be used in multiple :keyword:`with` statements,
651651
but may also be used *inside* a :keyword:`with` statement that is already
652652
using the same context manager.
653653

654-
:class:`threading.RLock` is an example of a reentrant context manager, as is
655-
:func:`suppress`. Here's a toy example of reentrant use (real world
656-
examples of reentrancy are more likely to occur with objects like recursive
657-
locks and are likely to be far more complicated than this example)::
658-
659-
>>> from contextlib import suppress
660-
>>> ignore_raised_exception = suppress(ZeroDivisionError)
661-
>>> with ignore_raised_exception:
662-
... with ignore_raised_exception:
663-
... 1/0
664-
... print("This line runs")
665-
... 1/0
666-
... print("This is skipped")
654+
:class:`threading.RLock` is an example of a reentrant context manager, as are
655+
:func:`suppress` and :func:`redirect_stdout`. Here's a very simple example of
656+
reentrant use::
657+
658+
>>> from contextlib import redirect_stdout
659+
>>> from io import StringIO
660+
>>> stream = StringIO()
661+
>>> write_to_stream = redirect_stdout(stream)
662+
>>> with write_to_stream:
663+
... print("This is written to the stream rather than stdout")
664+
... with write_to_stream:
665+
... print("This is also written to the stream")
667666
...
668-
This line runs
669-
>>> # The second exception is also suppressed
667+
>>> print("This is written directly to stdout")
668+
This is written directly to stdout
669+
>>> print(stream.getvalue())
670+
This is written to the stream rather than stdout
671+
This is also written to the stream
672+
673+
Real world examples of reentrancy are more likely to involve multiple
674+
functions calling each other and hence be far more complicated than this
675+
example.
676+
677+
Note also that being reentrant is *not* the same thing as being thread safe.
678+
:func:`redirect_stdout`, for example, is definitely not thread safe, as it
679+
makes a global modification to the system state by binding :data:`sys.stdout`
680+
to a different stream.
670681

671682

672683
.. _reusable-cms:
@@ -681,32 +692,58 @@ reusable). These context managers support being used multiple times, but
681692
will fail (or otherwise not work correctly) if the specific context manager
682693
instance has already been used in a containing with statement.
683694

684-
An example of a reusable context manager is :func:`redirect_stdout`::
695+
:class:`threading.Lock` is an example of a reusable, but not reentrant,
696+
context manager (for a reentrant lock, it is necessary to use
697+
:class:`threading.RLock` instead).
685698

686-
>>> from contextlib import redirect_stdout
687-
>>> from io import StringIO
688-
>>> f = StringIO()
689-
>>> collect_output = redirect_stdout(f)
690-
>>> with collect_output:
691-
... print("Collected")
699+
Another example of a reusable, but not reentrant, context manager is
700+
:class:`ExitStack`, as it invokes *all* currently registered callbacks
701+
when leaving any with statement, regardless of where those callbacks
702+
were added::
703+
704+
>>> from contextlib import ExitStack
705+
>>> stack = ExitStack()
706+
>>> with stack:
707+
... stack.callback(print, "Callback: from first context")
708+
... print("Leaving first context")
692709
...
693-
>>> print("Not collected")
694-
Not collected
695-
>>> with collect_output:
696-
... print("Also collected")
710+
Leaving first context
711+
Callback: from first context
712+
>>> with stack:
713+
... stack.callback(print, "Callback: from second context")
714+
... print("Leaving second context")
697715
...
698-
>>> print(f.getvalue())
699-
Collected
700-
Also collected
701-
702-
However, this context manager is not reentrant, so attempting to reuse it
703-
within a containing with statement fails:
704-
705-
>>> with collect_output:
706-
... # Nested reuse is not permitted
707-
... with collect_output:
708-
... pass
716+
Leaving second context
717+
Callback: from second context
718+
>>> with stack:
719+
... stack.callback(print, "Callback: from outer context")
720+
... with stack:
721+
... stack.callback(print, "Callback: from inner context")
722+
... print("Leaving inner context")
723+
... print("Leaving outer context")
709724
...
710-
Traceback (most recent call last):
711-
...
712-
RuntimeError: Cannot reenter <...>
725+
Leaving inner context
726+
Callback: from inner context
727+
Callback: from outer context
728+
Leaving outer context
729+
730+
As the output from the example shows, reusing a single stack object across
731+
multiple with statements works correctly, but attempting to nest them
732+
will cause the stack to be cleared at the end of the innermost with
733+
statement, which is unlikely to be desirable behaviour.
734+
735+
Using separate :class:`ExitStack` instances instead of reusing a single
736+
instance avoids that problem::
737+
738+
>>> from contextlib import ExitStack
739+
>>> with ExitStack() as outer_stack:
740+
... outer_stack.callback(print, "Callback: from outer context")
741+
... with ExitStack() as inner_stack:
742+
... inner_stack.callback(print, "Callback: from inner context")
743+
... print("Leaving inner context")
744+
... print("Leaving outer context")
745+
...
746+
Leaving inner context
747+
Callback: from inner context
748+
Leaving outer context
749+
Callback: from outer context

Lib/contextlib.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,20 +166,16 @@ class redirect_stdout:
166166

167167
def __init__(self, new_target):
168168
self._new_target = new_target
169-
self._old_target = self._sentinel = object()
169+
# We use a list of old targets to make this CM re-entrant
170+
self._old_targets = []
170171

171172
def __enter__(self):
172-
if self._old_target is not self._sentinel:
173-
raise RuntimeError("Cannot reenter {!r}".format(self))
174-
self._old_target = sys.stdout
173+
self._old_targets.append(sys.stdout)
175174
sys.stdout = self._new_target
176175
return self._new_target
177176

178177
def __exit__(self, exctype, excinst, exctb):
179-
restore_stdout = self._old_target
180-
self._old_target = self._sentinel
181-
sys.stdout = restore_stdout
182-
178+
sys.stdout = self._old_targets.pop()
183179

184180

185181
class suppress:

Lib/test/test_contextlib.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -666,11 +666,18 @@ def test_instance_docs(self):
666666
obj = redirect_stdout(None)
667667
self.assertEqual(obj.__doc__, cm_docstring)
668668

669+
def test_no_redirect_in_init(self):
670+
orig_stdout = sys.stdout
671+
redirect_stdout(None)
672+
self.assertIs(sys.stdout, orig_stdout)
673+
669674
def test_redirect_to_string_io(self):
670675
f = io.StringIO()
671676
msg = "Consider an API like help(), which prints directly to stdout"
677+
orig_stdout = sys.stdout
672678
with redirect_stdout(f):
673679
print(msg)
680+
self.assertIs(sys.stdout, orig_stdout)
674681
s = f.getvalue().strip()
675682
self.assertEqual(s, msg)
676683

@@ -682,23 +689,26 @@ def test_enter_result_is_target(self):
682689
def test_cm_is_reusable(self):
683690
f = io.StringIO()
684691
write_to_f = redirect_stdout(f)
692+
orig_stdout = sys.stdout
685693
with write_to_f:
686694
print("Hello", end=" ")
687695
with write_to_f:
688696
print("World!")
697+
self.assertIs(sys.stdout, orig_stdout)
689698
s = f.getvalue()
690699
self.assertEqual(s, "Hello World!\n")
691700

692-
# If this is ever made reentrant, update the reusable-but-not-reentrant
693-
# example at the end of the contextlib docs accordingly.
694-
def test_nested_reentry_fails(self):
701+
def test_cm_is_reentrant(self):
695702
f = io.StringIO()
696703
write_to_f = redirect_stdout(f)
697-
with self.assertRaisesRegex(RuntimeError, "Cannot reenter"):
704+
orig_stdout = sys.stdout
705+
with write_to_f:
706+
print("Hello", end=" ")
698707
with write_to_f:
699-
print("Hello", end=" ")
700-
with write_to_f:
701-
print("World!")
708+
print("World!")
709+
self.assertIs(sys.stdout, orig_stdout)
710+
s = f.getvalue()
711+
self.assertEqual(s, "Hello World!\n")
702712

703713

704714
class TestSuppress(unittest.TestCase):

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Core and Builtins
3131
Library
3232
-------
3333

34+
- Issue #19403: contextlib.redirect_stdout is now reentrant
35+
3436
- Issue #19286: Directories in ``package_data`` are no longer added to
3537
the filelist, preventing failure outlined in the ticket.
3638

0 commit comments

Comments
 (0)