Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit1aa094f

Browse files
Kentzo1st1
authored andcommitted
bpo-29302: Implement contextlib.AsyncExitStack. (#4790)
1 parent6ab6292 commit1aa094f

File tree

6 files changed

+451
-81
lines changed

6 files changed

+451
-81
lines changed

‎Doc/library/contextlib.rst‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,44 @@ Functions and classes provided:
435435
callbacks registered, the arguments passed in will indicate that no
436436
exception occurred.
437437

438+
..class::AsyncExitStack()
439+
440+
An:ref:`asynchronous context manager<async-context-managers>`, similar
441+
to:class:`ExitStack`, that supports combining both synchronous and
442+
asynchronous context managers, as well as having coroutines for
443+
cleanup logic.
444+
445+
The:meth:`close` method is not implemented,:meth:`aclose` must be used
446+
instead.
447+
448+
..method::enter_async_context(cm)
449+
450+
Similar to:meth:`enter_context` but expects an asynchronous context
451+
manager.
452+
453+
..method::push_async_exit(exit)
454+
455+
Similar to:meth:`push` but expects either an asynchronous context manager
456+
or a coroutine.
457+
458+
..method::push_async_callback(callback, *args, **kwds)
459+
460+
Similar to:meth:`callback` but expects a coroutine.
461+
462+
..method::aclose()
463+
464+
Similar to:meth:`close` but properly handles awaitables.
465+
466+
Continuing the example for:func:`asynccontextmanager`::
467+
468+
async with AsyncExitStack() as stack:
469+
connections = [await stack.enter_async_context(get_connection())
470+
for i in range(5)]
471+
# All opened connections will automatically be released at the end of
472+
# the async with statement, even if attempts to open a connection
473+
# later in the list raise an exception.
474+
475+
..versionadded::3.7
438476

439477
Examples and Recipes
440478
--------------------

‎Doc/whatsnew/3.7.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ contextlib
379379
:class:`~contextlib.AbstractAsyncContextManager` have been added. (Contributed
380380
by Jelle Zijlstra in:issue:`29679` and:issue:`30241`.)
381381

382+
:class:`contextlib.AsyncExitStack` has been added. (Contributed by
383+
Alexander Mohr and Ilya Kulakov in:issue:`29302`.)
384+
382385
cProfile
383386
--------
384387

‎Lib/contextlib.py‎

Lines changed: 205 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
__all__= ["asynccontextmanager","contextmanager","closing","nullcontext",
99
"AbstractContextManager","AbstractAsyncContextManager",
10-
"ContextDecorator","ExitStack",
10+
"AsyncExitStack","ContextDecorator","ExitStack",
1111
"redirect_stdout","redirect_stderr","suppress"]
1212

1313

@@ -365,85 +365,102 @@ def __exit__(self, exctype, excinst, exctb):
365365
returnexctypeisnotNoneandissubclass(exctype,self._exceptions)
366366

367367

368-
# Inspired by discussions on http://bugs.python.org/issue13585
369-
classExitStack(AbstractContextManager):
370-
"""Context manager for dynamic management of a stack of exit callbacks
368+
class_BaseExitStack:
369+
"""A base class for ExitStack and AsyncExitStack."""
371370

372-
For example:
371+
@staticmethod
372+
def_create_exit_wrapper(cm,cm_exit):
373+
def_exit_wrapper(exc_type,exc,tb):
374+
returncm_exit(cm,exc_type,exc,tb)
375+
return_exit_wrapper
373376

374-
with ExitStack() as stack:
375-
files = [stack.enter_context(open(fname)) for fname in filenames]
376-
# All opened files will automatically be closed at the end of
377-
# the with statement, even if attempts to open files later
378-
# in the list raise an exception
377+
@staticmethod
378+
def_create_cb_wrapper(callback,*args,**kwds):
379+
def_exit_wrapper(exc_type,exc,tb):
380+
callback(*args,**kwds)
381+
return_exit_wrapper
379382

380-
"""
381383
def__init__(self):
382384
self._exit_callbacks=deque()
383385

384386
defpop_all(self):
385-
"""Preserve the context stack by transferring it to a new instance"""
387+
"""Preserve the context stack by transferring it to a new instance."""
386388
new_stack=type(self)()
387389
new_stack._exit_callbacks=self._exit_callbacks
388390
self._exit_callbacks=deque()
389391
returnnew_stack
390392

391-
def_push_cm_exit(self,cm,cm_exit):
392-
"""Helper to correctly register callbacks to __exit__ methods"""
393-
def_exit_wrapper(*exc_details):
394-
returncm_exit(cm,*exc_details)
395-
_exit_wrapper.__self__=cm
396-
self.push(_exit_wrapper)
397-
398393
defpush(self,exit):
399-
"""Registers a callback with the standard __exit__ method signature
400-
401-
Can suppress exceptions the same way __exit__ methods can.
394+
"""Registers a callback with the standard __exit__ method signature.
402395
396+
Can suppress exceptions the same way __exit__ method can.
403397
Also accepts any object with an __exit__ method (registering a call
404-
to the method instead of the object itself)
398+
to the method instead of the object itself).
405399
"""
406400
# We use an unbound method rather than a bound method to follow
407-
# the standard lookup behaviour for special methods
401+
# the standard lookup behaviour for special methods.
408402
_cb_type=type(exit)
403+
409404
try:
410405
exit_method=_cb_type.__exit__
411406
exceptAttributeError:
412-
# Not a context manager, so assumeits a callable
413-
self._exit_callbacks.append(exit)
407+
# Not a context manager, so assumeit's a callable.
408+
self._push_exit_callback(exit)
414409
else:
415410
self._push_cm_exit(exit,exit_method)
416-
returnexit# Allow use as a decorator
417-
418-
defcallback(self,callback,*args,**kwds):
419-
"""Registers an arbitrary callback and arguments.
420-
421-
Cannot suppress exceptions.
422-
"""
423-
def_exit_wrapper(exc_type,exc,tb):
424-
callback(*args,**kwds)
425-
# We changed the signature, so using @wraps is not appropriate, but
426-
# setting __wrapped__ may still help with introspection
427-
_exit_wrapper.__wrapped__=callback
428-
self.push(_exit_wrapper)
429-
returncallback# Allow use as a decorator
411+
returnexit# Allow use as a decorator.
430412

431413
defenter_context(self,cm):
432-
"""Enters the supplied context manager
414+
"""Enters the supplied context manager.
433415
434416
If successful, also pushes its __exit__ method as a callback and
435417
returns the result of the __enter__ method.
436418
"""
437-
# We look up the special methods on the type to match the with statement
419+
# We look up the special methods on the type to match the with
420+
# statement.
438421
_cm_type=type(cm)
439422
_exit=_cm_type.__exit__
440423
result=_cm_type.__enter__(cm)
441424
self._push_cm_exit(cm,_exit)
442425
returnresult
443426

444-
defclose(self):
445-
"""Immediately unwind the context stack"""
446-
self.__exit__(None,None,None)
427+
defcallback(self,callback,*args,**kwds):
428+
"""Registers an arbitrary callback and arguments.
429+
430+
Cannot suppress exceptions.
431+
"""
432+
_exit_wrapper=self._create_cb_wrapper(callback,*args,**kwds)
433+
434+
# We changed the signature, so using @wraps is not appropriate, but
435+
# setting __wrapped__ may still help with introspection.
436+
_exit_wrapper.__wrapped__=callback
437+
self._push_exit_callback(_exit_wrapper)
438+
returncallback# Allow use as a decorator
439+
440+
def_push_cm_exit(self,cm,cm_exit):
441+
"""Helper to correctly register callbacks to __exit__ methods."""
442+
_exit_wrapper=self._create_exit_wrapper(cm,cm_exit)
443+
_exit_wrapper.__self__=cm
444+
self._push_exit_callback(_exit_wrapper,True)
445+
446+
def_push_exit_callback(self,callback,is_sync=True):
447+
self._exit_callbacks.append((is_sync,callback))
448+
449+
450+
# Inspired by discussions on http://bugs.python.org/issue13585
451+
classExitStack(_BaseExitStack,AbstractContextManager):
452+
"""Context manager for dynamic management of a stack of exit callbacks.
453+
454+
For example:
455+
with ExitStack() as stack:
456+
files = [stack.enter_context(open(fname)) for fname in filenames]
457+
# All opened files will automatically be closed at the end of
458+
# the with statement, even if attempts to open files later
459+
# in the list raise an exception.
460+
"""
461+
462+
def__enter__(self):
463+
returnself
447464

448465
def__exit__(self,*exc_details):
449466
received_exc=exc_details[0]isnotNone
@@ -470,7 +487,8 @@ def _fix_exception_context(new_exc, old_exc):
470487
suppressed_exc=False
471488
pending_raise=False
472489
whileself._exit_callbacks:
473-
cb=self._exit_callbacks.pop()
490+
is_sync,cb=self._exit_callbacks.pop()
491+
assertis_sync
474492
try:
475493
ifcb(*exc_details):
476494
suppressed_exc=True
@@ -493,6 +511,147 @@ def _fix_exception_context(new_exc, old_exc):
493511
raise
494512
returnreceived_excandsuppressed_exc
495513

514+
defclose(self):
515+
"""Immediately unwind the context stack."""
516+
self.__exit__(None,None,None)
517+
518+
519+
# Inspired by discussions on https://bugs.python.org/issue29302
520+
classAsyncExitStack(_BaseExitStack,AbstractAsyncContextManager):
521+
"""Async context manager for dynamic management of a stack of exit
522+
callbacks.
523+
524+
For example:
525+
async with AsyncExitStack() as stack:
526+
connections = [await stack.enter_async_context(get_connection())
527+
for i in range(5)]
528+
# All opened connections will automatically be released at the
529+
# end of the async with statement, even if attempts to open a
530+
# connection later in the list raise an exception.
531+
"""
532+
533+
@staticmethod
534+
def_create_async_exit_wrapper(cm,cm_exit):
535+
asyncdef_exit_wrapper(exc_type,exc,tb):
536+
returnawaitcm_exit(cm,exc_type,exc,tb)
537+
return_exit_wrapper
538+
539+
@staticmethod
540+
def_create_async_cb_wrapper(callback,*args,**kwds):
541+
asyncdef_exit_wrapper(exc_type,exc,tb):
542+
awaitcallback(*args,**kwds)
543+
return_exit_wrapper
544+
545+
asyncdefenter_async_context(self,cm):
546+
"""Enters the supplied async context manager.
547+
548+
If successful, also pushes its __aexit__ method as a callback and
549+
returns the result of the __aenter__ method.
550+
"""
551+
_cm_type=type(cm)
552+
_exit=_cm_type.__aexit__
553+
result=await_cm_type.__aenter__(cm)
554+
self._push_async_cm_exit(cm,_exit)
555+
returnresult
556+
557+
defpush_async_exit(self,exit):
558+
"""Registers a coroutine function with the standard __aexit__ method
559+
signature.
560+
561+
Can suppress exceptions the same way __aexit__ method can.
562+
Also accepts any object with an __aexit__ method (registering a call
563+
to the method instead of the object itself).
564+
"""
565+
_cb_type=type(exit)
566+
try:
567+
exit_method=_cb_type.__aexit__
568+
exceptAttributeError:
569+
# Not an async context manager, so assume it's a coroutine function
570+
self._push_exit_callback(exit,False)
571+
else:
572+
self._push_async_cm_exit(exit,exit_method)
573+
returnexit# Allow use as a decorator
574+
575+
defpush_async_callback(self,callback,*args,**kwds):
576+
"""Registers an arbitrary coroutine function and arguments.
577+
578+
Cannot suppress exceptions.
579+
"""
580+
_exit_wrapper=self._create_async_cb_wrapper(callback,*args,**kwds)
581+
582+
# We changed the signature, so using @wraps is not appropriate, but
583+
# setting __wrapped__ may still help with introspection.
584+
_exit_wrapper.__wrapped__=callback
585+
self._push_exit_callback(_exit_wrapper,False)
586+
returncallback# Allow use as a decorator
587+
588+
asyncdefaclose(self):
589+
"""Immediately unwind the context stack."""
590+
awaitself.__aexit__(None,None,None)
591+
592+
def_push_async_cm_exit(self,cm,cm_exit):
593+
"""Helper to correctly register coroutine function to __aexit__
594+
method."""
595+
_exit_wrapper=self._create_async_exit_wrapper(cm,cm_exit)
596+
_exit_wrapper.__self__=cm
597+
self._push_exit_callback(_exit_wrapper,False)
598+
599+
asyncdef__aenter__(self):
600+
returnself
601+
602+
asyncdef__aexit__(self,*exc_details):
603+
received_exc=exc_details[0]isnotNone
604+
605+
# We manipulate the exception state so it behaves as though
606+
# we were actually nesting multiple with statements
607+
frame_exc=sys.exc_info()[1]
608+
def_fix_exception_context(new_exc,old_exc):
609+
# Context may not be correct, so find the end of the chain
610+
while1:
611+
exc_context=new_exc.__context__
612+
ifexc_contextisold_exc:
613+
# Context is already set correctly (see issue 20317)
614+
return
615+
ifexc_contextisNoneorexc_contextisframe_exc:
616+
break
617+
new_exc=exc_context
618+
# Change the end of the chain to point to the exception
619+
# we expect it to reference
620+
new_exc.__context__=old_exc
621+
622+
# Callbacks are invoked in LIFO order to match the behaviour of
623+
# nested context managers
624+
suppressed_exc=False
625+
pending_raise=False
626+
whileself._exit_callbacks:
627+
is_sync,cb=self._exit_callbacks.pop()
628+
try:
629+
ifis_sync:
630+
cb_suppress=cb(*exc_details)
631+
else:
632+
cb_suppress=awaitcb(*exc_details)
633+
634+
ifcb_suppress:
635+
suppressed_exc=True
636+
pending_raise=False
637+
exc_details= (None,None,None)
638+
except:
639+
new_exc_details=sys.exc_info()
640+
# simulate the stack of exceptions by setting the context
641+
_fix_exception_context(new_exc_details[1],exc_details[1])
642+
pending_raise=True
643+
exc_details=new_exc_details
644+
ifpending_raise:
645+
try:
646+
# bare "raise exc_details[1]" replaces our carefully
647+
# set-up context
648+
fixed_ctx=exc_details[1].__context__
649+
raiseexc_details[1]
650+
exceptBaseException:
651+
exc_details[1].__context__=fixed_ctx
652+
raise
653+
returnreceived_excandsuppressed_exc
654+
496655

497656
classnullcontext(AbstractContextManager):
498657
"""Context manager that does no additional processing.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp