├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── addict ├── __init__.py └── addict.py ├── setup.py └── test_addict.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | python-version: [2.7, 3.6, 3.7, 3.8, pypy2, pypy3] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install pytest 29 | - name: Test with pytest 30 | run: | 31 | pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | 57 | #OSX Files 58 | .DS_Store 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | - "3.7-dev" 6 | before_script: pip install coveralls 7 | script: 8 | - "py.test" 9 | - "coverage run --source=addict setup.py test" 10 | after_success: 11 | - coveralls 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mats Julian Olsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include test_addict.py 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # addict 2 | ![Tests](https://github.com/mewwts/addict/workflows/Python%20test/badge.svg) [![Coverage Status](https://img.shields.io/coveralls/mewwts/addict.svg)](https://coveralls.io/r/mewwts/addict) [![PyPI version](https://badge.fury.io/py/addict.svg)](https://badge.fury.io/py/addict) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/addict/badges/version.svg)](https://anaconda.org/conda-forge/addict) 3 | 4 | addict is a Python module that gives you dictionaries whose values are both gettable and settable using attributes, in addition to standard item-syntax. 5 | 6 | This means that you **don't have to** write dictionaries like this anymore: 7 | ```Python 8 | body = { 9 | 'query': { 10 | 'filtered': { 11 | 'query': { 12 | 'match': {'description': 'addictive'} 13 | }, 14 | 'filter': { 15 | 'term': {'created_by': 'Mats'} 16 | } 17 | } 18 | } 19 | } 20 | ``` 21 | Instead, you can simply write the following three lines: 22 | ```Python 23 | body = Dict() 24 | body.query.filtered.query.match.description = 'addictive' 25 | body.query.filtered.filter.term.created_by = 'Mats' 26 | ``` 27 | 28 | ### Installing 29 | You can install via `pip` 30 | ```sh 31 | pip install addict 32 | ``` 33 | 34 | or through `conda` 35 | ```sh 36 | conda install addict -c conda-forge 37 | ``` 38 | 39 | Addict runs on Python 2 and Python 3, and every build is tested towards 2.7, 3.6 and 3.7. 40 | 41 | ### Usage 42 | addict inherits from ```dict```, but is more flexible in terms of accessing and setting its values. 43 | Working with dictionaries are now a *joy*! Setting the items of a nested Dict is a *dream*: 44 | 45 | ```Python 46 | >>> from addict import Dict 47 | >>> mapping = Dict() 48 | >>> mapping.a.b.c.d.e = 2 49 | >>> mapping 50 | {'a': {'b': {'c': {'d': {'e': 2}}}}} 51 | ``` 52 | 53 | If the `Dict` is instantiated with any iterable values, it will iterate through and clone these values, and turn `dict`s into `Dict`s. 54 | Hence, the following works 55 | ```Python 56 | >>> mapping = {'a': [{'b': 3}, {'b': 3}]} 57 | >>> dictionary = Dict(mapping) 58 | >>> dictionary.a[0].b 59 | 3 60 | ``` 61 | but `mapping['a']` is no longer the same reference as `dictionary['a']`. 62 | ```Python 63 | >>> mapping['a'] is dictionary['a'] 64 | False 65 | ``` 66 | This behavior is limited to the constructor, and not when items are set using attribute or item syntax, references are untouched: 67 | ```Python 68 | >>> a = Dict() 69 | >>> b = [1, 2, 3] 70 | >>> a.b = b 71 | >>> a.b is b 72 | True 73 | ``` 74 | 75 | ### Stuff to keep in mind 76 | Remember that ```int```s are not valid attribute names, so keys of the dict that are not strings must be set/get with the get-/setitem syntax 77 | ```Python 78 | >>> addicted = Dict() 79 | >>> addicted.a.b.c.d.e = 2 80 | >>> addicted[2] = [1, 2, 3] 81 | {2: [1, 2, 3], 'a': {'b': {'c': {'d': {'e': 2}}}}} 82 | ``` 83 | However feel free to mix the two syntaxes: 84 | ```Python 85 | >>> addicted.a.b['c'].d.e 86 | 2 87 | ``` 88 | 89 | ### Attributes like keys, items etc. 90 | addict will not let you override attributes that are native to ```dict```, so the following will not work 91 | ```Python 92 | >>> mapping = Dict() 93 | >>> mapping.keys = 2 94 | Traceback (most recent call last): 95 | File "", line 1, in 96 | File "addict/addict.py", line 53, in __setattr__ 97 | raise AttributeError("'Dict' object attribute '%s' is read-only" % name) 98 | AttributeError: 'Dict' object attribute 'keys' is read-only 99 | ``` 100 | However, the following is fine 101 | ```Python 102 | >>> a = Dict() 103 | >>> a['keys'] = 2 104 | >>> a 105 | {'keys': 2} 106 | >>> a['keys'] 107 | 2 108 | ``` 109 | just like a regular `dict`. There are no restrictions (other than what a regular dict imposes) regarding what keys you can use. 110 | 111 | ### Default values 112 | For keys that are not in the dictionary, addict behaves like ```defaultdict(Dict)```, so missing keys return an empty ```Dict``` 113 | rather than raising ```KeyError```. 114 | If this behaviour is not desired, it can be overridden using 115 | ```Python 116 | >>> class DictNoDefault(Dict): 117 | >>> def __missing__(self, key): 118 | >>> raise KeyError(key) 119 | ``` 120 | but beware that you will then lose the shorthand assignment functionality (```addicted.a.b.c.d.e = 2```). 121 | 122 | ### Recursive Fallback to dict 123 | If you don't feel safe shipping your addict around to other modules, use the `to_dict()`-method, which returns a regular dict clone of the addict dictionary. 124 | 125 | ```Python 126 | >>> regular_dict = my_addict.to_dict() 127 | >>> regular_dict.a = 2 128 | Traceback (most recent call last): 129 | File "", line 1, in 130 | AttributeError: 'dict' object has no attribute 'a' 131 | ``` 132 | This is perfect for when you wish to create a nested Dict in a few lines, and then ship it on to a different module. 133 | ```Python 134 | body = Dict() 135 | body.query.filtered.query.match.description = 'addictive' 136 | body.query.filtered.filter.term.created_by = 'Mats' 137 | third_party_module.search(query=body.to_dict()) 138 | ``` 139 | 140 | ### Counting 141 | `Dict`'s ability to easily access and modify deeply-nested attributes makes it ideal for counting. This offers a distinct advantage over `collections.Counter`, as it will easily allow for counting by multiple levels. 142 | 143 | Consider this data: 144 | 145 | ```python 146 | data = [ 147 | {'born': 1980, 'gender': 'M', 'eyes': 'green'}, 148 | {'born': 1980, 'gender': 'F', 'eyes': 'green'}, 149 | {'born': 1980, 'gender': 'M', 'eyes': 'blue'}, 150 | {'born': 1980, 'gender': 'M', 'eyes': 'green'}, 151 | {'born': 1980, 'gender': 'M', 'eyes': 'green'}, 152 | {'born': 1980, 'gender': 'F', 'eyes': 'blue'}, 153 | {'born': 1981, 'gender': 'M', 'eyes': 'blue'}, 154 | {'born': 1981, 'gender': 'F', 'eyes': 'green'}, 155 | {'born': 1981, 'gender': 'M', 'eyes': 'blue'}, 156 | {'born': 1981, 'gender': 'F', 'eyes': 'blue'}, 157 | {'born': 1981, 'gender': 'M', 'eyes': 'green'}, 158 | {'born': 1981, 'gender': 'F', 'eyes': 'blue'} 159 | ] 160 | ``` 161 | 162 | If you want to count how many people were born in `born` of gender `gender` with `eyes` eyes, you can easily calculate this information: 163 | 164 | ```python 165 | counter = Dict() 166 | 167 | for row in data: 168 | born = row['born'] 169 | gender = row['gender'] 170 | eyes = row['eyes'] 171 | 172 | counter[born][gender][eyes] += 1 173 | 174 | print(counter) 175 | ``` 176 | 177 | ``` 178 | {1980: {'M': {'blue': 1, 'green': 3}, 'F': {'blue': 1, 'green': 1}}, 1981: {'M': {'blue': 2, 'green': 1}, 'F': {'blue': 2, 'green': 1}}} 179 | ``` 180 | ### Update 181 | `addict`s update functionality is altered for convenience from a normal `dict`. Where updating nested item using a `dict` would overwrite it: 182 | ```Python 183 | >>> d = {'a': {'b': 3}} 184 | >>> d.update({'a': {'c': 4}}) 185 | >>> print(d) 186 | {'a': {'c': 4}} 187 | ``` 188 | `addict` will recurse and _actually_ update the nested `Dict`. 189 | ```Python 190 | >>> D = Dict({'a': {'b': 3}}) 191 | >>> D.update({'a': {'c': 4}}) 192 | >>> print(D) 193 | {'a': {'b': 3, 'c': 4}} 194 | ``` 195 | 196 | ### When is this **especially** useful? 197 | This module rose from the entirely tiresome creation of Elasticsearch queries in Python. Whenever you find yourself writing out dicts over multiple lines, just remember that you don't have to. Use *addict* instead. 198 | 199 | ### Perks 200 | As it is a ```dict```, it will serialize into JSON perfectly, and with the to_dict()-method you can feel safe shipping your addict anywhere. 201 | 202 | ### Testing, Development and CI 203 | Issues and Pull Requests are more than welcome. Feel free to open an issue to spark a discussion around a feature or a bug, or simply reply to the existing ones. As for Pull Requests, keeping in touch with the surrounding code style will be appreciated, and as such, writing tests are crucial. Pull requests and commits will be automatically run against TravisCI and coveralls. 204 | 205 | The unit tests are implemented in the `test_addict.py` file and use the unittest python framework. Running the tests is rather simple: 206 | ```sh 207 | python -m unittest -v test_addict 208 | 209 | # - or - 210 | python test_addict.py 211 | ``` 212 | 213 | ### Testimonials 214 | @spiritsack - *"Mother of God, this changes everything."* 215 | 216 | @some guy on Hacker News - *"...the purpose itself is grossly unpythonic"* 217 | -------------------------------------------------------------------------------- /addict/__init__.py: -------------------------------------------------------------------------------- 1 | from .addict import Dict 2 | from .addict import Dict as Addict 3 | 4 | 5 | __title__ = 'addict' 6 | __version__ = '2.4.0' 7 | __author__ = 'Mats Julian Olsen' 8 | __license__ = 'MIT' 9 | __copyright__ = 'Copyright 2014-2020 Mats Julian Olsen' 10 | __all__ = ['Dict'] 11 | -------------------------------------------------------------------------------- /addict/addict.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | class Dict(dict): 5 | 6 | def __init__(__self, *args, **kwargs): 7 | object.__setattr__(__self, '__parent', kwargs.pop('__parent', None)) 8 | object.__setattr__(__self, '__key', kwargs.pop('__key', None)) 9 | object.__setattr__(__self, '__frozen', False) 10 | for arg in args: 11 | if not arg: 12 | continue 13 | elif isinstance(arg, dict): 14 | for key, val in arg.items(): 15 | __self[key] = __self._hook(val) 16 | elif isinstance(arg, tuple) and (not isinstance(arg[0], tuple)): 17 | __self[arg[0]] = __self._hook(arg[1]) 18 | else: 19 | for key, val in iter(arg): 20 | __self[key] = __self._hook(val) 21 | 22 | for key, val in kwargs.items(): 23 | __self[key] = __self._hook(val) 24 | 25 | def __setattr__(self, name, value): 26 | if hasattr(self.__class__, name): 27 | raise AttributeError("'Dict' object attribute " 28 | "'{0}' is read-only".format(name)) 29 | else: 30 | self[name] = value 31 | 32 | def __setitem__(self, name, value): 33 | isFrozen = (hasattr(self, '__frozen') and 34 | object.__getattribute__(self, '__frozen')) 35 | if isFrozen and name not in super(Dict, self).keys(): 36 | raise KeyError(name) 37 | super(Dict, self).__setitem__(name, value) 38 | try: 39 | p = object.__getattribute__(self, '__parent') 40 | key = object.__getattribute__(self, '__key') 41 | except AttributeError: 42 | p = None 43 | key = None 44 | if p is not None: 45 | p[key] = self 46 | object.__delattr__(self, '__parent') 47 | object.__delattr__(self, '__key') 48 | 49 | def __add__(self, other): 50 | if not self.keys(): 51 | return other 52 | else: 53 | self_type = type(self).__name__ 54 | other_type = type(other).__name__ 55 | msg = "unsupported operand type(s) for +: '{}' and '{}'" 56 | raise TypeError(msg.format(self_type, other_type)) 57 | 58 | @classmethod 59 | def _hook(cls, item): 60 | if isinstance(item, dict): 61 | return cls(item) 62 | elif isinstance(item, (list, tuple)): 63 | return type(item)(cls._hook(elem) for elem in item) 64 | return item 65 | 66 | def __getattr__(self, item): 67 | return self.__getitem__(item) 68 | 69 | def __missing__(self, name): 70 | if object.__getattribute__(self, '__frozen'): 71 | raise KeyError(name) 72 | return self.__class__(__parent=self, __key=name) 73 | 74 | def __delattr__(self, name): 75 | del self[name] 76 | 77 | def to_dict(self): 78 | base = {} 79 | for key, value in self.items(): 80 | if isinstance(value, type(self)): 81 | base[key] = value.to_dict() 82 | elif isinstance(value, (list, tuple)): 83 | base[key] = type(value)( 84 | item.to_dict() if isinstance(item, type(self)) else 85 | item for item in value) 86 | else: 87 | base[key] = value 88 | return base 89 | 90 | def copy(self): 91 | return copy.copy(self) 92 | 93 | def deepcopy(self): 94 | return copy.deepcopy(self) 95 | 96 | def __deepcopy__(self, memo): 97 | other = self.__class__() 98 | memo[id(self)] = other 99 | for key, value in self.items(): 100 | other[copy.deepcopy(key, memo)] = copy.deepcopy(value, memo) 101 | return other 102 | 103 | def update(self, *args, **kwargs): 104 | other = {} 105 | if args: 106 | if len(args) > 1: 107 | raise TypeError() 108 | other.update(args[0]) 109 | other.update(kwargs) 110 | for k, v in other.items(): 111 | if ((k not in self) or 112 | (not isinstance(self[k], dict)) or 113 | (not isinstance(v, dict))): 114 | self[k] = v 115 | else: 116 | self[k].update(v) 117 | 118 | def __getnewargs__(self): 119 | return tuple(self.items()) 120 | 121 | def __getstate__(self): 122 | return self 123 | 124 | def __setstate__(self, state): 125 | self.update(state) 126 | 127 | def __or__(self, other): 128 | if not isinstance(other, (Dict, dict)): 129 | return NotImplemented 130 | new = Dict(self) 131 | new.update(other) 132 | return new 133 | 134 | def __ror__(self, other): 135 | if not isinstance(other, (Dict, dict)): 136 | return NotImplemented 137 | new = Dict(other) 138 | new.update(self) 139 | return new 140 | 141 | def __ior__(self, other): 142 | self.update(other) 143 | return self 144 | 145 | def setdefault(self, key, default=None): 146 | if key in self: 147 | return self[key] 148 | else: 149 | self[key] = default 150 | return default 151 | 152 | def freeze(self, shouldFreeze=True): 153 | object.__setattr__(self, '__frozen', shouldFreeze) 154 | for key, val in self.items(): 155 | if isinstance(val, Dict): 156 | val.freeze(shouldFreeze) 157 | 158 | def unfreeze(self): 159 | self.freeze(False) 160 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import addict 3 | 4 | SHORT='Addict is a dictionary whose items can be set using both attribute and item syntax.' 5 | LONG=('Addict is a module that exposes a dictionary subclass that allows items to be set like attributes. ' 6 | 'Values are gettable and settable using both attribute and item syntax. ' 7 | 'For more info check out the README at \'github.com/mewwts/addict\'.') 8 | 9 | setup( 10 | name='addict', 11 | version=addict.__version__, 12 | packages=['addict'], 13 | url='https://github.com/mewwts/addict', 14 | author=addict.__author__, 15 | author_email='mats@plysjbyen.net', 16 | classifiers=[ 17 | 'Programming Language :: Python', 18 | 'Programming Language :: Python :: 2.7', 19 | 'Programming Language :: Python :: 3.6', 20 | 'Programming Language :: Python :: 3.7', 21 | 'Programming Language :: Python :: 3.8', 22 | 'Programming Language :: Python :: 3.9', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | 'Intended Audience :: Developers', 26 | 'Topic :: Software Development :: Libraries :: Python Modules', 27 | ], 28 | description=SHORT, 29 | long_description=LONG, 30 | test_suite='test_addict', 31 | package_data={'': ['LICENSE']} 32 | ) 33 | -------------------------------------------------------------------------------- /test_addict.py: -------------------------------------------------------------------------------- 1 | import json 2 | import copy 3 | import unittest 4 | import pickle 5 | from addict import Dict 6 | 7 | 8 | # test whether unittests pass on child classes 9 | class CHILD_CLASS(Dict): 10 | child_class_attribute = 'child class attribute' 11 | 12 | def child_instance_attribute(self): 13 | return 'child instance attribute' 14 | 15 | 16 | TEST_VAL = [1, 2, 3] 17 | TEST_DICT = {'a': {'b': {'c': TEST_VAL}}} 18 | 19 | 20 | class AbstractTestsClass(object): 21 | dict_class = None 22 | 23 | def test_set_one_level_item(self): 24 | some_dict = {'a': TEST_VAL} 25 | prop = self.dict_class() 26 | prop['a'] = TEST_VAL 27 | self.assertDictEqual(prop, some_dict) 28 | 29 | def test_set_two_level_items(self): 30 | some_dict = {'a': {'b': TEST_VAL}} 31 | prop = self.dict_class() 32 | prop['a']['b'] = TEST_VAL 33 | self.assertDictEqual(prop, some_dict) 34 | 35 | def test_set_three_level_items(self): 36 | prop = self.dict_class() 37 | prop['a']['b']['c'] = TEST_VAL 38 | self.assertDictEqual(prop, TEST_DICT) 39 | 40 | def test_set_one_level_property(self): 41 | prop = self.dict_class() 42 | prop.a = TEST_VAL 43 | self.assertDictEqual(prop, {'a': TEST_VAL}) 44 | 45 | def test_set_two_level_properties(self): 46 | prop = self.dict_class() 47 | prop.a.b = TEST_VAL 48 | self.assertDictEqual(prop, {'a': {'b': TEST_VAL}}) 49 | 50 | def test_set_three_level_properties(self): 51 | prop = self.dict_class() 52 | prop.a.b.c = TEST_VAL 53 | self.assertDictEqual(prop, TEST_DICT) 54 | 55 | def test_init_with_dict(self): 56 | self.assertDictEqual(TEST_DICT, Dict(TEST_DICT)) 57 | 58 | def test_init_with_kws(self): 59 | prop = self.dict_class(a=2, b={'a': 2}, c=[{'a': 2}]) 60 | self.assertDictEqual(prop, {'a': 2, 'b': {'a': 2}, 'c': [{'a': 2}]}) 61 | 62 | def test_init_with_tuples(self): 63 | prop = self.dict_class((0, 1), (1, 2), (2, 3)) 64 | self.assertDictEqual(prop, {0: 1, 1: 2, 2: 3}) 65 | 66 | def test_init_with_list(self): 67 | prop = self.dict_class([(0, 1), (1, 2), (2, 3)]) 68 | self.assertDictEqual(prop, {0: 1, 1: 2, 2: 3}) 69 | 70 | def test_init_with_generator(self): 71 | prop = self.dict_class(((i, i + 1) for i in range(3))) 72 | self.assertDictEqual(prop, {0: 1, 1: 2, 2: 3}) 73 | 74 | def test_init_with_tuples_and_empty_list(self): 75 | prop = self.dict_class((0, 1), [], (2, 3)) 76 | self.assertDictEqual(prop, {0: 1, 2: 3}) 77 | 78 | def test_init_raises(self): 79 | def init(): 80 | self.dict_class(5) 81 | 82 | def init2(): 83 | Dict('a') 84 | self.assertRaises(TypeError, init) 85 | self.assertRaises(ValueError, init2) 86 | 87 | def test_init_with_empty_stuff(self): 88 | a = self.dict_class({}) 89 | b = self.dict_class([]) 90 | self.assertDictEqual(a, {}) 91 | self.assertDictEqual(b, {}) 92 | 93 | def test_init_with_list_of_dicts(self): 94 | a = self.dict_class({'a': [{'b': 2}]}) 95 | self.assertIsInstance(a.a[0], self.dict_class) 96 | self.assertEqual(a.a[0].b, 2) 97 | 98 | def test_init_with_kwargs(self): 99 | a = self.dict_class(a='b', c=dict(d='e', f=dict(g='h'))) 100 | 101 | self.assertEqual(a.a, 'b') 102 | self.assertIsInstance(a.c, self.dict_class) 103 | 104 | self.assertEqual(a.c.f.g, 'h') 105 | self.assertIsInstance(a.c.f, self.dict_class) 106 | 107 | def test_getitem(self): 108 | prop = self.dict_class(TEST_DICT) 109 | self.assertEqual(prop['a']['b']['c'], TEST_VAL) 110 | 111 | def test_empty_getitem(self): 112 | prop = self.dict_class() 113 | prop.a.b.c 114 | self.assertEqual(prop, {}) 115 | 116 | def test_getattr(self): 117 | prop = self.dict_class(TEST_DICT) 118 | self.assertEqual(prop.a.b.c, TEST_VAL) 119 | 120 | def test_isinstance(self): 121 | self.assertTrue(isinstance(self.dict_class(), dict)) 122 | 123 | def test_str(self): 124 | prop = self.dict_class(TEST_DICT) 125 | self.assertEqual(str(prop), str(TEST_DICT)) 126 | 127 | def test_json(self): 128 | some_dict = TEST_DICT 129 | some_json = json.dumps(some_dict) 130 | prop = self.dict_class() 131 | prop.a.b.c = TEST_VAL 132 | prop_json = json.dumps(prop) 133 | self.assertEqual(some_json, prop_json) 134 | 135 | def test_delitem(self): 136 | prop = self.dict_class({'a': 2}) 137 | del prop['a'] 138 | self.assertDictEqual(prop, {}) 139 | 140 | def test_delitem_nested(self): 141 | prop = self.dict_class(TEST_DICT) 142 | del prop['a']['b']['c'] 143 | self.assertDictEqual(prop, {'a': {'b': {}}}) 144 | 145 | def test_delattr(self): 146 | prop = self.dict_class({'a': 2}) 147 | del prop.a 148 | self.assertDictEqual(prop, {}) 149 | 150 | def test_delattr_nested(self): 151 | prop = self.dict_class(TEST_DICT) 152 | del prop.a.b.c 153 | self.assertDictEqual(prop, {'a': {'b': {}}}) 154 | 155 | def test_delitem_delattr(self): 156 | prop = self.dict_class(TEST_DICT) 157 | del prop.a['b'] 158 | self.assertDictEqual(prop, {'a': {}}) 159 | 160 | def test_tuple_key(self): 161 | prop = self.dict_class() 162 | prop[(1, 2)] = 2 163 | self.assertDictEqual(prop, {(1, 2): 2}) 164 | self.assertEqual(prop[(1, 2)], 2) 165 | 166 | def test_set_prop_invalid(self): 167 | prop = self.dict_class() 168 | 169 | def set_keys(): 170 | prop.keys = 2 171 | 172 | def set_items(): 173 | prop.items = 3 174 | 175 | self.assertRaises(AttributeError, set_keys) 176 | self.assertRaises(AttributeError, set_items) 177 | self.assertDictEqual(prop, {}) 178 | 179 | def test_dir(self): 180 | key = 'a' 181 | prop = self.dict_class({key: 1}) 182 | dir_prop = dir(prop) 183 | 184 | dir_dict = dir(self.dict_class) 185 | for d in dir_dict: 186 | self.assertTrue(d in dir_prop, d) 187 | 188 | self.assertTrue('__methods__' not in dir_prop) 189 | self.assertTrue('__members__' not in dir_prop) 190 | 191 | def test_dir_with_members(self): 192 | prop = self.dict_class({'__members__': 1}) 193 | dir(prop) 194 | self.assertTrue('__members__' in prop.keys()) 195 | 196 | def test_to_dict(self): 197 | nested = {'a': [{'a': 0}, 2], 'b': {}, 'c': 2} 198 | prop = self.dict_class(nested) 199 | regular = prop.to_dict() 200 | self.assertDictEqual(regular, prop) 201 | self.assertDictEqual(regular, nested) 202 | self.assertNotIsInstance(regular, self.dict_class) 203 | 204 | def get_attr(): 205 | regular.a = 2 206 | self.assertRaises(AttributeError, get_attr) 207 | 208 | def get_attr_deep(): 209 | regular['a'][0].a = 1 210 | self.assertRaises(AttributeError, get_attr_deep) 211 | 212 | def test_to_dict_with_tuple(self): 213 | nested = {'a': ({'a': 0}, {2: 0})} 214 | prop = self.dict_class(nested) 215 | regular = prop.to_dict() 216 | self.assertDictEqual(regular, prop) 217 | self.assertDictEqual(regular, nested) 218 | self.assertIsInstance(regular['a'], tuple) 219 | self.assertNotIsInstance(regular['a'][0], self.dict_class) 220 | 221 | def test_update(self): 222 | old = self.dict_class() 223 | old.child.a = 'a' 224 | old.child.b = 'b' 225 | old.foo = 'c' 226 | 227 | new = self.dict_class() 228 | new.child.b = 'b2' 229 | new.child.c = 'c' 230 | new.foo.bar = True 231 | 232 | old.update(new) 233 | 234 | reference = {'foo': {'bar': True}, 235 | 'child': {'a': 'a', 'c': 'c', 'b': 'b2'}} 236 | 237 | self.assertDictEqual(old, reference) 238 | 239 | def test_update_with_lists(self): 240 | org = self.dict_class() 241 | org.a = [1, 2, {'a': 'superman'}] 242 | someother = self.dict_class() 243 | someother.b = [{'b': 123}] 244 | org.update(someother) 245 | 246 | correct = {'a': [1, 2, {'a': 'superman'}], 247 | 'b': [{'b': 123}]} 248 | 249 | org.update(someother) 250 | self.assertDictEqual(org, correct) 251 | self.assertIsInstance(org.b[0], dict) 252 | 253 | def test_update_with_kws(self): 254 | org = self.dict_class(one=1, two=2) 255 | someother = self.dict_class(one=3) 256 | someother.update(one=1, two=2) 257 | self.assertDictEqual(org, someother) 258 | 259 | def test_update_with_args_and_kwargs(self): 260 | expected = {'a': 1, 'b': 2} 261 | org = self.dict_class() 262 | org.update({'a': 3, 'b': 2}, a=1) 263 | self.assertDictEqual(org, expected) 264 | 265 | def test_update_with_multiple_args(self): 266 | def update(): 267 | org.update({'a': 2}, {'a': 1}) 268 | org = self.dict_class() 269 | self.assertRaises(TypeError, update) 270 | 271 | def test_ior_operator(self): 272 | old = self.dict_class() 273 | old.child.a = 'a' 274 | old.child.b = 'b' 275 | old.foo = 'c' 276 | 277 | new = self.dict_class() 278 | new.child.b = 'b2' 279 | new.child.c = 'c' 280 | new.foo.bar = True 281 | 282 | old |= new 283 | 284 | reference = {'foo': {'bar': True}, 285 | 'child': {'a': 'a', 'c': 'c', 'b': 'b2'}} 286 | 287 | self.assertDictEqual(old, reference) 288 | 289 | def test_ior_operator_with_lists(self): 290 | org = self.dict_class() 291 | org.a = [1, 2, {'a': 'superman'}] 292 | someother = self.dict_class() 293 | someother.b = [{'b': 123}] 294 | org |= someother 295 | 296 | correct = {'a': [1, 2, {'a': 'superman'}], 297 | 'b': [{'b': 123}]} 298 | 299 | org |= someother 300 | self.assertDictEqual(org, correct) 301 | self.assertIsInstance(org.b[0], dict) 302 | 303 | def test_ior_operator_with_dict(self): 304 | org = self.dict_class(one=1, two=2) 305 | someother = self.dict_class(one=3) 306 | someother |= dict(one=1, two=2) 307 | self.assertDictEqual(org, someother) 308 | 309 | def test_or_operator(self): 310 | old = self.dict_class() 311 | old.child.a = 'a' 312 | old.child.b = 'b' 313 | old.foo = 'c' 314 | 315 | new = self.dict_class() 316 | new.child.b = 'b2' 317 | new.child.c = 'c' 318 | new.foo.bar = True 319 | 320 | old = old | new 321 | 322 | reference = {'foo': {'bar': True}, 323 | 'child': {'a': 'a', 'c': 'c', 'b': 'b2'}} 324 | 325 | self.assertDictEqual(old, reference) 326 | 327 | def test_or_operator_with_lists(self): 328 | org = self.dict_class() 329 | org.a = [1, 2, {'a': 'superman'}] 330 | someother = self.dict_class() 331 | someother.b = [{'b': 123}] 332 | org = org | someother 333 | 334 | correct = {'a': [1, 2, {'a': 'superman'}], 335 | 'b': [{'b': 123}]} 336 | 337 | org = org | someother 338 | self.assertDictEqual(org, correct) 339 | self.assertIsInstance(org.b[0], dict) 340 | 341 | def test_ror_operator(self): 342 | org = dict() 343 | org['a'] = [1, 2, {'a': 'superman'}] 344 | someother = self.dict_class() 345 | someother.b = [{'b': 123}] 346 | org = org | someother 347 | 348 | correct = {'a': [1, 2, {'a': 'superman'}], 349 | 'b': [{'b': 123}]} 350 | 351 | org = org | someother 352 | self.assertDictEqual(org, correct) 353 | self.assertIsInstance(org, Dict) 354 | self.assertIsInstance(org.b[0], dict) 355 | 356 | def test_or_operator_type_error(self): 357 | old = self.dict_class() 358 | with self.assertRaises(TypeError): 359 | old | 'test' 360 | 361 | def test_ror_operator_type_error(self): 362 | old = self.dict_class() 363 | with self.assertRaises(TypeError): 364 | 'test' | old 365 | 366 | def test_hook_in_constructor(self): 367 | a_dict = self.dict_class(TEST_DICT) 368 | self.assertIsInstance(a_dict['a'], self.dict_class) 369 | 370 | def test_copy(self): 371 | class MyMutableObject(object): 372 | 373 | def __init__(self): 374 | self.attribute = None 375 | 376 | foo = MyMutableObject() 377 | foo.attribute = True 378 | 379 | a = self.dict_class() 380 | a.child.immutable = 42 381 | a.child.mutable = foo 382 | 383 | b = a.copy() 384 | 385 | # immutable object should not change 386 | b.child.immutable = 21 387 | self.assertEqual(a.child.immutable, 21) 388 | 389 | # mutable object should change 390 | b.child.mutable.attribute = False 391 | self.assertEqual(a.child.mutable.attribute, b.child.mutable.attribute) 392 | 393 | # changing child of b should not affect a 394 | b.child = "new stuff" 395 | self.assertTrue(isinstance(a.child, self.dict_class)) 396 | 397 | def test_deepcopy(self): 398 | class MyMutableObject(object): 399 | def __init__(self): 400 | self.attribute = None 401 | 402 | foo = MyMutableObject() 403 | foo.attribute = True 404 | 405 | a = self.dict_class() 406 | a.child.immutable = 42 407 | a.child.mutable = foo 408 | 409 | b = copy.deepcopy(a) 410 | 411 | # immutable object should not change 412 | b.child.immutable = 21 413 | self.assertEqual(a.child.immutable, 42) 414 | 415 | # mutable object should not change 416 | b.child.mutable.attribute = False 417 | self.assertTrue(a.child.mutable.attribute) 418 | 419 | # changing child of b should not affect a 420 | b.child = "new stuff" 421 | self.assertTrue(isinstance(a.child, self.dict_class)) 422 | 423 | def test_deepcopy2(self): 424 | class MyMutableObject(object): 425 | def __init__(self): 426 | self.attribute = None 427 | 428 | foo = MyMutableObject() 429 | foo.attribute = True 430 | 431 | a = self.dict_class() 432 | a.child.immutable = 42 433 | a.child.mutable = foo 434 | 435 | b = a.deepcopy() 436 | 437 | # immutable object should not change 438 | b.child.immutable = 21 439 | self.assertEqual(a.child.immutable, 42) 440 | 441 | # mutable object should not change 442 | b.child.mutable.attribute = False 443 | self.assertTrue(a.child.mutable.attribute) 444 | 445 | # changing child of b should not affect a 446 | b.child = "new stuff" 447 | self.assertTrue(isinstance(a.child, self.dict_class)) 448 | 449 | def test_pickle(self): 450 | a = self.dict_class(TEST_DICT) 451 | self.assertEqual(a, pickle.loads(pickle.dumps(a))) 452 | 453 | def test_add_on_empty_dict(self): 454 | d = self.dict_class() 455 | d.x.y += 1 456 | 457 | self.assertEqual(d.x.y, 1) 458 | 459 | def test_add_on_non_empty_dict(self): 460 | d = self.dict_class() 461 | d.x.y = 'defined' 462 | 463 | with self.assertRaises(TypeError): 464 | d.x += 1 465 | 466 | def test_add_on_non_empty_value(self): 467 | d = self.dict_class() 468 | d.x.y = 1 469 | d.x.y += 1 470 | 471 | self.assertEqual(d.x.y, 2) 472 | 473 | def test_add_on_unsupported_type(self): 474 | d = self.dict_class() 475 | d.x.y = 'str' 476 | 477 | with self.assertRaises(TypeError): 478 | d.x.y += 1 479 | 480 | def test_init_from_zip(self): 481 | keys = ['a'] 482 | values = [42] 483 | items = zip(keys, values) 484 | d = self.dict_class(items) 485 | self.assertEqual(d.a, 42) 486 | 487 | def test_setdefault_simple(self): 488 | d = self.dict_class() 489 | d.setdefault('a', 2) 490 | self.assertEqual(d.a, 2) 491 | d.setdefault('a', 3) 492 | self.assertEqual(d.a, 2) 493 | d.setdefault('c', []).append(2) 494 | self.assertEqual(d.c, [2]) 495 | 496 | def test_setdefault_nested(self): 497 | d = self.dict_class() 498 | d.one.setdefault('two', []) 499 | self.assertEqual(d.one.two, []) 500 | d.one.setdefault('three', []).append(3) 501 | self.assertEqual(d.one.three, [3]) 502 | 503 | def test_parent_key_item(self): 504 | a = self.dict_class() 505 | try: 506 | a['keys']['x'] = 1 507 | except AttributeError as e: 508 | self.fail(e) 509 | try: 510 | a[1].x = 3 511 | except Exception as e: 512 | self.fail(e) 513 | self.assertEqual(a, {'keys': {'x': 1}, 1: {'x': 3}}) 514 | 515 | def test_parent_key_prop(self): 516 | a = self.dict_class() 517 | try: 518 | a.y.x = 1 519 | except AttributeError as e: 520 | self.fail(e) 521 | self.assertEqual(a, {'y': {'x': 1}}) 522 | 523 | def test_top_freeze_against_top_key(self): 524 | "Test that d.freeze() produces KeyError on d.missing." 525 | d = self.dict_class() 526 | self.assertEqual(d.missing, {}) 527 | d.freeze() 528 | with self.assertRaises(KeyError): 529 | d.missing 530 | d.unfreeze() 531 | self.assertEqual(d.missing, {}) 532 | 533 | def test_top_freeze_against_nested_key(self): 534 | "Test that d.freeze() produces KeyError on d.inner.missing." 535 | d = self.dict_class() 536 | d.inner.present = TEST_VAL 537 | self.assertIn("inner", d) 538 | self.assertEqual(d.inner.missing, {}) 539 | d.freeze() 540 | with self.assertRaises(KeyError): 541 | d.inner.missing 542 | with self.assertRaises(KeyError): 543 | d.missing 544 | d.unfreeze() 545 | self.assertEqual(d.inner.missing, {}) 546 | self.assertEqual(d.missing, {}) 547 | 548 | def test_nested_freeze_against_top_level(self): 549 | "Test that d.inner.freeze() leaves top-level `d` unfrozen." 550 | d = self.dict_class() 551 | d.inner.present = TEST_VAL 552 | self.assertEqual(d.inner.present, TEST_VAL) 553 | self.assertEqual(d.inner.missing, {}) 554 | self.assertEqual(d.missing, {}) 555 | d.inner.freeze() 556 | with self.assertRaises(KeyError): 557 | d.inner.missing # d.inner is frozen 558 | self.assertEqual(d.missing, {}) # but not `d` itself 559 | d.inner.unfreeze() 560 | self.assertEqual(d.inner.missing, {}) 561 | 562 | def test_top_freeze_disallows_new_key_addition(self): 563 | "Test that d.freeze() disallows adding new keys in d." 564 | d = self.dict_class({"oldKey": None}) 565 | d.freeze() 566 | d.oldKey = TEST_VAL # Can set pre-existing key. 567 | self.assertEqual(d.oldKey, TEST_VAL) 568 | with self.assertRaises(KeyError): 569 | d.newKey = TEST_VAL # But can't add a new key. 570 | self.assertNotIn("newKey", d) 571 | d.unfreeze() 572 | d.newKey = TEST_VAL 573 | self.assertEqual(d.newKey, TEST_VAL) 574 | 575 | class DictTests(unittest.TestCase, AbstractTestsClass): 576 | dict_class = Dict 577 | 578 | 579 | class ChildDictTests(unittest.TestCase, AbstractTestsClass): 580 | dict_class = CHILD_CLASS 581 | 582 | """ 583 | Allow for these test cases to be run from the command line 584 | via `python test_addict.py` 585 | """ 586 | if __name__ == '__main__': 587 | test_classes = (DictTests, ChildDictTests) 588 | loader = unittest.TestLoader() 589 | runner = unittest.TextTestRunner(verbosity=2) 590 | for class_ in test_classes: 591 | loaded_tests = loader.loadTestsFromTestCase(class_) 592 | runner.run(loaded_tests) 593 | --------------------------------------------------------------------------------