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

gh-144386: Add support for descriptors in ExitStack and AsyncExitStack#144420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletionsDoc/library/contextlib.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -564,6 +564,10 @@ Functions and classes provided:
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
is not a context manager.

.. versionchanged:: next
Added support for arbitrary descriptors :meth:`!__enter__` and
:meth:`!__exit__`.

.. method:: push(exit)

Adds a context manager's :meth:`~object.__exit__` method to the callback stack.
Expand All@@ -582,6 +586,9 @@ Functions and classes provided:
The passed in object is returned from the function, allowing this
method to be used as a function decorator.

.. versionchanged:: next
Added support for arbitrary descriptors :meth:`!__exit__`.

.. method:: callback(callback, /, *args, **kwds)

Accepts an arbitrary callback function and arguments and adds it to
Expand DownExpand Up@@ -639,11 +646,17 @@ Functions and classes provided:
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
is not an asynchronous context manager.

.. versionchanged:: next
Added support for arbitrary descriptors :meth:`!__aenter__` and :meth:`!__aexit__`.

.. method:: push_async_exit(exit)

Similar to :meth:`ExitStack.push` but expects either an asynchronous context manager
or a coroutine function.

.. versionchanged:: next
Added support for arbitrary descriptors :meth:`!__aexit__`.

.. method:: push_async_callback(callback, /, *args, **kwds)

Similar to :meth:`ExitStack.callback` but expects a coroutine function.
Expand Down
10 changes: 10 additions & 0 deletionsDoc/whatsnew/3.15.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -548,6 +548,16 @@ concurrent.futures
(Contributed by Jonathan Berg in:gh:`139486`.)


contextlib
----------

* Added support for arbitrary descriptors:meth:`!__enter__`,
:meth:`!__exit__`,:meth:`!__aenter__`, and:meth:`!__aexit__` in
:class:`~contextlib.ExitStack` and:class:`contextlib.AsyncExitStack`, for
consistency with the:keyword:`with` and:keyword:`async with` statements.
(Contributed by Serhiy Storchaka in:gh:`144386`.)


dataclasses
-----------

Expand Down
99 changes: 41 additions & 58 deletionsLib/contextlib.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@
import_collections_abc
fromcollectionsimportdeque
fromfunctoolsimportwraps
fromtypesimportMethodType,GenericAlias
fromtypesimportGenericAlias

__all__= ["asynccontextmanager","contextmanager","closing","nullcontext",
"AbstractContextManager","AbstractAsyncContextManager",
Expand DownExpand Up@@ -469,13 +469,23 @@ def __exit__(self, exctype, excinst, exctb):
returnFalse


def_lookup_special(obj,name,default):
# Follow the standard lookup behaviour for special methods.
frominspectimportgetattr_static,_descriptor_get
cls=type(obj)
try:
descr=getattr_static(cls,name)
exceptAttributeError:
returndefault
return_descriptor_get(descr,obj)


_sentinel= ['SENTINEL']


class_BaseExitStack:
"""A base class for ExitStack and AsyncExitStack."""

@staticmethod
def_create_exit_wrapper(cm,cm_exit):
returnMethodType(cm_exit,cm)

@staticmethod
def_create_cb_wrapper(callback,/,*args,**kwds):
def_exit_wrapper(exc_type,exc,tb):
Expand All@@ -499,17 +509,8 @@ def push(self, exit):
Also accepts any object with an __exit__ method (registering a call
to the method instead of the object itself).
"""
# We use an unbound method rather than a bound method to follow
# the standard lookup behaviour for special methods.
_cb_type=type(exit)

try:
exit_method=_cb_type.__exit__
exceptAttributeError:
# Not a context manager, so assume it's a callable.
self._push_exit_callback(exit)
else:
self._push_cm_exit(exit,exit_method)
exit_method=_lookup_special(exit,'__exit__',exit)
self._push_exit_callback(exit_method)
returnexit# Allow use as a decorator.

defenter_context(self,cm):
Expand All@@ -518,17 +519,18 @@ def enter_context(self, cm):
If successful, also pushes its __exit__ method as a callback and
returns the result of the __enter__ method.
"""
# We look up the special methods on the type to match the with
# statement.
cls=type(cm)
try:
_enter=cls.__enter__
_exit=cls.__exit__
exceptAttributeError:
_enter=_lookup_special(cm,'__enter__',_sentinel)
if_enteris_sentinel:
cls=type(cm)
raiseTypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol")fromNone
result=_enter(cm)
self._push_cm_exit(cm,_exit)
f"not support the context manager protocol")
_exit=_lookup_special(cm,'__exit__',_sentinel)
if_exitis_sentinel:
cls=type(cm)
raiseTypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol")
result=_enter()
self._push_exit_callback(_exit)
returnresult

defcallback(self,callback,/,*args,**kwds):
Expand All@@ -544,11 +546,6 @@ def callback(self, callback, /, *args, **kwds):
self._push_exit_callback(_exit_wrapper)
returncallback# Allow use as a decorator

def_push_cm_exit(self,cm,cm_exit):
"""Helper to correctly register callbacks to __exit__ methods."""
_exit_wrapper=self._create_exit_wrapper(cm,cm_exit)
self._push_exit_callback(_exit_wrapper,True)

def_push_exit_callback(self,callback,is_sync=True):
self._exit_callbacks.append((is_sync,callback))

Expand DownExpand Up@@ -641,10 +638,6 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager):
# connection later in the list raise an exception.
"""

@staticmethod
def_create_async_exit_wrapper(cm,cm_exit):
returnMethodType(cm_exit,cm)

@staticmethod
def_create_async_cb_wrapper(callback,/,*args,**kwds):
asyncdef_exit_wrapper(exc_type,exc,tb):
Expand All@@ -657,16 +650,18 @@ async def enter_async_context(self, cm):
If successful, also pushes its __aexit__ method as a callback and
returns the result of the __aenter__ method.
"""
cls=type(cm)
try:
_enter=cls.__aenter__
_exit=cls.__aexit__
exceptAttributeError:
_enter=_lookup_special(cm,'__aenter__',_sentinel)
if_enteris_sentinel:
cls=type(cm)
raiseTypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the asynchronous context manager protocol"
)fromNone
result=await_enter(cm)
self._push_async_cm_exit(cm,_exit)
f"not support the asynchronous context manager protocol")
_exit=_lookup_special(cm,'__aexit__',_sentinel)
if_exitis_sentinel:
cls=type(cm)
raiseTypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the asynchronous context manager protocol")
result=await_enter()
self._push_exit_callback(_exit,False)
returnresult

defpush_async_exit(self,exit):
Expand All@@ -677,14 +672,8 @@ def push_async_exit(self, exit):
Also accepts any object with an __aexit__ method (registering a call
to the method instead of the object itself).
"""
_cb_type=type(exit)
try:
exit_method=_cb_type.__aexit__
exceptAttributeError:
# Not an async context manager, so assume it's a coroutine function
self._push_exit_callback(exit,False)
else:
self._push_async_cm_exit(exit,exit_method)
exit_method=_lookup_special(exit,'__aexit__',exit)
self._push_exit_callback(exit_method,False)
returnexit# Allow use as a decorator

defpush_async_callback(self,callback,/,*args,**kwds):
Expand All@@ -704,12 +693,6 @@ async def aclose(self):
"""Immediately unwind the context stack."""
awaitself.__aexit__(None,None,None)

def_push_async_cm_exit(self,cm,cm_exit):
"""Helper to correctly register coroutine function to __aexit__
method."""
_exit_wrapper=self._create_async_exit_wrapper(cm,cm_exit)
self._push_exit_callback(_exit_wrapper,False)

asyncdef__aenter__(self):
returnself

Expand Down
69 changes: 69 additions & 0 deletionsLib/test/test_contextlib.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -788,6 +788,75 @@ def _exit():
result.append(2)
self.assertEqual(result, [1, 2, 3, 4])

def test_enter_context_classmethod(self):
class TestCM:
@classmethod
def __enter__(cls):
result.append(('enter', cls))
@classmethod
def __exit__(cls, *exc_details):
result.append(('exit', cls, *exc_details))

cm = TestCM()
result = []
with self.exit_stack() as stack:
stack.enter_context(cm)
self.assertEqual(result, [('enter', TestCM)])
self.assertEqual(result, [('enter', TestCM),
('exit', TestCM, None, None, None)])

result = []
with self.exit_stack() as stack:
stack.push(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit', TestCM, None, None, None)])

def test_enter_context_staticmethod(self):
class TestCM:
@staticmethod
def __enter__():
result.append('enter')
@staticmethod
def __exit__(*exc_details):
result.append(('exit', *exc_details))

cm = TestCM()
result = []
with self.exit_stack() as stack:
stack.enter_context(cm)
self.assertEqual(result, ['enter'])
self.assertEqual(result, ['enter', ('exit', None, None, None)])

result = []
with self.exit_stack() as stack:
stack.push(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit', None, None, None)])

def test_enter_context_slots(self):
class TestCM:
__slots__ = ('__enter__', '__exit__')
def __init__(self):
def enter():
result.append('enter')
def exit(*exc_details):
result.append(('exit', *exc_details))
self.__enter__ = enter
self.__exit__ = exit

cm = TestCM()
result = []
with self.exit_stack() as stack:
stack.enter_context(cm)
self.assertEqual(result, ['enter'])
self.assertEqual(result, ['enter', ('exit', None, None, None)])

result = []
with self.exit_stack() as stack:
stack.push(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit', None, None, None)])

def test_enter_context_errors(self):
class LacksEnterAndExit:
pass
Expand Down
72 changes: 72 additions & 0 deletionsLib/test/test_contextlib_async.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -641,6 +641,78 @@ async def _exit():

self.assertEqual(result, [1,2,3,4])

@_async_test
asyncdeftest_enter_async_context_classmethod(self):
classTestCM:
@classmethod
asyncdef__aenter__(cls):
result.append(('enter',cls))
@classmethod
asyncdef__aexit__(cls,*exc_details):
result.append(('exit',cls,*exc_details))

cm=TestCM()
result= []
asyncwithself.exit_stack()asstack:
awaitstack.enter_async_context(cm)
self.assertEqual(result, [('enter',TestCM)])
self.assertEqual(result, [('enter',TestCM),
('exit',TestCM,None,None,None)])

result= []
asyncwithself.exit_stack()asstack:
stack.push_async_exit(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit',TestCM,None,None,None)])

@_async_test
asyncdeftest_enter_async_context_staticmethod(self):
classTestCM:
@staticmethod
asyncdef__aenter__():
result.append('enter')
@staticmethod
asyncdef__aexit__(*exc_details):
result.append(('exit',*exc_details))

cm=TestCM()
result= []
asyncwithself.exit_stack()asstack:
awaitstack.enter_async_context(cm)
self.assertEqual(result, ['enter'])
self.assertEqual(result, ['enter', ('exit',None,None,None)])

result= []
asyncwithself.exit_stack()asstack:
stack.push_async_exit(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit',None,None,None)])

@_async_test
asyncdeftest_enter_async_context_slots(self):
classTestCM:
__slots__= ('__aenter__','__aexit__')
def__init__(self):
asyncdefenter():
result.append('enter')
asyncdefexit(*exc_details):
result.append(('exit',*exc_details))
self.__aenter__=enter
self.__aexit__=exit

cm=TestCM()
result= []
asyncwithself.exit_stack()asstack:
awaitstack.enter_async_context(cm)
self.assertEqual(result, ['enter'])
self.assertEqual(result, ['enter', ('exit',None,None,None)])

result= []
asyncwithself.exit_stack()asstack:
stack.push_async_exit(cm)
self.assertEqual(result, [])
self.assertEqual(result, [('exit',None,None,None)])

@_async_test
asyncdeftest_enter_async_context_errors(self):
classLacksEnterAndExit:
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
Add support for arbitrary descriptors:meth:`!__enter__`,:meth:`!__exit__`,
:meth:`!__aenter__`, and:meth:`!__aexit__` in:class:`contextlib.ExitStack`
and:class:`contextlib.AsyncExitStack`, for consistency with the
:keyword:`with` and:keyword:`async with` statements.
Loading

[8]ページ先頭

©2009-2026 Movatter.jp