diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index f003a5837ae0eb..20dd16dd05d12d 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -329,6 +329,22 @@ ... TypeError: Value after ** must be a mapping, not function + >>> class OnlyKeys: + ... def keys(self): + ... return ['key'] + >>> h(**OnlyKeys()) + Traceback (most recent call last): + ... + TypeError: 'OnlyKeys' object is not subscriptable + + >>> class BrokenKeys: + ... def keys(self): + ... return 1 + >>> h(**BrokenKeys()) + Traceback (most recent call last): + ... + TypeError: test.test_extcall.BrokenKeys.keys() must return an iterable, not int + >>> dir(b=1, **{'b': 1}) Traceback (most recent call last): ... @@ -540,6 +556,151 @@ """ +def test_errors_in_iter(): + """ + >>> class A: + ... def __iter__(self): + ... raise exc + ... + >>> def f(*args, **kwargs): pass + >>> exc = ZeroDivisionError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_next(): + """ + >>> class I: + ... def __iter__(self): + ... return self + ... def __next__(self): + ... raise exc + ... + >>> class A: + ... def __iter__(self): + ... return I() + ... + + >>> def f(*args, **kwargs): pass + >>> exc = ZeroDivisionError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> f(*A()) + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_keys(): + """ + >>> class D: + ... def keys(self): + ... raise exc + ... + >>> def f(*args, **kwargs): pass + >>> exc = ZeroDivisionError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_keys_next(): + """ + >>> class I: + ... def __iter__(self): + ... return self + ... def __next__(self): + ... raise exc + ... + >>> class D: + ... def keys(self): + ... return I() + ... + >>> def f(*args, **kwargs): pass + >>> exc = ZeroDivisionError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_getitem(): + """ + >>> class D: + ... def keys(self): + ... return ['key'] + ... def __getitem__(self, key): + ... raise exc + ... + >>> def f(*args, **kwargs): pass + >>> exc = ZeroDivisionError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> f(**D()) + Traceback (most recent call last): + ... + TypeError: some error + """ + import doctest import unittest diff --git a/Lib/test/test_unpack_ex.py b/Lib/test/test_unpack_ex.py index 904cf4f626ae78..d3ba8133c41d57 100644 --- a/Lib/test/test_unpack_ex.py +++ b/Lib/test/test_unpack_ex.py @@ -134,6 +134,22 @@ ... TypeError: 'list' object is not a mapping + >>> class OnlyKeys: + ... def keys(self): + ... return ['key'] + >>> {**OnlyKeys()} + Traceback (most recent call last): + ... + TypeError: 'OnlyKeys' object is not subscriptable + + >>> class BrokenKeys: + ... def keys(self): + ... return 1 + >>> {**BrokenKeys()} + Traceback (most recent call last): + ... + TypeError: test.test_unpack_ex.BrokenKeys.keys() must return an iterable, not int + >>> len(eval("{" + ", ".join("**{{{}: {}}}".format(i, i) ... for i in range(1000)) + "}")) 1000 @@ -560,6 +576,176 @@ """ +def test_errors_in_iter(): + """ + >>> class A: + ... def __iter__(self): + ... raise exc + ... + >>> exc = ZeroDivisionError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + AttributeError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + TypeError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_next(): + """ + >>> class I: + ... def __iter__(self): + ... return self + ... def __next__(self): + ... raise exc + ... + >>> class A: + ... def __iter__(self): + ... return I() + ... + + >>> exc = ZeroDivisionError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + AttributeError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> [*A()] + Traceback (most recent call last): + ... + TypeError: some error + + >>> {*A()} + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_keys(): + """ + >>> class D: + ... def keys(self): + ... raise exc + ... + >>> exc = ZeroDivisionError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_keys_next(): + """ + >>> class I: + ... def __iter__(self): + ... return self + ... def __next__(self): + ... raise exc + ... + >>> class D: + ... def keys(self): + ... return I() + ... + >>> exc = ZeroDivisionError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + TypeError: some error + """ + +def test_errors_in_getitem(): + """ + >>> class D: + ... def keys(self): + ... return ['key'] + ... def __getitem__(self, key): + ... raise exc + ... + >>> exc = ZeroDivisionError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + ZeroDivisionError: some error + + >>> exc = AttributeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + AttributeError: some error + + >>> exc = TypeError('some error') + >>> {**D()} + Traceback (most recent call last): + ... + TypeError: some error + """ + __test__ = {'doctests' : doctests} def load_tests(loader, tests, pattern): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst new file mode 100644 index 00000000000000..7923d80953fd53 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst @@ -0,0 +1,3 @@ +:exc:`AttributeError`\ s raised in :meth:`!keys` or :meth:`!__getitem__` +during dictionary unpacking (``{**mymapping}`` or ``func(**mymapping)``) are +no longer masked by :exc:`TypeError`. diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index b025f7b0eb7fe5..e7e796077811d9 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -5569,10 +5569,22 @@ stack_pointer = _PyFrame_GetStackPointer(frame); if (matches) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_Format(tstate, PyExc_TypeError, - "'%.200s' object is not a mapping", - Py_TYPE(update_o)->tp_name); + PyObject *exc = _PyErr_GetRaisedException(tstate); + int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys)); stack_pointer = _PyFrame_GetStackPointer(frame); + if (has_keys == 0) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_Format(tstate, PyExc_TypeError, + "'%T' object is not a mapping", + update_o); + Py_DECREF(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_ChainExceptions1(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 0eff5f76740f78..b9fca0759ffb5e 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2244,9 +2244,17 @@ dummy_func( if (err < 0) { int matches = _PyErr_ExceptionMatches(tstate, PyExc_AttributeError); if (matches) { - _PyErr_Format(tstate, PyExc_TypeError, - "'%.200s' object is not a mapping", - Py_TYPE(update_o)->tp_name); + PyObject *exc = _PyErr_GetRaisedException(tstate); + int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys)); + if (has_keys == 0) { + _PyErr_Format(tstate, PyExc_TypeError, + "'%T' object is not a mapping", + update_o); + Py_DECREF(exc); + } + else { + _PyErr_ChainExceptions1(exc); + } } PyStackRef_CLOSE(update); ERROR_IF(true); diff --git a/Python/ceval.c b/Python/ceval.c index 950050a6027116..a197056759f919 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3410,10 +3410,18 @@ _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwarg * is not a mapping. */ if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) { - _PyErr_Format( - tstate, PyExc_TypeError, - "Value after ** must be a mapping, not %.200s", - Py_TYPE(kwargs)->tp_name); + PyObject *exc = _PyErr_GetRaisedException(tstate); + int has_keys = PyObject_HasAttrWithError(kwargs, &_Py_ID(keys)); + if (has_keys == 0) { + _PyErr_Format( + tstate, PyExc_TypeError, + "Value after ** must be a mapping, not %T", + kwargs); + Py_DECREF(exc); + } + else { + _PyErr_ChainExceptions1Tstate(tstate, exc); + } } else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { PyObject *exc = _PyErr_GetRaisedException(tstate); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 032d6faeda6a96..a89742097ca441 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -8757,10 +8757,22 @@ stack_pointer = _PyFrame_GetStackPointer(frame); if (matches) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_Format(tstate, PyExc_TypeError, - "'%.200s' object is not a mapping", - Py_TYPE(update_o)->tp_name); + PyObject *exc = _PyErr_GetRaisedException(tstate); + int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys)); stack_pointer = _PyFrame_GetStackPointer(frame); + if (has_keys == 0) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_Format(tstate, PyExc_TypeError, + "'%T' object is not a mapping", + update_o); + Py_DECREF(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_ChainExceptions1(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index de036fb964b691..58559c061366c7 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -5569,10 +5569,22 @@ stack_pointer = _PyFrame_GetStackPointer(frame); if (matches) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyErr_Format(tstate, PyExc_TypeError, - "'%.200s' object is not a mapping", - Py_TYPE(update_o)->tp_name); + PyObject *exc = _PyErr_GetRaisedException(tstate); + int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys)); stack_pointer = _PyFrame_GetStackPointer(frame); + if (has_keys == 0) { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_Format(tstate, PyExc_TypeError, + "'%T' object is not a mapping", + update_o); + Py_DECREF(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } + else { + _PyFrame_SetStackPointer(frame, stack_pointer); + _PyErr_ChainExceptions1(exc); + stack_pointer = _PyFrame_GetStackPointer(frame); + } } stack_pointer += -1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__);