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
1aa094f7
Commit
1aa094f7
authored
Jan 25, 2018
by
Ilya Kulakov
Committed by
Yury Selivanov
Jan 25, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
bpo-29302: Implement contextlib.AsyncExitStack. (#4790)
parent
6ab62920
Changes
6
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
451 additions
and
81 deletions
+451
-81
Doc/library/contextlib.rst
Doc/library/contextlib.rst
+38
-0
Doc/whatsnew/3.7.rst
Doc/whatsnew/3.7.rst
+3
-0
Lib/contextlib.py
Lib/contextlib.py
+205
-46
Lib/test/test_contextlib.py
Lib/test/test_contextlib.py
+38
-34
Lib/test/test_contextlib_async.py
Lib/test/test_contextlib_async.py
+166
-1
Misc/NEWS.d/next/Library/2017-12-11-15-14-55.bpo-29302.Nczj9l.rst
...S.d/next/Library/2017-12-11-15-14-55.bpo-29302.Nczj9l.rst
+1
-0
No files found.
Doc/library/contextlib.rst
View file @
1aa094f7
...
...
@@ -435,6 +435,44 @@ Functions and classes provided:
callbacks registered, the arguments passed in will indicate that no
exception occurred.
.. class:: AsyncExitStack()
An :ref:`asynchronous context manager <async-context-managers>`, similar
to :class:`ExitStack`, that supports combining both synchronous and
asynchronous context managers, as well as having coroutines for
cleanup logic.
The :meth:`close` method is not implemented, :meth:`aclose` must be used
instead.
.. method:: enter_async_context(cm)
Similar to :meth:`enter_context` but expects an asynchronous context
manager.
.. method:: push_async_exit(exit)
Similar to :meth:`push` but expects either an asynchronous context manager
or a coroutine.
.. method:: push_async_callback(callback, *args, **kwds)
Similar to :meth:`callback` but expects a coroutine.
.. method:: aclose()
Similar to :meth:`close` but properly handles awaitables.
Continuing the example for :func:`asynccontextmanager`::
async with AsyncExitStack() as stack:
connections = [await stack.enter_async_context(get_connection())
for i in range(5)]
# All opened connections will automatically be released at the end of
# the async with statement, even if attempts to open a connection
# later in the list raise an exception.
.. versionadded:: 3.7
Examples and Recipes
--------------------
...
...
Doc/whatsnew/3.7.rst
View file @
1aa094f7
...
...
@@ -379,6 +379,9 @@ contextlib
:class:`~contextlib.AbstractAsyncContextManager` have been added. (Contributed
by Jelle Zijlstra in :issue:`29679` and :issue:`30241`.)
:class:`contextlib.AsyncExitStack` has been added. (Contributed by
Alexander Mohr and Ilya Kulakov in :issue:`29302`.)
cProfile
--------
...
...
Lib/contextlib.py
View file @
1aa094f7
This diff is collapsed.
Click to expand it.
Lib/test/test_contextlib.py
View file @
1aa094f7
"""Unit tests for contextlib.py, and other context managers."""
import
asyncio
import
io
import
sys
import
tempfile
...
...
@@ -505,17 +506,18 @@ class TestContextDecorator(unittest.TestCase):
self
.
assertEqual
(
state
,
[
1
,
'something else'
,
999
])
class
TestExitStack
(
unittest
.
TestCase
):
class
TestBaseExitStack
:
exit_stack
=
None
@
support
.
requires_docstrings
def
test_instance_docs
(
self
):
# Issue 19330: ensure context manager instances have good docstrings
cm_docstring
=
ExitS
tack
.
__doc__
obj
=
ExitS
tack
()
cm_docstring
=
self
.
exit_s
tack
.
__doc__
obj
=
self
.
exit_s
tack
()
self
.
assertEqual
(
obj
.
__doc__
,
cm_docstring
)
def
test_no_resources
(
self
):
with
ExitS
tack
():
with
self
.
exit_s
tack
():
pass
def
test_callback
(
self
):
...
...
@@ -531,7 +533,7 @@ class TestExitStack(unittest.TestCase):
def
_exit
(
*
args
,
**
kwds
):
"""Test metadata propagation"""
result
.
append
((
args
,
kwds
))
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
for
args
,
kwds
in
reversed
(
expected
):
if
args
and
kwds
:
f
=
stack
.
callback
(
_exit
,
*
args
,
**
kwds
)
...
...
@@ -543,9 +545,9 @@ class TestExitStack(unittest.TestCase):
f
=
stack
.
callback
(
_exit
)
self
.
assertIs
(
f
,
_exit
)
for
wrapper
in
stack
.
_exit_callbacks
:
self
.
assertIs
(
wrapper
.
__wrapped__
,
_exit
)
self
.
assertNotEqual
(
wrapper
.
__name__
,
_exit
.
__name__
)
self
.
assertIsNone
(
wrapper
.
__doc__
,
_exit
.
__doc__
)
self
.
assertIs
(
wrapper
[
1
]
.
__wrapped__
,
_exit
)
self
.
assertNotEqual
(
wrapper
[
1
]
.
__name__
,
_exit
.
__name__
)
self
.
assertIsNone
(
wrapper
[
1
]
.
__doc__
,
_exit
.
__doc__
)
self
.
assertEqual
(
result
,
expected
)
def
test_push
(
self
):
...
...
@@ -565,21 +567,21 @@ class TestExitStack(unittest.TestCase):
self
.
fail
(
"Should not be called!"
)
def
__exit__
(
self
,
*
exc_details
):
self
.
check_exc
(
*
exc_details
)
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
push
(
_expect_ok
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
],
_expect_ok
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
,
_expect_ok
)
cm
=
ExitCM
(
_expect_ok
)
stack
.
push
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
].
__self__
,
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
.
__self__
,
cm
)
stack
.
push
(
_suppress_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
],
_suppress_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
,
_suppress_exc
)
cm
=
ExitCM
(
_expect_exc
)
stack
.
push
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
].
__self__
,
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
.
__self__
,
cm
)
stack
.
push
(
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
],
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
,
_expect_exc
)
stack
.
push
(
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
],
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
,
_expect_exc
)
1
/
0
def
test_enter_context
(
self
):
...
...
@@ -591,19 +593,19 @@ class TestExitStack(unittest.TestCase):
result
=
[]
cm
=
TestCM
()
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
@
stack
.
callback
# Registered first => cleaned up last
def
_exit
():
result
.
append
(
4
)
self
.
assertIsNotNone
(
_exit
)
stack
.
enter_context
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
].
__self__
,
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
.
__self__
,
cm
)
result
.
append
(
2
)
self
.
assertEqual
(
result
,
[
1
,
2
,
3
,
4
])
def
test_close
(
self
):
result
=
[]
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
@
stack
.
callback
def
_exit
():
result
.
append
(
1
)
...
...
@@ -614,7 +616,7 @@ class TestExitStack(unittest.TestCase):
def
test_pop_all
(
self
):
result
=
[]
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
@
stack
.
callback
def
_exit
():
result
.
append
(
3
)
...
...
@@ -627,12 +629,12 @@ class TestExitStack(unittest.TestCase):
def
test_exit_raise
(
self
):
with
self
.
assertRaises
(
ZeroDivisionError
):
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
push
(
lambda
*
exc
:
False
)
1
/
0
def
test_exit_suppress
(
self
):
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
push
(
lambda
*
exc
:
True
)
1
/
0
...
...
@@ -696,7 +698,7 @@ class TestExitStack(unittest.TestCase):
return
True
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
callback
(
raise_exc
,
IndexError
)
stack
.
callback
(
raise_exc
,
KeyError
)
stack
.
callback
(
raise_exc
,
AttributeError
)
...
...
@@ -724,7 +726,7 @@ class TestExitStack(unittest.TestCase):
return
True
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
callback
(
lambda
:
None
)
stack
.
callback
(
raise_exc
,
IndexError
)
except
Exception
as
exc
:
...
...
@@ -733,7 +735,7 @@ class TestExitStack(unittest.TestCase):
self
.
fail
(
"Expected IndexError, but no exception was raised"
)
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
callback
(
raise_exc
,
KeyError
)
stack
.
push
(
suppress_exc
)
stack
.
callback
(
raise_exc
,
IndexError
)
...
...
@@ -760,7 +762,7 @@ class TestExitStack(unittest.TestCase):
# fix, ExitStack would try to fix it *again* and get into an
# infinite self-referential loop
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
enter_context
(
gets_the_context_right
(
exc4
))
stack
.
enter_context
(
gets_the_context_right
(
exc3
))
stack
.
enter_context
(
gets_the_context_right
(
exc2
))
...
...
@@ -787,7 +789,7 @@ class TestExitStack(unittest.TestCase):
exc4
=
Exception
(
4
)
exc5
=
Exception
(
5
)
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
callback
(
raise_nested
,
exc4
,
exc5
)
stack
.
callback
(
raise_nested
,
exc2
,
exc3
)
raise
exc1
...
...
@@ -801,27 +803,25 @@ class TestExitStack(unittest.TestCase):
self
.
assertIsNone
(
exc
.
__context__
.
__context__
.
__context__
.
__context__
.
__context__
)
def
test_body_exception_suppress
(
self
):
def
suppress_exc
(
*
exc_details
):
return
True
try
:
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
push
(
suppress_exc
)
1
/
0
except
IndexError
as
exc
:
self
.
fail
(
"Expected no exception, got IndexError"
)
def
test_exit_exception_chaining_suppress
(
self
):
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
stack
.
push
(
lambda
*
exc
:
True
)
stack
.
push
(
lambda
*
exc
:
1
/
0
)
stack
.
push
(
lambda
*
exc
:
{}[
1
])
def
test_excessive_nesting
(
self
):
# The original implementation would die with RecursionError here
with
ExitS
tack
()
as
stack
:
with
self
.
exit_s
tack
()
as
stack
:
for
i
in
range
(
10000
):
stack
.
callback
(
int
)
...
...
@@ -829,10 +829,10 @@ class TestExitStack(unittest.TestCase):
class
Example
(
object
):
pass
cm
=
Example
()
cm
.
__exit__
=
object
()
stack
=
ExitS
tack
()
stack
=
self
.
exit_s
tack
()
self
.
assertRaises
(
AttributeError
,
stack
.
enter_context
,
cm
)
stack
.
push
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
],
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
]
[
1
]
,
cm
)
def
test_dont_reraise_RuntimeError
(
self
):
# https://bugs.python.org/issue27122
...
...
@@ -856,7 +856,7 @@ class TestExitStack(unittest.TestCase):
# The UniqueRuntimeError should be caught by second()'s exception
# handler which chain raised a new UniqueException.
with
self
.
assertRaises
(
UniqueException
)
as
err_ctx
:
with
ExitS
tack
()
as
es_ctx
:
with
self
.
exit_s
tack
()
as
es_ctx
:
es_ctx
.
enter_context
(
second
())
es_ctx
.
enter_context
(
first
())
raise
UniqueRuntimeError
(
"please no infinite loop."
)
...
...
@@ -869,6 +869,10 @@ class TestExitStack(unittest.TestCase):
self
.
assertIs
(
exc
.
__cause__
,
exc
.
__context__
)
class
TestExitStack
(
TestBaseExitStack
,
unittest
.
TestCase
):
exit_stack
=
ExitStack
class
TestRedirectStream
:
redirect_stream
=
None
...
...
Lib/test/test_contextlib_async.py
View file @
1aa094f7
import
asyncio
from
contextlib
import
asynccontextmanager
,
AbstractAsyncContextManager
from
contextlib
import
asynccontextmanager
,
AbstractAsyncContextManager
,
AsyncExitStack
import
functools
from
test
import
support
import
unittest
from
.test_contextlib
import
TestBaseExitStack
def
_async_test
(
func
):
"""Decorator to turn an async function into a test case."""
...
...
@@ -255,5 +257,168 @@ class AsyncContextManagerTestCase(unittest.TestCase):
self
.
assertEqual
(
target
,
(
11
,
22
,
33
,
44
))
class
TestAsyncExitStack
(
TestBaseExitStack
,
unittest
.
TestCase
):
class
SyncAsyncExitStack
(
AsyncExitStack
):
@
staticmethod
def
run_coroutine
(
coro
):
loop
=
asyncio
.
get_event_loop
()
f
=
asyncio
.
ensure_future
(
coro
)
f
.
add_done_callback
(
lambda
f
:
loop
.
stop
())
loop
.
run_forever
()
exc
=
f
.
exception
()
if
not
exc
:
return
f
.
result
()
else
:
context
=
exc
.
__context__
try
:
raise
exc
except
:
exc
.
__context__
=
context
raise
exc
def
close
(
self
):
return
self
.
run_coroutine
(
self
.
aclose
())
def
__enter__
(
self
):
return
self
.
run_coroutine
(
self
.
__aenter__
())
def
__exit__
(
self
,
*
exc_details
):
return
self
.
run_coroutine
(
self
.
__aexit__
(
*
exc_details
))
exit_stack
=
SyncAsyncExitStack
def
setUp
(
self
):
self
.
loop
=
asyncio
.
new_event_loop
()
asyncio
.
set_event_loop
(
self
.
loop
)
self
.
addCleanup
(
self
.
loop
.
close
)
@
_async_test
async
def
test_async_callback
(
self
):
expected
=
[
((),
{}),
((
1
,),
{}),
((
1
,
2
),
{}),
((),
dict
(
example
=
1
)),
((
1
,),
dict
(
example
=
1
)),
((
1
,
2
),
dict
(
example
=
1
)),
]
result
=
[]
async
def
_exit
(
*
args
,
**
kwds
):
"""Test metadata propagation"""
result
.
append
((
args
,
kwds
))
async
with
AsyncExitStack
()
as
stack
:
for
args
,
kwds
in
reversed
(
expected
):
if
args
and
kwds
:
f
=
stack
.
push_async_callback
(
_exit
,
*
args
,
**
kwds
)
elif
args
:
f
=
stack
.
push_async_callback
(
_exit
,
*
args
)
elif
kwds
:
f
=
stack
.
push_async_callback
(
_exit
,
**
kwds
)
else
:
f
=
stack
.
push_async_callback
(
_exit
)
self
.
assertIs
(
f
,
_exit
)
for
wrapper
in
stack
.
_exit_callbacks
:
self
.
assertIs
(
wrapper
[
1
].
__wrapped__
,
_exit
)
self
.
assertNotEqual
(
wrapper
[
1
].
__name__
,
_exit
.
__name__
)
self
.
assertIsNone
(
wrapper
[
1
].
__doc__
,
_exit
.
__doc__
)
self
.
assertEqual
(
result
,
expected
)
@
_async_test
async
def
test_async_push
(
self
):
exc_raised
=
ZeroDivisionError
async
def
_expect_exc
(
exc_type
,
exc
,
exc_tb
):
self
.
assertIs
(
exc_type
,
exc_raised
)
async
def
_suppress_exc
(
*
exc_details
):
return
True
async
def
_expect_ok
(
exc_type
,
exc
,
exc_tb
):
self
.
assertIsNone
(
exc_type
)
self
.
assertIsNone
(
exc
)
self
.
assertIsNone
(
exc_tb
)
class
ExitCM
(
object
):
def
__init__
(
self
,
check_exc
):
self
.
check_exc
=
check_exc
async
def
__aenter__
(
self
):
self
.
fail
(
"Should not be called!"
)
async
def
__aexit__
(
self
,
*
exc_details
):
await
self
.
check_exc
(
*
exc_details
)
async
with
self
.
exit_stack
()
as
stack
:
stack
.
push_async_exit
(
_expect_ok
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
],
_expect_ok
)
cm
=
ExitCM
(
_expect_ok
)
stack
.
push_async_exit
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
].
__self__
,
cm
)
stack
.
push_async_exit
(
_suppress_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
],
_suppress_exc
)
cm
=
ExitCM
(
_expect_exc
)
stack
.
push_async_exit
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
].
__self__
,
cm
)
stack
.
push_async_exit
(
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
],
_expect_exc
)
stack
.
push_async_exit
(
_expect_exc
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
],
_expect_exc
)
1
/
0
@
_async_test
async
def
test_async_enter_context
(
self
):
class
TestCM
(
object
):
async
def
__aenter__
(
self
):
result
.
append
(
1
)
async
def
__aexit__
(
self
,
*
exc_details
):
result
.
append
(
3
)
result
=
[]
cm
=
TestCM
()
async
with
AsyncExitStack
()
as
stack
:
@
stack
.
push_async_callback
# Registered first => cleaned up last
async
def
_exit
():
result
.
append
(
4
)
self
.
assertIsNotNone
(
_exit
)
await
stack
.
enter_async_context
(
cm
)
self
.
assertIs
(
stack
.
_exit_callbacks
[
-
1
][
1
].
__self__
,
cm
)
result
.
append
(
2
)
self
.
assertEqual
(
result
,
[
1
,
2
,
3
,
4
])
@
_async_test
async
def
test_async_exit_exception_chaining
(
self
):
# Ensure exception chaining matches the reference behaviour
async
def
raise_exc
(
exc
):
raise
exc
saved_details
=
None
async
def
suppress_exc
(
*
exc_details
):
nonlocal
saved_details
saved_details
=
exc_details
return
True
try
:
async
with
self
.
exit_stack
()
as
stack
:
stack
.
push_async_callback
(
raise_exc
,
IndexError
)
stack
.
push_async_callback
(
raise_exc
,
KeyError
)
stack
.
push_async_callback
(
raise_exc
,
AttributeError
)
stack
.
push_async_exit
(
suppress_exc
)
stack
.
push_async_callback
(
raise_exc
,
ValueError
)
1
/
0
except
IndexError
as
exc
:
self
.
assertIsInstance
(
exc
.
__context__
,
KeyError
)
self
.
assertIsInstance
(
exc
.
__context__
.
__context__
,
AttributeError
)
# Inner exceptions were suppressed
self
.
assertIsNone
(
exc
.
__context__
.
__context__
.
__context__
)
else
:
self
.
fail
(
"Expected IndexError, but no exception was raised"
)
# Check the inner exceptions
inner_exc
=
saved_details
[
1
]
self
.
assertIsInstance
(
inner_exc
,
ValueError
)
self
.
assertIsInstance
(
inner_exc
.
__context__
,
ZeroDivisionError
)
if
__name__
==
'__main__'
:
unittest
.
main
()
Misc/NEWS.d/next/Library/2017-12-11-15-14-55.bpo-29302.Nczj9l.rst
0 → 100644
View file @
1aa094f7
Add contextlib.AsyncExitStack. Patch by Alexander Mohr and Ilya Kulakov.
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