Index: Modules/cPickle.c =================================================================== RCS file: /cvsroot/python/python/dist/src/Modules/cPickle.c,v retrieving revision 2.151 diff -u -r2.151 cPickle.c --- Modules/cPickle.c 27 Jul 2004 05:22:33 -0000 2.151 +++ Modules/cPickle.c 8 Nov 2004 07:51:15 -0000 @@ -330,6 +330,7 @@ PyObject *arg; PyObject *pers_func; PyObject *inst_pers_func; + PyObject *reducing_now; /* pickle protocol number, >= 0 */ int proto; @@ -782,6 +783,7 @@ PyTuple_SET_ITEM(t, 1, ob); Py_INCREF(ob); + assert(PyDict_GetItem(self->memo, py_ob_id) == NULL); if (PyDict_SetItem(self->memo, py_ob_id, t) < 0) goto finally; @@ -2125,9 +2127,11 @@ { PyObject *callable; PyObject *argtup; + PyObject *obid; PyObject *state = NULL; PyObject *listitems = NULL; PyObject *dictitems = NULL; + int i, res = -1; int use_newobj = self->proto >= 2; @@ -2150,6 +2154,29 @@ if (dictitems == Py_None) dictitems = NULL; + if (self->fast && !fast_save_enter(self, ob)) + goto finally; + + /* Catch cycles introduced by reduce functions by making sure that + * we're not reducing the same object recursively. A reduce + * function hasn't done its job if the callable and args it + * returned cycle through the object it tried to reduce. + */ + if (ob != NULL) { + if (!( obid = PyLong_FromVoidPtr(ob))) + goto finally; + if (PyDict_GetItem(self->reducing_now, obid)) { + cPickle_ErrFormat(PicklingError, + "reduce cycle through %s", "O", ob); + Py_DECREF(obid); + goto finally; + } + i = PyDict_SetItem(self->reducing_now, obid, Py_True); + Py_DECREF(obid); + if (i < 0) + goto finally; + } + /* Protocol 2 special case: if callable's name is __newobj__, use * NEWOBJ. This consumes a lot of code. */ @@ -2160,7 +2187,7 @@ if (PyErr_ExceptionMatches(PyExc_AttributeError)) PyErr_Clear(); else - return -1; + goto finally; use_newobj = 0; } else { @@ -2173,21 +2200,21 @@ if (use_newobj) { PyObject *cls; PyObject *newargtup; - int n, i; + int n; /* Sanity checks. */ n = PyTuple_Size(argtup); if (n < 1) { PyErr_SetString(PicklingError, "__newobj__ arglist " "is empty"); - return -1; + goto finally; } cls = PyTuple_GET_ITEM(argtup, 0); if (! PyObject_HasAttrString(cls, "__new__")) { PyErr_SetString(PicklingError, "args[0] from " "__newobj__ args has no __new__"); - return -1; + goto finally; } /* XXX How could ob be NULL? */ @@ -2200,24 +2227,24 @@ PyExc_AttributeError)) PyErr_Clear(); else - return -1; + goto finally; } i = ob_dot_class != cls; /* true iff a problem */ Py_XDECREF(ob_dot_class); if (i) { PyErr_SetString(PicklingError, "args[0] from " "__newobj__ args has the wrong class"); - return -1; + goto finally; } } /* Save the class and its __new__ arguments. */ if (save(self, cls, 0) < 0) - return -1; + goto finally; newargtup = PyTuple_New(n-1); /* argtup[1:] */ if (newargtup == NULL) - return -1; + goto finally; for (i = 1; i < n; ++i) { PyObject *temp = PyTuple_GET_ITEM(argtup, i); Py_INCREF(temp); @@ -2226,18 +2253,18 @@ i = save(self, newargtup, 0) < 0; Py_DECREF(newargtup); if (i < 0) - return -1; + goto finally; /* Add NEWOBJ opcode. */ if (self->write_func(self, &newobj, 1) < 0) - return -1; + goto finally; } else { /* Not using NEWOBJ. */ if (save(self, callable, 0) < 0 || save(self, argtup, 0) < 0 || self->write_func(self, &reduce, 1) < 0) - return -1; + goto finally; } /* Memoize. */ @@ -2245,26 +2272,37 @@ if (ob != NULL) { if (state && !PyDict_Check(state)) { if (put2(self, ob) < 0) - return -1; + goto finally; } else if (put(self, ob) < 0) - return -1; + goto finally; + + if (!( obid = PyLong_FromVoidPtr(ob))) + goto finally; + i = PyDict_DelItem(self->reducing_now, obid); + Py_DECREF(obid); + if (i < 0) + goto finally; } if (listitems && batch_list(self, listitems) < 0) - return -1; + goto finally; if (dictitems && batch_dict(self, dictitems) < 0) - return -1; + goto finally; if (state) { if (save(self, state, 0) < 0 || self->write_func(self, &build, 1) < 0) - return -1; + goto finally; } + res = 0; - return 0; +finally: + if (self->fast && !fast_save_leave(self, ob)) + res = -1; + return res; } static int @@ -2767,6 +2805,7 @@ self->arg = NULL; self->pers_func = NULL; self->inst_pers_func = NULL; + self->reducing_now = NULL; self->write_buf = NULL; self->fast = 0; self->nesting = 0; @@ -2787,6 +2826,8 @@ if (!( self->memo = PyDict_New())) goto err; + if (!( self->reducing_now = PyDict_New())) + goto err; if (PyFile_Check(file)) { self->fp = PyFile_AsFile(file); @@ -2884,6 +2925,7 @@ Py_XDECREF(self->arg); Py_XDECREF(self->file); Py_XDECREF(self->pers_func); + Py_XDECREF(self->reducing_now); Py_XDECREF(self->inst_pers_func); Py_XDECREF(self->dispatch_table); PyMem_Free(self->write_buf); Index: Lib/pickle.py =================================================================== RCS file: /cvsroot/python/python/dist/src/Lib/pickle.py,v retrieving revision 1.158 diff -u -r1.158 pickle.py --- Lib/pickle.py 7 Aug 2004 16:27:24 -0000 1.158 +++ Lib/pickle.py 8 Nov 2004 07:51:16 -0000 @@ -212,6 +212,7 @@ self.proto = int(protocol) self.bin = protocol >= 1 self.fast = 0 + self.reducing_now = set() def clear_memo(self): """Clears the pickler's "memo". @@ -371,6 +372,11 @@ save = self.save write = self.write + if obj is not None: + if id(obj) in self.reducing_now: + raise PicklingError("reduce cycle through %r" % obj) + self.reducing_now.add(id(obj)) + # Protocol 2 special case: if func's name is __newobj__, use NEWOBJ if self.proto >= 2 and getattr(func, "__name__", "") == "__newobj__": # A __reduce__ implementation can direct protocol 2 to @@ -417,6 +423,7 @@ if obj is not None: self.memoize(obj) + self.reducing_now.remove(id(obj)) # More new special cases (that work with older protocols as # well): when __reduce__ returns a tuple with 4 or 5 items, Index: Lib/copy_reg.py =================================================================== RCS file: /cvsroot/python/python/dist/src/Lib/copy_reg.py,v retrieving revision 1.27 diff -u -r1.27 copy_reg.py --- Lib/copy_reg.py 27 Jun 2003 16:58:43 -0000 1.27 +++ Lib/copy_reg.py 8 Nov 2004 07:51:16 -0000 @@ -43,12 +43,12 @@ # Support for pickling new-style objects -def _reconstructor(cls, base, state): +def _reconstructor(cls, base, *args): if base is object: obj = object.__new__(cls) else: - obj = base.__new__(cls, state) - base.__init__(obj, state) + obj = base.__new__(cls, *args) + base.__init__(obj, *args) return obj _HEAPTYPE = 1<<9 @@ -62,13 +62,21 @@ break else: base = object # not really reachable - if base is object: - state = None + if base in (object, list, dict): + args = self.__class__, base + if base is list: + listitems = iter(self) + else: + listitems = None + if base is dict: + dictitems = self.iteritems() + else: + dictitems = None else: if base is self.__class__: raise TypeError, "can't pickle %s objects" % base.__name__ - state = base(self) - args = (self.__class__, base, state) + args = self.__class__, base, base(self) + listitems = dictitems = None try: getstate = self.__getstate__ except AttributeError: @@ -76,15 +84,12 @@ raise TypeError("a class that defines __slots__ without " "defining __getstate__ cannot be pickled") try: - dict = self.__dict__ + state = self.__dict__ except AttributeError: - dict = None - else: - dict = getstate() - if dict: - return _reconstructor, args, dict + state = None else: - return _reconstructor, args + state = getstate() + return _reconstructor, args, state, listitems, dictitems # Helper for __reduce_ex__ protocol 2 Index: Lib/test/pickletester.py =================================================================== RCS file: /cvsroot/python/python/dist/src/Lib/test/pickletester.py,v retrieving revision 1.57 diff -u -r1.57 pickletester.py --- Lib/test/pickletester.py 27 Jul 2004 05:22:32 -0000 1.57 +++ Lib/test/pickletester.py 8 Nov 2004 07:51:16 -0000 @@ -363,6 +363,8 @@ class AbstractPickleTests(unittest.TestCase): # Subclass must define self.dumps, self.loads, self.error. + fast = False # Whether we should be testing the fast mode + _testdata = create_data() def setUp(self): @@ -798,6 +800,36 @@ self.assertEqual(x.foo, y.foo) self.assertEqual(x.bar, y.bar) + def test_stdreducecycle(self): + # Fast mode doesn't support cycles. + if self.fast: + return + + # Test standard __reduce__ implementations (reduce_2 in + # typeobject.c and _reduce_ex in copy_reg.py). Using the no-op + # subclasses is necessary to make those functions used instead + # of going through the pickle implementation's special + # handling of builtin types. + + # Simple cycles + L = MyList() + L.append(L) + d = MyDict() + d[0] = d + for proto in protocols: + newL = self.loads(self.dumps(L, proto)) + self.failUnless(newL[0] is newL) + newd = self.loads(self.dumps(d, proto)) + self.failUnless(d[0] is d) + + # More steps (this puts memoize in the cyclic path) + obj = MyList() + d = {'obj': obj} + obj.append(d) + for proto in protocols: + new = self.loads(self.dumps(obj, proto)) + self.failUnless(new[0]['obj'] is new) + def test_reduce_overrides_default_reduce_ex(self): for proto in 0, 1, 2: x = REX_one() @@ -825,6 +857,21 @@ y = self.loads(s) self.assertEqual(y._proto, None) + def test_reduce_cycle(self): + for proto in protocols: + x = REX_four() + try: + self.dumps(x, proto) + except Exception, e: + # XXX: Need reference to real PicklingError + exceps = 'PicklingError', + if self.fast: + exceps += 'ValueError', + if e.__class__.__name__ not in exceps: + raise + else: + self.fail("successfully pickled with reduce cycle") + # Test classes for reduce_ex class REX_one(object): @@ -849,6 +896,10 @@ def __reduce__(self): raise TestFailed, "This __reduce__ shouldn't be called" +class REX_four(object): + def __reduce__(self): + return REX_four, (self,) + # Test classes for newobj class MyInt(int): Index: Lib/test/test_cpickle.py =================================================================== RCS file: /cvsroot/python/python/dist/src/Lib/test/test_cpickle.py,v retrieving revision 1.15 diff -u -r1.15 test_cpickle.py --- Lib/test/test_cpickle.py 1 May 2003 17:45:36 -0000 1.15 +++ Lib/test/test_cpickle.py 8 Nov 2004 07:51:16 -0000 @@ -45,6 +45,8 @@ class cPickleFastPicklerTests(AbstractPickleTests): + fast = True + def dumps(self, arg, proto=0): f = StringIO() p = cPickle.Pickler(f, proto)