Skip to content

Commit 6a00f21

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 6a00f21

File tree

8 files changed

+902
-690
lines changed

8 files changed

+902
-690
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy