Skip to content

Commit 20a8800

Browse files
bpo-12022: Change error type for bad objects in "with" and "async with" (GH-26809)
A TypeError is now raised instead of an AttributeError in "with" and "async with" statements for objects which do not support the context manager or asynchronous context manager protocols correspondingly.
1 parent 48e3a1d commit 20a8800

File tree

6 files changed

+48
-26
lines changed

6 files changed

+48
-26
lines changed

Doc/whatsnew/3.11.rst

+6
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ Other Language Changes
7676
======================
7777

7878

79+
* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
80+
:keyword:`with` and :keyword:`async with` statements 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:`12022`.)
84+
7985

8086
New Modules
8187
===========

Lib/test/test_contextlib.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ def __unter__(self):
491491
def __exit__(self, *exc):
492492
pass
493493

494-
with self.assertRaises(AttributeError):
494+
with self.assertRaisesRegex(TypeError, 'the context manager'):
495495
with mycontext():
496496
pass
497497

@@ -503,7 +503,7 @@ def __enter__(self):
503503
def __uxit__(self, *exc):
504504
pass
505505

506-
with self.assertRaises(AttributeError):
506+
with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'):
507507
with mycontext():
508508
pass
509509

Lib/test/test_coroutines.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@ async def foo():
12121212
async with CM():
12131213
body_executed = True
12141214

1215-
with self.assertRaisesRegex(AttributeError, '__aexit__'):
1215+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager.*__aexit__'):
12161216
run_async(foo())
12171217
self.assertIs(body_executed, False)
12181218

@@ -1228,7 +1228,7 @@ async def foo():
12281228
async with CM():
12291229
body_executed = True
12301230

1231-
with self.assertRaisesRegex(AttributeError, '__aenter__'):
1231+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
12321232
run_async(foo())
12331233
self.assertIs(body_executed, False)
12341234

@@ -1243,7 +1243,7 @@ async def foo():
12431243
async with CM():
12441244
body_executed = True
12451245

1246-
with self.assertRaisesRegex(AttributeError, '__aenter__'):
1246+
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
12471247
run_async(foo())
12481248
self.assertIs(body_executed, False)
12491249

Lib/test/test_with.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def __exit__(self, type, value, traceback):
117117
def fooLacksEnter():
118118
foo = LacksEnter()
119119
with foo: pass
120-
self.assertRaisesRegex(AttributeError, '__enter__', fooLacksEnter)
120+
self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnter)
121121

122122
def testEnterAttributeError2(self):
123123
class LacksEnterAndExit(object):
@@ -126,7 +126,7 @@ class LacksEnterAndExit(object):
126126
def fooLacksEnterAndExit():
127127
foo = LacksEnterAndExit()
128128
with foo: pass
129-
self.assertRaisesRegex(AttributeError, '__enter__', fooLacksEnterAndExit)
129+
self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnterAndExit)
130130

131131
def testExitAttributeError(self):
132132
class LacksExit(object):
@@ -136,7 +136,7 @@ def __enter__(self):
136136
def fooLacksExit():
137137
foo = LacksExit()
138138
with foo: pass
139-
self.assertRaisesRegex(AttributeError, '__exit__', fooLacksExit)
139+
self.assertRaisesRegex(TypeError, 'the context manager.*__exit__', fooLacksExit)
140140

141141
def assertRaisesSyntaxError(self, codestr):
142142
def shouldRaiseSyntaxError(s):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
2+
:keyword:`with` and :keyword:`async with` statements for objects which do
3+
not support the :term:`context manager` or :term:`asynchronous context
4+
manager` protocols correspondingly.

Python/ceval.c

+30-18
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ static void format_exc_check_arg(PyThreadState *, PyObject *, const char *, PyOb
8282
static void format_exc_unbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
8383
static PyObject * unicode_concatenate(PyThreadState *, PyObject *, PyObject *,
8484
PyFrameObject *, const _Py_CODEUNIT *);
85-
static PyObject * special_lookup(PyThreadState *, PyObject *, _Py_Identifier *);
8685
static int check_args_iterable(PyThreadState *, PyObject *func, PyObject *vararg);
8786
static void format_kwargs_error(PyThreadState *, PyObject *func, PyObject *kwargs);
8887
static void format_awaitable_error(PyThreadState *, PyTypeObject *, int, int);
@@ -3844,13 +3843,26 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
38443843
_Py_IDENTIFIER(__aenter__);
38453844
_Py_IDENTIFIER(__aexit__);
38463845
PyObject *mgr = TOP();
3847-
PyObject *enter = special_lookup(tstate, mgr, &PyId___aenter__);
38483846
PyObject *res;
3847+
PyObject *enter = _PyObject_LookupSpecial(mgr, &PyId___aenter__);
38493848
if (enter == NULL) {
3849+
if (!_PyErr_Occurred(tstate)) {
3850+
_PyErr_Format(tstate, PyExc_TypeError,
3851+
"'%.200s' object does not support the "
3852+
"asynchronous context manager protocol",
3853+
Py_TYPE(mgr)->tp_name);
3854+
}
38503855
goto error;
38513856
}
3852-
PyObject *exit = special_lookup(tstate, mgr, &PyId___aexit__);
3857+
PyObject *exit = _PyObject_LookupSpecial(mgr, &PyId___aexit__);
38533858
if (exit == NULL) {
3859+
if (!_PyErr_Occurred(tstate)) {
3860+
_PyErr_Format(tstate, PyExc_TypeError,
3861+
"'%.200s' object does not support the "
3862+
"asynchronous context manager protocol "
3863+
"(missed __aexit__ method)",
3864+
Py_TYPE(mgr)->tp_name);
3865+
}
38543866
Py_DECREF(enter);
38553867
goto error;
38563868
}
@@ -3869,13 +3881,26 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
38693881
_Py_IDENTIFIER(__enter__);
38703882
_Py_IDENTIFIER(__exit__);
38713883
PyObject *mgr = TOP();
3872-
PyObject *enter = special_lookup(tstate, mgr, &PyId___enter__);
38733884
PyObject *res;
3885+
PyObject *enter = _PyObject_LookupSpecial(mgr, &PyId___enter__);
38743886
if (enter == NULL) {
3887+
if (!_PyErr_Occurred(tstate)) {
3888+
_PyErr_Format(tstate, PyExc_TypeError,
3889+
"'%.200s' object does not support the "
3890+
"context manager protocol",
3891+
Py_TYPE(mgr)->tp_name);
3892+
}
38753893
goto error;
38763894
}
3877-
PyObject *exit = special_lookup(tstate, mgr, &PyId___exit__);
3895+
PyObject *exit = _PyObject_LookupSpecial(mgr, &PyId___exit__);
38783896
if (exit == NULL) {
3897+
if (!_PyErr_Occurred(tstate)) {
3898+
_PyErr_Format(tstate, PyExc_TypeError,
3899+
"'%.200s' object does not support the "
3900+
"context manager protocol "
3901+
"(missed __exit__ method)",
3902+
Py_TYPE(mgr)->tp_name);
3903+
}
38793904
Py_DECREF(enter);
38803905
goto error;
38813906
}
@@ -5110,19 +5135,6 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
51105135
}
51115136

51125137

5113-
static PyObject *
5114-
special_lookup(PyThreadState *tstate, PyObject *o, _Py_Identifier *id)
5115-
{
5116-
PyObject *res;
5117-
res = _PyObject_LookupSpecial(o, id);
5118-
if (res == NULL && !_PyErr_Occurred(tstate)) {
5119-
_PyErr_SetObject(tstate, PyExc_AttributeError, _PyUnicode_FromId(id));
5120-
return NULL;
5121-
}
5122-
return res;
5123-
}
5124-
5125-
51265138
/* Logic for the raise statement (too complicated for inlining).
51275139
This *consumes* a reference count to each of its arguments. */
51285140
static int

0 commit comments

Comments
 (0)