From 7cbf2152e963fff9c682e122d013992a57c0d42f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 20 Jun 2021 10:58:08 +0300 Subject: [PATCH 1/3] bpo-12022: Change error type for bad objects in "with" and "async with" 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. --- Doc/whatsnew/3.11.rst | 6 +++ Lib/test/test_contextlib.py | 4 +- Lib/test/test_coroutines.py | 6 +-- Lib/test/test_with.py | 6 +-- .../2021-06-20-10-53-21.bpo-12022.SW240M.rst | 4 ++ Python/ceval.c | 46 +++++++++++-------- 6 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-06-20-10-53-21.bpo-12022.SW240M.rst diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 50d91a0adc141b..4d5ba50176a602 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -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 =========== diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 453ef6c9f0832f..8b573e0bb46d91 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -491,7 +491,7 @@ def __unter__(self): def __exit__(self, *exc): pass - with self.assertRaises(AttributeError): + with self.assertRaisesRegex(TypeError, "context manager"): with mycontext(): pass @@ -503,7 +503,7 @@ def __enter__(self): def __uxit__(self, *exc): pass - with self.assertRaises(AttributeError): + with self.assertRaisesRegex(TypeError, "context manager"): with mycontext(): pass diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 145adb67781701..e920761b2df211 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -1210,7 +1210,7 @@ async def foo(): async with CM(): body_executed = True - with self.assertRaisesRegex(AttributeError, '__aexit__'): + with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): run_async(foo()) self.assertFalse(body_executed) @@ -1224,7 +1224,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.assertFalse(body_executed) @@ -1237,7 +1237,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.assertFalse(body_executed) diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index f21bf65fed8499..49a4c2f3bb7bca 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -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, 'context manager', fooLacksEnter) def testEnterAttributeError2(self): class LacksEnterAndExit(object): @@ -126,7 +126,7 @@ class LacksEnterAndExit(object): def fooLacksEnterAndExit(): foo = LacksEnterAndExit() with foo: pass - self.assertRaisesRegex(AttributeError, '__enter__', fooLacksEnterAndExit) + self.assertRaisesRegex(TypeError, 'context manager', fooLacksEnterAndExit) def testExitAttributeError(self): class LacksExit(object): @@ -136,7 +136,7 @@ def __enter__(self): def fooLacksExit(): foo = LacksExit() with foo: pass - self.assertRaisesRegex(AttributeError, '__exit__', fooLacksExit) + self.assertRaisesRegex(TypeError, 'context manager', fooLacksExit) def assertRaisesSyntaxError(self, codestr): def shouldRaiseSyntaxError(s): diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-06-20-10-53-21.bpo-12022.SW240M.rst b/Misc/NEWS.d/next/Core and Builtins/2021-06-20-10-53-21.bpo-12022.SW240M.rst new file mode 100644 index 00000000000000..98c42283169d8e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-06-20-10-53-21.bpo-12022.SW240M.rst @@ -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. diff --git a/Python/ceval.c b/Python/ceval.c index 699cd865faa1be..d0f3678495b771 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -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); @@ -3841,13 +3840,25 @@ _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", + Py_TYPE(mgr)->tp_name); + } Py_DECREF(enter); goto error; } @@ -3866,13 +3877,25 @@ _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", + Py_TYPE(mgr)->tp_name); + } Py_DECREF(enter); goto error; } @@ -5107,19 +5130,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 From 4b994fc7a088c1fd17ddfbb352aeed9b132f3fa7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Jun 2021 08:26:04 +0300 Subject: [PATCH 2/3] Ad hint about missed __exit__ method. --- Lib/test/test_contextlib.py | 4 ++-- Lib/test/test_coroutines.py | 2 +- Lib/test/test_with.py | 2 +- Python/ceval.c | 6 ++++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 8b573e0bb46d91..fb6b61ccedd879 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -491,7 +491,7 @@ def __unter__(self): def __exit__(self, *exc): pass - with self.assertRaisesRegex(TypeError, "context manager"): + with self.assertRaisesRegex(TypeError, 'context manager'): with mycontext(): pass @@ -503,7 +503,7 @@ def __enter__(self): def __uxit__(self, *exc): pass - with self.assertRaisesRegex(TypeError, "context manager"): + with self.assertRaisesRegex(TypeError, 'context manager.*__exit__'): with mycontext(): pass diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index e920761b2df211..36bc2636bafc09 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -1210,7 +1210,7 @@ async def foo(): async with CM(): body_executed = True - with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): + with self.assertRaisesRegex(TypeError, 'asynchronous context manager.*__aexit__'): run_async(foo()) self.assertFalse(body_executed) diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index 49a4c2f3bb7bca..d59998a2f36ae4 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -136,7 +136,7 @@ def __enter__(self): def fooLacksExit(): foo = LacksExit() with foo: pass - self.assertRaisesRegex(TypeError, 'context manager', fooLacksExit) + self.assertRaisesRegex(TypeError, 'context manager.*__exit__', fooLacksExit) def assertRaisesSyntaxError(self, codestr): def shouldRaiseSyntaxError(s): diff --git a/Python/ceval.c b/Python/ceval.c index d0f3678495b771..9b92dfe1145c76 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3856,7 +3856,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag) if (!_PyErr_Occurred(tstate)) { _PyErr_Format(tstate, PyExc_TypeError, "'%.200s' object does not support the " - "asynchronous context manager protocol", + "asynchronous context manager protocol " + "(missed __aexit__ method)", Py_TYPE(mgr)->tp_name); } Py_DECREF(enter); @@ -3893,7 +3894,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag) if (!_PyErr_Occurred(tstate)) { _PyErr_Format(tstate, PyExc_TypeError, "'%.200s' object does not support the " - "context manager protocol", + "context manager protocol " + "(missed __exit__ method)", Py_TYPE(mgr)->tp_name); } Py_DECREF(enter); From e3d59207f508e87d54eae606705f5d0413374c99 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Jun 2021 21:17:57 +0300 Subject: [PATCH 3/3] More specific tests. --- Lib/test/test_contextlib.py | 4 ++-- Lib/test/test_with.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index fb6b61ccedd879..dbc3f5fbe2b273 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -491,7 +491,7 @@ def __unter__(self): def __exit__(self, *exc): pass - with self.assertRaisesRegex(TypeError, 'context manager'): + with self.assertRaisesRegex(TypeError, 'the context manager'): with mycontext(): pass @@ -503,7 +503,7 @@ def __enter__(self): def __uxit__(self, *exc): pass - with self.assertRaisesRegex(TypeError, 'context manager.*__exit__'): + with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'): with mycontext(): pass diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index d59998a2f36ae4..07522bda6a5583 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -117,7 +117,7 @@ def __exit__(self, type, value, traceback): def fooLacksEnter(): foo = LacksEnter() with foo: pass - self.assertRaisesRegex(TypeError, 'context manager', fooLacksEnter) + self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnter) def testEnterAttributeError2(self): class LacksEnterAndExit(object): @@ -126,7 +126,7 @@ class LacksEnterAndExit(object): def fooLacksEnterAndExit(): foo = LacksEnterAndExit() with foo: pass - self.assertRaisesRegex(TypeError, 'context manager', fooLacksEnterAndExit) + self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnterAndExit) def testExitAttributeError(self): class LacksExit(object): @@ -136,7 +136,7 @@ def __enter__(self): def fooLacksExit(): foo = LacksExit() with foo: pass - self.assertRaisesRegex(TypeError, 'context manager.*__exit__', fooLacksExit) + self.assertRaisesRegex(TypeError, 'the context manager.*__exit__', fooLacksExit) def assertRaisesSyntaxError(self, codestr): def shouldRaiseSyntaxError(s):