https://github.com/python/cpython/commit/8a9c6c4d16a746eea1e000d6701d1c274c1f331b
commit: 8a9c6c4d16a746eea1e000d6701d1c274c1f331b
branch: main
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2025-04-19T10:44:01+02:00
summary:
gh-128398: improve error messages when incorrectly using `with` and `async
with` (#132218)
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`).
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst
M Doc/whatsnew/3.14.rst
M Include/internal/pycore_ceval.h
M Lib/test/test_with.py
M Lib/unittest/async_case.py
M Lib/unittest/case.py
M Python/bytecodes.c
M Python/ceval.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index aaa4702d53df93..56858aee4493a3 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -479,6 +479,12 @@ Other language changes
: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:
diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h
index 18c8bc0624fea7..96ba54b274c804 100644
--- a/Include/internal/pycore_ceval.h
+++ b/Include/internal/pycore_ceval.h
@@ -279,6 +279,7 @@ PyAPI_DATA(const conversion_func) _PyEval_ConversionFuncs[];
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[];
@@ -309,6 +310,16 @@ PyAPI_FUNC(PyObject *) _PyEval_LoadName(PyThreadState
*tstate, _PyInterpreterFra
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)
diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py
index 1d2ce9eccc4507..fd7abd1782ec4d 100644
--- a/Lib/test/test_with.py
+++ b/Lib/test/test_with.py
@@ -1,9 +1,10 @@
-"""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
@@ -11,6 +12,16 @@
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)
@@ -110,34 +121,77 @@ def fooNotDeclared():
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):
@@ -190,6 +244,7 @@ def shouldThrow():
pass
self.assertRaises(RuntimeError, shouldThrow)
+
class ContextmanagerAssertionMixin(object):
def setUp(self):
diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py
index 6000af1cef0a78..a1c0d6c368ce8a 100644
--- a/Lib/unittest/async_case.py
+++ b/Lib/unittest/async_case.py
@@ -75,9 +75,17 @@ async def enterAsyncContext(self, cm):
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
diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py
index 10c3b7e122371e..884fc1b21f64d8 100644
--- a/Lib/unittest/case.py
+++ b/Lib/unittest/case.py
@@ -111,8 +111,17 @@ def _enter_context(cm, addcleanup):
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
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst
new file mode 100644
index 00000000000000..792332db6ef4c3
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst
@@ -0,0 +1,4 @@
+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.
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 2796c3f2e85732..07df22c761fc1c 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -3425,9 +3425,12 @@ dummy_func(
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);
}
diff --git a/Python/ceval.c b/Python/ceval.c
index e534c7e2b883d5..17e28439872ba2 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -545,23 +545,51 @@ const conversion_func _PyEval_ConversionFuncs[4] = {
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'?"
+ )
}
};
@@ -3380,3 +3408,33 @@ _PyEval_LoadName(PyThreadState *tstate,
_PyInterpreterFrame *frame, PyObject *na
}
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");
+ }
+}
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 122285ba12e6d1..cd265c383bd380 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -4425,9 +4425,14 @@
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();
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index cc85405f80b4c0..911f5ae3e7c0d7 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -9358,9 +9358,14 @@
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);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]