diff -r 77d24f51effc Include/dictobject.h --- a/Include/dictobject.h Tue Jan 12 06:18:32 2016 -0800 +++ b/Include/dictobject.h Wed Jan 13 11:37:54 2016 +0100 @@ -23,6 +23,7 @@ typedef struct _dictkeysobject PyDictKey typedef struct { PyObject_HEAD Py_ssize_t ma_used; + PY_UINT64_T ma_version; PyDictKeysObject *ma_keys; PyObject **ma_values; } PyDictObject; diff -r 77d24f51effc Lib/test/test_dict.py --- a/Lib/test/test_dict.py Tue Jan 12 06:18:32 2016 -0800 +++ b/Lib/test/test_dict.py Wed Jan 13 11:37:54 2016 +0100 @@ -963,5 +963,137 @@ class Dict(dict): class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol): type2test = Dict + +@support.cpython_only +class DictVersionTests(unittest.TestCase): + type2test = Dict + + def setUp(self): + import _testcapi + self.dict_get_version = _testcapi.dict_get_version + + def test_constructor(self): + d = self.type2test() + self.assertEqual(self.dict_get_version(d), 0) + + d = self.type2test(x='x') + self.assertEqual(self.dict_get_version(d), 1) + + d = self.type2test(x='x', y='y') + self.assertEqual(self.dict_get_version(d), 2) + + def test_setitem(self): + value = object() + + d = self.type2test() + self.assertEqual(self.dict_get_version(d), 0) + + d['x'] = 'x' + self.assertEqual(self.dict_get_version(d), 1) + + d['y'] = 'y' + self.assertEqual(self.dict_get_version(d), 2) + + d['x'] = value + self.assertEqual(self.dict_get_version(d), 3) + + # don't change the version if the value is the same + d['x'] = value + self.assertEqual(self.dict_get_version(d), 3) + + def test_setdefault(self): + d = self.type2test() + self.assertEqual(self.dict_get_version(d), 0) + + d.setdefault('key', 'value1') + self.assertEqual(self.dict_get_version(d), 1) + + # don't change the version if the key already exists + d.setdefault('key', 'value2') + self.assertEqual(self.dict_get_version(d), 1) + + def test_delitem(self): + d = self.type2test(x='x') + self.assertEqual(self.dict_get_version(d), 1) + + del d['x'] + self.assertEqual(self.dict_get_version(d), 2) + + # don't change the version if the key doesn't exist + self.assertRaises(KeyError, d.__delitem__, 'x') + self.assertEqual(self.dict_get_version(d), 2) + + def test_pop(self): + d = self.type2test(key='value') + self.assertEqual(self.dict_get_version(d), 1) + + d.pop('key') + self.assertEqual(self.dict_get_version(d), 2) + + # don't change the version if the key doesn't exists + self.assertRaises(KeyError, d.pop, 'key') + self.assertEqual(self.dict_get_version(d), 2) + + def test_popitem(self): + d = self.type2test(key='value') + self.assertEqual(self.dict_get_version(d), 1) + + d.popitem() + self.assertEqual(self.dict_get_version(d), 2) + + # don't change the version if the dict is empty + self.assertRaises(KeyError, d.popitem) + self.assertEqual(self.dict_get_version(d), 2) + + def test_update(self): + value = object() + + d = self.type2test(key=value) + self.assertEqual(self.dict_get_version(d), 1) + + # don't change the version if the value is the same + d.update(key=value) + self.assertEqual(self.dict_get_version(d), 1) + + d.update(key="other") + self.assertEqual(self.dict_get_version(d), 2) + + def test_clear(self): + value = object() + + d = self.type2test(key=value) + self.assertEqual(self.dict_get_version(d), 1) + + d.clear() + self.assertEqual(self.dict_get_version(d), 2) + + # don't change the version if the dict is empty + d.clear() + self.assertEqual(self.dict_get_version(d), 2) + + def test_copy(self): + d = self.type2test(a=1, b=2) + d2 = d.copy() + self.assertEqual(self.dict_get_version(d2), + self.dict_get_version(d)) + + @support.cpython_only + def test_version_overflow(self): + from _testcapi import dict_set_version + d = {} + dict_set_version(d, 2 ** 64 - 1) + self.assertEqual(self.dict_get_version(d), 2 ** 64 - 1) + d['x'] = 'x' + self.assertEqual(self.dict_get_version(d), 0) + del d['x'] + self.assertEqual(self.dict_get_version(d), 1) + d['y'] = 'y' + self.assertEqual(self.dict_get_version(d), 2) + + +class DictSubtypeVersionTests(DictVersionTests): + type2test = Dict + + if __name__ == "__main__": unittest.main() diff -r 77d24f51effc Lib/test/test_ordered_dict.py --- a/Lib/test/test_ordered_dict.py Tue Jan 12 06:18:32 2016 -0800 +++ b/Lib/test/test_ordered_dict.py Wed Jan 13 11:37:54 2016 +0100 @@ -614,7 +614,7 @@ class CPythonOrderedDictTests(OrderedDic size = support.calcobjsize check = self.check_sizeof - basicsize = size('n2P' + '3PnPn2P') + calcsize('2nPn') + basicsize = size('nL2P' + '3PnPn2P') + calcsize('2nPn') entrysize = calcsize('n2P') + calcsize('P') nodesize = calcsize('Pn2P') diff -r 77d24f51effc Lib/test/test_sys.py --- a/Lib/test/test_sys.py Tue Jan 12 06:18:32 2016 -0800 +++ b/Lib/test/test_sys.py Wed Jan 13 11:37:54 2016 +0100 @@ -916,9 +916,9 @@ class SizeofTest(unittest.TestCase): # method-wrapper (descriptor object) check({}.__iter__, size('2P')) # dict - check({}, size('n2P') + calcsize('2nPn') + 8*calcsize('n2P')) + check({}, size('n2P') + 8 + calcsize('2nPn') + 8*calcsize('n2P')) longdict = {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8} - check(longdict, size('n2P') + calcsize('2nPn') + 16*calcsize('n2P')) + check(longdict, size('n2P') + 8 + calcsize('2nPn') + 16*calcsize('n2P')) # dictionary-keyview check({}.keys(), size('P')) # dictionary-valueview @@ -1079,7 +1079,7 @@ class SizeofTest(unittest.TestCase): class newstyleclass(object): pass check(newstyleclass, s) # dict with shared keys - check(newstyleclass().__dict__, size('n2P' + '2nPn')) + check(newstyleclass().__dict__, size('n2P' + '2nPn') + 8) # unicode # each tuple contains a string and its expected character size # don't put any static strings here, as they may contain diff -r 77d24f51effc Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c Tue Jan 12 06:18:32 2016 -0800 +++ b/Modules/_testcapimodule.c Wed Jan 13 11:37:54 2016 +0100 @@ -3529,6 +3529,61 @@ get_recursion_depth(PyObject *self, PyOb return PyLong_FromLong(tstate->recursion_depth - 1); } +static PyObject * +dict_get_version(PyObject *self, PyObject *args) +{ + PyDictObject *dict; + PY_UINT64_T version; + + if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &dict)) + return NULL; + + version = dict->ma_version; + +#ifdef HAVE_LONG_LONG + Py_BUILD_ASSERT(sizeof(unsigned PY_LONG_LONG) >= sizeof(version)); + return PyLong_FromUnsignedLongLong((unsigned PY_LONG_LONG)version); +#else + Py_BUILD_ASSERT(sizeof(size_t) >= sizeof(version)); + return PyLong_FromSize_t((size_t)version); +#endif +} + +static PyObject * +dict_set_version(PyObject *self, PyObject *args) +{ + PyDictObject *dict; + PyObject *version_obj; +#ifdef HAVE_LONG_LONG + unsigned PY_LONG_LONG version; +#else + size_t version; +#endif + + if (!PyArg_ParseTuple(args, "O!O!", + &PyDict_Type, &dict, + &PyLong_Type, &version_obj)) + return NULL; + +#ifdef HAVE_LONG_LONG + Py_BUILD_ASSERT(sizeof(unsigned PY_LONG_LONG) <= sizeof(version)); + + version = PyLong_AsUnsignedLongLong(version_obj); + if (version == (unsigned PY_LONG_LONG)-1 && PyErr_Occurred()) + return NULL; +#else + Py_BUILD_ASSERT(sizeof(size_t) <= sizeof(version)); + + version = PyLong_AsSize_t(version_obj); + if (version == (size_t)-1 && PyErr_Occurred()) + return NULL; +#endif + + dict->ma_version = version; + + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, @@ -3706,6 +3761,8 @@ static PyMethodDef TestMethods[] = { {"PyTime_AsMilliseconds", test_PyTime_AsMilliseconds, METH_VARARGS}, {"PyTime_AsMicroseconds", test_PyTime_AsMicroseconds, METH_VARARGS}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, + {"dict_get_version", dict_get_version, METH_VARARGS}, + {"dict_set_version", dict_set_version, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff -r 77d24f51effc Objects/dictobject.c --- a/Objects/dictobject.c Tue Jan 12 06:18:32 2016 -0800 +++ b/Objects/dictobject.c Wed Jan 13 11:37:54 2016 +0100 @@ -383,6 +383,7 @@ new_dict(PyDictKeysObject *keys, PyObjec mp->ma_keys = keys; mp->ma_values = values; mp->ma_used = 0; + mp->ma_version = 0; return (PyObject *)mp; } @@ -802,13 +803,18 @@ insertdict(PyDictObject *mp, PyObject *k assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict); Py_INCREF(value); MAINTAIN_TRACKING(mp, key, value); + old_value = *value_addr; if (old_value != NULL) { assert(ep->me_key != NULL && ep->me_key != dummy); *value_addr = value; + if (value != old_value) + mp->ma_version++; Py_DECREF(old_value); /* which **CAN** re-enter (see issue #22653) */ } else { + mp->ma_version++; + if (ep->me_key == NULL) { Py_INCREF(key); if (mp->ma_keys->dk_usable <= 0) { @@ -1280,6 +1286,8 @@ PyDict_DelItem(PyObject *op, PyObject *k _PyErr_SetKeyError(key); return -1; } + + mp->ma_version++; old_value = *value_addr; *value_addr = NULL; mp->ma_used--; @@ -1350,6 +1358,7 @@ PyDict_Clear(PyObject *op) mp->ma_keys = Py_EMPTY_KEYS; mp->ma_values = empty_values; mp->ma_used = 0; + mp->ma_version++; /* ...then clear the keys and values */ if (oldvalues != NULL) { n = DK_SIZE(oldkeys); @@ -1483,8 +1492,11 @@ PyObject * _PyErr_SetKeyError(key); return NULL; } + *value_addr = NULL; + mp->ma_version++; mp->ma_used--; + if (!_PyDict_HasSplitTable(mp)) { ENSURE_ALLOWS_DELETIONS(mp); old_key = ep->me_key; @@ -2417,6 +2429,7 @@ PyDict_SetDefault(PyObject *d, PyObject val = defaultobj; mp->ma_keys->dk_usable--; mp->ma_used++; + mp->ma_version++; } return val; } @@ -2515,6 +2528,7 @@ dict_popitem(PyDictObject *mp) Py_INCREF(dummy); ep->me_key = dummy; ep->me_value = NULL; + mp->ma_version++; mp->ma_used--; assert(mp->ma_keys->dk_entries[0].me_value == NULL); mp->ma_keys->dk_entries[0].me_hash = i + 1; /* next place to start */ @@ -2720,6 +2734,7 @@ dict_new(PyTypeObject *type, PyObject *a _PyObject_GC_UNTRACK(d); d->ma_used = 0; + d->ma_version = 0; d->ma_keys = new_keys_object(PyDict_MINSIZE_COMBINED); if (d->ma_keys == NULL) { Py_DECREF(self);