├── .gitignore ├── LICENSE ├── MANIFEST ├── README ├── README.rst ├── dictproxyhack.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | *.pyc 3 | *.pyo 4 | __pycache__/ 5 | /dist/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Alex "Eevee" Munroe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README 3 | dictproxyhack.py 4 | setup.py 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | dictproxyhack 2 | ============= 3 | 4 | Exposes the immutable ``dictproxy`` type to all (?) versions of Python. 5 | 6 | PEP416_ asked for a ``frozendict`` type, but was rejected. The alternative was 7 | to publicly expose ``dictproxy``, the type used for and immutable object's 8 | ``__dict__``, which wraps an existing dict and provides a read-only interface 9 | to it. The type has existed since 2.2, but it's never had a Python-land 10 | constructor. 11 | 12 | Until now. But only in 3.3+. Which is not all that helpful to some of us. 13 | 14 | This module clumsily exposes the same type to previous versions of Python. 15 | 16 | .. _PEP416: http://www.python.org/dev/peps/pep-0416/ 17 | 18 | Usage 19 | ----- 20 | 21 | :: 22 | 23 | from dictproxyhack import dictproxy 24 | 25 | myproxy = dictproxy(dict(foo="bar")) 26 | print(myproxy['foo']) 27 | myproxy['baz'] = "quux" # TypeError 28 | 29 | Since the proxy holds a reference to the underlying ``dict`` (but doesn't provide 30 | any way to get it back), you can trivially implement ``frozendict``:: 31 | 32 | def frozendict(*args, **kwargs): 33 | return dictproxy(dict(*args, **kwargs)) 34 | 35 | Might as well just inline that where you need it, really. 36 | 37 | Dependencies 38 | ------------ 39 | 40 | Python. Should work anywhere. Maybe. 41 | 42 | On **Python 3.3+**, you get the real ``mappingproxy`` type, which lives in the 43 | ``types`` module as ``MappingProxyType``. 44 | 45 | On **CPython 2.5+**, you get a fake class that forcibly instantiates 46 | ``dictproxy`` objects via ``ctypes`` shenanigans. 47 | 48 | On **nearly anything else**, you get a regular class that wraps a dict and 49 | doesn't implement any mutating parts of the mapping interface. Not a fabulous 50 | solution, but good enough, and only applies until your favorite port catches up 51 | with 3.3's standard library. 52 | 53 | On **non-C Python ports that predate 2.6**, you get pretty much a ``dict``, 54 | because the ``Mapping`` ABC doesn't even exist. Sorry. 55 | 56 | The shim classes also fool ``isinstance`` and ``issubclass``, so your dirty 57 | typechecking should work equally poorly anywhere. 58 | 59 | I've only actually tried this library on CPython 2.6, 2.7, 3.2, 3.3, and PyPy 60 | 2.1, but I'm interested in hearing whether it works elsewhere. 61 | 62 | Gotchas 63 | ------- 64 | 65 | Don't subclass ``dictproxy``. Python 3.3+ won't let you, and the shims for other 66 | versions are of extremely dubious use. 67 | 68 | ``dictproxy`` has been renamed to ``mappingproxy`` in 3.3+, so don't rely on 69 | ``repr`` to match across versions or anything. (It seemed apropos to use the 70 | older name for this module.) 71 | -------------------------------------------------------------------------------- /dictproxyhack.py: -------------------------------------------------------------------------------- 1 | """Exposes the immutable `dictproxy` type to all (?) versions of Python. 2 | Should work in 2.3+, 3.0+. Actually tried on 2.6, 2.7, 3.2, 3.3, and PyPy 2.1. 3 | 4 | Usage is easy: 5 | 6 | from dictproxyhack import dictproxy 7 | myproxy = dictproxy(dict(foo="bar")) 8 | print(myproxy['foo']) 9 | myproxy['baz'] = "quux" # TypeError 10 | 11 | dictproxy is GvR's answer to the desire for a "frozendict" type; see the 12 | PEP at http://www.python.org/dev/peps/pep-0416/#rejection-notice. It's the 13 | type of immutable `__dict__` attributes, and it exposes a read-only view of an 14 | existing dict. Changes to the original dict are visible through the proxy, but 15 | the proxy itself cannot be changed in any way. 16 | 17 | dictproxy has been publicly exposed in Python 3.3 as `types.MappingProxyType`. 18 | It already exists in earlier versions of Python, back to the dark days of 2.2, 19 | but doesn't expose a constructor to Python-land. This module hacks one in. 20 | Enjoy. 21 | """ 22 | 23 | import unittest 24 | 25 | __all__ = ['dictproxy'] 26 | 27 | def _get_from_types(): 28 | # dictproxy is exposed publicly in 3.3+. Easy. 29 | from types import MappingProxyType 30 | return MappingProxyType 31 | 32 | 33 | realdictproxy = type(type.__dict__) 34 | 35 | class dictproxymeta(type): 36 | def __instancecheck__(self, instance): 37 | return ( 38 | isinstance(instance, realdictproxy) 39 | # This bit is for PyPy, which is failing to notice that objects are 40 | # in fact instances of their own classes -- probably an MRO thing 41 | or type.__instancecheck__(self, instance) 42 | ) 43 | 44 | def __subclasscheck__(self, cls): 45 | return issubclass(cls, realdictproxy) 46 | 47 | def _add_isinstance_tomfoolery(cls): 48 | # Given a class (presumably, a dictproxy implementation), toss in a 49 | # metaclass that will fool isinstance (and issubclass) into thinking that 50 | # real dictproxy objects are also instances of the class. 51 | 52 | # But first we need to agree with the metaclasses on the existing class 53 | # oh boy! This is only a problem when using the super fallback of just 54 | # writing a class, because it ends up with ABCMeta tacked on. 55 | try: 56 | from sets import Set as set 57 | except NameError: 58 | pass 59 | 60 | metas = set([type(supercls) for supercls in cls.__mro__]) 61 | metas.discard(type) 62 | if metas: 63 | metacls = type('dictproxymeta', (dictproxymeta,) + tuple(metas), {}) 64 | else: 65 | metacls = dictproxymeta 66 | 67 | # And this bit is necessary to work under both Python 2 and 3. 68 | return metacls(cls.__name__, (cls,), {}) 69 | 70 | 71 | def _get_from_c_api(): 72 | """dictproxy does exist in previous versions, but the Python constructor 73 | refuses to create new objects, so we must be underhanded and sneaky with 74 | ctypes. 75 | """ 76 | from ctypes import pythonapi, py_object 77 | 78 | PyDictProxy_New = pythonapi.PyDictProxy_New 79 | PyDictProxy_New.argtypes = (py_object,) 80 | PyDictProxy_New.restype = py_object 81 | 82 | # To actually create new dictproxy instances, we need a class that calls 83 | # the above C API functions, but a subclass would also bring with it some 84 | # indirection baggage. Let's skip all that and have the subclass's __new__ 85 | # return an object of the real dictproxy type. 86 | # In other words, the `dictproxy` class being created here is never 87 | # instantiated and never does anything. It's a glorified function. 88 | # (That's why it just inherits from `object`, too.) 89 | class dictproxy(object): 90 | """Read-only proxy for a dict, using the same mechanism Python uses for 91 | the __dict__ attribute on objects. 92 | 93 | Create with dictproxy(some_dict). The original dict is still 94 | read/write, and changes to the original dict are reflected in real time 95 | via the proxy, but the proxy cannot be edited in any way. 96 | """ 97 | 98 | def __new__(cls, d): 99 | if not isinstance(d, dict): 100 | # I suspect bad things would happen if this were not true. 101 | raise TypeError("dictproxy can only proxy to a real dict") 102 | 103 | return PyDictProxy_New(d) 104 | 105 | # Try this once to make sure it actually works. Because PyPy has all the 106 | # parts and then crashes when trying to actually use them. 107 | # See also: https://bugs.pypy.org/issue1233 108 | dictproxy(dict()) 109 | 110 | # And slap on a metaclass that fools isinstance() while we're at it. 111 | return _add_isinstance_tomfoolery(dictproxy) 112 | 113 | def _get_from_the_hard_way(): 114 | # Okay, so ctypes didn't work. Maybe this is PyPy or something. The only 115 | # choice left is to just write the damn class. 116 | try: 117 | from collections.abc import Mapping 118 | except ImportError: 119 | try: 120 | from collections import Mapping 121 | except ImportError: 122 | # OK this one is actually mutable, but this will only fire on a 123 | # non-C pre-2.6 Python implementation, so whatever. You can 124 | # already swap out the `d` attribute below so it's not like this is 125 | # a perfect solution. 126 | from UserDict import DictMixin as Mapping 127 | 128 | class dictproxy(Mapping): 129 | def __init__(self, d): 130 | self.d = d 131 | 132 | def __getitem__(self, key): 133 | return self.d[key] 134 | 135 | def __len__(self): 136 | return len(self.d) 137 | 138 | def __iter__(self): 139 | return iter(self.d) 140 | 141 | def __contains__(self, key): 142 | return key in self.d 143 | 144 | return _add_isinstance_tomfoolery(dictproxy) 145 | 146 | 147 | try: 148 | dictproxy = _get_from_types() 149 | except ImportError: 150 | try: 151 | dictproxy = _get_from_c_api() 152 | except Exception: 153 | dictproxy = _get_from_the_hard_way() 154 | 155 | 156 | class UselessWrapperClassTestCase(unittest.TestCase): 157 | def test_dict_interface(self): 158 | d = dict(a=1, b=2, c=3) 159 | dp = dictproxy(d) 160 | 161 | self.assertEqual(dp['a'], 1) 162 | self.assertEqual(dp['b'], 2) 163 | self.assertEqual(dp['c'], 3) 164 | 165 | def try_bad_key(): 166 | dp['e'] 167 | 168 | self.assertRaises(KeyError, try_bad_key) 169 | 170 | def test_readonly(self): 171 | dp = dictproxy({}) 172 | 173 | def try_assignment(): 174 | dp['foo'] = 'bar' 175 | 176 | self.assertRaises(TypeError, try_assignment) 177 | 178 | def try_clear(): 179 | dp.clear() 180 | 181 | self.assertRaises(AttributeError, try_clear) 182 | 183 | def test_live_update(self): 184 | d = dict() 185 | dp = dictproxy(d) 186 | 187 | d['a'] = 1 188 | 189 | self.assertEqual(dp['a'], 1) 190 | 191 | 192 | def test_isinstance(self): 193 | dp = dictproxy({}) 194 | 195 | self.assertTrue(isinstance(dp, dictproxy)) 196 | self.assertTrue(isinstance(type.__dict__, dictproxy)) 197 | 198 | if __name__ == '__main__': 199 | unittest.main() 200 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | with open('README.rst') as f: 4 | readme = f.read() 5 | 6 | setup( 7 | name='dictproxyhack', 8 | version='1.1', 9 | description="PEP 417's dictproxy (immutable dicts) for everyone", 10 | author='Eevee (Alex Munroe)', 11 | author_email='eevee.pypi@veekun.com', 12 | url='https://github.com/eevee/dictproxyhack', 13 | classifiers=[ 14 | 'Development Status :: 5 - Production/Stable', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 3', 19 | 'Programming Language :: Python :: Implementation :: CPython', 20 | 'Programming Language :: Python :: Implementation :: PyPy', 21 | 'Topic :: Software Development :: Libraries :: Python Modules', 22 | ], 23 | long_description=readme, 24 | 25 | py_modules=['dictproxyhack'], 26 | ) 27 | --------------------------------------------------------------------------------