From 0db176f8f6cfaf3277e6ef41d92b09a01b263f27 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 16 Apr 2012 00:16:30 +0200 Subject: Issue #14386: Expose the dict_proxy internal type as types.MappingProxyType --- Doc/c-api/dict.rst | 8 +-- Doc/library/stdtypes.rst | 16 +++-- Doc/library/types.rst | 52 ++++++++++++++ Doc/whatsnew/3.3.rst | 7 ++ Lib/test/test_descr.py | 4 +- Lib/test/test_types.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++- Lib/types.py | 1 + Misc/NEWS | 2 + Objects/descrobject.c | 170 +++++++++++++++++++++++++++---------------- 9 files changed, 369 insertions(+), 75 deletions(-) diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index ac714a6..6bacc32 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -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.MappingProxyType` 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 --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 8b8400e..f06f579 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2258,13 +2258,13 @@ pairs within braces, for example: ``{'jack': 4098, 'sjoerd': 4127}`` or ``{4098: .. 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: ``{'jack': 4098, 'sjoerd': 4127}`` or ``{4098: .. 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.MappingProxyType` can be used to create a read-only view + of a :class:`dict`. .. _dict-views: diff --git a/Doc/library/types.rst b/Doc/library/types.rst index d4a76b6..0368177 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -85,3 +85,55 @@ The module defines the following names: In other implementations of Python, this type may be identical to ``GetSetDescriptorType``. + +.. class:: MappingProxyType(mapping) + + Read-only proxy 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 proxy + + Return ``True`` if the underlying mapping has a key *key*, else + ``False``. + + .. describe:: proxy[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(proxy) + + Return an iterator over the keys of the underlying mapping. This is a + shortcut for ``iter(proxy.keys())``. + + .. describe:: len(proxy) + + Return the number of items in the underlying mapping. + + .. method:: copy() + + Return a shallow copy of the underlying mapping. + + .. 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). + + .. method:: keys() + + Return a new view of the underlying mapping's keys. + + .. method:: values() + + Return a new view of the underlying mapping's values. + + diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index 766d3f3..495243f 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -1068,6 +1068,13 @@ The :mod:`time` module has new functions: (Contributed by Victor Stinner in :issue:`10278`) +types +----- + +Add a new :class:`types.MappingProxyType` class: Read-only proxy of a mapping. +(:issue:`14386`) + + urllib ------ diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 3051e57..c0c7414 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4574,11 +4574,11 @@ class DictProxyTests(unittest.TestCase): self.assertEqual(type(C.__dict__), type(B.__dict__)) def test_repr(self): - # Testing dict_proxy.__repr__. + # Testing mappingproxy.__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('mappingproxy('), r) self.assertTrue(r.endswith(')'), r) for k, v in self.C.__dict__.items(): self.assertIn('{!r}: {!r}'.format(k, v), r) diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 8a98a03..9a2e0d4 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -1,9 +1,11 @@ # Python test set -- part 6, built-in types from test.support import run_unittest, run_with_locale -import unittest -import sys +import collections import locale +import sys +import types +import unittest class TypesTests(unittest.TestCase): @@ -569,8 +571,184 @@ class TypesTests(unittest.TestCase): self.assertGreater(tuple.__itemsize__, 0) +class MappingProxyTests(unittest.TestCase): + mappingproxy = types.MappingProxyType + + def test_constructor(self): + class userdict(dict): + pass + + mapping = {'x': 1, 'y': 2} + self.assertEqual(self.mappingproxy(mapping), mapping) + mapping = userdict(x=1, y=2) + self.assertEqual(self.mappingproxy(mapping), mapping) + mapping = collections.ChainMap({'x': 1}, {'y': 2}) + self.assertEqual(self.mappingproxy(mapping), mapping) + + self.assertRaises(TypeError, self.mappingproxy, 10) + self.assertRaises(TypeError, self.mappingproxy, ("a", "tuple")) + self.assertRaises(TypeError, self.mappingproxy, ["a", "list"]) + + def test_methods(self): + attrs = set(dir(self.mappingproxy({}))) - set(dir(object())) + self.assertEqual(attrs, { + '__contains__', + '__getitem__', + '__iter__', + '__len__', + 'copy', + 'get', + 'items', + 'keys', + 'values', + }) + + def test_get(self): + view = self.mappingproxy({'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.mappingproxy(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.mappingproxy(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.mappingproxy(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.assertIsInstance(copy, collections.ChainMap) + 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.mappingproxy(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.mappingproxy(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.mappingproxy(data) + self.assertEqual(len(view), expected) + + def test_iterators(self): + keys = ('x', 'y') + values = (1, 2) + items = tuple(zip(keys, values)) + view = self.mappingproxy(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.mappingproxy(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_unittest(TypesTests) + run_unittest(TypesTests, MappingProxyTests) if __name__ == '__main__': test_main() diff --git a/Lib/types.py b/Lib/types.py index ab354d1..08cbb83 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -12,6 +12,7 @@ def _f(): pass FunctionType = type(_f) LambdaType = type(lambda: None) # Same as FunctionType CodeType = type(_f.__code__) +MappingProxyType = type(type.__dict__) def _g(): yield 1 diff --git a/Misc/NEWS b/Misc/NEWS index e2e14b7..9e81cd8 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -32,6 +32,8 @@ Core and Builtins Library ------- +- Issue #14386: Expose the dict_proxy internal type as types.MappingProxyType. + - Issue #13959: Make imp.reload() always use a module's __loader__ to perform the reload. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index ecf372a..b679c4bc 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -698,41 +698,44 @@ PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped) } -/* --- Readonly proxy for dictionaries (actually any mapping) --- */ +/* --- mappingproxy: 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; +} mappingproxyobject; static Py_ssize_t -proxy_len(proxyobject *pp) +mappingproxy_len(mappingproxyobject *pp) { - return PyObject_Size(pp->dict); + return PyObject_Size(pp->mapping); } static PyObject * -proxy_getitem(proxyobject *pp, PyObject *key) +mappingproxy_getitem(mappingproxyobject *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 mappingproxy_as_mapping = { + (lenfunc)mappingproxy_len, /* mp_length */ + (binaryfunc)mappingproxy_getitem, /* mp_subscript */ 0, /* mp_ass_subscript */ }; static int -proxy_contains(proxyobject *pp, PyObject *key) +mappingproxy_contains(mappingproxyobject *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 mappingproxy_as_sequence = { 0, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ @@ -740,152 +743,199 @@ static PySequenceMethods proxy_as_sequence = { 0, /* sq_slice */ 0, /* sq_ass_item */ 0, /* sq_ass_slice */ - (objobjproc)proxy_contains, /* sq_contains */ + (objobjproc)mappingproxy_contains, /* sq_contains */ 0, /* sq_inplace_concat */ 0, /* sq_inplace_repeat */ }; static PyObject * -proxy_get(proxyobject *pp, PyObject *args) +mappingproxy_get(mappingproxyobject *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) +mappingproxy_keys(mappingproxyobject *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) +mappingproxy_values(mappingproxyobject *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) +mappingproxy_items(mappingproxyobject *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) +mappingproxy_copy(mappingproxyobject *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, +/* WARNING: mappingproxy methods must not give access + to the underlying mapping */ + +static PyMethodDef mappingproxy_methods[] = { + {"get", (PyCFunction)mappingproxy_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, + " d defaults to None.")}, + {"keys", (PyCFunction)mappingproxy_keys, METH_NOARGS, PyDoc_STR("D.keys() -> list of D's keys")}, - {"values", (PyCFunction)proxy_values, METH_NOARGS, + {"values", (PyCFunction)mappingproxy_values, METH_NOARGS, PyDoc_STR("D.values() -> list of D's values")}, - {"items", (PyCFunction)proxy_items, METH_NOARGS, + {"items", (PyCFunction)mappingproxy_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)mappingproxy_copy, METH_NOARGS, PyDoc_STR("D.copy() -> a shallow copy of D")}, {0} }; static void -proxy_dealloc(proxyobject *pp) +mappingproxy_dealloc(mappingproxyobject *pp) { _PyObject_GC_UNTRACK(pp); - Py_DECREF(pp->dict); + Py_DECREF(pp->mapping); PyObject_GC_Del(pp); } static PyObject * -proxy_getiter(proxyobject *pp) +mappingproxy_getiter(mappingproxyobject *pp) { - return PyObject_GetIter(pp->dict); + return PyObject_GetIter(pp->mapping); } static PyObject * -proxy_str(proxyobject *pp) +mappingproxy_str(mappingproxyobject *pp) { - return PyObject_Str(pp->dict); + return PyObject_Str(pp->mapping); } static PyObject * -proxy_repr(proxyobject *pp) +mappingproxy_repr(mappingproxyobject *pp) { - return PyUnicode_FromFormat("dict_proxy(%R)", pp->dict); + return PyUnicode_FromFormat("mappingproxy(%R)", pp->mapping); } static int -proxy_traverse(PyObject *self, visitproc visit, void *arg) +mappingproxy_traverse(PyObject *self, visitproc visit, void *arg) { - proxyobject *pp = (proxyobject *)self; - Py_VISIT(pp->dict); + mappingproxyobject *pp = (mappingproxyobject *)self; + Py_VISIT(pp->mapping); return 0; } static PyObject * -proxy_richcompare(proxyobject *v, PyObject *w, int op) +mappingproxy_richcompare(mappingproxyobject *v, PyObject *w, int op) +{ + return PyObject_RichCompare(v->mapping, w, op); +} + +static int +mappingproxy_check_mapping(PyObject *mapping) +{ + if (!PyMapping_Check(mapping) + || PyList_Check(mapping) + || PyTuple_Check(mapping)) { + PyErr_Format(PyExc_TypeError, + "mappingproxy() argument must be a mapping, not %s", + Py_TYPE(mapping)->tp_name); + return -1; + } + return 0; +} + +static PyObject* +mappingproxy_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - return PyObject_RichCompare(v->dict, w, op); + static char *kwlist[] = {"mapping", NULL}; + PyObject *mapping; + mappingproxyobject *mappingproxy; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:mappingproxy", + kwlist, &mapping)) + return NULL; + + if (mappingproxy_check_mapping(mapping) == -1) + return NULL; + + mappingproxy = PyObject_GC_New(mappingproxyobject, &PyDictProxy_Type); + if (mappingproxy == NULL) + return NULL; + Py_INCREF(mapping); + mappingproxy->mapping = mapping; + _PyObject_GC_TRACK(mappingproxy); + return (PyObject *)mappingproxy; } PyTypeObject PyDictProxy_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) - "dict_proxy", /* tp_name */ - sizeof(proxyobject), /* tp_basicsize */ + "mappingproxy", /* tp_name */ + sizeof(mappingproxyobject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ - (destructor)proxy_dealloc, /* tp_dealloc */ + (destructor)mappingproxy_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ - (reprfunc)proxy_repr, /* tp_repr */ + (reprfunc)mappingproxy_repr, /* tp_repr */ 0, /* tp_as_number */ - &proxy_as_sequence, /* tp_as_sequence */ - &proxy_as_mapping, /* tp_as_mapping */ + &mappingproxy_as_sequence, /* tp_as_sequence */ + &mappingproxy_as_mapping, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ - (reprfunc)proxy_str, /* tp_str */ + (reprfunc)mappingproxy_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 */ + mappingproxy_traverse, /* tp_traverse */ 0, /* tp_clear */ - (richcmpfunc)proxy_richcompare, /* tp_richcompare */ + (richcmpfunc)mappingproxy_richcompare, /* tp_richcompare */ 0, /* tp_weaklistoffset */ - (getiterfunc)proxy_getiter, /* tp_iter */ + (getiterfunc)mappingproxy_getiter, /* tp_iter */ 0, /* tp_iternext */ - proxy_methods, /* tp_methods */ + mappingproxy_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 */ + mappingproxy_new, /* tp_new */ }; PyObject * -PyDictProxy_New(PyObject *dict) +PyDictProxy_New(PyObject *mapping) { - proxyobject *pp; + mappingproxyobject *pp; + + if (mappingproxy_check_mapping(mapping) == -1) + return NULL; - pp = PyObject_GC_New(proxyobject, &PyDictProxy_Type); + pp = PyObject_GC_New(mappingproxyobject, &PyDictProxy_Type); if (pp != NULL) { - Py_INCREF(dict); - pp->dict = dict; + Py_INCREF(mapping); + pp->mapping = mapping; _PyObject_GC_TRACK(pp); } return (PyObject *)pp; -- cgit v0.12