Skip to content

Commit 9574d21

Browse files
committed
gh-145876: Fix AttributeError masked during dict unpacking
1 parent cd52172 commit 9574d21

File tree

8 files changed

+244
-38
lines changed

8 files changed

+244
-38
lines changed

Lib/test/test_extcall.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,30 @@
334334
...
335335
TypeError: dir() got multiple values for keyword argument 'b'
336336
337+
AttributeError raised inside keys() or __getitem__() should not be masked
338+
(see https://github.com/python/cpython/issues/145876)
339+
340+
>>> class KeysRaisesAttributeError:
341+
... def keys(self):
342+
... raise AttributeError("error in keys")
343+
... def __getitem__(self, key):
344+
... return key
345+
>>> def f(**kwargs): pass
346+
>>> f(**KeysRaisesAttributeError())
347+
Traceback (most recent call last):
348+
...
349+
AttributeError: error in keys
350+
351+
>>> class GetitemRaisesAttributeError:
352+
... def keys(self):
353+
... return ['a', 'b']
354+
... def __getitem__(self, key):
355+
... raise AttributeError("error in __getitem__")
356+
>>> f(**GetitemRaisesAttributeError())
357+
Traceback (most recent call last):
358+
...
359+
AttributeError: error in __getitem__
360+
337361
Test a kwargs mapping with duplicated keys.
338362
339363
>>> from collections.abc import Mapping

Lib/test/test_unpack_ex.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,29 @@
134134
...
135135
TypeError: 'list' object is not a mapping
136136
137+
AttributeError raised inside keys() or __getitem__() should not be masked
138+
(see https://github.com/python/cpython/issues/145876)
139+
140+
>>> class KeysRaisesAttributeError:
141+
... def keys(self):
142+
... raise AttributeError("error in keys")
143+
... def __getitem__(self, key):
144+
... return key
145+
>>> {**KeysRaisesAttributeError()}
146+
Traceback (most recent call last):
147+
...
148+
AttributeError: error in keys
149+
150+
>>> class GetitemRaisesAttributeError:
151+
... def keys(self):
152+
... return ['a', 'b']
153+
... def __getitem__(self, key):
154+
... raise AttributeError("error in __getitem__")
155+
>>> {**GetitemRaisesAttributeError()}
156+
Traceback (most recent call last):
157+
...
158+
AttributeError: error in __getitem__
159+
137160
>>> len(eval("{" + ", ".join("**{{{}: {}}}".format(i, i)
138161
... for i in range(1000)) + "}"))
139162
1000
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :exc:`AttributeError` raised inside ``.keys()`` or ``.__getitem__()``
2+
being incorrectly masked as :exc:`TypeError` during ``{**mapping}`` and
3+
``f(**mapping)`` unpacking. Patched by Shamil Abdulaev.

Modules/_testinternalcapi/test_cases.c.h

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

Python/bytecodes.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,14 +2240,23 @@ dummy_func(
22402240
PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
22412241
PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
22422242

2243-
int err = PyDict_Update(dict_o, update_o);
2244-
if (err < 0) {
2245-
int matches = _PyErr_ExceptionMatches(tstate, PyExc_AttributeError);
2246-
if (matches) {
2243+
if (!PyAnyDict_Check(update_o)) {
2244+
int has_keys = PyObject_HasAttrWithError(
2245+
update_o, &_Py_ID(keys));
2246+
if (has_keys < 0) {
2247+
PyStackRef_CLOSE(update);
2248+
ERROR_IF(true);
2249+
}
2250+
if (!has_keys) {
22472251
_PyErr_Format(tstate, PyExc_TypeError,
22482252
"'%.200s' object is not a mapping",
22492253
Py_TYPE(update_o)->tp_name);
2254+
PyStackRef_CLOSE(update);
2255+
ERROR_IF(true);
22502256
}
2257+
}
2258+
int err = PyDict_Update(dict_o, update_o);
2259+
if (err < 0) {
22512260
PyStackRef_CLOSE(update);
22522261
ERROR_IF(true);
22532262
}
@@ -2259,6 +2268,21 @@ dummy_func(
22592268
PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
22602269
PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
22612270

2271+
if (!PyAnyDict_Check(update_o)) {
2272+
int has_keys = PyObject_HasAttrWithError(
2273+
update_o, &_Py_ID(keys));
2274+
if (has_keys < 0) {
2275+
PyStackRef_CLOSE(update);
2276+
ERROR_IF(true);
2277+
}
2278+
if (!has_keys) {
2279+
_PyErr_Format(tstate, PyExc_TypeError,
2280+
"Value after ** must be a mapping, not %.200s",
2281+
Py_TYPE(update_o)->tp_name);
2282+
PyStackRef_CLOSE(update);
2283+
ERROR_IF(true);
2284+
}
2285+
}
22622286
int err = _PyDict_MergeEx(dict_o, update_o, 2);
22632287
if (err < 0) {
22642288
_PyEval_FormatKwargsError(tstate, callable_o, update_o);

Python/ceval.c

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3403,19 +3403,7 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args)
34033403
void
34043404
_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
34053405
{
3406-
/* _PyDict_MergeEx raises attribute
3407-
* error (percolated from an attempt
3408-
* to get 'keys' attribute) instead of
3409-
* a type error if its second argument
3410-
* is not a mapping.
3411-
*/
3412-
if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
3413-
_PyErr_Format(
3414-
tstate, PyExc_TypeError,
3415-
"Value after ** must be a mapping, not %.200s",
3416-
Py_TYPE(kwargs)->tp_name);
3417-
}
3418-
else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
3406+
if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
34193407
PyObject *exc = _PyErr_GetRaisedException(tstate);
34203408
PyObject *args = PyException_GetArgs(exc);
34213409
if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) {

Python/executor_cases.c.h

Lines changed: 65 additions & 9 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: 50 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)