Skip to content

Commit

Permalink
bpo-12022: Change error type for bad objects in "with" and "async wit…
Browse files Browse the repository at this point in the history
…h" (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.
  • Loading branch information
serhiy-storchaka committed Jun 29, 2021
1 parent 48e3a1d commit 20a8800
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 26 deletions.
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.11.rst
Expand Up @@ -76,6 +76,12 @@ Other Language Changes
======================


* A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
:keyword:`with` and :keyword:`async with` statements for objects which do not
support the :term:`context manager` or :term:`asynchronous context manager`
protocols correspondingly.
(Contributed by Serhiy Storchaka in :issue:`12022`.)


New Modules
===========
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_contextlib.py
Expand Up @@ -491,7 +491,7 @@ def __unter__(self):
def __exit__(self, *exc):
pass

with self.assertRaises(AttributeError):
with self.assertRaisesRegex(TypeError, 'the context manager'):
with mycontext():
pass

Expand All @@ -503,7 +503,7 @@ def __enter__(self):
def __uxit__(self, *exc):
pass

with self.assertRaises(AttributeError):
with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'):
with mycontext():
pass

Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_coroutines.py
Expand Up @@ -1212,7 +1212,7 @@ async def foo():
async with CM():
body_executed = True

with self.assertRaisesRegex(AttributeError, '__aexit__'):
with self.assertRaisesRegex(TypeError, 'asynchronous context manager.*__aexit__'):
run_async(foo())
self.assertIs(body_executed, False)

Expand All @@ -1228,7 +1228,7 @@ async def foo():
async with CM():
body_executed = True

with self.assertRaisesRegex(AttributeError, '__aenter__'):
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
run_async(foo())
self.assertIs(body_executed, False)

Expand All @@ -1243,7 +1243,7 @@ async def foo():
async with CM():
body_executed = True

with self.assertRaisesRegex(AttributeError, '__aenter__'):
with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
run_async(foo())
self.assertIs(body_executed, False)

Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_with.py
Expand Up @@ -117,7 +117,7 @@ def __exit__(self, type, value, traceback):
def fooLacksEnter():
foo = LacksEnter()
with foo: pass
self.assertRaisesRegex(AttributeError, '__enter__', fooLacksEnter)
self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnter)

def testEnterAttributeError2(self):
class LacksEnterAndExit(object):
Expand All @@ -126,7 +126,7 @@ class LacksEnterAndExit(object):
def fooLacksEnterAndExit():
foo = LacksEnterAndExit()
with foo: pass
self.assertRaisesRegex(AttributeError, '__enter__', fooLacksEnterAndExit)
self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnterAndExit)

def testExitAttributeError(self):
class LacksExit(object):
Expand All @@ -136,7 +136,7 @@ def __enter__(self):
def fooLacksExit():
foo = LacksExit()
with foo: pass
self.assertRaisesRegex(AttributeError, '__exit__', fooLacksExit)
self.assertRaisesRegex(TypeError, 'the context manager.*__exit__', fooLacksExit)

def assertRaisesSyntaxError(self, codestr):
def shouldRaiseSyntaxError(s):
Expand Down
@@ -0,0 +1,4 @@
A :exc:`TypeError` is now raised instead of an :exc:`AttributeError` in
:keyword:`with` and :keyword:`async with` statements for objects which do
not support the :term:`context manager` or :term:`asynchronous context
manager` protocols correspondingly.
48 changes: 30 additions & 18 deletions Python/ceval.c
Expand Up @@ -82,7 +82,6 @@ static void format_exc_check_arg(PyThreadState *, PyObject *, const char *, PyOb
static void format_exc_unbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
static PyObject * unicode_concatenate(PyThreadState *, PyObject *, PyObject *,
PyFrameObject *, const _Py_CODEUNIT *);
static PyObject * special_lookup(PyThreadState *, PyObject *, _Py_Identifier *);
static int check_args_iterable(PyThreadState *, PyObject *func, PyObject *vararg);
static void format_kwargs_error(PyThreadState *, PyObject *func, PyObject *kwargs);
static void format_awaitable_error(PyThreadState *, PyTypeObject *, int, int);
Expand Down Expand Up @@ -3844,13 +3843,26 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
_Py_IDENTIFIER(__aenter__);
_Py_IDENTIFIER(__aexit__);
PyObject *mgr = TOP();
PyObject *enter = special_lookup(tstate, mgr, &PyId___aenter__);
PyObject *res;
PyObject *enter = _PyObject_LookupSpecial(mgr, &PyId___aenter__);
if (enter == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object does not support the "
"asynchronous context manager protocol",
Py_TYPE(mgr)->tp_name);
}
goto error;
}
PyObject *exit = special_lookup(tstate, mgr, &PyId___aexit__);
PyObject *exit = _PyObject_LookupSpecial(mgr, &PyId___aexit__);
if (exit == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object does not support the "
"asynchronous context manager protocol "
"(missed __aexit__ method)",
Py_TYPE(mgr)->tp_name);
}
Py_DECREF(enter);
goto error;
}
Expand All @@ -3869,13 +3881,26 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
_Py_IDENTIFIER(__enter__);
_Py_IDENTIFIER(__exit__);
PyObject *mgr = TOP();
PyObject *enter = special_lookup(tstate, mgr, &PyId___enter__);
PyObject *res;
PyObject *enter = _PyObject_LookupSpecial(mgr, &PyId___enter__);
if (enter == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object does not support the "
"context manager protocol",
Py_TYPE(mgr)->tp_name);
}
goto error;
}
PyObject *exit = special_lookup(tstate, mgr, &PyId___exit__);
PyObject *exit = _PyObject_LookupSpecial(mgr, &PyId___exit__);
if (exit == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_Format(tstate, PyExc_TypeError,
"'%.200s' object does not support the "
"context manager protocol "
"(missed __exit__ method)",
Py_TYPE(mgr)->tp_name);
}
Py_DECREF(enter);
goto error;
}
Expand Down Expand Up @@ -5110,19 +5135,6 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
}


static PyObject *
special_lookup(PyThreadState *tstate, PyObject *o, _Py_Identifier *id)
{
PyObject *res;
res = _PyObject_LookupSpecial(o, id);
if (res == NULL && !_PyErr_Occurred(tstate)) {
_PyErr_SetObject(tstate, PyExc_AttributeError, _PyUnicode_FromId(id));
return NULL;
}
return res;
}


/* Logic for the raise statement (too complicated for inlining).
This *consumes* a reference count to each of its arguments. */
static int
Expand Down

0 comments on commit 20a8800

Please sign in to comment.