Skip to content

Commit 79b17f2

Browse files
csm10495gpsheaderlend-aasland
authored
gh-103333: Pickle the keyword attributes of AttributeError (#103352)
* Pickle the `name` and `args` attributes of AttributeError when present. Co-authored-by: Gregory P. Smith <[email protected]> Co-authored-by: Erlend E. Aasland <[email protected]>
1 parent cf720ac commit 79b17f2

File tree

3 files changed

+78
-28
lines changed

3 files changed

+78
-28
lines changed

Lib/test/test_exceptions.py

+34-27
Original file line numberDiff line numberDiff line change
@@ -417,45 +417,45 @@ def testAttributes(self):
417417
# test that exception attributes are happy
418418

419419
exceptionList = [
420-
(BaseException, (), {'args' : ()}),
421-
(BaseException, (1, ), {'args' : (1,)}),
422-
(BaseException, ('foo',),
420+
(BaseException, (), {}, {'args' : ()}),
421+
(BaseException, (1, ), {}, {'args' : (1,)}),
422+
(BaseException, ('foo',), {},
423423
{'args' : ('foo',)}),
424-
(BaseException, ('foo', 1),
424+
(BaseException, ('foo', 1), {},
425425
{'args' : ('foo', 1)}),
426-
(SystemExit, ('foo',),
426+
(SystemExit, ('foo',), {},
427427
{'args' : ('foo',), 'code' : 'foo'}),
428-
(OSError, ('foo',),
428+
(OSError, ('foo',), {},
429429
{'args' : ('foo',), 'filename' : None, 'filename2' : None,
430430
'errno' : None, 'strerror' : None}),
431-
(OSError, ('foo', 'bar'),
431+
(OSError, ('foo', 'bar'), {},
432432
{'args' : ('foo', 'bar'),
433433
'filename' : None, 'filename2' : None,
434434
'errno' : 'foo', 'strerror' : 'bar'}),
435-
(OSError, ('foo', 'bar', 'baz'),
435+
(OSError, ('foo', 'bar', 'baz'), {},
436436
{'args' : ('foo', 'bar'),
437437
'filename' : 'baz', 'filename2' : None,
438438
'errno' : 'foo', 'strerror' : 'bar'}),
439-
(OSError, ('foo', 'bar', 'baz', None, 'quux'),
439+
(OSError, ('foo', 'bar', 'baz', None, 'quux'), {},
440440
{'args' : ('foo', 'bar'), 'filename' : 'baz', 'filename2': 'quux'}),
441-
(OSError, ('errnoStr', 'strErrorStr', 'filenameStr'),
441+
(OSError, ('errnoStr', 'strErrorStr', 'filenameStr'), {},
442442
{'args' : ('errnoStr', 'strErrorStr'),
443443
'strerror' : 'strErrorStr', 'errno' : 'errnoStr',
444444
'filename' : 'filenameStr'}),
445-
(OSError, (1, 'strErrorStr', 'filenameStr'),
445+
(OSError, (1, 'strErrorStr', 'filenameStr'), {},
446446
{'args' : (1, 'strErrorStr'), 'errno' : 1,
447447
'strerror' : 'strErrorStr',
448448
'filename' : 'filenameStr', 'filename2' : None}),
449-
(SyntaxError, (), {'msg' : None, 'text' : None,
449+
(SyntaxError, (), {}, {'msg' : None, 'text' : None,
450450
'filename' : None, 'lineno' : None, 'offset' : None,
451451
'end_offset': None, 'print_file_and_line' : None}),
452-
(SyntaxError, ('msgStr',),
452+
(SyntaxError, ('msgStr',), {},
453453
{'args' : ('msgStr',), 'text' : None,
454454
'print_file_and_line' : None, 'msg' : 'msgStr',
455455
'filename' : None, 'lineno' : None, 'offset' : None,
456456
'end_offset': None}),
457457
(SyntaxError, ('msgStr', ('filenameStr', 'linenoStr', 'offsetStr',
458-
'textStr', 'endLinenoStr', 'endOffsetStr')),
458+
'textStr', 'endLinenoStr', 'endOffsetStr')), {},
459459
{'offset' : 'offsetStr', 'text' : 'textStr',
460460
'args' : ('msgStr', ('filenameStr', 'linenoStr',
461461
'offsetStr', 'textStr',
@@ -465,46 +465,48 @@ def testAttributes(self):
465465
'end_lineno': 'endLinenoStr', 'end_offset': 'endOffsetStr'}),
466466
(SyntaxError, ('msgStr', 'filenameStr', 'linenoStr', 'offsetStr',
467467
'textStr', 'endLinenoStr', 'endOffsetStr',
468-
'print_file_and_lineStr'),
468+
'print_file_and_lineStr'), {},
469469
{'text' : None,
470470
'args' : ('msgStr', 'filenameStr', 'linenoStr', 'offsetStr',
471471
'textStr', 'endLinenoStr', 'endOffsetStr',
472472
'print_file_and_lineStr'),
473473
'print_file_and_line' : None, 'msg' : 'msgStr',
474474
'filename' : None, 'lineno' : None, 'offset' : None,
475475
'end_lineno': None, 'end_offset': None}),
476-
(UnicodeError, (), {'args' : (),}),
476+
(UnicodeError, (), {}, {'args' : (),}),
477477
(UnicodeEncodeError, ('ascii', 'a', 0, 1,
478-
'ordinal not in range'),
478+
'ordinal not in range'), {},
479479
{'args' : ('ascii', 'a', 0, 1,
480480
'ordinal not in range'),
481481
'encoding' : 'ascii', 'object' : 'a',
482482
'start' : 0, 'reason' : 'ordinal not in range'}),
483483
(UnicodeDecodeError, ('ascii', bytearray(b'\xff'), 0, 1,
484-
'ordinal not in range'),
484+
'ordinal not in range'), {},
485485
{'args' : ('ascii', bytearray(b'\xff'), 0, 1,
486486
'ordinal not in range'),
487487
'encoding' : 'ascii', 'object' : b'\xff',
488488
'start' : 0, 'reason' : 'ordinal not in range'}),
489489
(UnicodeDecodeError, ('ascii', b'\xff', 0, 1,
490-
'ordinal not in range'),
490+
'ordinal not in range'), {},
491491
{'args' : ('ascii', b'\xff', 0, 1,
492492
'ordinal not in range'),
493493
'encoding' : 'ascii', 'object' : b'\xff',
494494
'start' : 0, 'reason' : 'ordinal not in range'}),
495-
(UnicodeTranslateError, ("\u3042", 0, 1, "ouch"),
495+
(UnicodeTranslateError, ("\u3042", 0, 1, "ouch"), {},
496496
{'args' : ('\u3042', 0, 1, 'ouch'),
497497
'object' : '\u3042', 'reason' : 'ouch',
498498
'start' : 0, 'end' : 1}),
499-
(NaiveException, ('foo',),
499+
(NaiveException, ('foo',), {},
500500
{'args': ('foo',), 'x': 'foo'}),
501-
(SlottedNaiveException, ('foo',),
501+
(SlottedNaiveException, ('foo',), {},
502502
{'args': ('foo',), 'x': 'foo'}),
503+
(AttributeError, ('foo',), dict(name='name', obj='obj'),
504+
dict(args=('foo',), name='name', obj='obj')),
503505
]
504506
try:
505507
# More tests are in test_WindowsError
506508
exceptionList.append(
507-
(WindowsError, (1, 'strErrorStr', 'filenameStr'),
509+
(WindowsError, (1, 'strErrorStr', 'filenameStr'), {},
508510
{'args' : (1, 'strErrorStr'),
509511
'strerror' : 'strErrorStr', 'winerror' : None,
510512
'errno' : 1,
@@ -513,11 +515,11 @@ def testAttributes(self):
513515
except NameError:
514516
pass
515517

516-
for exc, args, expected in exceptionList:
518+
for exc, args, kwargs, expected in exceptionList:
517519
try:
518-
e = exc(*args)
520+
e = exc(*args, **kwargs)
519521
except:
520-
print("\nexc=%r, args=%r" % (exc, args), file=sys.stderr)
522+
print(f"\nexc={exc!r}, args={args!r}", file=sys.stderr)
521523
# raise
522524
else:
523525
# Verify module name
@@ -540,7 +542,12 @@ def testAttributes(self):
540542
new = p.loads(s)
541543
for checkArgName in expected:
542544
got = repr(getattr(new, checkArgName))
543-
want = repr(expected[checkArgName])
545+
if exc == AttributeError and checkArgName == 'obj':
546+
# See GH-103352, we're not pickling
547+
# obj at this point. So verify it's None.
548+
want = repr(None)
549+
else:
550+
want = repr(expected[checkArgName])
544551
self.assertEqual(got, want,
545552
'pickled "%r", attribute "%s' %
546553
(e, checkArgName))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:exc:`AttributeError` now retains the ``name`` attribute when pickled and unpickled.

Objects/exceptions.c

+43-1
Original file line numberDiff line numberDiff line change
@@ -2287,14 +2287,56 @@ AttributeError_traverse(PyAttributeErrorObject *self, visitproc visit, void *arg
22872287
return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
22882288
}
22892289

2290+
/* Pickling support */
2291+
static PyObject *
2292+
AttributeError_getstate(PyAttributeErrorObject *self, PyObject *Py_UNUSED(ignored))
2293+
{
2294+
PyObject *dict = ((PyAttributeErrorObject *)self)->dict;
2295+
if (self->name || self->args) {
2296+
dict = dict ? PyDict_Copy(dict) : PyDict_New();
2297+
if (dict == NULL) {
2298+
return NULL;
2299+
}
2300+
if (self->name && PyDict_SetItemString(dict, "name", self->name) < 0) {
2301+
Py_DECREF(dict);
2302+
return NULL;
2303+
}
2304+
/* We specifically are not pickling the obj attribute since there are many
2305+
cases where it is unlikely to be picklable. See GH-103352.
2306+
*/
2307+
if (self->args && PyDict_SetItemString(dict, "args", self->args) < 0) {
2308+
Py_DECREF(dict);
2309+
return NULL;
2310+
}
2311+
return dict;
2312+
}
2313+
else if (dict) {
2314+
return Py_NewRef(dict);
2315+
}
2316+
Py_RETURN_NONE;
2317+
}
2318+
2319+
static PyObject *
2320+
AttributeError_reduce(PyAttributeErrorObject *self, PyObject *Py_UNUSED(ignored))
2321+
{
2322+
PyObject *state = AttributeError_getstate(self, NULL);
2323+
if (state == NULL) {
2324+
return NULL;
2325+
}
2326+
2327+
return PyTuple_Pack(3, Py_TYPE(self), self->args, state);
2328+
}
2329+
22902330
static PyMemberDef AttributeError_members[] = {
22912331
{"name", T_OBJECT, offsetof(PyAttributeErrorObject, name), 0, PyDoc_STR("attribute name")},
22922332
{"obj", T_OBJECT, offsetof(PyAttributeErrorObject, obj), 0, PyDoc_STR("object")},
22932333
{NULL} /* Sentinel */
22942334
};
22952335

22962336
static PyMethodDef AttributeError_methods[] = {
2297-
{NULL} /* Sentinel */
2337+
{"__getstate__", (PyCFunction)AttributeError_getstate, METH_NOARGS},
2338+
{"__reduce__", (PyCFunction)AttributeError_reduce, METH_NOARGS },
2339+
{NULL}
22982340
};
22992341

23002342
ComplexExtendsException(PyExc_Exception, AttributeError,

0 commit comments

Comments
 (0)