New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-27015: Save kwargs given to exceptions constructor #11580
base: main
Are you sure you want to change the base?
Changes from 6 commits
6a00f21
12dc912
1d12d28
88ba13e
9986e86
7794862
6c906da
4657d7e
2fc3366
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Exceptions now save the keyword arguments given to their constructor in their | ||
``kwargs`` attribute. Exceptions with overridden ``__init__`` and using keyword | ||
arguments are now picklable. Contributed by Rémi Lapeyre. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,20 +41,23 @@ BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds) | |
return NULL; | ||
/* the dict is created on the fly in PyObject_GenericSetAttr */ | ||
self->dict = NULL; | ||
self->kwargs = NULL; | ||
self->traceback = self->cause = self->context = NULL; | ||
self->suppress_context = 0; | ||
|
||
if (args) { | ||
self->args = args; | ||
Py_INCREF(args); | ||
return (PyObject *)self; | ||
} else { | ||
self->args = PyTuple_New(0); | ||
if (!self->args) { | ||
Py_DECREF(self); | ||
return NULL; | ||
} | ||
} | ||
|
||
self->args = PyTuple_New(0); | ||
if (!self->args) { | ||
Py_DECREF(self); | ||
return NULL; | ||
} | ||
self->kwargs = kwds; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assignment should only be done if While calls like If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be good to do that for |
||
Py_XINCREF(kwds); | ||
|
||
return (PyObject *)self; | ||
} | ||
|
@@ -76,6 +79,7 @@ BaseException_clear(PyBaseExceptionObject *self) | |
{ | ||
Py_CLEAR(self->dict); | ||
Py_CLEAR(self->args); | ||
Py_CLEAR(self->kwargs); | ||
Py_CLEAR(self->traceback); | ||
Py_CLEAR(self->cause); | ||
Py_CLEAR(self->context); | ||
|
@@ -95,6 +99,7 @@ BaseException_traverse(PyBaseExceptionObject *self, visitproc visit, void *arg) | |
{ | ||
Py_VISIT(self->dict); | ||
Py_VISIT(self->args); | ||
Py_VISIT(self->kwargs); | ||
Py_VISIT(self->traceback); | ||
Py_VISIT(self->cause); | ||
Py_VISIT(self->context); | ||
|
@@ -118,21 +123,153 @@ static PyObject * | |
BaseException_repr(PyBaseExceptionObject *self) | ||
{ | ||
const char *name = _PyType_Name(Py_TYPE(self)); | ||
if (PyTuple_GET_SIZE(self->args) == 1) | ||
return PyUnicode_FromFormat("%s(%R)", name, | ||
PyTuple_GET_ITEM(self->args, 0)); | ||
else | ||
return PyUnicode_FromFormat("%s%R", name, self->args); | ||
PyObject *separator = NULL; | ||
PyObject *args = NULL; | ||
PyObject *kwargs = NULL; | ||
PyObject *seq = NULL; | ||
PyObject *repr = NULL; | ||
PyObject *item = NULL; | ||
PyObject *items = NULL; | ||
PyObject *it = NULL; | ||
PyObject *key = NULL; | ||
PyObject *value = NULL; | ||
PyObject *result = NULL; | ||
|
||
separator = PyUnicode_FromString(", "); | ||
|
||
if (PyTuple_Check(self->args)) { | ||
const Py_ssize_t len = PyTuple_Size(self->args); | ||
seq = PyTuple_New(len); | ||
if (seq == NULL) { | ||
goto fail; | ||
} | ||
for (Py_ssize_t i=0; i < len; i++) { | ||
repr = PyObject_Repr(PyTuple_GET_ITEM(self->args, i)); | ||
if (repr == NULL) { | ||
goto fail; | ||
} | ||
PyTuple_SET_ITEM(seq, i, repr); | ||
} | ||
args = PyUnicode_Join(separator, seq); | ||
Py_DECREF(seq); | ||
} | ||
|
||
if (PyMapping_Check(self->kwargs)) { | ||
const Py_ssize_t len = PyMapping_Length(self->kwargs); | ||
if (len == -1) { | ||
goto fail; | ||
} | ||
seq = PyTuple_New(len); | ||
items = PyMapping_Items(self->kwargs); | ||
if (seq == NULL || items == NULL) { | ||
goto fail; | ||
} | ||
it = PyObject_GetIter(items); | ||
if (it == NULL) { | ||
goto fail; | ||
} | ||
Py_ssize_t i = 0; | ||
while ((item = PyIter_Next(it)) != NULL) { | ||
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) { | ||
PyErr_SetString(PyExc_ValueError, "items must return 2-tuples"); | ||
goto fail; | ||
} | ||
key = PyTuple_GET_ITEM(item, 0); | ||
value = PyTuple_GET_ITEM(item, 1); | ||
repr = PyUnicode_FromFormat("%S=%R", key, value); | ||
if (repr == NULL) { | ||
goto fail; | ||
} | ||
PyTuple_SET_ITEM(seq, i, repr); | ||
i++; | ||
Py_DECREF(item); | ||
} | ||
kwargs = PyUnicode_Join(separator, seq); | ||
Py_DECREF(seq); | ||
Py_DECREF(items); | ||
Py_DECREF(it); | ||
} | ||
Py_DECREF(separator); | ||
|
||
if (args == NULL && kwargs == NULL) { | ||
result = PyUnicode_FromFormat("%s()", name, kwargs); | ||
remilapeyre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if (kwargs == NULL || PyUnicode_GET_LENGTH(kwargs) == 0) { | ||
result = PyUnicode_FromFormat("%s(%S)", name, args); | ||
} else if (args == NULL || PyUnicode_GET_LENGTH(args) == 0) { | ||
result = PyUnicode_FromFormat("%s(%S)", name, kwargs); | ||
} else { | ||
result = PyUnicode_FromFormat("%s(%S, %S)", name, args, kwargs); | ||
} | ||
Py_XDECREF(args); | ||
Py_XDECREF(kwargs); | ||
return result; | ||
|
||
fail: | ||
Py_XDECREF(separator); | ||
Py_XDECREF(args); | ||
Py_XDECREF(kwargs); | ||
Py_XDECREF(seq); | ||
Py_XDECREF(repr); | ||
Py_XDECREF(item); | ||
Py_XDECREF(items); | ||
Py_XDECREF(it); | ||
Py_XDECREF(key); | ||
Py_XDECREF(value); | ||
return NULL; | ||
} | ||
|
||
/* Pickling support */ | ||
static PyObject * | ||
BaseException_reduce(PyBaseExceptionObject *self, PyObject *Py_UNUSED(ignored)) | ||
{ | ||
if (self->args && self->dict) | ||
return PyTuple_Pack(3, Py_TYPE(self), self->args, self->dict); | ||
else | ||
return PyTuple_Pack(2, Py_TYPE(self), self->args); | ||
PyObject *functools; | ||
PyObject *partial; | ||
PyObject *constructor; | ||
PyObject *result; | ||
PyObject **newargs; | ||
|
||
_Py_IDENTIFIER(partial); | ||
functools = PyImport_ImportModule("functools"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reduce implementation concerns me, as it looks like it will make everything much slower, even for exception instances where Instead, I'd recommend migrating That way the pickle machinery will take care of calling (That would have potential backwards compatibility implications for subclasses implementing reduce based on the parent class implementation, but the same would hold true for introduce a partial object in place of a direct reference to the class - either way, there'll need to be a note in the Porting section of the What's New guide, and switching to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to do that, I removed static PyObject *
BaseException_getnewargs_ex(PyBaseExceptionObject *self, PyObject *Py_UNUSED(ignored))
{
PyObject *args = PyObject_GetAttrString((PyObject *) self, "args");
PyObject *kwargs = PyObject_GetAttrString((PyObject *) self, "kwargs");
if (args == NULL || kwargs == NULL) {
return NULL;
}
return Py_BuildValue("(OO)", args, kwargs);
} but it brocke pickling. Did I miss something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, found my mistake, using I don't think this happened when using a partial reference on the the constructor of the class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's ok to broke pickling support for protocols 0 and 1 since it was broken for keyword args anyway? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defining @pitrou Do you have any recommendations here? (Context: trying to get There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How should I call It seems to me that calling the builtin super is not done anywhere in the source code but I don't find the right way to do it. Do I need to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ncoghlan Well, I'm not sure why you wouldn't implement the entire logic in Or, rather, you could just define There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pitrou I only suggested delegating to But if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @pitrou @ncoghlan, thanks for you input. I pushed a new commit that implement
If I remove the
Do I need to define a custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dug further and it seems my issue comes from https://github.com/python/cpython/blob/master/Lib/copyreg.py#L66, I will look into the details tomorrow. |
||
if (functools == NULL) { | ||
return NULL; | ||
} | ||
partial = _PyObject_GetAttrId(functools, &PyId_partial); | ||
Py_DECREF(functools); | ||
if (partial == NULL) { | ||
return NULL; | ||
} | ||
|
||
Py_ssize_t len = 1; | ||
if (PyTuple_Check(self->args)) { | ||
len += PyTuple_GET_SIZE(self->args); | ||
} | ||
newargs = PyMem_New(PyObject *, len); | ||
if (newargs == NULL) { | ||
PyErr_NoMemory(); | ||
return NULL; | ||
} | ||
newargs[0] = (PyObject *)Py_TYPE(self); | ||
|
||
for (Py_ssize_t i=1; i < len; i++) { | ||
newargs[i] = PyTuple_GetItem(self->args, i-1); | ||
} | ||
constructor = _PyObject_FastCallDict(partial, newargs, len, self->kwargs); | ||
PyMem_Free(newargs); | ||
|
||
Py_DECREF(partial); | ||
|
||
PyObject *args = PyTuple_New(0); | ||
if (args == NULL) { | ||
return NULL; | ||
} | ||
if (self->args && self->dict){ | ||
result = PyTuple_Pack(3, constructor, args, self->dict); | ||
} else { | ||
result = PyTuple_Pack(2, constructor, args); | ||
} | ||
Py_DECREF(constructor); | ||
Py_DECREF(args); | ||
return result; | ||
} | ||
|
||
/* | ||
|
@@ -206,6 +343,26 @@ BaseException_set_args(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUS | |
return 0; | ||
} | ||
|
||
static PyObject * | ||
BaseException_get_kwargs(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) { | ||
if (self->kwargs == NULL) { | ||
self->kwargs = PyDict_New(); | ||
remilapeyre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
Py_XINCREF(self->kwargs); | ||
return self->kwargs; | ||
} | ||
|
||
static int | ||
BaseException_set_kwargs(PyBaseExceptionObject *self, PyObject *val, void *Py_UNUSED(ignored)) { | ||
if (val == NULL) { | ||
PyErr_SetString(PyExc_TypeError, "kwargs may not be deleted"); | ||
return -1; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about doing that but it seems to me that
since tuples support slicing? If so, is the check still worth it? |
||
Py_INCREF(val); | ||
self->kwargs = val; | ||
remilapeyre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return 0; | ||
} | ||
|
||
static PyObject * | ||
BaseException_get_tb(PyBaseExceptionObject *self, void *Py_UNUSED(ignored)) | ||
{ | ||
|
@@ -296,6 +453,7 @@ BaseException_set_cause(PyObject *self, PyObject *arg, void *Py_UNUSED(ignored)) | |
static PyGetSetDef BaseException_getset[] = { | ||
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, | ||
{"args", (getter)BaseException_get_args, (setter)BaseException_set_args}, | ||
{"kwargs", (getter)BaseException_get_kwargs, (setter)BaseException_set_kwargs}, | ||
{"__traceback__", (getter)BaseException_get_tb, (setter)BaseException_set_tb}, | ||
{"__context__", BaseException_get_context, | ||
BaseException_set_context, PyDoc_STR("exception context")}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: dictionnary -> dictionary