diff -r 6588812d79a9 Include/dictobject.h --- a/Include/dictobject.h Fri Jan 22 15:59:02 2016 +0100 +++ b/Include/dictobject.h Fri Jan 22 17:19:59 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 6588812d79a9 Lib/test/test_dict.py --- a/Lib/test/test_dict.py Fri Jan 22 15:59:02 2016 +0100 +++ b/Lib/test/test_dict.py Fri Jan 22 17:19:59 2016 +0100 @@ -1,11 +1,14 @@ +import collections +import collections.abc +import contextlib +import gc +import pickle +import random +import string import unittest +import weakref from test import support -import collections, random, string -import collections.abc -import gc, weakref -import pickle - class DictTest(unittest.TestCase): @@ -963,5 +966,141 @@ 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 + self.seen_versions = set() + + @contextlib.contextmanager + def check_version_dont_change(self, d): + version1 = self.dict_get_version(d) + self.seen_versions.add(version1) + try: + yield + finally: + version2 = self.dict_get_version(d) + self.assertEqual(version2, version1, "version changed") + + def check_version_changed(self, d): + version = self.dict_get_version(d) + self.assertNotIn(version, self.seen_versions) + self.seen_versions.add(version) + + def test_constructor(self): + d = self.type2test() + self.check_version_changed(d) + + d2 = self.type2test(x='x') + self.check_version_changed(d2) + + d3 = self.type2test(x='x', y='y') + self.check_version_changed(d3) + + def test_setitem(self): + value = object() + + d = self.type2test() + self.check_version_changed(d) + + d['x'] = 'x' + self.check_version_changed(d) + + d['y'] = 'y' + self.check_version_changed(d) + + d['x'] = value + self.check_version_changed(d) + + # don't change the version if the value is the same + with self.check_version_dont_change(d): + d['x'] = value + + def test_setdefault(self): + d = self.type2test() + self.check_version_changed(d) + + d.setdefault('key', 'value1') + self.check_version_changed(d) + + # don't change the version if the key already exists + with self.check_version_dont_change(d): + d.setdefault('key', 'value2') + + def test_delitem(self): + d = self.type2test(x='x') + self.check_version_changed(d) + + del d['x'] + self.check_version_changed(d) + + # don't change the version if the key doesn't exist + with self.check_version_dont_change(d): + self.assertRaises(KeyError, d.__delitem__, 'x') + + def test_pop(self): + d = self.type2test(key='value') + self.check_version_changed(d) + + d.pop('key') + self.check_version_changed(d) + + # don't change the version if the key doesn't exists + with self.check_version_dont_change(d): + self.assertRaises(KeyError, d.pop, 'key') + + def test_popitem(self): + d = self.type2test(key='value') + self.check_version_changed(d) + + d.popitem() + self.check_version_changed(d) + + # don't change the version if the dict is empty + with self.check_version_dont_change(d): + self.assertRaises(KeyError, d.popitem) + + def test_update(self): + value = object() + + d = self.type2test(key=value) + self.check_version_changed(d) + + # don't change the version if the value is the same + with self.check_version_dont_change(d): + d.update(key=value) + + d.update(key="other") + self.check_version_changed(d) + + def test_clear(self): + value = object() + + d = self.type2test(key=value) + self.check_version_changed(d) + + d.clear() + self.check_version_changed(d) + + # don't change the version if the dict is empty + with self.check_version_dont_change(d): + d.clear() + + def test_copy(self): + d = self.type2test(a=1, b=2) + self.check_version_changed(d) + + d2 = d.copy() + self.check_version_changed(d2) + + +class DictSubtypeVersionTests(DictVersionTests): + type2test = Dict + + if __name__ == "__main__": unittest.main() diff -r 6588812d79a9 Lib/test/test_ordered_dict.py --- a/Lib/test/test_ordered_dict.py Fri Jan 22 15:59:02 2016 +0100 +++ b/Lib/test/test_ordered_dict.py Fri Jan 22 17:19:59 2016 +0100 @@ -627,7 +627,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 6588812d79a9 Lib/test/test_sys.py --- a/Lib/test/test_sys.py Fri Jan 22 15:59:02 2016 +0100 +++ b/Lib/test/test_sys.py Fri Jan 22 17:19:59 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 6588812d79a9 Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c Fri Jan 22 15:59:02 2016 +0100 +++ b/Modules/_testcapimodule.c Fri Jan 22 17:19:59 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 6588812d79a9 Objects/dictobject.c --- a/Objects/dictobject.c Fri Jan 22 15:59:02 2016 +0100 +++ b/Objects/dictobject.c Fri Jan 22 17:19:59 2016 +0100 @@ -209,6 +209,13 @@ static PyDictKeyEntry *lookdict_split(Py static int dictresize(PyDictObject *mp, Py_ssize_t minused); +/* Global counter used to set ma_version field of dictionary. + * It is incremented each time that a dictionary is created and each + * time that a dictionary is modified. */ +static PY_UINT64_T pydict_global_version = 0; + +#define DICT_NEXT_VERSION() (++pydict_global_version ) + /* Dictionary reuse scheme to save calls to malloc, free, and memset */ #ifndef PyDict_MAXFREELIST #define PyDict_MAXFREELIST 80 @@ -383,6 +390,7 @@ new_dict(PyDictKeysObject *keys, PyObjec mp->ma_keys = keys; mp->ma_values = values; mp->ma_used = 0; + mp->ma_version = DICT_NEXT_VERSION(); return (PyObject *)mp; } @@ -802,13 +810,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 = DICT_NEXT_VERSION(); Py_DECREF(old_value); /* which **CAN** re-enter (see issue #22653) */ } else { + mp->ma_version = DICT_NEXT_VERSION(); + if (ep->me_key == NULL) { Py_INCREF(key); if (mp->ma_keys->dk_usable <= 0) { @@ -1278,6 +1291,8 @@ PyDict_DelItem(PyObject *op, PyObject *k _PyErr_SetKeyError(key); return -1; } + + mp->ma_version = DICT_NEXT_VERSION(); old_value = *value_addr; *value_addr = NULL; mp->ma_used--; @@ -1348,6 +1363,7 @@ PyDict_Clear(PyObject *op) mp->ma_keys = Py_EMPTY_KEYS; mp->ma_values = empty_values; mp->ma_used = 0; + mp->ma_version = DICT_NEXT_VERSION(); /* ...then clear the keys and values */ if (oldvalues != NULL) { n = DK_SIZE(oldkeys); @@ -1481,8 +1497,11 @@ PyObject * _PyErr_SetKeyError(key); return NULL; } + *value_addr = NULL; + mp->ma_version = DICT_NEXT_VERSION(); mp->ma_used--; + if (!_PyDict_HasSplitTable(mp)) { ENSURE_ALLOWS_DELETIONS(mp); old_key = ep->me_key; @@ -2415,6 +2434,7 @@ PyDict_SetDefault(PyObject *d, PyObject val = defaultobj; mp->ma_keys->dk_usable--; mp->ma_used++; + mp->ma_version = DICT_NEXT_VERSION(); } return val; } @@ -2513,6 +2533,7 @@ dict_popitem(PyDictObject *mp) Py_INCREF(dummy); ep->me_key = dummy; ep->me_value = NULL; + mp->ma_version = DICT_NEXT_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 */ @@ -2718,6 +2739,7 @@ dict_new(PyTypeObject *type, PyObject *a _PyObject_GC_UNTRACK(d); d->ma_used = 0; + d->ma_version = DICT_NEXT_VERSION(); d->ma_keys = new_keys_object(PyDict_MINSIZE_COMBINED); if (d->ma_keys == NULL) { Py_DECREF(self);