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
from collections import deque
from functools import wraps
from types importMethodType,GenericAlias
from types import GenericAlias

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


def _lookup_special(obj, name, default):
# Follow the standard lookup behaviour for special methods.
from inspect import getattr_static, _descriptor_get
cls = type(obj)
try:
descr = getattr_static(cls, name)
except AttributeError:
return default
return _descriptor_get(descr, obj)


_sentinel = ['SENTINEL']


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

@staticmethod
def _create_exit_wrapper(cm, cm_exit):
return MethodType(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__
except AttributeError:
# 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)
return exit # Allow use as a decorator.

def enter_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__
except AttributeError:
_enter = _lookup_special(cm, '__enter__', _sentinel)
if _enter is _sentinel:
cls = type(cm)
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol") from None
result = _enter(cm)
self._push_cm_exit(cm, _exit)
f"not support the context manager protocol")
_exit = _lookup_special(cm, '__exit__', _sentinel)
if _exit is _sentinel:
cls = type(cm)
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol")
result = _enter()
self._push_exit_callback(_exit)
return result

def callback(self, callback, /, *args, **kwds):
Expand All@@ -544,11 +546,6 @@ def callback(self, callback, /, *args, **kwds):
self._push_exit_callback(_exit_wrapper)
return callback # 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):
return MethodType(cm_exit, cm)

@staticmethod
def _create_async_cb_wrapper(callback, /, *args, **kwds):
async def _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__
except AttributeError:
_enter = _lookup_special(cm, '__aenter__', _sentinel)
if _enter is _sentinel:
cls = type(cm)
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the asynchronous context manager protocol"
) from None
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 _exit is _sentinel:
cls = type(cm)
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the asynchronous context manager protocol")
result = await _enter()
self._push_exit_callback(_exit, False)
return result

def push_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__
except AttributeError:
# 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)
return exit # Allow use as a decorator

def push_async_callback(self, callback, /, *args, **kwds):
Expand All@@ -704,12 +693,6 @@ async def aclose(self):
"""Immediately unwind the context stack."""
await self.__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)

async def __aenter__(self):
return self

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