diff -r 91bafdf7d7a4 Doc/c-api/dict.rst --- a/Doc/c-api/dict.rst Thu Mar 29 06:49:29 2012 -0400 +++ b/Doc/c-api/dict.rst Thu Mar 29 13:46:25 2012 +0200 @@ -36,11 +36,11 @@ Dictionary Objects Return a new empty dictionary, or *NULL* on failure. -.. c:function:: PyObject* PyDictProxy_New(PyObject *dict) +.. c:function:: PyObject* PyDictProxy_New(PyObject *mapping) - Return a proxy object for a mapping which enforces read-only behavior. - This is normally used to create a proxy to prevent modification of the - dictionary for non-dynamic class types. + Return a :class:`types.MappingViewType` object for a mapping which + enforces read-only behavior. This is normally used to create a view to + prevent modification of the dictionary for non-dynamic class types. .. c:function:: void PyDict_Clear(PyObject *p) diff -r 91bafdf7d7a4 Doc/library/stdtypes.rst --- a/Doc/library/stdtypes.rst Thu Mar 29 06:49:29 2012 -0400 +++ b/Doc/library/stdtypes.rst Thu Mar 29 13:46:25 2012 +0200 @@ -2258,13 +2258,13 @@ pairs within braces, for example: ``{'ja .. method:: items() - Return a new view of the dictionary's items (``(key, value)`` pairs). See - below for documentation of view objects. + Return a new view of the dictionary's items (``(key, value)`` pairs). + See the :ref:`documentation of view objects `. .. method:: keys() - Return a new view of the dictionary's keys. See below for documentation of - view objects. + Return a new view of the dictionary's keys. See the :ref:`documentation + of view objects `. .. method:: pop(key[, default]) @@ -2298,8 +2298,12 @@ pairs within braces, for example: ``{'ja .. method:: values() - Return a new view of the dictionary's values. See below for documentation of - view objects. + Return a new view of the dictionary's values. See the + :ref:`documentation of view objects `. + +.. seealso:: + :class:`types.MappingViewType` can be used to create a read-only view + of a :class:`dict`. .. _dict-views: diff -r 91bafdf7d7a4 Doc/library/types.rst --- a/Doc/library/types.rst Thu Mar 29 06:49:29 2012 -0400 +++ b/Doc/library/types.rst Thu Mar 29 13:46:25 2012 +0200 @@ -85,3 +85,57 @@ The module defines the following names: In other implementations of Python, this type may be identical to ``GetSetDescriptorType``. + +.. class:: MappingViewType(mapping) + + Read-only view of a mapping. It provides a dynamic view on the mapping's + entries, which means that when the mapping changes, the view reflects these + changes. + + .. versionadded:: 3.3 + + .. describe:: key in mappingview + + Return ``True`` if the underlying mapping has a key *key*, else + ``False``. + + .. describe:: mappingview[key] + + Return the item of the underlying mapping with key *key*. Raises a + :exc:`KeyError` if *key* is not in the underlying mapping. + + .. describe:: iter(mappingview) + + Return an iterator over the keys of the underlying mapping. This is a + shortcut for ``iter(mappingview.keys())``. + + .. describe:: len(mappingview) + + Return the number of items in the underlying mapping. + + .. method:: copy() + + Return a shallow copy of the underlying mapping as a :class:`dict`. + + .. method:: get(key[, default]) + + Return the value for *key* if *key* is in the underlying mapping, else + *default*. If *default* is not given, it defaults to ``None``, so that + this method never raises a :exc:`KeyError`. + + .. method:: items() + + Return a new view of the underlying mapping's items (``(key, value)`` + pairs). See the :ref:`documentation of view objects `. + + .. method:: keys() + + Return a new view of the underlying mapping's keys. See the + :ref:`documentation of view objects `. + + .. method:: values() + + Return a new view of the underlying mapping's values. See the + :ref:`documentation of view objects `. + + diff -r 91bafdf7d7a4 Lib/test/test_descr.py --- a/Lib/test/test_descr.py Thu Mar 29 06:49:29 2012 -0400 +++ b/Lib/test/test_descr.py Thu Mar 29 13:46:25 2012 +0200 @@ -1,8 +1,9 @@ import builtins +import collections import gc +import math import sys import types -import math import unittest import weakref @@ -4574,11 +4575,11 @@ class DictProxyTests(unittest.TestCase): self.assertEqual(type(C.__dict__), type(B.__dict__)) def test_repr(self): - # Testing dict_proxy.__repr__. + # Testing mappingview.__repr__. # We can't blindly compare with the repr of another dict as ordering # of keys and values is arbitrary and may differ. r = repr(self.C.__dict__) - self.assertTrue(r.startswith('dict_proxy('), r) + self.assertTrue(r.startswith('mappingview('), r) self.assertTrue(r.endswith(')'), r) for k, v in self.C.__dict__.items(): self.assertIn('{!r}: {!r}'.format(k, v), r) @@ -4633,11 +4634,186 @@ class MiscTests(unittest.TestCase): self.assertEqual(X.mykey2, 'from Base2') +class MappingViewTests(unittest.TestCase): + mappingview = types.MappingViewType + + def test_constructor(self): + class userdict(dict): + pass + + mapping = {'x': 1, 'y': 2} + self.assertEqual(self.mappingview(mapping), mapping) + mapping = userdict(x=1, y=2) + self.assertEqual(self.mappingview(mapping), mapping) + mapping = collections.ChainMap({'x': 1}, {'y': 2}) + self.assertEqual(self.mappingview(mapping), mapping) + + self.assertRaises(TypeError, self.mappingview, 10) + self.assertRaises(TypeError, self.mappingview, ("a", "tuple")) + self.assertRaises(TypeError, self.mappingview, ["a", "list"]) + + def test_methods(self): + attrs = dir(self.mappingview({})) - set(dir(object())) + self.assertEqual(attrs, { + '__contains__', + '__getitem__', + '__iter__', + '__len__', + 'copy', + 'get', + 'items', + 'keys', + 'values', + }) + + def test_get(self): + view = self.mappingview({'a': 'A', 'b': 'B'}) + self.assertEqual(view['a'], 'A') + self.assertEqual(view['b'], 'B') + self.assertRaises(KeyError, view.__getitem__, 'xxx') + self.assertEqual(view.get('a'), 'A') + self.assertIsNone(view.get('xxx')) + self.assertEqual(view.get('xxx', 42), 42) + + def test_missing(self): + class dictmissing(dict): + def __missing__(self, key): + return "missing=%s" % key + + view = self.mappingview(dictmissing(x=1)) + self.assertEqual(view['x'], 1) + self.assertEqual(view['y'], 'missing=y') + self.assertEqual(view.get('x'), 1) + self.assertEqual(view.get('y'), None) + self.assertEqual(view.get('y', 42), 42) + self.assertTrue('x' in view) + self.assertFalse('y' in view) + + def test_customdict(self): + class customdict(dict): + def __contains__(self, key): + if key == 'magic': + return True + else: + return dict.__contains__(self, key) + + def __iter__(self): + return iter(('iter',)) + + def __len__(self): + return 500 + + def copy(self): + return 'copy' + + def keys(self): + return 'keys' + + def items(self): + return 'items' + + def values(self): + return 'values' + + def __getitem__(self, key): + return "getitem=%s" % dict.__getitem__(self, key) + + def get(self, key, default=None): + return "get=%s" % dict.get(self, key, 'default=%r' % default) + + custom = customdict({'key': 'value'}) + view = self.mappingview(custom) + self.assertTrue('key' in view) + self.assertTrue('magic' in view) + self.assertFalse('xxx' in view) + self.assertEqual(view['key'], 'getitem=value') + self.assertRaises(KeyError, view.__getitem__, 'xxx') + self.assertEqual(tuple(view), ('iter',)) + self.assertEqual(len(view), 500) + self.assertEqual(view.copy(), 'copy') + self.assertEqual(view.get('key'), 'get=value') + self.assertEqual(view.get('xxx'), 'get=default=None') + self.assertEqual(view.items(), 'items') + self.assertEqual(view.keys(), 'keys') + self.assertEqual(view.values(), 'values') + + def test_chainmap(self): + d1 = {'x': 1} + d2 = {'y': 2} + mapping = collections.ChainMap(d1, d2) + view = self.mappingview(mapping) + self.assertTrue('x' in view) + self.assertTrue('y' in view) + self.assertFalse('z' in view) + self.assertEqual(view['x'], 1) + self.assertEqual(view['y'], 2) + self.assertRaises(KeyError, view.__getitem__, 'z') + self.assertEqual(tuple(sorted(view)), ('x', 'y')) + self.assertEqual(len(view), 2) + copy = view.copy() + self.assertIsNot(copy, mapping) + self.assertEqual(copy, mapping) + self.assertEqual(view.get('x'), 1) + self.assertEqual(view.get('y'), 2) + self.assertIsNone(view.get('z')) + self.assertEqual(tuple(sorted(view.items())), (('x', 1), ('y', 2))) + self.assertEqual(tuple(sorted(view.keys())), ('x', 'y')) + self.assertEqual(tuple(sorted(view.values())), (1, 2)) + + def test_contains(self): + view = self.mappingview(dict.fromkeys('abc')) + self.assertTrue('a' in view) + self.assertTrue('b' in view) + self.assertTrue('c' in view) + self.assertFalse('xxx' in view) + + def test_views(self): + mapping = {} + view = self.mappingview(mapping) + keys = view.keys() + values = view.values() + items = view.items() + self.assertEqual(list(keys), []) + self.assertEqual(list(values), []) + self.assertEqual(list(items), []) + mapping['key'] = 'value' + self.assertEqual(list(keys), ['key']) + self.assertEqual(list(values), ['value']) + self.assertEqual(list(items), [('key', 'value')]) + + def test_len(self): + for expected in range(6): + data = dict.fromkeys('abcde'[:expected]) + self.assertEqual(len(data), expected) + view = self.mappingview(data) + self.assertEqual(len(view), expected) + + def test_iterators(self): + keys = ('x', 'y') + values = (1, 2) + items = tuple(zip(keys, values)) + view = self.mappingview(dict(items)) + self.assertEqual(set(view), set(keys)) + self.assertEqual(set(view.keys()), set(keys)) + self.assertEqual(set(view.values()), set(values)) + self.assertEqual(set(view.items()), set(items)) + + def test_copy(self): + original = {'key1': 27, 'key2': 51, 'key3': 93} + view = self.mappingview(original) + copy = view.copy() + self.assertEqual(type(copy), dict) + self.assertEqual(copy, original) + original['key1'] = 70 + self.assertEqual(view['key1'], 70) + self.assertEqual(copy['key1'], 27) + + def test_main(): # Run all local test cases, with PTypesLongInitTest first. support.run_unittest(PTypesLongInitTest, OperatorsTest, ClassPropertiesAndMethods, DictProxyTests, - MiscTests) + MiscTests, MappingViewTests) if __name__ == "__main__": test_main() diff -r 91bafdf7d7a4 Lib/types.py --- a/Lib/types.py Thu Mar 29 06:49:29 2012 -0400 +++ b/Lib/types.py Thu Mar 29 13:46:25 2012 +0200 @@ -20,6 +20,7 @@ GeneratorType = type(_g()) class _C: def _m(self): pass MethodType = type(_C()._m) +MappingViewType = type(_C.__dict__) BuiltinFunctionType = type(len) BuiltinMethodType = type([].append) # Same as BuiltinFunctionType diff -r 91bafdf7d7a4 Objects/descrobject.c --- a/Objects/descrobject.c Thu Mar 29 06:49:29 2012 -0400 +++ b/Objects/descrobject.c Thu Mar 29 13:46:25 2012 +0200 @@ -698,41 +698,44 @@ PyDescr_NewWrapper(PyTypeObject *type, s } -/* --- Readonly proxy for dictionaries (actually any mapping) --- */ +/* --- Mapping view: read-only proxy for mappings --- */ /* This has no reason to be in this file except that adding new files is a bit of a pain */ typedef struct { PyObject_HEAD - PyObject *dict; -} proxyobject; + PyObject *mapping; +} mappingviewobject; static Py_ssize_t -proxy_len(proxyobject *pp) +mappingview_len(mappingviewobject *pp) { - return PyObject_Size(pp->dict); + return PyObject_Size(pp->mapping); } static PyObject * -proxy_getitem(proxyobject *pp, PyObject *key) +mappingview_getitem(mappingviewobject *pp, PyObject *key) { - return PyObject_GetItem(pp->dict, key); + return PyObject_GetItem(pp->mapping, key); } -static PyMappingMethods proxy_as_mapping = { - (lenfunc)proxy_len, /* mp_length */ - (binaryfunc)proxy_getitem, /* mp_subscript */ +static PyMappingMethods mappingview_as_mapping = { + (lenfunc)mappingview_len, /* mp_length */ + (binaryfunc)mappingview_getitem, /* mp_subscript */ 0, /* mp_ass_subscript */ }; static int -proxy_contains(proxyobject *pp, PyObject *key) +mappingview_contains(mappingviewobject *pp, PyObject *key) { - return PyDict_Contains(pp->dict, key); + if (PyDict_CheckExact(pp->mapping)) + return PyDict_Contains(pp->mapping, key); + else + return PySequence_Contains(pp->mapping, key); } -static PySequenceMethods proxy_as_sequence = { +static PySequenceMethods mappingview_as_sequence = { 0, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ @@ -740,152 +743,196 @@ static PySequenceMethods proxy_as_sequen 0, /* sq_slice */ 0, /* sq_ass_item */ 0, /* sq_ass_slice */ - (objobjproc)proxy_contains, /* sq_contains */ + (objobjproc)mappingview_contains, /* sq_contains */ 0, /* sq_inplace_concat */ 0, /* sq_inplace_repeat */ }; static PyObject * -proxy_get(proxyobject *pp, PyObject *args) +mappingview_get(mappingviewobject *pp, PyObject *args) { PyObject *key, *def = Py_None; _Py_IDENTIFIER(get); if (!PyArg_UnpackTuple(args, "get", 1, 2, &key, &def)) return NULL; - return _PyObject_CallMethodId(pp->dict, &PyId_get, "(OO)", key, def); + return _PyObject_CallMethodId(pp->mapping, &PyId_get, "(OO)", key, def); } static PyObject * -proxy_keys(proxyobject *pp) +mappingview_keys(mappingviewobject *pp) { _Py_IDENTIFIER(keys); - return _PyObject_CallMethodId(pp->dict, &PyId_keys, NULL); + return _PyObject_CallMethodId(pp->mapping, &PyId_keys, NULL); } static PyObject * -proxy_values(proxyobject *pp) +mappingview_values(mappingviewobject *pp) { _Py_IDENTIFIER(values); - return _PyObject_CallMethodId(pp->dict, &PyId_values, NULL); + return _PyObject_CallMethodId(pp->mapping, &PyId_values, NULL); } static PyObject * -proxy_items(proxyobject *pp) +mappingview_items(mappingviewobject *pp) { _Py_IDENTIFIER(items); - return _PyObject_CallMethodId(pp->dict, &PyId_items, NULL); + return _PyObject_CallMethodId(pp->mapping, &PyId_items, NULL); } static PyObject * -proxy_copy(proxyobject *pp) +mappingview_copy(mappingviewobject *pp) { _Py_IDENTIFIER(copy); - return _PyObject_CallMethodId(pp->dict, &PyId_copy, NULL); + return _PyObject_CallMethodId(pp->mapping, &PyId_copy, NULL); } -static PyMethodDef proxy_methods[] = { - {"get", (PyCFunction)proxy_get, METH_VARARGS, +static PyMethodDef mappingview_methods[] = { + {"get", (PyCFunction)mappingview_get, METH_VARARGS, PyDoc_STR("D.get(k[,d]) -> D[k] if k in D, else d." " d defaults to None.")}, - {"keys", (PyCFunction)proxy_keys, METH_NOARGS, + {"keys", (PyCFunction)mappingview_keys, METH_NOARGS, PyDoc_STR("D.keys() -> list of D's keys")}, - {"values", (PyCFunction)proxy_values, METH_NOARGS, + {"values", (PyCFunction)mappingview_values, METH_NOARGS, PyDoc_STR("D.values() -> list of D's values")}, - {"items", (PyCFunction)proxy_items, METH_NOARGS, + {"items", (PyCFunction)mappingview_items, METH_NOARGS, PyDoc_STR("D.items() -> list of D's (key, value) pairs, as 2-tuples")}, - {"copy", (PyCFunction)proxy_copy, METH_NOARGS, + {"copy", (PyCFunction)mappingview_copy, METH_NOARGS, PyDoc_STR("D.copy() -> a shallow copy of D")}, {0} }; static void -proxy_dealloc(proxyobject *pp) +mappingview_dealloc(mappingviewobject *pp) { _PyObject_GC_UNTRACK(pp); - Py_DECREF(pp->dict); + Py_DECREF(pp->mapping); PyObject_GC_Del(pp); } static PyObject * -proxy_getiter(proxyobject *pp) +mappingview_getiter(mappingviewobject *pp) { - return PyObject_GetIter(pp->dict); + return PyObject_GetIter(pp->mapping); } static PyObject * -proxy_str(proxyobject *pp) +mappingview_str(mappingviewobject *pp) { - return PyObject_Str(pp->dict); + return PyObject_Str(pp->mapping); } static PyObject * -proxy_repr(proxyobject *pp) +mappingview_repr(mappingviewobject *pp) { - return PyUnicode_FromFormat("dict_proxy(%R)", pp->dict); + return PyUnicode_FromFormat("mappingview(%R)", pp->mapping); } static int -proxy_traverse(PyObject *self, visitproc visit, void *arg) +mappingview_traverse(PyObject *self, visitproc visit, void *arg) { - proxyobject *pp = (proxyobject *)self; - Py_VISIT(pp->dict); + mappingviewobject *pp = (mappingviewobject *)self; + Py_VISIT(pp->mapping); return 0; } static PyObject * -proxy_richcompare(proxyobject *v, PyObject *w, int op) +mappingview_richcompare(mappingviewobject *v, PyObject *w, int op) { - return PyObject_RichCompare(v->dict, w, op); + return PyObject_RichCompare(v->mapping, w, op); +} + +static int +mappingview_check_dict(PyObject *mapping) +{ + if (!PyMapping_Check(mapping) + || PyList_Check(mapping) + || PyTuple_Check(mapping)) { + PyErr_Format(PyExc_TypeError, + "mappingview() argument must be a mapping, not %s", + Py_TYPE(mapping)->tp_name); + return -1; + } + return 0; +} + +static PyObject* +mappingview_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"mapping", NULL}; + PyObject *mapping; + mappingviewobject *mappingview; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:mappingview", + kwlist, &mapping)) + return NULL; + + if (mappingview_check_dict(mapping) == -1) + return NULL; + + mappingview = PyObject_GC_New(mappingviewobject, &PyDictProxy_Type); + if (mappingview == NULL) + return NULL; + Py_INCREF(mapping); + mappingview->mapping = mapping; + _PyObject_GC_TRACK(mappingview); + return (PyObject *)mappingview; } PyTypeObject PyDictProxy_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) - "dict_proxy", /* tp_name */ - sizeof(proxyobject), /* tp_basicsize */ + "mappingview", /* tp_name */ + sizeof(mappingviewobject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ - (destructor)proxy_dealloc, /* tp_dealloc */ + (destructor)mappingview_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ - (reprfunc)proxy_repr, /* tp_repr */ + (reprfunc)mappingview_repr, /* tp_repr */ 0, /* tp_as_number */ - &proxy_as_sequence, /* tp_as_sequence */ - &proxy_as_mapping, /* tp_as_mapping */ + &mappingview_as_sequence, /* tp_as_sequence */ + &mappingview_as_mapping, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ - (reprfunc)proxy_str, /* tp_str */ + (reprfunc)mappingview_str, /* tp_str */ PyObject_GenericGetAttr, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ 0, /* tp_doc */ - proxy_traverse, /* tp_traverse */ + mappingview_traverse, /* tp_traverse */ 0, /* tp_clear */ - (richcmpfunc)proxy_richcompare, /* tp_richcompare */ + (richcmpfunc)mappingview_richcompare, /* tp_richcompare */ 0, /* tp_weaklistoffset */ - (getiterfunc)proxy_getiter, /* tp_iter */ + (getiterfunc)mappingview_getiter, /* tp_iter */ 0, /* tp_iternext */ - proxy_methods, /* tp_methods */ + mappingview_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + mappingview_new, /* tp_new */ }; PyObject * -PyDictProxy_New(PyObject *dict) +PyDictProxy_New(PyObject *mapping) { - proxyobject *pp; + mappingviewobject *pp; - pp = PyObject_GC_New(proxyobject, &PyDictProxy_Type); + if (mappingview_check_dict(mapping) == -1) + return NULL; + + pp = PyObject_GC_New(mappingviewobject, &PyDictProxy_Type); if (pp != NULL) { - Py_INCREF(dict); - pp->dict = dict; + Py_INCREF(mapping); + pp->mapping = mapping; _PyObject_GC_TRACK(pp); } return (PyObject *)pp;