Index: Lib/pickle.py =================================================================== --- Lib/pickle.py (revision 56920) +++ Lib/pickle.py (working copy) @@ -1044,17 +1044,23 @@ args = tuple(self.stack[k+1:]) del self.stack[k:] instantiated = 0 - if (not args and - type(klass) is ClassType and - not hasattr(klass, "__getinitargs__")): - try: - value = _EmptyClass() - value.__class__ = klass + if not args and not hasattr(klass, "__getinitargs__"): + if type(klass) is ClassType: + try: + value = _EmptyClass() + value.__class__ = klass + instantiated = 1 + except RuntimeError: + # In restricted execution, assignment to inst.__class__ is + # prohibited + pass + # Backwards comptatibility. Exceptions used to be + # old style classes in Python 2.5, and don't expect + # to get their __init__ called on unpickling. This + # code can be removed in Python 2.6. + elif isinstance(klass, type) and issubclass(klass, BaseException): + value = klass.__new__(klass) instantiated = 1 - except RuntimeError: - # In restricted execution, assignment to inst.__class__ is - # prohibited - pass if not instantiated: try: value = klass(*args) Index: Lib/test/test_exceptions.py =================================================================== --- Lib/test/test_exceptions.py (revision 56920) +++ Lib/test/test_exceptions.py (working copy) @@ -8,6 +8,27 @@ from test.test_support import TESTFN, unlink, run_unittest +class NaiveException(Exception): + def __init__(self, x): + self.x = x + +class SomewhatNaiveException(Exception): + def __init__(self, x): + self.x = x + Exception.__init__(self) + +class NotNaiveException(Exception): + def __init__(self, *args): + Exception.__init__(self, *args) + self.message = "changed" + self.args = ("also", "changed") + +class SlotedNaiveException(Exception): + __slots__ = ("x",) + def __init__(self, x): + self.x = x + + # XXX This is not really enough, each *operation* should be tested! class ExceptionTests(unittest.TestCase): @@ -259,6 +280,14 @@ {'message' : '', 'args' : (u'\u3042', 0, 1, 'ouch'), 'object' : u'\u3042', 'reason' : 'ouch', 'start' : 0, 'end' : 1}), + (NaiveException, ('foo',), + {'message': '', 'args': (), 'x': 'foo'}), + (SomewhatNaiveException, ('foo',), + {'message': '', 'args': (), 'x': 'foo'}), + (NotNaiveException, ('foo',), + {'message': 'changed', 'args': ('also', 'changed')}), + (SlotedNaiveException, ('foo',), + {'message': '', 'args': (), 'x': 'foo'}), ] try: exceptionList.append( @@ -277,7 +306,8 @@ if type(e) is not exc: raise # Verify module name - self.assertEquals(type(e).__module__, 'exceptions') + if not type(e).__name__.endswith('NaiveException'): + self.assertEquals(type(e).__module__, 'exceptions') # Verify no ref leaks in Exc_str() s = str(e) for checkArgName in expected: @@ -337,7 +367,37 @@ self.failUnless(str(Exception('a'))) self.failUnless(unicode(Exception(u'a'))) + def testOldStyleExceptionUnpickling(self): + # unpickling failed for exceptions with a required argument + # that were pickled on Python 2.4 or older. Use global switching + # to emulate "promotion" of exceptions from old to new style + # classes. + global CurrentException + class CurrentException(Exception): + def __init__(self, x): + self.x = x + + NewStyleException = CurrentException + + class CurrentException: + def __init__(self, x): + self.x = x + + OldStyleException = CurrentException + + e = OldStyleException("foo") + for p in pickle, cPickle: + for protocol in range(p.HIGHEST_PROTOCOL + 1): + s = p.dumps(e, protocol) + CurrentException = NewStyleException + new = p.loads(s) + CurrentException = OldStyleException + self.assertEquals(new.message, "") + self.assertEquals(new.args, ()) + self.assertEquals(new.x, "foo") + + def test_main(): run_unittest(ExceptionTests) Index: Modules/cPickle.c =================================================================== --- Modules/cPickle.c (revision 56920) +++ Modules/cPickle.c (working copy) @@ -3604,30 +3604,37 @@ static PyObject * Instance_New(PyObject *cls, PyObject *args) { + Py_ssize_t l; PyObject *r = 0; - if (PyClass_Check(cls)) { - int l; + if ((l=PyObject_Size(args)) < 0) + goto err; + if (l == 0 && !PyObject_HasAttr(cls, __getinitargs___str)) { + if (PyClass_Check(cls)) { + /* We have a class with no __getinitargs__, + so bypass usual construction */ + PyObject *inst; - if ((l=PyObject_Size(args)) < 0) goto err; - if (!( l )) { - PyObject *__getinitargs__; + if (!( inst=PyInstance_NewRaw(cls, NULL))) + goto err; + return inst; + } + else if (PyType_Check(cls) && + PyObject_IsSubclass(cls, PyExc_BaseException)) { + /* Backwards comptatibility. Exceptions used + to be old style classes in Python 2.5, and + don't expect to get their __init__ called + on unpickling. This code can be removed in + Python 2.6. */ + PyTypeObject *type = (PyTypeObject *)cls; + PyObject *inst = type->tp_new(type, args, NULL); - __getinitargs__ = PyObject_GetAttr(cls, - __getinitargs___str); - if (!__getinitargs__) { - /* We have a class with no __getinitargs__, - so bypass usual construction */ - PyObject *inst; - - PyErr_Clear(); - if (!( inst=PyInstance_NewRaw(cls, NULL))) - goto err; - return inst; - } - Py_DECREF(__getinitargs__); + if (inst == NULL) + goto err; + return inst; } - + } + else if (PyClass_Check(cls)) { if ((r=PyInstance_New(cls, args, NULL))) return r; else goto err; } Index: Objects/exceptions.c =================================================================== --- Objects/exceptions.c (revision 56920) +++ Objects/exceptions.c (working copy) @@ -143,22 +143,127 @@ return repr; } -/* Pickling support */ +/* + * Pickling support. We use the __getstate__() and __setstate__() hooks + * for exception pickling. The __reduce_ex__() method is needed only to + * call object.__reduce_ex__() with protocol == 2, because other protocols + * complain about C types without custom __reduce__() methods. + */ static PyObject * -BaseException_reduce(PyBaseExceptionObject *self) +BaseException_reduce_ex(PyObject *self, PyObject *proto) { - if (self->args && self->dict) - return PyTuple_Pack(3, self->ob_type, self->args, self->dict); - else - return PyTuple_Pack(2, self->ob_type, self->args); + if (!PyIndex_Check(proto)) { + PyErr_Format(PyExc_TypeError, "BaseException.__reduce_ex__() requires " + "an integer argument, got '%s' object instead", + proto->ob_type->tp_name); + return NULL; + } + return PyObject_CallMethod((PyObject *)&PyBaseObject_Type, + "__reduce_ex__", "Oi", self, 2); } +/* Helpers for BaseException_getstate(). */ +static int +is_special_attribute(const char *name) +{ + return (strcmp(name, "__dict__") == 0 || + strcmp(name, "__weakref__") == 0); +} + /* - * Needed for backward compatibility, since exceptions used to store - * all their attributes in the __dict__. Code is taken from cPickle's - * load_build function. + * Store attribute named 'name' with value 'value' in the 'state' + * dictionary. Return 0 on success, -1 on failure. Also steals + * a reference to the 'value' and checks for attribute errors that + * might have happened during 'value' retrieval. */ +static int +store_attribute(PyObject *state, const char *name, PyObject *value) +{ + int res; + + if (value == NULL) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; + } + res = PyDict_SetItemString(state, name, value); + Py_DECREF(value); + return res; +} + +/* + * __getstate__() hook. Because of backwards compatibility we can't + * use the __getnewargs__() or __reduce__() hooks for exception + * pickling, and __getstate__() really has to store all attributes + * in the state dictionary. We achieve this by copying exception's + * __dict__ and updating it with the values of tp_members and + * tp_getset descriptors of all types in exception's mro. + */ static PyObject * +BaseException_getstate(PyObject *self, PyObject *unused) +{ + Py_ssize_t i, n; + PyObject *state, *dict, *mro; + + state = PyDict_New(); + if (state == NULL) + return NULL; + + dict = ((PyBaseExceptionObject *)self)->dict; + if (dict != NULL) { + if (PyDict_Update(state, dict) == -1) + goto error; + } + mro = self->ob_type->tp_mro; + if (mro == NULL) { + PyErr_Format(PyExc_SystemError, "'%s' doesn't have a __mro__ tuple", + self->ob_type->tp_name); + goto error; + } + n = PyTuple_GET_SIZE(mro); + for (i = 0; i < n; i++) { + PyObject *base = PyTuple_GET_ITEM(mro, i); + if (PyType_Check(base) && + PyObject_IsSubclass(base, PyExc_BaseException)) { + PyObject *value; + PyMemberDef *member = ((PyTypeObject *)base)->tp_members; + PyGetSetDef *getset = ((PyTypeObject *)base)->tp_getset; + if (member != NULL) { + for (; member->name != NULL; member++) { + if (!is_special_attribute(member->name)) { + value = PyMember_GetOne((char *)self, member); + if (store_attribute(state, member->name, value) == -1) + goto error; + } + } + } + if (getset != NULL) { + for (; getset->name != NULL; getset++) { + if (getset->get != NULL && + !is_special_attribute(getset->name)) { + value = getset->get(self, getset->closure); + if (store_attribute(state, getset->name, value) == -1) + goto error; + } + } + } + } + } + return state; + +error: + Py_DECREF(state); + return NULL; +} + +/* + * We have to use setattr() for attributes in the state, since some of + * the attributes aren't stored in the __dict__. Code is taken from + * cPickle's load_build() function. + */ +static PyObject * BaseException_setstate(PyObject *self, PyObject *state) { PyObject *d_key, *d_value; @@ -179,8 +284,12 @@ static PyMethodDef BaseException_methods[] = { - {"__reduce__", (PyCFunction)BaseException_reduce, METH_NOARGS }, - {"__setstate__", (PyCFunction)BaseException_setstate, METH_O }, + {"__reduce_ex__", (PyCFunction)BaseException_reduce_ex, + METH_O, "pickle helper"}, + {"__getstate__", (PyCFunction)BaseException_getstate, + METH_NOARGS, "pickle helper"}, + {"__setstate__", (PyCFunction)BaseException_setstate, + METH_O, "pickle helper"}, {NULL, NULL, 0, NULL}, }; @@ -676,48 +785,9 @@ }; -static PyObject * -EnvironmentError_reduce(PyEnvironmentErrorObject *self) -{ - PyObject *args = self->args; - PyObject *res = NULL, *tmp; - - /* self->args is only the first two real arguments if there was a - * file name given to EnvironmentError. */ - if (PyTuple_GET_SIZE(args) == 2 && self->filename) { - args = PyTuple_New(3); - if (!args) return NULL; - - tmp = PyTuple_GET_ITEM(self->args, 0); - Py_INCREF(tmp); - PyTuple_SET_ITEM(args, 0, tmp); - - tmp = PyTuple_GET_ITEM(self->args, 1); - Py_INCREF(tmp); - PyTuple_SET_ITEM(args, 1, tmp); - - Py_INCREF(self->filename); - PyTuple_SET_ITEM(args, 2, self->filename); - } else - Py_INCREF(args); - - if (self->dict) - res = PyTuple_Pack(3, self->ob_type, args, self->dict); - else - res = PyTuple_Pack(2, self->ob_type, args); - Py_DECREF(args); - return res; -} - - -static PyMethodDef EnvironmentError_methods[] = { - {"__reduce__", (PyCFunction)EnvironmentError_reduce, METH_NOARGS}, - {NULL} -}; - ComplexExtendsException(PyExc_StandardError, EnvironmentError, EnvironmentError, EnvironmentError_dealloc, - EnvironmentError_methods, EnvironmentError_members, + 0, EnvironmentError_members, EnvironmentError_str, "Base class for I/O related errors.");