mirror of
https://github.com/python/cpython.git
synced 2025-07-08 03:45:36 +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
|
These context managers may suppress exceptions just as they normally
|
||||||
would if used directly as part of a :keyword:`with` statement.
|
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)
|
.. method:: push(exit)
|
||||||
|
|
||||||
Adds a context manager's :meth:`__exit__` method to the callback stack.
|
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
|
Similar to :meth:`enter_context` but expects an asynchronous context
|
||||||
manager.
|
manager.
|
||||||
|
|
||||||
|
... versionchanged:: 3.11
|
||||||
|
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
|
||||||
|
is not an asynchronous context manager.
|
||||||
|
|
||||||
.. method:: push_async_exit(exit)
|
.. method:: push_async_exit(exit)
|
||||||
|
|
||||||
Similar to :meth:`push` but expects either an asynchronous context manager
|
Similar to :meth:`push` but expects either an asynchronous context manager
|
||||||
|
|
|
@ -75,6 +75,12 @@ New Features
|
||||||
Other Language Changes
|
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
|
* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
|
||||||
:keyword:`with` and :keyword:`async with` statements for objects which do not
|
: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
|
# We look up the special methods on the type to match the with
|
||||||
# statement.
|
# statement.
|
||||||
_cm_type = type(cm)
|
cls = type(cm)
|
||||||
_exit = _cm_type.__exit__
|
try:
|
||||||
result = _cm_type.__enter__(cm)
|
_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)
|
self._push_cm_exit(cm, _exit)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -600,9 +605,15 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager):
|
||||||
If successful, also pushes its __aexit__ method as a callback and
|
If successful, also pushes its __aexit__ method as a callback and
|
||||||
returns the result of the __aenter__ method.
|
returns the result of the __aenter__ method.
|
||||||
"""
|
"""
|
||||||
_cm_type = type(cm)
|
cls = type(cm)
|
||||||
_exit = _cm_type.__aexit__
|
try:
|
||||||
result = await _cm_type.__aenter__(cm)
|
_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)
|
self._push_async_cm_exit(cm, _exit)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -661,6 +661,25 @@ class TestBaseExitStack:
|
||||||
result.append(2)
|
result.append(2)
|
||||||
self.assertEqual(result, [1, 2, 3, 4])
|
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):
|
def test_close(self):
|
||||||
result = []
|
result = []
|
||||||
with self.exit_stack() as stack:
|
with self.exit_stack() as stack:
|
||||||
|
@ -886,9 +905,11 @@ class TestBaseExitStack:
|
||||||
def test_instance_bypass(self):
|
def test_instance_bypass(self):
|
||||||
class Example(object): pass
|
class Example(object): pass
|
||||||
cm = Example()
|
cm = Example()
|
||||||
|
cm.__enter__ = object()
|
||||||
cm.__exit__ = object()
|
cm.__exit__ = object()
|
||||||
stack = self.exit_stack()
|
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)
|
stack.push(cm)
|
||||||
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
||||||
|
|
||||||
|
|
|
@ -483,7 +483,7 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
||||||
1/0
|
1/0
|
||||||
|
|
||||||
@_async_test
|
@_async_test
|
||||||
async def test_async_enter_context(self):
|
async def test_enter_async_context(self):
|
||||||
class TestCM(object):
|
class TestCM(object):
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
result.append(1)
|
result.append(1)
|
||||||
|
@ -504,6 +504,26 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(result, [1, 2, 3, 4])
|
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_test
|
||||||
async def test_async_exit_exception_chaining(self):
|
async def test_async_exit_exception_chaining(self):
|
||||||
# Ensure exception chaining matches the reference behaviour
|
# 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, ValueError)
|
||||||
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
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):
|
class TestAsyncNullcontext(unittest.TestCase):
|
||||||
@_async_test
|
@_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