Improve the error message with a suggestion when an object supporting the synchronous
(resp. asynchronous) context manager protocol is entered using `async with` (resp. `with`)
instead of `with` (resp. `async with`).
:func:`textwrap.dedent`.
(Contributed by Jon Crall and Steven Sun in :gh:`103998`.)
+* Improve error message when an object supporting the synchronous (resp.
+ asynchronous) context manager protocol is entered using :keyword:`async
+ with` (resp. :keyword:`with`) instead of :keyword:`with` (resp.
+ :keyword:`async with`).
+ (Contributed by Bénédikt Tran in :gh:`128398`.)
+
.. _whatsnew314-pep765:
typedef struct _special_method {
PyObject *name;
const char *error;
+ const char *error_suggestion; // improved optional suggestion
} _Py_SpecialMethod;
PyAPI_DATA(const _Py_SpecialMethod) _Py_SpecialMethods[];
PyAPI_FUNC(int)
_Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args);
+/*
+ * Indicate whether a special method of given 'oparg' can use the (improved)
+ * alternative error message instead. Only methods loaded by LOAD_SPECIAL
+ * support alternative error messages.
+ *
+ * Symbol is exported for the JIT (see discussion on GH-132218).
+ */
+PyAPI_FUNC(int)
+_PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg);
+
/* Bits that can be set in PyThreadState.eval_breaker */
#define _PY_GIL_DROP_REQUEST_BIT (1U << 0)
#define _PY_SIGNALS_PENDING_BIT (1U << 1)
-"""Unit tests for the with statement specified in PEP 343."""
+"""Unit tests for the 'with/async with' statements specified in PEP 343/492."""
__author__ = "Mike Bland"
__email__ = "mbland at acm dot org"
+import re
import sys
import traceback
import unittest
from contextlib import _GeneratorContextManager, contextmanager, nullcontext
+def do_with(obj):
+ with obj:
+ pass
+
+
+async def do_async_with(obj):
+ async with obj:
+ pass
+
+
class MockContextManager(_GeneratorContextManager):
def __init__(self, *args):
super().__init__(*args)
with foo: pass
self.assertRaises(NameError, fooNotDeclared)
- def testEnterAttributeError1(self):
- class LacksEnter(object):
- def __exit__(self, type, value, traceback):
- pass
-
- def fooLacksEnter():
- foo = LacksEnter()
- with foo: pass
- self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnter)
-
- def testEnterAttributeError2(self):
- class LacksEnterAndExit(object):
- pass
+ def testEnterAttributeError(self):
+ class LacksEnter:
+ def __exit__(self, type, value, traceback): ...
- def fooLacksEnterAndExit():
- foo = LacksEnterAndExit()
- with foo: pass
- self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnterAndExit)
+ with self.assertRaisesRegex(TypeError, re.escape((
+ "object does not support the context manager protocol "
+ "(missed __enter__ method)"
+ ))):
+ do_with(LacksEnter())
def testExitAttributeError(self):
- class LacksExit(object):
- def __enter__(self):
- pass
-
- def fooLacksExit():
- foo = LacksExit()
- with foo: pass
- self.assertRaisesRegex(TypeError, 'the context manager.*__exit__', fooLacksExit)
+ class LacksExit:
+ def __enter__(self): ...
+
+ msg = re.escape((
+ "object does not support the context manager protocol "
+ "(missed __exit__ method)"
+ ))
+ # a missing __exit__ is reported missing before a missing __enter__
+ with self.assertRaisesRegex(TypeError, msg):
+ do_with(object())
+ with self.assertRaisesRegex(TypeError, msg):
+ do_with(LacksExit())
+
+ def testWithForAsyncManager(self):
+ class AsyncManager:
+ async def __aenter__(self): ...
+ async def __aexit__(self, type, value, traceback): ...
+
+ with self.assertRaisesRegex(TypeError, re.escape((
+ "object does not support the context manager protocol "
+ "(missed __exit__ method) but it supports the asynchronous "
+ "context manager protocol. Did you mean to use 'async with'?"
+ ))):
+ do_with(AsyncManager())
+
+ def testAsyncEnterAttributeError(self):
+ class LacksAsyncEnter:
+ async def __aexit__(self, type, value, traceback): ...
+
+ with self.assertRaisesRegex(TypeError, re.escape((
+ "object does not support the asynchronous context manager protocol "
+ "(missed __aenter__ method)"
+ ))):
+ do_async_with(LacksAsyncEnter()).send(None)
+
+ def testAsyncExitAttributeError(self):
+ class LacksAsyncExit:
+ async def __aenter__(self): ...
+
+ msg = re.escape((
+ "object does not support the asynchronous context manager protocol "
+ "(missed __aexit__ method)"
+ ))
+ # a missing __aexit__ is reported missing before a missing __aenter__
+ with self.assertRaisesRegex(TypeError, msg):
+ do_async_with(object()).send(None)
+ with self.assertRaisesRegex(TypeError, msg):
+ do_async_with(LacksAsyncExit()).send(None)
+
+ def testAsyncWithForSyncManager(self):
+ class SyncManager:
+ def __enter__(self): ...
+ def __exit__(self, type, value, traceback): ...
+
+ with self.assertRaisesRegex(TypeError, re.escape((
+ "object does not support the asynchronous context manager protocol "
+ "(missed __aexit__ method) but it supports the context manager "
+ "protocol. Did you mean to use 'with'?"
+ ))):
+ do_async_with(SyncManager()).send(None)
def assertRaisesSyntaxError(self, codestr):
def shouldRaiseSyntaxError(s):
pass
self.assertRaises(RuntimeError, shouldThrow)
+
class ContextmanagerAssertionMixin(object):
def setUp(self):
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
+ msg = (f"'{cls.__module__}.{cls.__qualname__}' object does "
+ "not support the asynchronous context manager protocol")
+ try:
+ cls.__enter__
+ cls.__exit__
+ except AttributeError:
+ pass
+ else:
+ msg += (" but it supports the context manager protocol. "
+ "Did you mean to use enterContext()?")
+ raise TypeError(msg) from None
result = await enter(cm)
self.addAsyncCleanup(exit, cm, None, None, None)
return result
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
+ msg = (f"'{cls.__module__}.{cls.__qualname__}' object does "
+ "not support the context manager protocol")
+ try:
+ cls.__aenter__
+ cls.__aexit__
+ except AttributeError:
+ pass
+ else:
+ msg += (" but it supports the asynchronous context manager "
+ "protocol. Did you mean to use enterAsyncContext()?")
+ raise TypeError(msg) from None
result = enter(cm)
addcleanup(exit, cm, None, None, None)
return result
--- /dev/null
+Improve error message when an object supporting the synchronous (resp.
+asynchronous) context manager protocol is entered using :keyword:`async
+with` (resp. :keyword:`with`) instead of :keyword:`with` (resp.
+:keyword:`async with`). Patch by Bénédikt Tran.
PyObject *attr_o = _PyObject_LookupSpecialMethod(owner_o, name, &self_or_null_o);
if (attr_o == NULL) {
if (!_PyErr_Occurred(tstate)) {
- _PyErr_Format(tstate, PyExc_TypeError,
- _Py_SpecialMethods[oparg].error,
- Py_TYPE(owner_o)->tp_name);
+ const char *errfmt = _PyEval_SpecialMethodCanSuggest(owner_o, oparg)
+ ? _Py_SpecialMethods[oparg].error_suggestion
+ : _Py_SpecialMethods[oparg].error;
+ assert(!_PyErr_Occurred(tstate));
+ assert(errfmt != NULL);
+ _PyErr_Format(tstate, PyExc_TypeError, errfmt, owner_o);
}
ERROR_IF(true, error);
}
const _Py_SpecialMethod _Py_SpecialMethods[] = {
[SPECIAL___ENTER__] = {
.name = &_Py_ID(__enter__),
- .error = "'%.200s' object does not support the "
- "context manager protocol (missed __enter__ method)",
+ .error = (
+ "'%T' object does not support the context manager protocol "
+ "(missed __enter__ method)"
+ ),
+ .error_suggestion = (
+ "'%T' object does not support the context manager protocol "
+ "(missed __enter__ method) but it supports the asynchronous "
+ "context manager protocol. Did you mean to use 'async with'?"
+ )
},
[SPECIAL___EXIT__] = {
.name = &_Py_ID(__exit__),
- .error = "'%.200s' object does not support the "
- "context manager protocol (missed __exit__ method)",
+ .error = (
+ "'%T' object does not support the context manager protocol "
+ "(missed __exit__ method)"
+ ),
+ .error_suggestion = (
+ "'%T' object does not support the context manager protocol "
+ "(missed __exit__ method) but it supports the asynchronous "
+ "context manager protocol. Did you mean to use 'async with'?"
+ )
},
[SPECIAL___AENTER__] = {
.name = &_Py_ID(__aenter__),
- .error = "'%.200s' object does not support the asynchronous "
- "context manager protocol (missed __aenter__ method)",
+ .error = (
+ "'%T' object does not support the asynchronous "
+ "context manager protocol (missed __aenter__ method)"
+ ),
+ .error_suggestion = (
+ "'%T' object does not support the asynchronous context manager "
+ "protocol (missed __aenter__ method) but it supports the context "
+ "manager protocol. Did you mean to use 'with'?"
+ )
},
[SPECIAL___AEXIT__] = {
.name = &_Py_ID(__aexit__),
- .error = "'%.200s' object does not support the asynchronous "
- "context manager protocol (missed __aexit__ method)",
+ .error = (
+ "'%T' object does not support the asynchronous "
+ "context manager protocol (missed __aexit__ method)"
+ ),
+ .error_suggestion = (
+ "'%T' object does not support the asynchronous context manager "
+ "protocol (missed __aexit__ method) but it supports the context "
+ "manager protocol. Did you mean to use 'with'?"
+ )
}
};
}
return value;
}
+
+/* Check if a 'cls' provides the given special method. */
+static inline int
+type_has_special_method(PyTypeObject *cls, PyObject *name)
+{
+ // _PyType_Lookup() does not set an exception and returns a borrowed ref
+ assert(!PyErr_Occurred());
+ PyObject *r = _PyType_Lookup(cls, name);
+ return r != NULL && Py_TYPE(r)->tp_descr_get != NULL;
+}
+
+int
+_PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg)
+{
+ PyTypeObject *type = Py_TYPE(self);
+ switch (oparg) {
+ case SPECIAL___ENTER__:
+ case SPECIAL___EXIT__: {
+ return type_has_special_method(type, &_Py_ID(__aenter__))
+ && type_has_special_method(type, &_Py_ID(__aexit__));
+ }
+ case SPECIAL___AENTER__:
+ case SPECIAL___AEXIT__: {
+ return type_has_special_method(type, &_Py_ID(__enter__))
+ && type_has_special_method(type, &_Py_ID(__exit__));
+ }
+ default:
+ Py_FatalError("unsupported special method");
+ }
+}
if (attr_o == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyFrame_SetStackPointer(frame, stack_pointer);
- _PyErr_Format(tstate, PyExc_TypeError,
- _Py_SpecialMethods[oparg].error,
- Py_TYPE(owner_o)->tp_name);
+ const char *errfmt = _PyEval_SpecialMethodCanSuggest(owner_o, oparg)
+ ? _Py_SpecialMethods[oparg].error_suggestion
+ : _Py_SpecialMethods[oparg].error;
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ assert(!_PyErr_Occurred(tstate));
+ assert(errfmt != NULL);
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ _PyErr_Format(tstate, PyExc_TypeError, errfmt, owner_o);
stack_pointer = _PyFrame_GetStackPointer(frame);
}
JUMP_TO_ERROR();
if (attr_o == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyFrame_SetStackPointer(frame, stack_pointer);
- _PyErr_Format(tstate, PyExc_TypeError,
- _Py_SpecialMethods[oparg].error,
- Py_TYPE(owner_o)->tp_name);
+ const char *errfmt = _PyEval_SpecialMethodCanSuggest(owner_o, oparg)
+ ? _Py_SpecialMethods[oparg].error_suggestion
+ : _Py_SpecialMethods[oparg].error;
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ assert(!_PyErr_Occurred(tstate));
+ assert(errfmt != NULL);
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ _PyErr_Format(tstate, PyExc_TypeError, errfmt, owner_o);
stack_pointer = _PyFrame_GetStackPointer(frame);
}
JUMP_TO_LABEL(error);