Skip to content
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
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