Skip to content

Commit aff8184

Browse files
committed
improve error messages when incorrectly using context managers
1 parent f2daa96 commit aff8184

File tree

9 files changed

+210
-22
lines changed

9 files changed

+210
-22
lines changed

Include/internal/pycore_ceval.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ PyAPI_DATA(const conversion_func) _PyEval_ConversionFuncs[];
279279
typedef struct _special_method {
280280
PyObject *name;
281281
const char *error;
282+
const char *error_suggestion; // improved optional suggestion
282283
} _Py_SpecialMethod;
283284

284285
PyAPI_DATA(const _Py_SpecialMethod) _Py_SpecialMethods[];
@@ -309,6 +310,8 @@ PyAPI_FUNC(PyObject *) _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFra
309310
PyAPI_FUNC(int)
310311
_Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args);
311312

313+
PyAPI_FUNC(int) _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg);
314+
312315
/* Bits that can be set in PyThreadState.eval_breaker */
313316
#define _PY_GIL_DROP_REQUEST_BIT (1U << 0)
314317
#define _PY_SIGNALS_PENDING_BIT (1U << 1)

Lib/test/test_compile.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import math
66
import opcode
77
import os
8+
import re
89
import unittest
910
import sys
1011
import ast
@@ -24,6 +25,35 @@
2425
from test.support.bytecode_helper import instructions_with_positions
2526
from test.support.os_helper import FakePath
2627

28+
29+
class DummyEnter:
30+
def __enter__(self, *args, **kwargs):
31+
pass
32+
33+
34+
class DummyExit:
35+
def __exit__(self, *args, **kwargs):
36+
pass
37+
38+
39+
class SyncDummy(DummyEnter, DummyExit):
40+
pass
41+
42+
43+
class AsyncDummyEnter:
44+
async def __aenter__(self, *args, **kwargs):
45+
pass
46+
47+
48+
class AsyncDummyExit:
49+
async def __aexit__(self, *args, **kwargs):
50+
pass
51+
52+
53+
class AsyncDummy(AsyncDummyEnter, AsyncDummyExit):
54+
pass
55+
56+
2757
class TestSpecifics(unittest.TestCase):
2858

2959
def compile_single(self, source):
@@ -1636,6 +1666,69 @@ async def name_4():
16361666
pass
16371667
[[]]
16381668

1669+
def test_invalid_with_usages(self):
1670+
def f(obj):
1671+
with obj:
1672+
pass
1673+
1674+
with self.assertRaisesRegex(TypeError, re.escape((
1675+
"object does not support the context manager protocol "
1676+
"(missed __exit__ method)"
1677+
))):
1678+
f(DummyEnter())
1679+
1680+
with self.assertRaisesRegex(TypeError, re.escape((
1681+
"object does not support the context manager protocol "
1682+
"(missed __enter__ method)"
1683+
))):
1684+
f(DummyExit())
1685+
1686+
# a missing __exit__ is reported missing before a missing __enter__
1687+
with self.assertRaisesRegex(TypeError, re.escape((
1688+
"object does not support the context manager protocol "
1689+
"(missed __exit__ method)"
1690+
))):
1691+
f(object())
1692+
1693+
with self.assertRaisesRegex(TypeError, re.escape((
1694+
"object does not support the context manager protocol "
1695+
"(missed __exit__ method) but it supports the asynchronous "
1696+
"context manager protocol. Did you mean to use 'async with'?"
1697+
))):
1698+
f(AsyncDummy())
1699+
1700+
def test_invalid_async_with_usages(self):
1701+
async def f(obj):
1702+
async with obj:
1703+
pass
1704+
1705+
with self.assertRaisesRegex(TypeError, re.escape((
1706+
"object does not support the asynchronous context manager protocol "
1707+
"(missed __aexit__ method)"
1708+
))):
1709+
f(AsyncDummyEnter()).send(None)
1710+
1711+
with self.assertRaisesRegex(TypeError, re.escape((
1712+
"object does not support the asynchronous context manager protocol "
1713+
"(missed __aenter__ method)"
1714+
))):
1715+
f(AsyncDummyExit()).send(None)
1716+
1717+
# a missing __aexit__ is reported missing before a missing __aenter__
1718+
with self.assertRaisesRegex(TypeError, re.escape((
1719+
"object does not support the asynchronous context manager protocol "
1720+
"(missed __aexit__ method)"
1721+
))):
1722+
f(object()).send(None)
1723+
1724+
with self.assertRaisesRegex(TypeError, re.escape((
1725+
"object does not support the asynchronous context manager protocol "
1726+
"(missed __aexit__ method) but it supports the context manager "
1727+
"protocol. Did you mean to use 'with'?"
1728+
))):
1729+
f(SyncDummy()).send(None)
1730+
1731+
16391732
class TestBooleanExpression(unittest.TestCase):
16401733
class Value:
16411734
def __init__(self):

Lib/unittest/async_case.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,17 @@ async def enterAsyncContext(self, cm):
7575
enter = cls.__aenter__
7676
exit = cls.__aexit__
7777
except AttributeError:
78-
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
79-
f"not support the asynchronous context manager protocol"
80-
) from None
78+
msg = (f"'{cls.__module__}.{cls.__qualname__}' object does "
79+
"not support the asynchronous context manager protocol")
80+
try:
81+
cls.__enter__
82+
cls.__exit__
83+
except AttributeError:
84+
pass
85+
else:
86+
msg += (" but it supports the context manager protocol. "
87+
"Did you mean to use enterContext()?")
88+
raise TypeError(msg) from None
8189
result = await enter(cm)
8290
self.addAsyncCleanup(exit, cm, None, None, None)
8391
return result

Lib/unittest/case.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,17 @@ def _enter_context(cm, addcleanup):
111111
enter = cls.__enter__
112112
exit = cls.__exit__
113113
except AttributeError:
114-
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
115-
f"not support the context manager protocol") from None
114+
msg = (f"'{cls.__module__}.{cls.__qualname__}' object does "
115+
"not support the context manager protocol")
116+
try:
117+
cls.__aenter__
118+
cls.__aexit__
119+
except AttributeError:
120+
pass
121+
else:
122+
msg += (" but it supports the asynchronous context manager "
123+
"protocol. Did you mean to use enterAsyncContext()?")
124+
raise TypeError(msg) from None
116125
result = enter(cm)
117126
addcleanup(exit, cm, None, None, None)
118127
return result
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve error message when an object supporting the synchronous (resp.
2+
asynchronous) context manager protocol is entered using :keyword:`async
3+
with` (resp. :keyword:`with`) block instead of :keyword:`with` (resp.
4+
:keyword:`async with`). Patch by Bénédikt Tran.

Python/bytecodes.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3373,9 +3373,12 @@ dummy_func(
33733373
PyObject *attr_o = _PyObject_LookupSpecialMethod(owner_o, name, &self_or_null_o);
33743374
if (attr_o == NULL) {
33753375
if (!_PyErr_Occurred(tstate)) {
3376-
_PyErr_Format(tstate, PyExc_TypeError,
3377-
_Py_SpecialMethods[oparg].error,
3378-
Py_TYPE(owner_o)->tp_name);
3376+
const char *errfmt = _PyEval_SpecialMethodCanSuggest(owner_o, oparg)
3377+
? _Py_SpecialMethods[oparg].error_suggestion
3378+
: _Py_SpecialMethods[oparg].error;
3379+
assert(!_PyErr_Occurred(tstate));
3380+
assert(errfmt != NULL);
3381+
_PyErr_Format(tstate, PyExc_TypeError, errfmt, owner_o);
33793382
}
33803383
ERROR_IF(true, error);
33813384
}

Python/ceval.c

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -545,23 +545,51 @@ const conversion_func _PyEval_ConversionFuncs[4] = {
545545
const _Py_SpecialMethod _Py_SpecialMethods[] = {
546546
[SPECIAL___ENTER__] = {
547547
.name = &_Py_ID(__enter__),
548-
.error = "'%.200s' object does not support the "
549-
"context manager protocol (missed __enter__ method)",
548+
.error = (
549+
"'%T' object does not support the context manager protocol "
550+
"(missed __enter__ method)"
551+
),
552+
.error_suggestion = (
553+
"'%T' object does not support the context manager protocol "
554+
"(missed __enter__ method) but it supports the asynchronous "
555+
"context manager protocol. Did you mean to use 'async with'?"
556+
)
550557
},
551558
[SPECIAL___EXIT__] = {
552559
.name = &_Py_ID(__exit__),
553-
.error = "'%.200s' object does not support the "
554-
"context manager protocol (missed __exit__ method)",
560+
.error = (
561+
"'%T' object does not support the context manager protocol "
562+
"(missed __exit__ method)"
563+
),
564+
.error_suggestion = (
565+
"'%T' object does not support the context manager protocol "
566+
"(missed __exit__ method) but it supports the asynchronous "
567+
"context manager protocol. Did you mean to use 'async with'?"
568+
)
555569
},
556570
[SPECIAL___AENTER__] = {
557571
.name = &_Py_ID(__aenter__),
558-
.error = "'%.200s' object does not support the asynchronous "
559-
"context manager protocol (missed __aenter__ method)",
572+
.error = (
573+
"'%T' object does not support the asynchronous "
574+
"context manager protocol (missed __aenter__ method)"
575+
),
576+
.error_suggestion = (
577+
"'%T' object does not support the asynchronous context manager "
578+
"protocol (missed __aenter__ method) but it supports the context "
579+
"manager protocol. Did you mean to use 'with'?"
580+
)
560581
},
561582
[SPECIAL___AEXIT__] = {
562583
.name = &_Py_ID(__aexit__),
563-
.error = "'%.200s' object does not support the asynchronous "
564-
"context manager protocol (missed __aexit__ method)",
584+
.error = (
585+
"'%T' object does not support the asynchronous "
586+
"context manager protocol (missed __aexit__ method)"
587+
),
588+
.error_suggestion = (
589+
"'%T' object does not support the asynchronous context manager "
590+
"protocol (missed __aexit__ method) but it supports the context "
591+
"manager protocol. Did you mean to use 'with'?"
592+
)
565593
}
566594
};
567595

@@ -3363,3 +3391,33 @@ _PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *na
33633391
}
33643392
return value;
33653393
}
3394+
3395+
/* Check if a 'cls' provides the given special method. */
3396+
static inline int
3397+
type_has_special_method(PyTypeObject *cls, PyObject *name)
3398+
{
3399+
// _PyType_Lookup() does not set an exception and returns a borrowed ref
3400+
assert(!PyErr_Occurred());
3401+
PyObject *r = _PyType_Lookup(cls, name);
3402+
return r != NULL && Py_TYPE(r)->tp_descr_get != NULL;
3403+
}
3404+
3405+
int
3406+
_PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg)
3407+
{
3408+
PyTypeObject *type = Py_TYPE(self);
3409+
switch (oparg) {
3410+
case SPECIAL___ENTER__:
3411+
case SPECIAL___EXIT__: {
3412+
return type_has_special_method(type, &_Py_ID(__aenter__))
3413+
&& type_has_special_method(type, &_Py_ID(__aexit__));
3414+
}
3415+
case SPECIAL___AENTER__:
3416+
case SPECIAL___AEXIT__: {
3417+
return type_has_special_method(type, &_Py_ID(__enter__))
3418+
&& type_has_special_method(type, &_Py_ID(__exit__));
3419+
}
3420+
default:
3421+
Py_FatalError("unsupported special method");
3422+
}
3423+
}

Python/executor_cases.c.h

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/generated_cases.c.h

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)