Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
cpython
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
cpython
Commits
0901cbc9
Commit
0901cbc9
authored
Nov 03, 2013
by
Nick Coghlan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Close #19403: make contextlib.redirect_stdout reentrant
parent
c4c3c6b1
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
100 additions
and
55 deletions
+100
-55
Doc/library/contextlib.rst
Doc/library/contextlib.rst
+77
-40
Lib/contextlib.py
Lib/contextlib.py
+4
-8
Lib/test/test_contextlib.py
Lib/test/test_contextlib.py
+17
-7
Misc/NEWS
Misc/NEWS
+2
-0
No files found.
Doc/library/contextlib.rst
View file @
0901cbc9
...
@@ -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
Lib/contextlib.py
View file @
0901cbc9
...
@@ -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
:
...
...
Lib/test/test_contextlib.py
View file @
0901cbc9
...
@@ -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
):
...
...
Misc/NEWS
View file @
0901cbc9
...
@@ -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.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment