diff -r 60b266d7dcbd Include/dictobject.h --- a/Include/dictobject.h Sat Jan 09 03:28:01 2016 -0500 +++ b/Include/dictobject.h Sat Jan 09 11:59:14 2016 +0100 @@ -23,6 +23,7 @@ typedef struct _dictkeysobject PyDictKey typedef struct { PyObject_HEAD Py_ssize_t ma_used; + size_t ma_version; PyDictKeysObject *ma_keys; PyObject **ma_values; } PyDictObject; diff -r 60b266d7dcbd Lib/test/test_dict.py --- a/Lib/test/test_dict.py Sat Jan 09 03:28:01 2016 -0500 +++ b/Lib/test/test_dict.py Sat Jan 09 11:59:14 2016 +0100 @@ -963,5 +963,131 @@ class Dict(dict): class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol): type2test = Dict + +class DictVersionTests(unittest.TestCase): + type2test = Dict + + def test_constructor(self): + d = self.type2test() + self.assertEqual(d.__version__, 0) + + d = self.type2test(x='x') + self.assertEqual(d.__version__, 1) + + d = self.type2test(x='x', y='y') + self.assertEqual(d.__version__, 2) + + def test_setitem(self): + value = object() + + d = self.type2test() + self.assertEqual(d.__version__, 0) + + d['x'] = 'x' + self.assertEqual(d.__version__, 1) + + d['y'] = 'y' + self.assertEqual(d.__version__, 2) + + d['x'] = value + self.assertEqual(d.__version__, 3) + + # don't change the version if the value is the same + d['x'] = value + self.assertEqual(d.__version__, 3) + + def test_setdefault(self): + d = self.type2test() + self.assertEqual(d.__version__, 0) + + d.setdefault('key', 'value1') + self.assertEqual(d.__version__, 1) + + # don't change the version if the key already exists + d.setdefault('key', 'value2') + self.assertEqual(d.__version__, 1) + + def test_delitem(self): + d = self.type2test(x='x') + self.assertEqual(d.__version__, 1) + + del d['x'] + self.assertEqual(d.__version__, 2) + + # don't change the version if the key doesn't exist + self.assertRaises(KeyError, d.__delitem__, 'x') + self.assertEqual(d.__version__, 2) + + def test_pop(self): + d = self.type2test(key='value') + self.assertEqual(d.__version__, 1) + + d.pop('key') + self.assertEqual(d.__version__, 2) + + # don't change the version if the key doesn't exists + self.assertRaises(KeyError, d.pop, 'key') + self.assertEqual(d.__version__, 2) + + def test_popitem(self): + d = self.type2test(key='value') + self.assertEqual(d.__version__, 1) + + d.popitem() + self.assertEqual(d.__version__, 2) + + # don't change the version if the dict is empty + self.assertRaises(KeyError, d.popitem) + self.assertEqual(d.__version__, 2) + + def test_update(self): + value = object() + + d = self.type2test(key=value) + self.assertEqual(d.__version__, 1) + + # don't change the version if the value is the same + d.update(key=value) + self.assertEqual(d.__version__, 1) + + d.update(key="other") + self.assertEqual(d.__version__, 2) + + def test_clear(self): + value = object() + + d = self.type2test(key=value) + self.assertEqual(d.__version__, 1) + + d.clear() + self.assertEqual(d.__version__, 2) + + # don't change the version if the dict is empty + d.clear() + self.assertEqual(d.__version__, 2) + + def test_copy(self): + d = self.type2test(a=1, b=2) + d2 = d.copy() + self.assertEqual(d2.__version__, d.__version__) + + @support.cpython_only + def test_version_overflow(self): + from _testcapi import dict_setversion, PY_SIZE_MAX + d = {} + dict_setversion(d, PY_SIZE_MAX) + self.assertEqual(d.__version__, PY_SIZE_MAX) + d['x'] = 'x' + self.assertEqual(d.__version__, 0) + del d['x'] + self.assertEqual(d.__version__, 1) + d['y'] = 'y' + self.assertEqual(d.__version__, 2) + + +class DictSubtypeVersionTests(DictVersionTests): + type2test = Dict + + if __name__ == "__main__": unittest.main() diff -r 60b266d7dcbd Modules/_testcapimodule.c --- a/Modules/_testcapimodule.c Sat Jan 09 03:28:01 2016 -0500 +++ b/Modules/_testcapimodule.c Sat Jan 09 11:59:14 2016 +0100 @@ -3529,6 +3529,26 @@ get_recursion_depth(PyObject *self, PyOb return PyLong_FromLong(tstate->recursion_depth - 1); } +static PyObject * +dict_setversion(PyObject *self, PyObject *args) +{ + PyObject *dict, *version_obj; + size_t version; + + if (!PyArg_ParseTuple(args, "O!O!", + &PyDict_Type, &dict, + &PyLong_Type, &version_obj)) + return NULL; + + version = PyLong_AsSize_t(version_obj); + if (version == (size_t)-1 && PyErr_Occurred()) + return NULL; + + ((PyDictObject *)dict)->ma_version = version; + + Py_RETURN_NONE; +} + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, @@ -3706,6 +3726,7 @@ 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_setversion", dict_setversion, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -4114,6 +4135,7 @@ PyInit__testcapi(void) PyModule_AddObject(m, "LONG_MAX", PyLong_FromLong(LONG_MAX)); PyModule_AddObject(m, "LONG_MIN", PyLong_FromLong(LONG_MIN)); PyModule_AddObject(m, "ULONG_MAX", PyLong_FromUnsignedLong(ULONG_MAX)); + PyModule_AddObject(m, "PY_SIZE_MAX", PyLong_FromSize_t(PY_SIZE_MAX)); PyModule_AddObject(m, "FLT_MAX", PyFloat_FromDouble(FLT_MAX)); PyModule_AddObject(m, "FLT_MIN", PyFloat_FromDouble(FLT_MIN)); PyModule_AddObject(m, "DBL_MAX", PyFloat_FromDouble(DBL_MAX)); diff -r 60b266d7dcbd Objects/dictobject.c --- a/Objects/dictobject.c Sat Jan 09 03:28:01 2016 -0500 +++ b/Objects/dictobject.c Sat Jan 09 11:59:14 2016 +0100 @@ -69,6 +69,7 @@ to the combined-table form. #include "Python.h" #include "dict-common.h" #include "stringlib/eq.h" +#include "structmember.h" /*[clinic input] class dict "PyDictObject *" "&PyDict_Type" @@ -188,6 +189,11 @@ static PyObject _dummy_struct; #define dummy (&_dummy_struct) +/* FIXME: handle integer overflow: never use value 0. + The version 0 is reserved for "missing key". */ +#define DICT_INC_VERSION(mp) \ + do { ((PyDictObject *)(mp))->ma_version++; } while (0) + #ifdef Py_REF_DEBUG PyObject * _PyDict_Dummy(void) @@ -383,6 +389,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 +809,19 @@ 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) { + DICT_INC_VERSION(mp); + } Py_DECREF(old_value); /* which **CAN** re-enter (see issue #22653) */ } else { + DICT_INC_VERSION(mp); + if (ep->me_key == NULL) { Py_INCREF(key); if (mp->ma_keys->dk_usable <= 0) { @@ -1280,6 +1293,8 @@ PyDict_DelItem(PyObject *op, PyObject *k _PyErr_SetKeyError(key); return -1; } + + DICT_INC_VERSION(mp); old_value = *value_addr; *value_addr = NULL; mp->ma_used--; @@ -1350,6 +1365,7 @@ PyDict_Clear(PyObject *op) mp->ma_keys = Py_EMPTY_KEYS; mp->ma_values = empty_values; mp->ma_used = 0; + DICT_INC_VERSION(mp); /* ...then clear the keys and values */ if (oldvalues != NULL) { n = DK_SIZE(oldkeys); @@ -1483,8 +1499,11 @@ PyObject * _PyErr_SetKeyError(key); return NULL; } + *value_addr = NULL; + DICT_INC_VERSION(mp); mp->ma_used--; + if (!_PyDict_HasSplitTable(mp)) { ENSURE_ALLOWS_DELETIONS(mp); old_key = ep->me_key; @@ -2417,6 +2436,7 @@ PyDict_SetDefault(PyObject *d, PyObject val = defaultobj; mp->ma_keys->dk_usable--; mp->ma_used++; + DICT_INC_VERSION(mp); } return val; } @@ -2515,6 +2535,7 @@ dict_popitem(PyDictObject *mp) Py_INCREF(dummy); ep->me_key = dummy; ep->me_value = NULL; + DICT_INC_VERSION(mp); 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 +2741,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); @@ -2740,6 +2762,12 @@ dict_iter(PyDictObject *dict) return dictiter_new(dict, &PyDictIterKey_Type); } +static PyMemberDef mapp_memberlist[] = { + {"__version__", T_ULONG, offsetof(PyDictObject, ma_version), READONLY, + "dictionary object"}, + {NULL} /* Sentinel */ +}; + PyDoc_STRVAR(dictionary_doc, "dict() -> new empty dictionary\n" "dict(mapping) -> new dictionary initialized from a mapping object's\n" @@ -2781,7 +2809,7 @@ PyTypeObject PyDict_Type = { (getiterfunc)dict_iter, /* tp_iter */ 0, /* tp_iternext */ mapp_methods, /* tp_methods */ - 0, /* tp_members */ + mapp_memberlist, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */