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:
Serhiy Storchaka 2021-06-29 11:28:15 +03:00 committed by GitHub
parent 20a88004ba
commit 6cb145d23f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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.