Skip to content

Commit 6932c3e

Browse files
gh-145876: Do not mask KeyErrors raised during dictionary unpacking in call (GH-146472)
KeyErrors raised in keys() or __getitem__() during dictionary unpacking in call (func(**mymapping)) are no longer masked by TypeError.
1 parent 1af025d commit 6932c3e

File tree

11 files changed

+72
-74
lines changed

11 files changed

+72
-74
lines changed

Include/internal/pycore_ceval.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ PyAPI_FUNC(int) _PyEval_ExceptionGroupMatch(_PyInterpreterFrame *, PyObject* exc
302302
PyAPI_FUNC(void) _PyEval_FormatAwaitableError(PyThreadState *tstate, PyTypeObject *type, int oparg);
303303
PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc, const char *format_str, PyObject *obj);
304304
PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg);
305-
PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs);
305+
PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey);
306306
PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *);
307307

308308
PyAPI_FUNC(PyObject *) _PyEval_LazyImportName(

Include/internal/pycore_dict.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extern Py_ssize_t _PyDict_SizeOf_LockHeld(PyDictObject *);
5555
of a key wins, if override is 2, a KeyError with conflicting key as
5656
argument is raised.
5757
*/
58-
PyAPI_FUNC(int) _PyDict_MergeEx(PyObject *mp, PyObject *other, int override);
58+
PyAPI_FUNC(int) _PyDict_MergeUniq(PyObject *mp, PyObject *other, PyObject **dupkey);
5959

6060
extern void _PyDict_DebugMallocStats(FILE *out);
6161

Lib/test/test_extcall.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -634,11 +634,11 @@ def test_errors_in_keys():
634634
...
635635
AttributeError: some error
636636
637-
>>> exc = TypeError('some error')
637+
>>> exc = KeyError('some error')
638638
>>> f(**D())
639639
Traceback (most recent call last):
640640
...
641-
TypeError: some error
641+
KeyError: 'some error'
642642
"""
643643

644644
def test_errors_in_keys_next():
@@ -666,11 +666,11 @@ def test_errors_in_keys_next():
666666
...
667667
AttributeError: some error
668668
669-
>>> exc = TypeError('some error')
669+
>>> exc = KeyError('some error')
670670
>>> f(**D())
671671
Traceback (most recent call last):
672672
...
673-
TypeError: some error
673+
KeyError: 'some error'
674674
"""
675675

676676
def test_errors_in_getitem():
@@ -694,11 +694,11 @@ def test_errors_in_getitem():
694694
...
695695
AttributeError: some error
696696
697-
>>> exc = TypeError('some error')
697+
>>> exc = KeyError('some error')
698698
>>> f(**D())
699699
Traceback (most recent call last):
700700
...
701-
TypeError: some error
701+
KeyError: 'some error'
702702
"""
703703

704704
import doctest

Lib/test/test_unpack_ex.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -681,11 +681,11 @@ def test_errors_in_keys():
681681
...
682682
AttributeError: some error
683683
684-
>>> exc = TypeError('some error')
684+
>>> exc = KeyError('some error')
685685
>>> {**D()}
686686
Traceback (most recent call last):
687687
...
688-
TypeError: some error
688+
KeyError: 'some error'
689689
"""
690690

691691
def test_errors_in_keys_next():
@@ -712,11 +712,11 @@ def test_errors_in_keys_next():
712712
...
713713
AttributeError: some error
714714
715-
>>> exc = TypeError('some error')
715+
>>> exc = KeyError('some error')
716716
>>> {**D()}
717717
Traceback (most recent call last):
718718
...
719-
TypeError: some error
719+
KeyError: 'some error'
720720
"""
721721

722722
def test_errors_in_getitem():
@@ -739,11 +739,11 @@ def test_errors_in_getitem():
739739
...
740740
AttributeError: some error
741741
742-
>>> exc = TypeError('some error')
742+
>>> exc = KeyError('some error')
743743
>>> {**D()}
744744
Traceback (most recent call last):
745745
...
746-
TypeError: some error
746+
KeyError: 'some error'
747747
"""
748748

749749
__test__ = {'doctests' : doctests}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
:exc:`AttributeError`\ s raised in :meth:`!keys` or :meth:`!__getitem__`
1+
:exc:`AttributeError`\ s and :exc:`KeyError`\ s raised in :meth:`!keys` or :meth:`!__getitem__`
22
during dictionary unpacking (``{**mymapping}`` or ``func(**mymapping)``) are
33
no longer masked by :exc:`TypeError`.

Modules/_testinternalcapi/test_cases.c.h

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

Objects/dictobject.c

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ As a consequence of this, split keys have a maximum size of 16.
139139
static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
140140
PyObject *kwds);
141141
static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
142-
static int dict_merge(PyObject *a, PyObject *b, int override);
142+
static int dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey);
143143
static int dict_contains(PyObject *op, PyObject *key);
144144
static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override);
145145

@@ -3391,7 +3391,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)
33913391
Py_DECREF(d);
33923392
return NULL;
33933393
}
3394-
if (dict_merge(copy, d, 1) < 0) {
3394+
if (dict_merge(copy, d, 1, NULL) < 0) {
33953395
Py_DECREF(d);
33963396
Py_DECREF(copy);
33973397
return NULL;
@@ -3887,14 +3887,14 @@ static int
38873887
dict_update_arg(PyObject *self, PyObject *arg)
38883888
{
38893889
if (PyAnyDict_CheckExact(arg)) {
3890-
return dict_merge(self, arg, 1);
3890+
return dict_merge(self, arg, 1, NULL);
38913891
}
38923892
int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys));
38933893
if (has_keys < 0) {
38943894
return -1;
38953895
}
38963896
if (has_keys) {
3897-
return dict_merge(self, arg, 1);
3897+
return dict_merge(self, arg, 1, NULL);
38983898
}
38993899
return dict_merge_from_seq2(self, arg, 1);
39003900
}
@@ -3915,7 +3915,7 @@ dict_update_common(PyObject *self, PyObject *args, PyObject *kwds,
39153915

39163916
if (result == 0 && kwds != NULL) {
39173917
if (PyArg_ValidateKeywordArguments(kwds))
3918-
result = dict_merge(self, kwds, 1);
3918+
result = dict_merge(self, kwds, 1, NULL);
39193919
else
39203920
result = -1;
39213921
}
@@ -4059,7 +4059,7 @@ PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override)
40594059
}
40604060

40614061
static int
4062-
dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
4062+
dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override, PyObject **dupkey)
40634063
{
40644064
assert(can_modify_dict(mp));
40654065
ASSERT_DICT_LOCKED(other);
@@ -4068,10 +4068,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
40684068
/* a.update(a) or a.update({}); nothing to do */
40694069
return 0;
40704070
if (mp->ma_used == 0) {
4071-
/* Since the target dict is empty, PyDict_GetItem()
4072-
* always returns NULL. Setting override to 1
4073-
* skips the unnecessary test.
4074-
*/
4071+
/* Since the target dict is empty, _PyDict_Contains_KnownHash()
4072+
* always returns 0. Setting override to 1
4073+
* skips the unnecessary test.
4074+
*/
40754075
override = 1;
40764076
PyDictKeysObject *okeys = other->ma_keys;
40774077

@@ -4131,11 +4131,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
41314131
err = insertdict(mp, Py_NewRef(key), hash, Py_NewRef(value));
41324132
}
41334133
else if (err > 0) {
4134-
if (override != 0) {
4135-
_PyErr_SetKeyError(key);
4134+
if (dupkey != NULL) {
4135+
*dupkey = key;
41364136
Py_DECREF(value);
4137-
Py_DECREF(key);
4138-
return -1;
4137+
return -2;
41394138
}
41404139
err = 0;
41414140
}
@@ -4155,7 +4154,7 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
41554154
}
41564155

41574156
static int
4158-
dict_merge(PyObject *a, PyObject *b, int override)
4157+
dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey)
41594158
{
41604159
assert(a != NULL);
41614160
assert(b != NULL);
@@ -4167,7 +4166,7 @@ dict_merge(PyObject *a, PyObject *b, int override)
41674166
PyDictObject *other = (PyDictObject*)b;
41684167
int res;
41694168
Py_BEGIN_CRITICAL_SECTION2(a, b);
4170-
res = dict_dict_merge((PyDictObject *)a, other, override);
4169+
res = dict_dict_merge((PyDictObject *)a, other, override, dupkey);
41714170
ASSERT_CONSISTENT(a);
41724171
Py_END_CRITICAL_SECTION2();
41734172
return res;
@@ -4202,15 +4201,18 @@ dict_merge(PyObject *a, PyObject *b, int override)
42024201
status = dict_contains(a, key);
42034202
if (status != 0) {
42044203
if (status > 0) {
4205-
if (override == 0) {
4204+
if (dupkey == NULL) {
42064205
Py_DECREF(key);
42074206
continue;
42084207
}
4209-
_PyErr_SetKeyError(key);
4208+
*dupkey = key;
4209+
res = -2;
4210+
}
4211+
else {
4212+
Py_DECREF(key);
4213+
res = -1;
42104214
}
4211-
Py_DECREF(key);
42124215
Py_DECREF(iter);
4213-
res = -1;
42144216
goto slow_exit;
42154217
}
42164218
}
@@ -4246,7 +4248,7 @@ dict_merge(PyObject *a, PyObject *b, int override)
42464248
}
42474249

42484250
static int
4249-
dict_merge_api(PyObject *a, PyObject *b, int override)
4251+
dict_merge_api(PyObject *a, PyObject *b, int override, PyObject **dupkey)
42504252
{
42514253
/* We accept for the argument either a concrete dictionary object,
42524254
* or an abstract "mapping" object. For the former, we can do
@@ -4262,26 +4264,26 @@ dict_merge_api(PyObject *a, PyObject *b, int override)
42624264
}
42634265
return -1;
42644266
}
4265-
return dict_merge(a, b, override);
4267+
return dict_merge(a, b, override, dupkey);
42664268
}
42674269

42684270
int
42694271
PyDict_Update(PyObject *a, PyObject *b)
42704272
{
4271-
return dict_merge_api(a, b, 1);
4273+
return dict_merge_api(a, b, 1, NULL);
42724274
}
42734275

42744276
int
42754277
PyDict_Merge(PyObject *a, PyObject *b, int override)
42764278
{
42774279
/* XXX Deprecate override not in (0, 1). */
4278-
return dict_merge_api(a, b, override != 0);
4280+
return dict_merge_api(a, b, override != 0, NULL);
42794281
}
42804282

42814283
int
4282-
_PyDict_MergeEx(PyObject *a, PyObject *b, int override)
4284+
_PyDict_MergeUniq(PyObject *a, PyObject *b, PyObject **dupkey)
42834285
{
4284-
return dict_merge_api(a, b, override);
4286+
return dict_merge_api(a, b, 2, dupkey);
42854287
}
42864288

42874289
/*[clinic input]
@@ -4421,7 +4423,7 @@ copy_lock_held(PyObject *o, int as_frozendict)
44214423
}
44224424
if (copy == NULL)
44234425
return NULL;
4424-
if (dict_merge(copy, o, 1) == 0)
4426+
if (dict_merge(copy, o, 1, NULL) == 0)
44254427
return copy;
44264428
Py_DECREF(copy);
44274429
return NULL;

Python/bytecodes.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2416,10 +2416,12 @@ dummy_func(
24162416
PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable);
24172417
PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
24182418
PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);
2419+
PyObject *dupkey = NULL;
24192420

2420-
int err = _PyDict_MergeEx(dict_o, update_o, 2);
2421+
int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey);
24212422
if (err < 0) {
2422-
_PyEval_FormatKwargsError(tstate, callable_o, update_o);
2423+
_PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey);
2424+
Py_XDECREF(dupkey);
24232425
ERROR_NO_POP();
24242426
}
24252427
u = update;

Python/ceval.c

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3456,9 +3456,18 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args)
34563456
}
34573457

34583458
void
3459-
_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
3460-
{
3461-
/* _PyDict_MergeEx raises attribute
3459+
_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey)
3460+
{
3461+
if (dupkey != NULL) {
3462+
PyObject *funcstr = _PyObject_FunctionStr(func);
3463+
_PyErr_Format(
3464+
tstate, PyExc_TypeError,
3465+
"%V got multiple values for keyword argument '%S'",
3466+
funcstr, "finction", dupkey);
3467+
Py_XDECREF(funcstr);
3468+
return;
3469+
}
3470+
/* _PyDict_MergeUniq raises attribute
34623471
* error (percolated from an attempt
34633472
* to get 'keys' attribute) instead of
34643473
* a type error if its second argument
@@ -3478,27 +3487,6 @@ _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwarg
34783487
_PyErr_ChainExceptions1Tstate(tstate, exc);
34793488
}
34803489
}
3481-
else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
3482-
PyObject *exc = _PyErr_GetRaisedException(tstate);
3483-
PyObject *args = PyException_GetArgs(exc);
3484-
if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) {
3485-
_PyErr_Clear(tstate);
3486-
PyObject *funcstr = _PyObject_FunctionStr(func);
3487-
if (funcstr != NULL) {
3488-
PyObject *key = PyTuple_GET_ITEM(args, 0);
3489-
_PyErr_Format(
3490-
tstate, PyExc_TypeError,
3491-
"%U got multiple values for keyword argument '%S'",
3492-
funcstr, key);
3493-
Py_DECREF(funcstr);
3494-
}
3495-
Py_XDECREF(exc);
3496-
}
3497-
else {
3498-
_PyErr_SetRaisedException(tstate, exc);
3499-
}
3500-
Py_DECREF(args);
3501-
}
35023490
}
35033491

35043492
void

Python/executor_cases.c.h

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

0 commit comments

Comments
 (0)