8000 gh-128398: improve error message when incorrectly `with` and `async with` by picnixz · Pull Request #132218 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-128398: improve error message when incorrectly with and async with #132218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 19, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -309,6 +310,8 @@ PyAPI_FUNC(PyObject *) _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFra
PyAPI_FUNC(int)
_Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args);

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)
Expand Down
107 changes: 81 additions & 26 deletions Lib/test/test_with.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"""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 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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -190,6 +244,7 @@ def shouldThrow():
pass
self.assertRaises(RuntimeError, shouldThrow)


class ContextmanagerAssertionMixin(object):

def setUp(self):
Expand Down
14 changes: 11 additions & 3 deletions Lib/unittest/async_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions Lib/unittest/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
A3E2
Original file line number Diff line number Diff line change
@@ -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`) block instead of :keyword:`with` (resp.
:keyword:`async with`). Patch by Bénédikt Tran.
9 changes: 6 additions & 3 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -3426,9 +3426,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);
}
Expand Down
74 changes: 66 additions & 8 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -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'?"
)
}
};

Expand Down Expand Up @@ -3376,3 +3404,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 changes: 8 additions & 3 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
0