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 1 commit
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
Next Next commit
improve error messages when incorrectly using context managers
  • Loading branch informa 8000 tion
picnixz committed Apr 7, 2025
commit aff8184ad8c56a3edb5da4f8a1ed9ad41353a5ce
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
93 changes: 93 additions & 0 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import math
import opcode
import os
import re
import unittest
import sys
import ast
Expand All @@ -24,6 +25,35 @@
from test.support.bytecode_helper import instructions_with_positions
from test.support.os_helper import FakePath


class DummyEnter:
def __enter__(self, *args, **kwargs):
pass


class DummyExit:
def __exit__(self, *args, **kwargs):
pass


class SyncDummy(DummyEnter, DummyExit):
pass


class AsyncDummyEnter:
async def __aenter__(self, *args, **kwargs):
pass


class AsyncDummyExit:
async def __aexit__(self, *args, **kwargs):
pass


class AsyncDummy(AsyncDummyEnter, AsyncDummyExit):
pass


class TestSpecifics(unittest.TestCase):

def compile_single(self, source):
Expand Down Expand Up @@ -1636,6 +1666,69 @@ async def name_4():
pass
[[]]

def test_invalid_with_usages(self):
def f(obj):
with obj:
pass

with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the context manager protocol "
"(missed __exit__ method)"
))):
f(DummyEnter())

with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the context manager protocol "
"(missed __enter__ method)"
))):
f(DummyExit())

# a missing __exit__ is reported missing before a missing __enter__
with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the context manager protocol "
"(missed __exit__ method)"
))):
f(object())

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'?"
))):
f(AsyncDummy())

def test_invalid_async_with_usages(self):
async def f(obj):
async with obj:
pass

with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the asynchronous context manager protocol "
"(missed __aexit__ method)"
))):
f(AsyncDummyEnter()).send(None)

with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the asynchronous context manager protocol "
"(missed __aenter__ method)"
))):
f(AsyncDummyExit()).send(None)

# a missing __aexit__ is reported missing before a missing __aenter__
with self.assertRaisesRegex(TypeError, re.escape((
"object does not support the asynchronous context manager protocol "
"(missed __aexit__ method)"
))):
f(object()).send(None)

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'?"
))):
f(SyncDummy()).send(None)


class TestBooleanExpression(unittest.TestCase):
class Value:
def __init__(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
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 @@ -3373,9 +3373,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 @@ -3363,3 +3391,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 SPEC B51C IAL___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