Skip to content

Commit 6cb145d

Browse files
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.
1 parent 20a8800 commit 6cb145d

File tree

6 files changed

+91
-8
lines changed

6 files changed

+91
-8
lines changed

Doc/library/contextlib.rst

+8
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ Functions and classes provided:
515515
These context managers may suppress exceptions just as they normally
516516
would if used directly as part of a :keyword:`with` statement.
517517

518+
... versionchanged:: 3.11
519+
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
520+
is not a context manager.
521+
518522
.. method:: push(exit)
519523

520524
Adds a context manager's :meth:`__exit__` method to the callback stack.
@@ -585,6 +589,10 @@ Functions and classes provided:
585589
Similar to :meth:`enter_context` but expects an asynchronous context
586590
manager.
587591

592+
... versionchanged:: 3.11
593+
Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
594+
is not an asynchronous context manager.
595+
588596
.. method:: push_async_exit(exit)
589597

590598
Similar to :meth:`push` but expects either an asynchronous context manager

Doc/whatsnew/3.11.rst

+6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ New Features
7575
Other Language Changes
7676
======================
7777

78+
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
79+
:meth:`contextlib.ExitStack.enter_context` and
80+
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do not
81+
support the :term:`context manager` or :term:`asynchronous context manager`
82+
protocols correspondingly.
83+
(Contributed by Serhiy Storchaka in :issue:`44471`.)
7884

7985
* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
8086
:keyword:`with` and :keyword:`async with` statements for objects which do not

Lib/contextlib.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,14 @@ def enter_context(self, cm):
473473
"""
474474
# We look up the special methods on the type to match the with
475475
# statement.
476-
_cm_type = type(cm)
477-
_exit = _cm_type.__exit__
478-
result = _cm_type.__enter__(cm)
476+
cls = type(cm)
477+
try:
478+
_enter = cls.__enter__
479+
_exit = cls.__exit__
480+
except AttributeError:
481+
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
482+
f"not support the context manager protocol") from None
483+
result = _enter(cm)
479484
self._push_cm_exit(cm, _exit)
480485
return result
481486

@@ -600,9 +605,15 @@ async def enter_async_context(self, cm):
600605
If successful, also pushes its __aexit__ method as a callback and
601606
returns the result of the __aenter__ method.
602607
"""
603-
_cm_type = type(cm)
604-
_exit = _cm_type.__aexit__
605-
result = await _cm_type.__aenter__(cm)
608+
cls = type(cm)
609+
try:
610+
_enter = cls.__aenter__
611+
_exit = cls.__aexit__
612+
except AttributeError:
613+
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
614+
f"not support the asynchronous context manager protocol"
615+
) from None
616+
result = await _enter(cm)
606617
self._push_async_cm_exit(cm, _exit)
607618
return result
608619

Lib/test/test_contextlib.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,25 @@ def _exit():
661661
result.append(2)
662662
self.assertEqual(result, [1, 2, 3, 4])
663663

664+
def test_enter_context_errors(self):
665+
class LacksEnterAndExit:
666+
pass
667+
class LacksEnter:
668+
def __exit__(self, *exc_info):
669+
pass
670+
class LacksExit:
671+
def __enter__(self):
672+
pass
673+
674+
with self.exit_stack() as stack:
675+
with self.assertRaisesRegex(TypeError, 'the context manager'):
676+
stack.enter_context(LacksEnterAndExit())
677+
with self.assertRaisesRegex(TypeError, 'the context manager'):
678+
stack.enter_context(LacksEnter())
679+
with self.assertRaisesRegex(TypeError, 'the context manager'):
680+
stack.enter_context(LacksExit())
681+
self.assertFalse(stack._exit_callbacks)
682+
664683
def test_close(self):
665684
result = []
666685
with self.exit_stack() as stack:
@@ -886,9 +905,11 @@ def test_excessive_nesting(self):
886905
def test_instance_bypass(self):
887906
class Example(object): pass
888907
cm = Example()
908+
cm.__enter__ = object()
889909
cm.__exit__ = object()
890910
stack = self.exit_stack()
891-
self.assertRaises(AttributeError, stack.enter_context, cm)
911+
with self.assertRaisesRegex(TypeError, 'the context manager'):
912+
stack.enter_context(cm)
892913
stack.push(cm)
893914
self.assertIs(stack._exit_callbacks[-1][1], cm)
894915

Lib/test/test_contextlib_async.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ async def __aexit__(self, *exc_details):
483483
1/0
484484

485485
@_async_test
486-
async def test_async_enter_context(self):
486+
async def test_enter_async_context(self):
487487
class TestCM(object):
488488
async def __aenter__(self):
489489
result.append(1)
@@ -504,6 +504,26 @@ async def _exit():
504504

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

507+
@_async_test
508+
async def test_enter_async_context_errors(self):
509+
class LacksEnterAndExit:
510+
pass
511+
class LacksEnter:
512+
async def __aexit__(self, *exc_info):
513+
pass
514+
class LacksExit:
515+
async def __aenter__(self):
516+
pass
517+
518+
async with self.exit_stack() as stack:
519+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
520+
await stack.enter_async_context(LacksEnterAndExit())
521+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
522+
await stack.enter_async_context(LacksEnter())
523+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
524+
await stack.enter_async_context(LacksExit())
525+
self.assertFalse(stack._exit_callbacks)
526+
507527
@_async_test
508528
async def test_async_exit_exception_chaining(self):
509529
# Ensure exception chaining matches the reference behaviour
@@ -536,6 +556,18 @@ async def suppress_exc(*exc_details):
536556
self.assertIsInstance(inner_exc, ValueError)
537557
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
538558

559+
@_async_test
560+
async def test_instance_bypass_async(self):
561+
class Example(object): pass
562+
cm = Example()
563+
cm.__aenter__ = object()
564+
cm.__aexit__ = object()
565+
stack = self.exit_stack()
566+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
567+
await stack.enter_async_context(cm)
568+
stack.push_async_exit(cm)
569+
self.assertIs(stack._exit_callbacks[-1][1], cm)
570+
539571

540572
class TestAsyncNullcontext(unittest.TestCase):
541573
@_async_test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
2+
:meth:`contextlib.ExitStack.enter_context` and
3+
:meth:`contextlib.AsyncExitStack.enter_async_context` for objects which do
4+
not support the :term:`context manager` or :term:`asynchronous context
5+
manager` protocols correspondingly.

0 commit comments

Comments
 (0)