mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
bpo-44471: Change error type for bad objects in ExitStack.enter_context() (GH-26820)
A TypeError is now raised instead of an AttributeError in ExitStack.enter_context() and AsyncExitStack.enter_async_context() for objects which do not support the context manager or asynchronous context manager protocols correspondingly.
This commit is contained in:
parent
20a88004ba
commit
6cb145d23f
6 changed files with 91 additions and 8 deletions
|
@ -515,6 +515,10 @@ Functions and classes provided:
|
|||
These context managers may suppress exceptions just as they normally
|
||||
would if used directly as part of a :keyword:`with` statement.
|
||||
|
||||
... versionchanged:: 3.11
|
||||
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
|
||||
is not a context manager.
|
||||
|
||||
.. method:: push(exit)
|
||||
|
||||
Adds a context manager's :meth:`__exit__` method to the callback stack.
|
||||
|
@ -585,6 +589,10 @@ Functions and classes provided:
|
|||
Similar to :meth:`enter_context` but expects an asynchronous context
|
||||
manager.
|
||||
|
||||
... versionchanged:: 3.11
|
||||
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
|
||||
is not an asynchronous context manager.
|
||||
|
||||
.. method:: push_async_exit(exit)
|
||||
|
||||
Similar to :meth:`push` but expects either an asynchronous context manager
|
||||
|
|
|
@ -75,6 +75,12 @@ New Features
|
|||
Other Language Changes
|
||||
======================
|
||||
|
||||
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
|
||||
:meth:`contextlib.ExitStack.enter_context` and
|
||||
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do not
|
||||
support the :term:`context manager` or :term:`asynchronous context manager`
|
||||
protocols correspondingly.
|
||||
(Contributed by Serhiy Storchaka in :issue:`44471`.)
|
||||
|
||||
* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
|
||||
:keyword:`with` and :keyword:`async with` statements for objects which do not
|
||||
|
|
|
@ -473,9 +473,14 @@ class _BaseExitStack:
|
|||
"""
|
||||
# We look up the special methods on the type to match the with
|
||||
# statement.
|
||||
_cm_type = type(cm)
|
||||
_exit = _cm_type.__exit__
|
||||
result = _cm_type.__enter__(cm)
|
||||
cls = type(cm)
|
||||
try:
|
||||
_enter = cls.__enter__
|
||||
_exit = cls.__exit__
|
||||
except AttributeError:
|
||||
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)
|
||||
return result
|
||||
|
||||
|
@ -600,9 +605,15 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager):
|
|||
If successful, also pushes its __aexit__ method as a callback and
|
||||
returns the result of the __aenter__ method.
|
||||
"""
|
||||
_cm_type = type(cm)
|
||||
_exit = _cm_type.__aexit__
|
||||
result = await _cm_type.__aenter__(cm)
|
||||
cls = type(cm)
|
||||
try:
|
||||
_enter = cls.__aenter__
|
||||
_exit = cls.__aexit__
|
||||
except AttributeError:
|
||||
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)
|
||||
return result
|
||||
|
||||
|
|
|
@ -661,6 +661,25 @@ class TestBaseExitStack:
|
|||
result.append(2)
|
||||
self.assertEqual(result, [1, 2, 3, 4])
|
||||
|
||||
def test_enter_context_errors(self):
|
||||
class LacksEnterAndExit:
|
||||
pass
|
||||
class LacksEnter:
|
||||
def __exit__(self, *exc_info):
|
||||
pass
|
||||
class LacksExit:
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
with self.exit_stack() as stack:
|
||||
with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||
stack.enter_context(LacksEnterAndExit())
|
||||
with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||
stack.enter_context(LacksEnter())
|
||||
with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||
stack.enter_context(LacksExit())
|
||||
self.assertFalse(stack._exit_callbacks)
|
||||
|
||||
def test_close(self):
|
||||
result = []
|
||||
with self.exit_stack() as stack:
|
||||
|
@ -886,9 +905,11 @@ class TestBaseExitStack:
|
|||
def test_instance_bypass(self):
|
||||
class Example(object): pass
|
||||
cm = Example()
|
||||
cm.__enter__ = object()
|
||||
cm.__exit__ = object()
|
||||
stack = self.exit_stack()
|
||||
self.assertRaises(AttributeError, stack.enter_context, cm)
|
||||
with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||
stack.enter_context(cm)
|
||||
stack.push(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
||||
|
||||
|
|
|
@ -483,7 +483,7 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
|||
1/0
|
||||
|
||||
@_async_test
|
||||
async def test_async_enter_context(self):
|
||||
async def test_enter_async_context(self):
|
||||
class TestCM(object):
|
||||
async def __aenter__(self):
|
||||
result.append(1)
|
||||
|
@ -504,6 +504,26 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
|||
|
||||
self.assertEqual(result, [1, 2, 3, 4])
|
||||
|
||||
@_async_test
|
||||
async def test_enter_async_context_errors(self):
|
||||
class LacksEnterAndExit:
|
||||
pass
|
||||
class LacksEnter:
|
||||
async def __aexit__(self, *exc_info):
|
||||
pass
|
||||
class LacksExit:
|
||||
async def __aenter__(self):
|
||||
pass
|
||||
|
||||
async with self.exit_stack() as stack:
|
||||
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||
await stack.enter_async_context(LacksEnterAndExit())
|
||||
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||
await stack.enter_async_context(LacksEnter())
|
||||
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||
await stack.enter_async_context(LacksExit())
|
||||
self.assertFalse(stack._exit_callbacks)
|
||||
|
||||
@_async_test
|
||||
async def test_async_exit_exception_chaining(self):
|
||||
# Ensure exception chaining matches the reference behaviour
|
||||
|
@ -536,6 +556,18 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
|||
self.assertIsInstance(inner_exc, ValueError)
|
||||
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
||||
|
||||
@_async_test
|
||||
async def test_instance_bypass_async(self):
|
||||
class Example(object): pass
|
||||
cm = Example()
|
||||
cm.__aenter__ = object()
|
||||
cm.__aexit__ = object()
|
||||
stack = self.exit_stack()
|
||||
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||
await stack.enter_async_context(cm)
|
||||
stack.push_async_exit(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
||||
|
||||
|
||||
class TestAsyncNullcontext(unittest.TestCase):
|
||||
@_async_test
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
|
||||
:meth:`contextlib.ExitStack.enter_context` and
|
||||
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do
|
||||
not support the :term:`context manager` or :term:`asynchronous context
|
||||
manager` protocols correspondingly.
|
Loading…
Add table
Add a link
Reference in a new issue