Skip to content

Commit ce25dc8

Browse files
author
Rémi Lapeyre
committed
Save kwargs given to exceptions constructor
Fix unpickling bug when exception class expect keyword arguments
1 parent 1cffd0e commit ce25dc8

File tree

6 files changed

+231
-19
lines changed

6 files changed

+231
-19
lines changed

Doc/library/exceptions.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ The following exceptions are used mostly as base classes for other exceptions.
8787
assign a special meaning to the elements of this tuple, while others are
8888
usually called only with a single string giving an error message.
8989

90+
.. attribute:: kwargs
91+
92+
The dictionnary of keyword arguments given to the exception constructor.
93+
9094
.. method:: with_traceback(tb)
9195

9296
This method sets *tb* as the new traceback for the exception and returns

Include/cpython/pyerrors.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extern "C" {
1010

1111
/* PyException_HEAD defines the initial segment of every exception class. */
1212
#define PyException_HEAD PyObject_HEAD PyObject *dict;\
13-
PyObject *args; PyObject *traceback;\
13+
PyObject *args; PyObject *kwargs; PyObject *traceback;\
1414
PyObject *context; PyObject *cause;\
1515
char suppress_context;
1616

Lib/test/test_exceptions.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,43 @@ def g():
12991299
next(i)
13001300
next(i)
13011301

1302+
def test_get_kwargs(self):
1303+
self.assertEqual(BaseException().kwargs, {})
1304+
self.assertEqual(NaiveException(x=1).kwargs, {'x': 1})
1305+
1306+
def test_set_kwargs(self):
1307+
b = BaseException()
1308+
b.kwargs = {'x': 1}
1309+
self.assertEqual(b.kwargs, {'x': 1})
1310+
1311+
b = NaiveException(x=1)
1312+
b.kwargs = {'x': 2}
1313+
self.assertEqual(b.kwargs, {'x': 2})
1314+
1315+
def test_del_args_kwargs(self):
1316+
b = BaseException()
1317+
1318+
with self.assertRaisesRegex(TypeError, "args may not be deleted"):
1319+
del b.args
1320+
1321+
with self.assertRaisesRegex(TypeError, "kwargs may not be deleted"):
1322+
del b.kwargs
1323+
1324+
def test_repr(self):
1325+
class MixedArgsKwargs(Exception):
1326+
def __init__(*args, **kwargs):
1327+
pass
1328+
1329+
self.assertEqual(repr(BaseException()), "BaseException()")
1330+
self.assertEqual(repr(BaseException(1)), "BaseException(1)")
1331+
self.assertEqual(repr(NaiveException(1)), "NaiveException(1)")
1332+
self.assertEqual(repr(NaiveException(x=1)), "NaiveException(x=1)")
1333+
self.assertEqual(repr(MixedArgsKwargs(1, b=2)), "MixedArgsKwargs(1, b=2)")
1334+
1335+
class NoKwargs(Exception):
1336+
def __init__(self, foo,):
1337+
self.args = (foo,)
1338+
13021339

13031340
class ImportErrorTests(unittest.TestCase):
13041341

@@ -1376,6 +1413,17 @@ def test_copy_pickle(self):
13761413
self.assertEqual(exc.name, orig.name)
13771414
self.assertEqual(exc.path, orig.path)
13781415

1416+
def test_pickle_overriden_init(self):
1417+
# Issue #27015
1418+
from subprocess import CalledProcessError
1419+
1420+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
1421+
orig = CalledProcessError(returncode=2, cmd='foo')
1422+
exc = pickle.loads(pickle.dumps(orig, proto))
1423+
self.assertEqual(orig.cmd, exc.cmd)
1424+
self.assertEqual(orig.returncode, exc.returncode)
1425+
1426+
13791427

13801428
if __name__ == '__main__':
13811429
unittest.main()

Lib/test/test_sys.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,13 +1004,13 @@ def inner():
10041004
class C(object): pass
10051005
check(C.__dict__, size('P'))
10061006
# BaseException
1007-
check(BaseException(), size('5Pb'))
1007+
check(BaseException(), size('6Pb'))
10081008
# UnicodeEncodeError
1009-
check(UnicodeEncodeError("", "", 0, 0, ""), size('5Pb 2P2nP'))
1009+
check(UnicodeEncodeError("", "", 0, 0, ""), size('6Pb 2P2nP'))
10101010
# UnicodeDecodeError
1011-
check(UnicodeDecodeError("", b"", 0, 0, ""), size('5Pb 2P2nP'))
1011+
check(UnicodeDecodeError("", b"", 0, 0, ""), size('6Pb 2P2nP'))
10121012
# UnicodeTranslateError
1013-
check(UnicodeTranslateError("", 0, 1, ""), size('5Pb 2P2nP'))
1013+
check(UnicodeTranslateError("", 0, 1, ""), size('6Pb 2P2nP'))
10141014
# ellipses
10151015
check(Ellipsis, size(''))
10161016
# EncodingMap
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Exceptions now save the keyword arguments given to their constructorin their
2+
`kwargs` attribute. Exceptions with overridden __init__ and using keyword
3+
arguments are now picklable. Patch contributed by Rémi Lapeyre.

Objects/exceptions.c

Lines changed: 171 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,30 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
4141
return NULL;
4242
/* the dict is created on the fly in PyObject_GenericSetAttr */
4343
self->dict = NULL;
44+
self->kwargs = NULL;
4445
self->traceback = self->cause = self->context = NULL;
4546
self->suppress_context = 0;
4647

4748
if (args) {
4849
self->args = args;
4950
Py_INCREF(args);
50-
return (PyObject *)self;
51+
} else {
52+
self->args = PyTuple_New(2);
53+
if (!self->args) {
54+
Py_DECREF(self);
55+
return NULL;
56+
}
5157
}
5258

53-
self->args = PyTuple_New(0);
54-
if (!self->args) {
55-
Py_DECREF(self);
56-
return NULL;
59+
if (kwds) {
60+
self->kwargs = kwds;
61+
Py_INCREF(kwds);
62+
} else {
63+
self->kwargs = PyDict_New();
64+
if (!self->kwargs) {
65+
Py_DECREF(self);
66+
return NULL;
67+
}
5768
}
5869

5970
return (PyObject *)self;
@@ -76,6 +87,7 @@ BaseException_clear(PyBaseExceptionObject *self)
7687
{
7788
Py_CLEAR(self->dict);
7889
Py_CLEAR(self->args);
90+
Py_CLEAR(self->kwargs);
7991
Py_CLEAR(self->traceback);
8092
Py_CLEAR(self->cause);
8193
Py_CLEAR(self->context);
@@ -95,6 +107,7 @@ BaseException_traverse(PyBaseExceptionObject *self, visitproc visit, void *arg)
95107
{
96108
Py_VISIT(self->dict);
97109
Py_VISIT(self->args);
110+
Py_VISIT(self->kwargs);
98111
Py_VISIT(self->traceback);
99112
Py_VISIT(self->cause);
100113
Py_VISIT(self->context);
@@ -118,21 +131,144 @@ static PyObject *
118131
BaseException_repr(PyBaseExceptionObject *self)
119132
{
120133
const char *name = _PyType_Name(Py_TYPE(self));
121-
if (PyTuple_GET_SIZE(self->args) == 1)
122-
return PyUnicode_FromFormat("%s(%R)", name,
123-
PyTuple_GET_ITEM(self->args, 0));
124-
else
125-
return PyUnicode_FromFormat("%s%R", name, self->args);
134+
PyObject *separator = NULL;
135+
PyObject *args = NULL;
136+
PyObject *kwargs = NULL;
137+
PyObject *seq = NULL;
138+
PyObject *repr = NULL;
139+
PyObject *item = NULL;
140+
PyObject *items = NULL;
141+
PyObject *it = NULL;
142+
PyObject *key = NULL;
143+
PyObject *value = NULL;
144+
PyObject *result = NULL;
145+
146+
separator = PyUnicode_FromString(", ");
147+
148+
if (PyTuple_Check(self->args)) {
149+
const Py_ssize_t len = PyTuple_Size(self->args);
150+
seq = PyTuple_New(len);
151+
if (seq == NULL) {
152+
goto fail;
153+
}
154+
for (Py_ssize_t i=0; i < len; i++) {
155+
repr = PyObject_Repr(PyTuple_GET_ITEM(self->args, i));
156+
if (repr == NULL) {
157+
goto fail;
158+
}
159+
PyTuple_SET_ITEM(seq, i, repr);
160+
}
161+
args = PyUnicode_Join(separator, seq);
162+
Py_DECREF(seq);
163+
}
164+
165+
if (PyMapping_Check(self->kwargs)) {
166+
const Py_ssize_t len = PyMapping_Length(self->kwargs);
167+
if (len == -1) {
168+
goto fail;
169+
}
170+
seq = PyTuple_New(len);
171+
items = PyMapping_Items(self->kwargs);
172+
if (seq == NULL || items == NULL) {
173+
goto fail;
174+
}
175+
it = PyObject_GetIter(items);
176+
if (it == NULL) {
177+
goto fail;
178+
}
179+
Py_ssize_t i = 0;
180+
while ((item = PyIter_Next(it)) != NULL) {
181+
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) {
182+
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples");
183+
goto fail;
184+
}
185+
key = PyTuple_GET_ITEM(item, 0);
186+
value = PyTuple_GET_ITEM(item, 1);
187+
PyTuple_SET_ITEM(seq, i, PyUnicode_FromFormat("%S=%R", key, value));
188+
i++;
189+
Py_DECREF(item);
190+
}
191+
kwargs = PyUnicode_Join(separator, seq);
192+
Py_DECREF(seq);
193+
Py_DECREF(items);
194+
Py_DECREF(it);
195+
}
196+
Py_DECREF(separator);
197+
198+
if (args == NULL && kwargs == NULL) {
199+
result = PyUnicode_FromFormat("%s()", name, kwargs);
200+
} else if (kwargs == NULL || PyUnicode_GET_LENGTH(kwargs) == 0) {
201+
result = PyUnicode_FromFormat("%s(%S)", name, args);
202+
} else if (args == NULL || PyUnicode_GET_LENGTH(args) == 0) {
203+
result = PyUnicode_FromFormat("%s(%S)", name, kwargs);
204+
} else {
205+
result = PyUnicode_FromFormat("%s(%S, %S)", name, args, kwargs);
206+
}
207+
Py_XDECREF(args);
208+
Py_XDECREF(kwargs);
209+
return result;
210+
211+
fail:
212+
Py_XDECREF(separator);
213+
Py_XDECREF(args);
214+
Py_XDECREF(kwargs);
215+
Py_XDECREF(seq);
216+
Py_XDECREF(repr);
217+
Py_XDECREF(item);
218+
Py_XDECREF(items);
219+
Py_XDECREF(it);
220+
Py_XDECREF(key);
221+
Py_XDECREF(value);
222+
return NULL;
126223
}
127224

128225
/* Pickling support */
129226
static PyObject *
130227
BaseException_reduce(PyBaseExceptionObject *self, PyObject *Py_UNUSED(ignored))
131228
{
132-
if (self->args && self->dict)
133-
return PyTuple_Pack(3, Py_TYPE(self), self->args, self->dict);
134-
else
135-
return PyTuple_Pack(2, Py_TYPE(self), self->args);
229+
PyObject *functools;
230+
PyObject *partial;
231+
PyObject *constructor;
232+
PyObject *args;
233+
PyObject *result;
234+
PyObject **newargs;
235+
236+
_Py_IDENTIFIER(partial);
237+
functools = PyImport_ImportModule("functools");
238+
if (!functools)
239+
return NULL;
240+
partial = _PyObject_GetAttrId(functools, &PyId_partial);
241+
Py_DECREF(functools);
242+
if (!partial)
243+
return NULL;
244+
245+
Py_ssize_t len = 1;
246+
if (PyTuple_Check(self->args)) {
247+
len += PyTuple_GET_SIZE(self->args);
248+
}
249+
newargs = PyMem_RawMalloc(len*sizeof(PyObject*));
250+
newargs[0] = (PyObject *)Py_TYPE(self);
251+
252+
for (Py_ssize_t i=1; i < len; i++) {
253+
newargs[i] = PyTuple_GetItem(self->args, i-1);
254+
}
255+
constructor = _PyObject_FastCallDict(partial, newargs, len, self->kwargs);
256+
PyMem_RawFree(newargs);
257+
258+
Py_DECREF(partial);
259+
260+
args = PyTuple_New(0);
261+
if (!args) {
262+
return NULL;
263+
}
264+
if (self->args && self->dict){
265+
result = PyTuple_Pack(3, constructor, args, self->dict);
266+
} else {
267+
result = PyTuple_Pack(2, constructor, args);
268+
}
269+
Py_DECREF(constructor);
270+
Py_DECREF(args);
271+
return result;
136272
}
137273

138274
/*
@@ -206,6 +342,26 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS
206342
return 0;
207343
}
208344

345+
static PyObject *
346+
BaseException_get_kwargs(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) {
347+
if (self->kwargs == NULL) {
348+
Py_RETURN_NONE;
349+
}
350+
Py_INCREF(self->kwargs);
351+
return self->kwargs;
352+
}
353+
354+
static int
355+
BaseException_set_kwargs(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUSED(ignored)) {
356+
if (val == NULL) {
357+
PyErr_SetString(PyExc_TypeError, "kwargs may not be deleted");
358+
return -1;
359+
}
360+
Py_INCREF(val);
361+
self->kwargs = val;
362+
return 0;
363+
}
364+
209365
static PyObject *
210366
BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored))
211367
{
@@ -296,6 +452,7 @@ BaseException_set_cause(PyObject *self, PyObject *arg, void *Py_UNUSED(ignored))
296452
static PyGetSetDef BaseException_getset[] = {
297453
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
298454
{"args", (getter)BaseException_get_args, (setter)BaseException_set_args},
455+
{"kwargs", (getter)BaseException_get_kwargs, (setter)BaseException_set_kwargs},
299456
{"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb},
300457
{"__context__", BaseException_get_context,
301458
BaseException_set_context, PyDoc_STR("exception context")},

0 commit comments

Comments
 (0)