From aff8184ad8c56a3edb5da4f8a1ed9ad41353a5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:44:02 +0200 Subject: [PATCH 1/9] improve error messages when incorrectly using context managers --- Include/internal/pycore_ceval.h | 3 + Lib/test/test_compile.py | 93 +++++++++++++++++++ Lib/unittest/async_case.py | 14 ++- Lib/unittest/case.py | 13 ++- ...-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst | 4 + Python/bytecodes.c | 9 +- Python/ceval.c | 74 +++++++++++++-- Python/executor_cases.c.h | 11 ++- Python/generated_cases.c.h | 11 ++- 9 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 18c8bc0624fea7..a6b26fd9398a26 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -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,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) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index ce9c060641d6c5..889bcae6d0356f 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -5,6 +5,7 @@ import math import opcode import os +import re import unittest import sys import ast @@ -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): @@ -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): diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 6000af1cef0a78..a1c0d6c368ce8a 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -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 diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 10c3b7e122371e..884fc1b21f64d8 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -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 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst new file mode 100644 index 00000000000000..861adcd939ca2c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst @@ -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. diff --git a/Python/bytecodes.c b/Python/bytecodes.c index d17cac2473b101..b32255e57ad393 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -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); } diff --git a/Python/ceval.c b/Python/ceval.c index a59b2b7a16866d..61648087155b17 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -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'?" + ) } }; @@ -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 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"); + } +} diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 497aa909b329c1..06022fc3ce0890 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -4391,9 +4391,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(); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index fa3de197f4bcab..90769fe7b69344 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -9288,9 +9288,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); From 69dc81ff64d830618f949124281ae436217886a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 13 Apr 2025 17:56:54 +0200 Subject: [PATCH 2/9] move tests --- Lib/test/test_compile.py | 28 ---------- Lib/test/test_with.py | 107 +++++++++++++++++++++++++++++---------- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index b2b969c8633565..bcd5bcdf5051d7 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -26,34 +26,6 @@ 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): diff --git a/Lib/test/test_with.py b/Lib/test/test_with.py index e3e2de09496728..a342858c379267 100644 --- a/Lib/test/test_with.py +++ b/Lib/test/test_with.py @@ -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 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 @@ 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): @@ -190,6 +244,7 @@ def shouldThrow(): pass self.assertRaises(RuntimeError, shouldThrow) + class ContextmanagerAssertionMixin(object): def setUp(self): From 6fce800c41f4c18aeb0e2e8f778424e46571dc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 13 Apr 2025 17:58:35 +0200 Subject: [PATCH 3/9] restore file --- Lib/test/test_compile.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index bcd5bcdf5051d7..0377b3f954f44b 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -5,7 +5,6 @@ import math import opcode import os -import re import unittest import sys import ast @@ -25,7 +24,6 @@ from test.support.bytecode_helper import instructions_with_positions from test.support.os_helper import FakePath - class TestSpecifics(unittest.TestCase): def compile_single(self, source): @@ -1666,7 +1664,6 @@ def test_compile_warnings(self): self.assertEqual(len(caught), 2) - class TestBooleanExpression(unittest.TestCase): class Value: def __init__(self): From a1303a41effe2acda1c0f36b1bbba10860b82131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:09:32 +0200 Subject: [PATCH 4/9] reduce symbol visibility --- Include/internal/pycore_ceval.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index a6b26fd9398a26..2920745f7ca805 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -310,7 +310,13 @@ 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); +/* + * Indicate whether a special method of given 'oparg' can use the (improved) + * alternative errror message instead. Only methods loaded by LOAD_SPECIAL + * support alternative errror messages. + */ +extern int +_PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg); /* Bits that can be set in PyThreadState.eval_breaker */ #define _PY_GIL_DROP_REQUEST_BIT (1U << 0) From f77056e8b832a5aad050afd17f4ebcd64ac25fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:11:35 +0200 Subject: [PATCH 5/9] fix typo --- Include/internal/pycore_ceval.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 2920745f7ca805..cae85ec42b880e 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -312,8 +312,8 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args); /* * Indicate whether a special method of given 'oparg' can use the (improved) - * alternative errror message instead. Only methods loaded by LOAD_SPECIAL - * support alternative errror messages. + * alternative error message instead. Only methods loaded by LOAD_SPECIAL + * support alternative error messages. */ extern int _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg); From d617d898657527e209be39c174733331db8464aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:12:29 +0200 Subject: [PATCH 6/9] Update Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst --- .../2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst index 861adcd939ca2c..792332db6ef4c3 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-07-13-46-57.gh-issue-128398.gJ2zIF.rst @@ -1,4 +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. +with` (resp. :keyword:`with`) instead of :keyword:`with` (resp. :keyword:`async with`). Patch by Bénédikt Tran. From 018e4ae0b16d4c08b02ca759cc86b70a499aa5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:13:34 +0200 Subject: [PATCH 7/9] add What's New --- Doc/whatsnew/3.14.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 7d469e83dc27ad..1b489f150e74fe 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -474,6 +474,13 @@ Other language changes explicitly overridden in the subclass. (Contributed by Tomasz Pytel in :gh:`132329`.) +* 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: PEP 765: Disallow return/break/continue that exit a finally block From bbf6d2e168c0d730a4b45147e4775e9c3170f6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:23:42 +0200 Subject: [PATCH 8/9] Update Include/internal/pycore_ceval.h --- Include/internal/pycore_ceval.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index cae85ec42b880e..474b2722a4ad8a 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -314,6 +314,8 @@ _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). */ extern int _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg); From 34ad9a45e2a8ae3345a64aa43397dd86b8a5b10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:23:54 +0200 Subject: [PATCH 9/9] Update Include/internal/pycore_ceval.h --- Include/internal/pycore_ceval.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 474b2722a4ad8a..96ba54b274c804 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -317,7 +317,7 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args); * * Symbol is exported for the JIT (see discussion on GH-132218). */ -extern int +PyAPI_FUNC(int) _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg); /* Bits that can be set in PyThreadState.eval_breaker */