mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
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`).
This commit is contained in:
parent
95800fe6e7
commit
8a9c6c4d16
10 changed files with 211 additions and 47 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 collections import deque
|
|||
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 @@ class FailureTestCase(unittest.TestCase):
|
|||
with foo: pass
|
||||
self.assertRaises(NameError, fooNotDeclared)
|
||||
|
||||
def testEnterAttributeError1(self):
|
||||
class LacksEnter(object):
|
||||
def __exit__(self, type, value, traceback):
|
||||
pass
|
||||
def testEnterAttributeError(self):
|
||||
class LacksEnter:
|
||||
def __exit__(self, type, value, traceback): ...
|
||||
|
||||
def fooLacksEnter():
|
||||
foo = LacksEnter()
|
||||
with foo: pass
|
||||
self.assertRaisesRegex(TypeError, 'the context manager', fooLacksEnter)
|
||||
|
||||
def testEnterAttributeError2(self):
|
||||
class LacksEnterAndExit(object):
|
||||
pass
|
||||
|
||||
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
|
||||
class LacksExit:
|
||||
def __enter__(self): ...
|
||||
|
||||
def fooLacksExit():
|
||||
foo = LacksExit()
|
||||
with foo: pass
|
||||
self.assertRaisesRegex(TypeError, 'the context manager.*__exit__', fooLacksExit)
|
||||
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 @@ class FailureTestCase(unittest.TestCase):
|
|||
pass
|
||||
self.assertRaises(RuntimeError, shouldThrow)
|
||||
|
||||
|
||||
class ContextmanagerAssertionMixin(object):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -75,9 +75,17 @@ class IsolatedAsyncioTestCase(TestCase):
|
|||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
11
Python/executor_cases.c.h
generated
11
Python/executor_cases.c.h
generated
|
@ -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();
|
||||
|
|
11
Python/generated_cases.c.h
generated
11
Python/generated_cases.c.h
generated
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue