├── .gitignore ├── .travis.yml ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── attrdict ├── __init__.py ├── default.py ├── dictionary.py ├── mapping.py ├── merge.py └── mixins.py ├── requirements-tests.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_attrdefault.py ├── test_attrdict.py ├── test_attrmap.py ├── test_common.py ├── test_depricated.py ├── test_merge.py └── test_mixins.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.pyc 3 | MANIFEST 4 | build/* 5 | dist/* 6 | .tox 7 | attrdict.egg-info/* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.3" 5 | - "3.2" 6 | - "2.7" 7 | - "2.6" 8 | - "pypy" 9 | - "pypy3" 10 | install: 11 | - "pip install -r requirements-tests.txt" 12 | - "python setup.py install" 13 | script: "python setup.py nosetests && flake8 attrdict tests" 14 | after_success: 15 | - coveralls 16 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.0, 2013/11/22 -- Initial release. 2 | v0.1.1, 2013/11/22 -- Minor documentation fix. 3 | v0.2.0, 2014/01/03 -- Handle lists as attributes. 4 | v0.2.1, 2014/03/19 -- Bug Fixes: Sequences no longer converted to lists, dict-style access fixed 5 | v0.3.0, 2014/05/28 -- 100 coverage, tests moved out of install 6 | v0.3.1, 2014/06/03 -- documentation typo fix (by @donsignore) 7 | v0.4.0, 2014/07/01 -- Happy Canada Day, have some defaultdict support 8 | v0.5.0, 2014/07/14 —- documentation fix (by @eukaryote), load function 9 | v0.5.1, 2014/07/14 -- tox for local testing, README fix, 0.5.0 no longer from the future 10 | v1.0.0, 2014/08/18 -- Development Status :: 5 - Production/Stable 11 | v1.1.0, 2014/10/29 -- has_key support to match python2 dicts (by Nikolaos-Digenis Karagiannis @Digenis) 12 | v1.2.0, 2014/11/26 -- Happy U.S. Thanksgiving, now you can pickle AttrDict! (by @jtratner), bugfix: default_factory will no longer be erroneously called when accessing private attributes. 13 | v2.0, 2015/04/09 -- Happy PyCon. An almost-complete rewrite. Hopefully in a good way 14 | v2.0.1 2019/02/01 -- Haven't used or looked at this in years so updating tests to the current version of python and then marking it inactive. 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Brendan Curran-Johnson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | AttrDict 3 | ======== 4 | .. image:: https://travis-ci.org/bcj/AttrDict.svg?branch=master 5 | :target: https://travis-ci.org/bcj/AttrDict?branch=master 6 | .. image:: https://coveralls.io/repos/bcj/AttrDict/badge.png?branch=master 7 | :target: https://coveralls.io/r/bcj/AttrDict?branch=master 8 | 9 | AttrDict is an MIT-licensed library that provides mapping objects that allow 10 | their elements to be accessed both as keys and as attributes:: 11 | 12 | > from attrdict import AttrDict 13 | > a = AttrDict({'foo': 'bar'}) 14 | > a.foo 15 | 'bar' 16 | > a['foo'] 17 | 'bar' 18 | 19 | Attribute access makes it easy to create convenient, hierarchical settings 20 | objects:: 21 | 22 | with open('settings.yaml') as fileobj: 23 | settings = AttrDict(yaml.safe_load(fileobj)) 24 | 25 | cursor = connect(**settings.db.credentials).cursor() 26 | 27 | cursor.execute("SELECT column FROM table;") 28 | 29 | Installation 30 | ============ 31 | AttrDict is in PyPI, so it can be installed directly using:: 32 | 33 | $ pip install attrdict 34 | 35 | Or from Github:: 36 | 37 | $ git clone https://github.com/bcj/AttrDict 38 | $ cd AttrDict 39 | $ python setup.py install 40 | 41 | Basic Usage 42 | =========== 43 | AttrDict comes with three different classes, `AttrMap`, `AttrDict`, and 44 | `AttrDefault`. They are all fairly similar, as they all are MutableMappings ( 45 | read: dictionaries) that allow creating, accessing, and deleting key-value 46 | pairs as attributes. 47 | 48 | Valid Names 49 | ----------- 50 | Any key can be used as an attribute as long as: 51 | 52 | #. The key represents a valid attribute (i.e., it is a string comprised only of 53 | alphanumeric characters and underscores that doesn't start with a number) 54 | #. The key represents a public attribute (i.e., it doesn't start with an 55 | underscore). This is done (in part) so that implementation changes between 56 | minor and micro versions don't force major version changes. 57 | #. The key does not shadow a class attribute (e.g., get). 58 | 59 | Attributes vs. Keys 60 | ------------------- 61 | There is a minor difference between accessing a value as an attribute vs. 62 | accessing it as a key, is that when a dict is accessed as an attribute, it will 63 | automatically be converted to an Attr object. This allows you to recursively 64 | access keys:: 65 | 66 | > attr = AttrDict({'foo': {'bar': 'baz'}}) 67 | > attr.foo.bar 68 | 'baz' 69 | 70 | Relatedly, by default, sequence types that aren't `bytes`, `str`, or `unicode` 71 | (e.g., lists, tuples) will automatically be converted to tuples, with any 72 | mappings converted to Attrs:: 73 | 74 | > attr = AttrDict({'foo': [{'bar': 'baz'}, {'bar': 'qux'}]}) 75 | > for sub_attr in attr.foo: 76 | > print(sub_attr.foo) 77 | 'baz' 78 | 'qux' 79 | 80 | To get this recursive functionality for keys that cannot be used as attributes, 81 | you can replicate the behavior by calling the Attr object:: 82 | 83 | > attr = AttrDict({1: {'two': 3}}) 84 | > attr(1).two 85 | 3 86 | 87 | Classes 88 | ------- 89 | AttrDict comes with three different objects, `AttrMap`, `AttrDict`, and 90 | `AttrDefault`. 91 | 92 | AttrMap 93 | ^^^^^^^ 94 | The most basic implementation. Use this if you want to limit the number of 95 | invalid keys, or otherwise cannot use `AttrDict` 96 | 97 | AttrDict 98 | ^^^^^^^^ 99 | An Attr object that subclasses `dict`. You should be able to use this 100 | absolutely anywhere you can use a `dict`. While this is probably the class you 101 | want to use, there are a few caveats that follow from this being a `dict` under 102 | the hood. 103 | 104 | The `copy` method (which returns a shallow copy of the mapping) returns a 105 | `dict` instead of an `AttrDict`. 106 | 107 | Recursive attribute access results in a shallow copy, so recursive assignment 108 | will fail (as you will be writing to a copy of that dictionary):: 109 | 110 | > attr = AttrDict('foo': {}) 111 | > attr.foo.bar = 'baz' 112 | > attr.foo 113 | {} 114 | 115 | Assignment as keys will still work:: 116 | 117 | > attr = AttrDict('foo': {}) 118 | > attr['foo']['bar'] = 'baz' 119 | > attr.foo 120 | {'bar': 'baz'} 121 | 122 | If either of these caveats are deal-breakers, or you don't need your object to 123 | be a `dict`, consider using `AttrMap` instead. 124 | 125 | AttrDefault 126 | ^^^^^^^^^^^ 127 | At Attr object that behaves like a `defaultdict`. This allows on-the-fly, 128 | automatic key creation:: 129 | 130 | > attr = AttrDefault(int, {}) 131 | > attr.foo += 1 132 | > attr.foo 133 | 1 134 | 135 | AttrDefault also has a `pass_key` option that passes the supplied key to the 136 | `default_factory`:: 137 | 138 | > attr = AttrDefault(sorted, {}, pass_key=True) 139 | > attr.banana 140 | ['a', 'a', 'a', 'b', 'n', 'n'] 141 | 142 | Merging 143 | ------- 144 | All three Attr classes can be merged with eachother or other Mappings using the 145 | ``+`` operator. For conflicting keys, the right dict's value will be 146 | preferred, but in the case of two dictionary values, they will be 147 | recursively merged:: 148 | 149 | > a = {'foo': 'bar', 'alpha': {'beta': 'a', 'a': 'a'}} 150 | > b = {'lorem': 'ipsum', 'alpha': {'bravo': 'b', 'a': 'b'}} 151 | > AttrDict(a) + b 152 | {'foo': 'bar', 'lorem': 'ipsum', 'alpha': {'beta': 'a', 'bravo': 'b', 'a': 'b'}} 153 | 154 | NOTE: AttrDict's add is not commutative, ``a + b != b + a``:: 155 | 156 | > a = {'foo': 'bar', 'alpha': {'beta': 'b', 'a': 0}} 157 | > b = {'lorem': 'ipsum', 'alpha': {'bravo': 'b', 'a': 1}} 158 | > b + AttrDict(a) 159 | {'foo': 'bar', 'lorem': 'ipsum', 'alpha': {'beta': 'a', 'bravo': 'b', 'a': }} 160 | 161 | Sequences 162 | --------- 163 | By default, items in non-string Sequences (e.g. lists, tuples) will be 164 | converted to AttrDicts:: 165 | 166 | > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}) 167 | > for element in adict.list: 168 | > element.value 169 | 1 170 | 2 171 | 172 | This will not occur if you access the AttrDict as a dictionary:: 173 | 174 | > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}) 175 | > for element in adict['list']: 176 | > isinstance(element, AttrDict) 177 | False 178 | False 179 | 180 | To disable this behavior globally, pass the attribute ``recursive=False`` to 181 | the constructor:: 182 | 183 | > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}, recursive=False) 184 | > for element in adict.list: 185 | > isinstance(element, AttrDict) 186 | False 187 | False 188 | 189 | When merging an AttrDict with another mapping, this behavior will be disabled 190 | if at least one of the merged items is an AttrDict that has set ``recursive`` 191 | to ``False``. 192 | 193 | License 194 | ======= 195 | AttrDict is released under a MIT license. 196 | -------------------------------------------------------------------------------- /attrdict/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | attrdict contains several mapping objects that allow access to their 3 | keys as attributes. 4 | """ 5 | from attrdict.mapping import AttrMap 6 | from attrdict.dictionary import AttrDict 7 | from attrdict.default import AttrDefault 8 | 9 | 10 | __all__ = ['AttrMap', 'AttrDict', 'AttrDefault'] 11 | -------------------------------------------------------------------------------- /attrdict/default.py: -------------------------------------------------------------------------------- 1 | """ 2 | A subclass of MutableAttr that has defaultdict support. 3 | """ 4 | from collections import Mapping 5 | 6 | import six 7 | 8 | from attrdict.mixins import MutableAttr 9 | 10 | 11 | __all__ = ['AttrDefault'] 12 | 13 | 14 | class AttrDefault(MutableAttr): 15 | """ 16 | An implementation of MutableAttr with defaultdict support 17 | """ 18 | def __init__(self, default_factory=None, items=None, sequence_type=tuple, 19 | pass_key=False): 20 | if items is None: 21 | items = {} 22 | elif not isinstance(items, Mapping): 23 | items = dict(items) 24 | 25 | self._setattr('_default_factory', default_factory) 26 | self._setattr('_mapping', items) 27 | self._setattr('_sequence_type', sequence_type) 28 | self._setattr('_pass_key', pass_key) 29 | self._setattr('_allow_invalid_attributes', False) 30 | 31 | def _configuration(self): 32 | """ 33 | The configuration for a AttrDefault instance 34 | """ 35 | return self._sequence_type, self._default_factory, self._pass_key 36 | 37 | def __getitem__(self, key): 38 | """ 39 | Access a value associated with a key. 40 | 41 | Note: values returned will not be wrapped, even if recursive 42 | is True. 43 | """ 44 | if key in self._mapping: 45 | return self._mapping[key] 46 | elif self._default_factory is not None: 47 | return self.__missing__(key) 48 | 49 | raise KeyError(key) 50 | 51 | def __setitem__(self, key, value): 52 | """ 53 | Add a key-value pair to the instance. 54 | """ 55 | self._mapping[key] = value 56 | 57 | def __delitem__(self, key): 58 | """ 59 | Delete a key-value pair 60 | """ 61 | del self._mapping[key] 62 | 63 | def __len__(self): 64 | """ 65 | Check the length of the mapping. 66 | """ 67 | return len(self._mapping) 68 | 69 | def __iter__(self): 70 | """ 71 | Iterated through the keys. 72 | """ 73 | return iter(self._mapping) 74 | 75 | def __missing__(self, key): 76 | """ 77 | Add a missing element. 78 | """ 79 | if self._pass_key: 80 | self[key] = value = self._default_factory(key) 81 | else: 82 | self[key] = value = self._default_factory() 83 | 84 | return value 85 | 86 | def __repr__(self): 87 | """ 88 | Return a string representation of the object. 89 | """ 90 | return six.u( 91 | "AttrDefault({default_factory}, {pass_key}, {mapping})" 92 | ).format( 93 | default_factory=repr(self._default_factory), 94 | pass_key=repr(self._pass_key), 95 | mapping=repr(self._mapping), 96 | ) 97 | 98 | def __getstate__(self): 99 | """ 100 | Serialize the object. 101 | """ 102 | return ( 103 | self._default_factory, 104 | self._mapping, 105 | self._sequence_type, 106 | self._pass_key, 107 | self._allow_invalid_attributes, 108 | ) 109 | 110 | def __setstate__(self, state): 111 | """ 112 | Deserialize the object. 113 | """ 114 | (default_factory, mapping, sequence_type, pass_key, 115 | allow_invalid_attributes) = state 116 | 117 | self._setattr('_default_factory', default_factory) 118 | self._setattr('_mapping', mapping) 119 | self._setattr('_sequence_type', sequence_type) 120 | self._setattr('_pass_key', pass_key) 121 | self._setattr('_allow_invalid_attributes', allow_invalid_attributes) 122 | 123 | @classmethod 124 | def _constructor(cls, mapping, configuration): 125 | """ 126 | A standardized constructor. 127 | """ 128 | sequence_type, default_factory, pass_key = configuration 129 | return cls(default_factory, mapping, sequence_type=sequence_type, 130 | pass_key=pass_key) 131 | -------------------------------------------------------------------------------- /attrdict/dictionary.py: -------------------------------------------------------------------------------- 1 | """ 2 | A dict that implements MutableAttr. 3 | """ 4 | from attrdict.mixins import MutableAttr 5 | 6 | import six 7 | 8 | 9 | __all__ = ['AttrDict'] 10 | 11 | 12 | class AttrDict(dict, MutableAttr): 13 | """ 14 | A dict that implements MutableAttr. 15 | """ 16 | def __init__(self, *args, **kwargs): 17 | super(AttrDict, self).__init__(*args, **kwargs) 18 | 19 | self._setattr('_sequence_type', tuple) 20 | self._setattr('_allow_invalid_attributes', False) 21 | 22 | def _configuration(self): 23 | """ 24 | The configuration for an attrmap instance. 25 | """ 26 | return self._sequence_type 27 | 28 | def __getstate__(self): 29 | """ 30 | Serialize the object. 31 | """ 32 | return ( 33 | self.copy(), 34 | self._sequence_type, 35 | self._allow_invalid_attributes 36 | ) 37 | 38 | def __setstate__(self, state): 39 | """ 40 | Deserialize the object. 41 | """ 42 | mapping, sequence_type, allow_invalid_attributes = state 43 | self.update(mapping) 44 | self._setattr('_sequence_type', sequence_type) 45 | self._setattr('_allow_invalid_attributes', allow_invalid_attributes) 46 | 47 | def __repr__(self): 48 | return six.u('AttrDict({contents})').format( 49 | contents=super(AttrDict, self).__repr__() 50 | ) 51 | 52 | @classmethod 53 | def _constructor(cls, mapping, configuration): 54 | """ 55 | A standardized constructor. 56 | """ 57 | attr = cls(mapping) 58 | attr._setattr('_sequence_type', configuration) 59 | 60 | return attr 61 | -------------------------------------------------------------------------------- /attrdict/mapping.py: -------------------------------------------------------------------------------- 1 | """ 2 | An implementation of MutableAttr. 3 | """ 4 | from collections import Mapping 5 | 6 | import six 7 | 8 | from attrdict.mixins import MutableAttr 9 | 10 | 11 | __all__ = ['AttrMap'] 12 | 13 | 14 | class AttrMap(MutableAttr): 15 | """ 16 | An implementation of MutableAttr. 17 | """ 18 | def __init__(self, items=None, sequence_type=tuple): 19 | if items is None: 20 | items = {} 21 | elif not isinstance(items, Mapping): 22 | items = dict(items) 23 | 24 | self._setattr('_sequence_type', sequence_type) 25 | self._setattr('_mapping', items) 26 | self._setattr('_allow_invalid_attributes', False) 27 | 28 | def _configuration(self): 29 | """ 30 | The configuration for an attrmap instance. 31 | """ 32 | return self._sequence_type 33 | 34 | def __getitem__(self, key): 35 | """ 36 | Access a value associated with a key. 37 | """ 38 | return self._mapping[key] 39 | 40 | def __setitem__(self, key, value): 41 | """ 42 | Add a key-value pair to the instance. 43 | """ 44 | self._mapping[key] = value 45 | 46 | def __delitem__(self, key): 47 | """ 48 | Delete a key-value pair 49 | """ 50 | del self._mapping[key] 51 | 52 | def __len__(self): 53 | """ 54 | Check the length of the mapping. 55 | """ 56 | return len(self._mapping) 57 | 58 | def __iter__(self): 59 | """ 60 | Iterated through the keys. 61 | """ 62 | return iter(self._mapping) 63 | 64 | def __repr__(self): 65 | """ 66 | Return a string representation of the object. 67 | """ 68 | # sequence type seems like more trouble than it is worth. 69 | # If people want full serialization, they can pickle, and in 70 | # 99% of cases, sequence_type won't change anyway 71 | return six.u("AttrMap({mapping})").format(mapping=repr(self._mapping)) 72 | 73 | def __getstate__(self): 74 | """ 75 | Serialize the object. 76 | """ 77 | return ( 78 | self._mapping, 79 | self._sequence_type, 80 | self._allow_invalid_attributes 81 | ) 82 | 83 | def __setstate__(self, state): 84 | """ 85 | Deserialize the object. 86 | """ 87 | mapping, sequence_type, allow_invalid_attributes = state 88 | self._setattr('_mapping', mapping) 89 | self._setattr('_sequence_type', sequence_type) 90 | self._setattr('_allow_invalid_attributes', allow_invalid_attributes) 91 | 92 | @classmethod 93 | def _constructor(cls, mapping, configuration): 94 | """ 95 | A standardized constructor. 96 | """ 97 | return cls(mapping, sequence_type=configuration) 98 | -------------------------------------------------------------------------------- /attrdict/merge.py: -------------------------------------------------------------------------------- 1 | """ 2 | A right-favoring Mapping merge. 3 | """ 4 | from collections import Mapping 5 | 6 | 7 | __all__ = ['merge'] 8 | 9 | 10 | def merge(left, right): 11 | """ 12 | Merge two mappings objects together, combining overlapping Mappings, 13 | and favoring right-values 14 | 15 | left: The left Mapping object. 16 | right: The right (favored) Mapping object. 17 | 18 | NOTE: This is not commutative (merge(a,b) != merge(b,a)). 19 | """ 20 | merged = {} 21 | 22 | left_keys = frozenset(left) 23 | right_keys = frozenset(right) 24 | 25 | # Items only in the left Mapping 26 | for key in left_keys - right_keys: 27 | merged[key] = left[key] 28 | 29 | # Items only in the right Mapping 30 | for key in right_keys - left_keys: 31 | merged[key] = right[key] 32 | 33 | # in both 34 | for key in left_keys & right_keys: 35 | left_value = left[key] 36 | right_value = right[key] 37 | 38 | if (isinstance(left_value, Mapping) and 39 | isinstance(right_value, Mapping)): # recursive merge 40 | merged[key] = merge(left_value, right_value) 41 | else: # overwrite with right value 42 | merged[key] = right_value 43 | 44 | return merged 45 | -------------------------------------------------------------------------------- /attrdict/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mixin Classes for Attr-support. 3 | """ 4 | from abc import ABCMeta, abstractmethod 5 | from collections import Mapping, MutableMapping, Sequence 6 | import re 7 | 8 | import six 9 | 10 | from attrdict.merge import merge 11 | 12 | 13 | __all__ = ['Attr', 'MutableAttr'] 14 | 15 | 16 | @six.add_metaclass(ABCMeta) 17 | class Attr(Mapping): 18 | """ 19 | A mixin class for a mapping that allows for attribute-style access 20 | of values. 21 | 22 | A key may be used as an attribute if: 23 | * It is a string 24 | * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) 25 | * The key doesn't overlap with any class attributes (for Attr, 26 | those would be 'get', 'items', 'keys', 'values', 'mro', and 27 | 'register'). 28 | 29 | If a values which is accessed as an attribute is a Sequence-type 30 | (and is not a string/bytes), it will be converted to a 31 | _sequence_type with any mappings within it converted to Attrs. 32 | 33 | NOTE: This means that if _sequence_type is not None, then a 34 | sequence accessed as an attribute will be a different object 35 | than if accessed as an attribute than if it is accessed as an 36 | item. 37 | """ 38 | @abstractmethod 39 | def _configuration(self): 40 | """ 41 | All required state for building a new instance with the same 42 | settings as the current object. 43 | """ 44 | 45 | @classmethod 46 | def _constructor(cls, mapping, configuration): 47 | """ 48 | A standardized constructor used internally by Attr. 49 | 50 | mapping: A mapping of key-value pairs. It is HIGHLY recommended 51 | that you use this as the internal key-value pair mapping, as 52 | that will allow nested assignment (e.g., attr.foo.bar = baz) 53 | configuration: The return value of Attr._configuration 54 | """ 55 | raise NotImplementedError("You need to implement this") 56 | 57 | def __call__(self, key): 58 | """ 59 | Dynamically access a key-value pair. 60 | 61 | key: A key associated with a value in the mapping. 62 | 63 | This differs from __getitem__, because it returns a new instance 64 | of an Attr (if the value is a Mapping object). 65 | """ 66 | if key not in self: 67 | raise AttributeError( 68 | "'{cls} instance has no attribute '{name}'".format( 69 | cls=self.__class__.__name__, name=key 70 | ) 71 | ) 72 | 73 | return self._build(self[key]) 74 | 75 | def __getattr__(self, key): 76 | """ 77 | Access an item as an attribute. 78 | """ 79 | if key not in self or not self._valid_name(key): 80 | raise AttributeError( 81 | "'{cls}' instance has no attribute '{name}'".format( 82 | cls=self.__class__.__name__, name=key 83 | ) 84 | ) 85 | 86 | return self._build(self[key]) 87 | 88 | def __add__(self, other): 89 | """ 90 | Add a mapping to this Attr, creating a new, merged Attr. 91 | 92 | other: A mapping. 93 | 94 | NOTE: Addition is not commutative. a + b != b + a. 95 | """ 96 | if not isinstance(other, Mapping): 97 | return NotImplemented 98 | 99 | return self._constructor(merge(self, other), self._configuration()) 100 | 101 | def __radd__(self, other): 102 | """ 103 | Add this Attr to a mapping, creating a new, merged Attr. 104 | 105 | other: A mapping. 106 | 107 | NOTE: Addition is not commutative. a + b != b + a. 108 | """ 109 | if not isinstance(other, Mapping): 110 | return NotImplemented 111 | 112 | return self._constructor(merge(other, self), self._configuration()) 113 | 114 | def _build(self, obj): 115 | """ 116 | Conditionally convert an object to allow for recursive mapping 117 | access. 118 | 119 | obj: An object that was a key-value pair in the mapping. If obj 120 | is a mapping, self._constructor(obj, self._configuration()) 121 | will be called. If obj is a non-string/bytes sequence, and 122 | self._sequence_type is not None, the obj will be converted 123 | to type _sequence_type and build will be called on its 124 | elements. 125 | """ 126 | if isinstance(obj, Mapping): 127 | obj = self._constructor(obj, self._configuration()) 128 | elif (isinstance(obj, Sequence) and 129 | not isinstance(obj, (six.string_types, six.binary_type))): 130 | sequence_type = getattr(self, '_sequence_type', None) 131 | 132 | if sequence_type: 133 | obj = sequence_type(self._build(element) for element in obj) 134 | 135 | return obj 136 | 137 | @classmethod 138 | def _valid_name(cls, key): 139 | """ 140 | Check whether a key is a valid attribute name. 141 | 142 | A key may be used as an attribute if: 143 | * It is a string 144 | * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) 145 | * The key doesn't overlap with any class attributes (for Attr, 146 | those would be 'get', 'items', 'keys', 'values', 'mro', and 147 | 'register'). 148 | """ 149 | return ( 150 | isinstance(key, six.string_types) and 151 | re.match('^[A-Za-z][A-Za-z0-9_]*$', key) and 152 | not hasattr(cls, key) 153 | ) 154 | 155 | 156 | @six.add_metaclass(ABCMeta) 157 | class MutableAttr(Attr, MutableMapping): 158 | """ 159 | A mixin class for a mapping that allows for attribute-style access 160 | of values. 161 | """ 162 | def _setattr(self, key, value): 163 | """ 164 | Add an attribute to the object, without attempting to add it as 165 | a key to the mapping. 166 | """ 167 | super(MutableAttr, self).__setattr__(key, value) 168 | 169 | def __setattr__(self, key, value): 170 | """ 171 | Add an attribute. 172 | 173 | key: The name of the attribute 174 | value: The attributes contents 175 | """ 176 | if self._valid_name(key): 177 | self[key] = value 178 | elif getattr(self, '_allow_invalid_attributes', True): 179 | super(MutableAttr, self).__setattr__(key, value) 180 | else: 181 | raise TypeError( 182 | "'{cls}' does not allow attribute creation.".format( 183 | cls=self.__class__.__name__ 184 | ) 185 | ) 186 | 187 | def _delattr(self, key): 188 | """ 189 | Delete an attribute from the object, without attempting to 190 | remove it from the mapping. 191 | """ 192 | super(MutableAttr, self).__delattr__(key) 193 | 194 | def __delattr__(self, key, force=False): 195 | """ 196 | Delete an attribute. 197 | 198 | key: The name of the attribute 199 | """ 200 | if self._valid_name(key): 201 | del self[key] 202 | elif getattr(self, '_allow_invalid_attributes', True): 203 | super(MutableAttr, self).__delattr__(key) 204 | else: 205 | raise TypeError( 206 | "'{cls}' does not allow attribute deletion.".format( 207 | cls=self.__class__.__name__ 208 | ) 209 | ) 210 | -------------------------------------------------------------------------------- /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | flake8 3 | nose 4 | python-coveralls 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | detailed-errors=1 4 | with-coverage=1 5 | cover-package=attrdict 6 | 7 | [flake8] 8 | exclude = tests/test_depricated.py 9 | 10 | [wheel] 11 | universal = 1 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | To install AttrDict: 3 | 4 | python setup.py install 5 | """ 6 | from setuptools import setup 7 | 8 | 9 | DESCRIPTION = "A dict with attribute-style access" 10 | 11 | try: 12 | LONG_DESCRIPTION = open('README.rst').read() 13 | except: 14 | LONG_DESCRIPTION = DESCRIPTION 15 | 16 | 17 | setup( 18 | name="attrdict", 19 | version="2.0.1", 20 | author="Brendan Curran-Johnson", 21 | author_email="brendan@bcjbcj.ca", 22 | packages=("attrdict",), 23 | url="https://github.com/bcj/AttrDict", 24 | license="MIT License", 25 | description=DESCRIPTION, 26 | long_description=LONG_DESCRIPTION, 27 | classifiers=( 28 | "Development Status :: 7 - Inactive", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: MIT License", 31 | "Programming Language :: Python :: 2", 32 | "Programming Language :: Python :: 2.7", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: Implementation :: CPython", 35 | "Programming Language :: Python :: Implementation :: PyPy", 36 | ), 37 | install_requires=( 38 | 'six', 39 | ), 40 | tests_require=( 41 | 'nose>=1.0', 42 | 'coverage', 43 | ), 44 | zip_safe=True, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the attrdict module 3 | """ 4 | -------------------------------------------------------------------------------- /tests/test_attrdefault.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the AttrDefault class. 3 | """ 4 | from nose.tools import assert_equals, assert_raises 5 | from six import PY2 6 | 7 | 8 | def test_method_missing(): 9 | """ 10 | default values for AttrDefault 11 | """ 12 | from attrdict.default import AttrDefault 13 | 14 | default_none = AttrDefault() 15 | default_list = AttrDefault(list, sequence_type=None) 16 | default_double = AttrDefault(lambda value: value * 2, pass_key=True) 17 | 18 | assert_raises(AttributeError, lambda: default_none.foo) 19 | assert_raises(KeyError, lambda: default_none['foo']) 20 | assert_equals(default_none, {}) 21 | 22 | assert_equals(default_list.foo, []) 23 | assert_equals(default_list['bar'], []) 24 | assert_equals(default_list, {'foo': [], 'bar': []}) 25 | 26 | assert_equals(default_double.foo, 'foofoo') 27 | assert_equals(default_double['bar'], 'barbar') 28 | assert_equals(default_double, {'foo': 'foofoo', 'bar': 'barbar'}) 29 | 30 | 31 | def test_repr(): 32 | """ 33 | repr(AttrDefault) 34 | """ 35 | from attrdict.default import AttrDefault 36 | 37 | assert_equals(repr(AttrDefault(None)), "AttrDefault(None, False, {})") 38 | 39 | # list's repr changes between python 2 and python 3 40 | type_or_class = 'type' if PY2 else 'class' 41 | 42 | assert_equals( 43 | repr(AttrDefault(list)), 44 | type_or_class.join(("AttrDefault(<", " 'list'>, False, {})")) 45 | ) 46 | 47 | assert_equals( 48 | repr(AttrDefault(list, {'foo': 'bar'}, pass_key=True)), 49 | type_or_class.join( 50 | ("AttrDefault(<", " 'list'>, True, {'foo': 'bar'})") 51 | ) 52 | ) 53 | -------------------------------------------------------------------------------- /tests/test_attrdict.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | """ 3 | Tests for the AttrDict class. 4 | """ 5 | from nose.tools import assert_equals, assert_false 6 | from six import PY2 7 | 8 | 9 | def test_init(): 10 | """ 11 | Create a new AttrDict. 12 | """ 13 | from attrdict.dictionary import AttrDict 14 | 15 | # empty 16 | assert_equals(AttrDict(), {}) 17 | assert_equals(AttrDict(()), {}) 18 | assert_equals(AttrDict({}), {}) 19 | 20 | # with items 21 | assert_equals(AttrDict({'foo': 'bar'}), {'foo': 'bar'}) 22 | assert_equals(AttrDict((('foo', 'bar'),)), {'foo': 'bar'}) 23 | assert_equals(AttrDict(foo='bar'), {'foo': 'bar'}) 24 | 25 | # non-overlapping 26 | assert_equals(AttrDict({}, foo='bar'), {'foo': 'bar'}) 27 | assert_equals(AttrDict((), foo='bar'), {'foo': 'bar'}) 28 | 29 | assert_equals( 30 | AttrDict({'alpha': 'bravo'}, foo='bar'), 31 | {'foo': 'bar', 'alpha': 'bravo'} 32 | ) 33 | 34 | assert_equals( 35 | AttrDict((('alpha', 'bravo'),), foo='bar'), 36 | {'foo': 'bar', 'alpha': 'bravo'} 37 | ) 38 | 39 | # updating 40 | assert_equals( 41 | AttrDict({'alpha': 'bravo'}, foo='bar', alpha='beta'), 42 | {'foo': 'bar', 'alpha': 'beta'} 43 | ) 44 | 45 | assert_equals( 46 | AttrDict((('alpha', 'bravo'), ('alpha', 'beta')), foo='bar'), 47 | {'foo': 'bar', 'alpha': 'beta'} 48 | ) 49 | 50 | assert_equals( 51 | AttrDict((('alpha', 'bravo'), ('alpha', 'beta')), alpha='bravo'), 52 | {'alpha': 'bravo'} 53 | ) 54 | 55 | 56 | def test_copy(): 57 | """ 58 | Make a dict copy of an AttrDict. 59 | """ 60 | from attrdict.dictionary import AttrDict 61 | 62 | mapping_a = AttrDict({'foo': {'bar': 'baz'}}) 63 | mapping_b = mapping_a.copy() 64 | mapping_c = mapping_b 65 | 66 | mapping_b['foo']['lorem'] = 'ipsum' 67 | 68 | assert_equals(mapping_a, mapping_b) 69 | assert_equals(mapping_b, mapping_c) 70 | 71 | 72 | def test_fromkeys(): 73 | """ 74 | make a new sequence from a set of keys. 75 | """ 76 | from attrdict.dictionary import AttrDict 77 | 78 | # default value 79 | assert_equals(AttrDict.fromkeys(()), {}) 80 | assert_equals( 81 | AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}), 82 | {'foo': None, 'baz': None} 83 | ) 84 | assert_equals( 85 | AttrDict.fromkeys(('foo', 'baz')), 86 | {'foo': None, 'baz': None} 87 | ) 88 | 89 | # custom value 90 | assert_equals(AttrDict.fromkeys((), 0), {}) 91 | assert_equals( 92 | AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}, 0), 93 | {'foo': 0, 'baz': 0} 94 | ) 95 | assert_equals( 96 | AttrDict.fromkeys(('foo', 'baz'), 0), 97 | {'foo': 0, 'baz': 0} 98 | ) 99 | 100 | 101 | def test_repr(): 102 | """ 103 | repr(AttrDict) 104 | """ 105 | from attrdict.dictionary import AttrDict 106 | 107 | assert_equals(repr(AttrDict()), "AttrDict({})") 108 | assert_equals(repr(AttrDict({'foo': 'bar'})), "AttrDict({'foo': 'bar'})") 109 | assert_equals( 110 | repr(AttrDict({1: {'foo': 'bar'}})), "AttrDict({1: {'foo': 'bar'}})" 111 | ) 112 | assert_equals( 113 | repr(AttrDict({1: AttrDict({'foo': 'bar'})})), 114 | "AttrDict({1: AttrDict({'foo': 'bar'})})" 115 | ) 116 | 117 | 118 | if not PY2: 119 | def test_has_key(): 120 | """ 121 | The now-depricated has_keys method 122 | """ 123 | from attrdict.dictionary import AttrDict 124 | 125 | assert_false(hasattr(AttrDict(), 'has_key')) 126 | -------------------------------------------------------------------------------- /tests/test_attrmap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the AttrMap class. 3 | """ 4 | from nose.tools import assert_equals 5 | 6 | 7 | def test_repr(): 8 | """ 9 | repr(AttrMap) 10 | """ 11 | from attrdict.mapping import AttrMap 12 | 13 | assert_equals(repr(AttrMap()), "AttrMap({})") 14 | assert_equals(repr(AttrMap({'foo': 'bar'})), "AttrMap({'foo': 'bar'})") 15 | assert_equals( 16 | repr(AttrMap({1: {'foo': 'bar'}})), "AttrMap({1: {'foo': 'bar'}})" 17 | ) 18 | assert_equals( 19 | repr(AttrMap({1: AttrMap({'foo': 'bar'})})), 20 | "AttrMap({1: AttrMap({'foo': 'bar'})})" 21 | ) 22 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | """ 3 | Common tests that apply to multiple Attr-derived classes. 4 | """ 5 | import copy 6 | from collections import namedtuple, Mapping, ItemsView, KeysView, ValuesView 7 | from itertools import chain 8 | import pickle 9 | from sys import version_info 10 | 11 | from nose.tools import (assert_equals, assert_not_equals, 12 | assert_true, assert_false, assert_raises) 13 | import six 14 | 15 | from attrdict.mixins import Attr 16 | 17 | 18 | Options = namedtuple( 19 | 'Options', 20 | ('cls', 'constructor', 'mutable', 'iter_methods', 'view_methods', 21 | 'recursive') 22 | ) 23 | 24 | 25 | class AttrImpl(Attr): 26 | """ 27 | An implementation of Attr. 28 | """ 29 | def __init__(self, items=None, sequence_type=tuple): 30 | if items is None: 31 | items = {} 32 | elif not isinstance(items, Mapping): 33 | items = dict(items) 34 | 35 | self._mapping = items 36 | self._sequence_type = sequence_type 37 | 38 | def _configuration(self): 39 | """ 40 | The configuration for an attrmap instance. 41 | """ 42 | return self._sequence_type 43 | 44 | def __getitem__(self, key): 45 | """ 46 | Access a value associated with a key. 47 | """ 48 | return self._mapping[key] 49 | 50 | def __len__(self): 51 | """ 52 | Check the length of the mapping. 53 | """ 54 | return len(self._mapping) 55 | 56 | def __iter__(self): 57 | """ 58 | Iterated through the keys. 59 | """ 60 | return iter(self._mapping) 61 | 62 | def __getstate__(self): 63 | """ 64 | Serialize the object. 65 | """ 66 | return (self._mapping, self._sequence_type) 67 | 68 | def __setstate__(self, state): 69 | """ 70 | Deserialize the object. 71 | """ 72 | mapping, sequence_type = state 73 | self._mapping = mapping 74 | self._sequence_type = sequence_type 75 | 76 | @classmethod 77 | def _constructor(cls, mapping, configuration): 78 | """ 79 | A standardized constructor. 80 | """ 81 | return cls(mapping, sequence_type=configuration) 82 | 83 | 84 | def test_attr(): 85 | """ 86 | Tests for an class that implements Attr. 87 | """ 88 | for test in common(AttrImpl, mutable=False): 89 | yield test 90 | 91 | 92 | def test_attrmap(): 93 | """ 94 | Run AttrMap against the common tests. 95 | """ 96 | from attrdict.mapping import AttrMap 97 | 98 | for test in common(AttrMap, mutable=True): 99 | yield test 100 | 101 | 102 | def test_attrdict(): 103 | """ 104 | Run AttrDict against the common tests. 105 | """ 106 | from attrdict.dictionary import AttrDict 107 | 108 | view_methods = (2, 7) <= version_info < (3,) 109 | 110 | def constructor(items=None, sequence_type=tuple): 111 | """ 112 | Build a new AttrDict. 113 | """ 114 | if items is None: 115 | items = {} 116 | 117 | return AttrDict._constructor(items, sequence_type) 118 | 119 | for test in common(AttrDict, constructor=constructor, 120 | mutable=True, iter_methods=True, 121 | view_methods=view_methods, recursive=False): 122 | yield test 123 | 124 | 125 | def test_attrdefault(): 126 | """ 127 | Run AttrDefault against the common tests. 128 | """ 129 | from attrdict.default import AttrDefault 130 | 131 | def constructor(items=None, sequence_type=tuple): 132 | """ 133 | Build a new AttrDefault. 134 | """ 135 | if items is None: 136 | items = {} 137 | 138 | return AttrDefault(None, items, sequence_type) 139 | 140 | for test in common(AttrDefault, constructor=constructor, mutable=True): 141 | yield test 142 | 143 | 144 | def common(cls, constructor=None, mutable=False, iter_methods=False, 145 | view_methods=False, recursive=True): 146 | """ 147 | Iterates over tests common to multiple Attr-derived classes 148 | 149 | cls: The class that is being tested. 150 | mutable: (optional, False) Whether the object is supposed to be 151 | mutable. 152 | iter_methods: (optional, False) Whether the class implements 153 | iter under Python 2. 154 | view_methods: (optional, False) Whether the class implements 155 | view under Python 2. 156 | recursive: (optional, True) Whether recursive assignment works. 157 | """ 158 | tests = ( 159 | item_access, iteration, containment, length, equality, 160 | item_creation, item_deletion, sequence_typing, addition, 161 | to_kwargs, pickling, 162 | ) 163 | 164 | mutable_tests = ( 165 | pop, popitem, clear, update, setdefault, copying, deepcopying, 166 | ) 167 | 168 | if constructor is None: 169 | constructor = cls 170 | 171 | options = Options(cls, constructor, mutable, iter_methods, view_methods, 172 | recursive) 173 | 174 | if mutable: 175 | tests = chain(tests, mutable_tests) 176 | 177 | for test in tests: 178 | test.description = test.__doc__.format(cls=cls.__name__) 179 | 180 | yield test, options 181 | 182 | 183 | def item_access(options): 184 | """Access items in {cls}.""" 185 | mapping = options.constructor( 186 | { 187 | 'foo': 'bar', 188 | '_lorem': 'ipsum', 189 | six.u('👻'): 'boo', 190 | 3: 'three', 191 | 'get': 'not the function', 192 | 'sub': {'alpha': 'bravo'}, 193 | 'bytes': b'bytes', 194 | 'tuple': ({'a': 'b'}, 'c'), 195 | 'list': [{'a': 'b'}, {'c': 'd'}], 196 | } 197 | ) 198 | 199 | # key that can be an attribute 200 | assert_equals(mapping['foo'], 'bar') 201 | assert_equals(mapping.foo, 'bar') 202 | assert_equals(mapping('foo'), 'bar') 203 | assert_equals(mapping.get('foo'), 'bar') 204 | 205 | # key that cannot be an attribute 206 | assert_equals(mapping[3], 'three') 207 | assert_raises(TypeError, getattr, mapping, 3) 208 | assert_equals(mapping(3), 'three') 209 | assert_equals(mapping.get(3), 'three') 210 | 211 | # key that cannot be an attribute (sadly) 212 | assert_equals(mapping[six.u('👻')], 'boo') 213 | if six.PY2: 214 | assert_raises(UnicodeEncodeError, getattr, mapping, six.u('👻')) 215 | else: 216 | assert_raises(AttributeError, getattr, mapping, six.u('👻')) 217 | assert_equals(mapping(six.u('👻')), 'boo') 218 | assert_equals(mapping.get(six.u('👻')), 'boo') 219 | 220 | # key that represents a hidden attribute 221 | assert_equals(mapping['_lorem'], 'ipsum') 222 | assert_raises(AttributeError, lambda: mapping._lorem) 223 | assert_equals(mapping('_lorem'), 'ipsum') 224 | assert_equals(mapping.get('_lorem'), 'ipsum') 225 | 226 | # key that represents an attribute that already exists 227 | assert_equals(mapping['get'], 'not the function') 228 | assert_not_equals(mapping.get, 'not the function') 229 | assert_equals(mapping('get'), 'not the function') 230 | assert_equals(mapping.get('get'), 'not the function') 231 | 232 | # does recursion work 233 | assert_raises(AttributeError, lambda: mapping['sub'].alpha) 234 | assert_equals(mapping.sub.alpha, 'bravo') 235 | assert_equals(mapping('sub').alpha, 'bravo') 236 | assert_raises(AttributeError, lambda: mapping.get('sub').alpha) 237 | 238 | # bytes 239 | assert_equals(mapping['bytes'], b'bytes') 240 | assert_equals(mapping.bytes, b'bytes') 241 | assert_equals(mapping('bytes'), b'bytes') 242 | assert_equals(mapping.get('bytes'), b'bytes') 243 | 244 | # tuple 245 | assert_equals(mapping['tuple'], ({'a': 'b'}, 'c')) 246 | assert_equals(mapping.tuple, ({'a': 'b'}, 'c')) 247 | assert_equals(mapping('tuple'), ({'a': 'b'}, 'c')) 248 | assert_equals(mapping.get('tuple'), ({'a': 'b'}, 'c')) 249 | 250 | assert_raises(AttributeError, lambda: mapping['tuple'][0].a) 251 | assert_equals(mapping.tuple[0].a, 'b') 252 | assert_equals(mapping('tuple')[0].a, 'b') 253 | assert_raises(AttributeError, lambda: mapping.get('tuple')[0].a) 254 | 255 | assert_true(isinstance(mapping['tuple'], tuple)) 256 | assert_true(isinstance(mapping.tuple, tuple)) 257 | assert_true(isinstance(mapping('tuple'), tuple)) 258 | assert_true(isinstance(mapping.get('tuple'), tuple)) 259 | 260 | assert_true(isinstance(mapping['tuple'][0], dict)) 261 | assert_true(isinstance(mapping.tuple[0], options.cls)) 262 | assert_true(isinstance(mapping('tuple')[0], options.cls)) 263 | assert_true(isinstance(mapping.get('tuple')[0], dict)) 264 | 265 | assert_true(isinstance(mapping['tuple'][1], str)) 266 | assert_true(isinstance(mapping.tuple[1], str)) 267 | assert_true(isinstance(mapping('tuple')[1], str)) 268 | assert_true(isinstance(mapping.get('tuple')[1], str)) 269 | 270 | # list 271 | assert_equals(mapping['list'], [{'a': 'b'}, {'c': 'd'}]) 272 | assert_equals(mapping.list, ({'a': 'b'}, {'c': 'd'})) 273 | assert_equals(mapping('list'), ({'a': 'b'}, {'c': 'd'})) 274 | assert_equals(mapping.get('list'), [{'a': 'b'}, {'c': 'd'}]) 275 | 276 | assert_raises(AttributeError, lambda: mapping['list'][0].a) 277 | assert_equals(mapping.list[0].a, 'b') 278 | assert_equals(mapping('list')[0].a, 'b') 279 | assert_raises(AttributeError, lambda: mapping.get('list')[0].a) 280 | 281 | assert_true(isinstance(mapping['list'], list)) 282 | assert_true(isinstance(mapping.list, tuple)) 283 | assert_true(isinstance(mapping('list'), tuple)) 284 | assert_true(isinstance(mapping.get('list'), list)) 285 | 286 | assert_true(isinstance(mapping['list'][0], dict)) 287 | assert_true(isinstance(mapping.list[0], options.cls)) 288 | assert_true(isinstance(mapping('list')[0], options.cls)) 289 | assert_true(isinstance(mapping.get('list')[0], dict)) 290 | 291 | assert_true(isinstance(mapping['list'][1], dict)) 292 | assert_true(isinstance(mapping.list[1], options.cls)) 293 | assert_true(isinstance(mapping('list')[1], options.cls)) 294 | assert_true(isinstance(mapping.get('list')[1], dict)) 295 | 296 | # Nonexistent key 297 | assert_raises(KeyError, lambda: mapping['fake']) 298 | assert_raises(AttributeError, lambda: mapping.fake) 299 | assert_raises(AttributeError, lambda: mapping('fake')) 300 | assert_equals(mapping.get('fake'), None) 301 | assert_equals(mapping.get('fake', 'bake'), 'bake') 302 | 303 | 304 | def iteration(options): 305 | "Iterate over keys/values/items in {cls}" 306 | raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} 307 | 308 | mapping = options.constructor(raw) 309 | 310 | expected_keys = frozenset(('foo', 'lorem', 'alpha')) 311 | expected_values = frozenset(('bar', 'ipsum', 'bravo')) 312 | expected_items = frozenset( 313 | (('foo', 'bar'), ('lorem', 'ipsum'), ('alpha', 'bravo')) 314 | ) 315 | 316 | assert_equals(set(iter(mapping)), expected_keys) 317 | 318 | actual_keys = mapping.keys() 319 | actual_values = mapping.values() 320 | actual_items = mapping.items() 321 | 322 | if six.PY2: 323 | for collection in (actual_keys, actual_values, actual_items): 324 | assert_true(isinstance(collection, list)) 325 | 326 | assert_equals(frozenset(actual_keys), expected_keys) 327 | assert_equals(frozenset(actual_values), expected_values) 328 | assert_equals(frozenset(actual_items), expected_items) 329 | 330 | if options.iter_methods: 331 | actual_keys = mapping.iterkeys() 332 | actual_values = mapping.itervalues() 333 | actual_items = mapping.iteritems() 334 | 335 | for iterable in (actual_keys, actual_values, actual_items): 336 | assert_false(isinstance(iterable, list)) 337 | 338 | assert_equals(frozenset(actual_keys), expected_keys) 339 | assert_equals(frozenset(actual_values), expected_values) 340 | assert_equals(frozenset(actual_items), expected_items) 341 | 342 | if options.view_methods: 343 | actual_keys = mapping.viewkeys() 344 | actual_values = mapping.viewvalues() 345 | actual_items = mapping.viewitems() 346 | 347 | # These views don't actually extend from collections.*View 348 | for iterable in (actual_keys, actual_values, actual_items): 349 | assert_false(isinstance(iterable, list)) 350 | 351 | assert_equals(frozenset(actual_keys), expected_keys) 352 | assert_equals(frozenset(actual_values), expected_values) 353 | assert_equals(frozenset(actual_items), expected_items) 354 | 355 | # What happens if mapping isn't a dict 356 | from attrdict.mapping import AttrMap 357 | 358 | mapping = options.constructor(AttrMap(raw)) 359 | 360 | actual_keys = mapping.viewkeys() 361 | actual_values = mapping.viewvalues() 362 | actual_items = mapping.viewitems() 363 | 364 | # These views don't actually extend from collections.*View 365 | for iterable in (actual_keys, actual_values, actual_items): 366 | assert_false(isinstance(iterable, list)) 367 | 368 | assert_equals(frozenset(actual_keys), expected_keys) 369 | assert_equals(frozenset(actual_values), expected_values) 370 | assert_equals(frozenset(actual_items), expected_items) 371 | 372 | else: # methods are actually views 373 | assert_true(isinstance(actual_keys, KeysView)) 374 | assert_equals(frozenset(actual_keys), expected_keys) 375 | 376 | assert_true(isinstance(actual_values, ValuesView)) 377 | assert_equals(frozenset(actual_values), expected_values) 378 | 379 | assert_true(isinstance(actual_items, ItemsView)) 380 | assert_equals(frozenset(actual_items), expected_items) 381 | 382 | # make sure empty iteration works 383 | assert_equals(tuple(options.constructor().items()), ()) 384 | 385 | 386 | def containment(options): 387 | "Check whether {cls} contains keys" 388 | mapping = options.constructor( 389 | {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} 390 | ) 391 | empty = options.constructor() 392 | 393 | assert_true('foo' in mapping) 394 | assert_false('foo' in empty) 395 | 396 | assert_true(frozenset((1, 2, 3)) in mapping) 397 | assert_false(frozenset((1, 2, 3)) in empty) 398 | 399 | assert_true(1 in mapping) 400 | assert_false(1 in empty) 401 | 402 | assert_false('banana' in mapping) 403 | assert_false('banana' in empty) 404 | 405 | 406 | def length(options): 407 | "Get the length of an {cls} instance" 408 | assert_equals(len(options.constructor()), 0) 409 | assert_equals(len(options.constructor({'foo': 'bar'})), 1) 410 | assert_equals(len(options.constructor({'foo': 'bar', 'baz': 'qux'})), 2) 411 | 412 | 413 | def equality(options): 414 | "Equality checks for {cls}" 415 | empty = {} 416 | mapping_a = {'foo': 'bar'} 417 | mapping_b = {'lorem': 'ipsum'} 418 | 419 | constructor = options.constructor 420 | 421 | assert_true(constructor(empty) == empty) 422 | assert_false(constructor(empty) != empty) 423 | assert_false(constructor(empty) == mapping_a) 424 | assert_true(constructor(empty) != mapping_a) 425 | assert_false(constructor(empty) == mapping_b) 426 | assert_true(constructor(empty) != mapping_b) 427 | 428 | assert_false(constructor(mapping_a) == empty) 429 | assert_true(constructor(mapping_a) != empty) 430 | assert_true(constructor(mapping_a) == mapping_a) 431 | assert_false(constructor(mapping_a) != mapping_a) 432 | assert_false(constructor(mapping_a) == mapping_b) 433 | assert_true(constructor(mapping_a) != mapping_b) 434 | 435 | assert_false(constructor(mapping_b) == empty) 436 | assert_true(constructor(mapping_b) != empty) 437 | assert_false(constructor(mapping_b) == mapping_a) 438 | assert_true(constructor(mapping_b) != mapping_a) 439 | assert_true(constructor(mapping_b) == mapping_b) 440 | assert_false(constructor(mapping_b) != mapping_b) 441 | 442 | assert_true(constructor(empty) == constructor(empty)) 443 | assert_false(constructor(empty) != constructor(empty)) 444 | assert_false(constructor(empty) == constructor(mapping_a)) 445 | assert_true(constructor(empty) != constructor(mapping_a)) 446 | assert_false(constructor(empty) == constructor(mapping_b)) 447 | assert_true(constructor(empty) != constructor(mapping_b)) 448 | 449 | assert_false(constructor(mapping_a) == constructor(empty)) 450 | assert_true(constructor(mapping_a) != constructor(empty)) 451 | assert_true(constructor(mapping_a) == constructor(mapping_a)) 452 | assert_false(constructor(mapping_a) != constructor(mapping_a)) 453 | assert_false(constructor(mapping_a) == constructor(mapping_b)) 454 | assert_true(constructor(mapping_a) != constructor(mapping_b)) 455 | 456 | assert_false(constructor(mapping_b) == constructor(empty)) 457 | assert_true(constructor(mapping_b) != constructor(empty)) 458 | assert_false(constructor(mapping_b) == constructor(mapping_a)) 459 | assert_true(constructor(mapping_b) != constructor(mapping_a)) 460 | assert_true(constructor(mapping_b) == constructor(mapping_b)) 461 | assert_false(constructor(mapping_b) != constructor(mapping_b)) 462 | 463 | assert_true(constructor((('foo', 'bar'),)) == {'foo': 'bar'}) 464 | 465 | 466 | def item_creation(options): 467 | "Add a key-value pair to an {cls}" 468 | 469 | if not options.mutable: 470 | # Assignment shouldn't add to the dict 471 | mapping = options.constructor() 472 | 473 | try: 474 | mapping.foo = 'bar' 475 | except TypeError: 476 | pass # may fail if setattr modified 477 | else: 478 | pass # may assign, but shouldn't assign to dict 479 | 480 | def item(): 481 | """ 482 | Attempt to add an item. 483 | """ 484 | mapping['foo'] = 'bar' 485 | 486 | assert_raises(TypeError, item) 487 | 488 | assert_false('foo' in mapping) 489 | else: 490 | mapping = options.constructor() 491 | 492 | # key that can be an attribute 493 | mapping.foo = 'bar' 494 | 495 | assert_equals(mapping.foo, 'bar') 496 | assert_equals(mapping['foo'], 'bar') 497 | assert_equals(mapping('foo'), 'bar') 498 | assert_equals(mapping.get('foo'), 'bar') 499 | 500 | mapping['baz'] = 'qux' 501 | 502 | assert_equals(mapping.baz, 'qux') 503 | assert_equals(mapping['baz'], 'qux') 504 | assert_equals(mapping('baz'), 'qux') 505 | assert_equals(mapping.get('baz'), 'qux') 506 | 507 | # key that cannot be an attribute 508 | assert_raises(TypeError, setattr, mapping, 1, 'one') 509 | 510 | assert_true(1 not in mapping) 511 | 512 | mapping[2] = 'two' 513 | 514 | assert_equals(mapping[2], 'two') 515 | assert_equals(mapping(2), 'two') 516 | assert_equals(mapping.get(2), 'two') 517 | 518 | # key that represents a hidden attribute 519 | def add_foo(): 520 | "add _foo to mapping" 521 | mapping._foo = '_bar' 522 | 523 | assert_raises(TypeError, add_foo) 524 | assert_false('_foo' in mapping) 525 | 526 | mapping['_baz'] = 'qux' 527 | 528 | def get_baz(): 529 | "get the _foo attribute" 530 | return mapping._baz 531 | 532 | assert_raises(AttributeError, get_baz) 533 | assert_equals(mapping['_baz'], 'qux') 534 | assert_equals(mapping('_baz'), 'qux') 535 | assert_equals(mapping.get('_baz'), 'qux') 536 | 537 | # key that represents an attribute that already exists 538 | def add_get(): 539 | "add get to mapping" 540 | mapping.get = 'attribute' 541 | 542 | assert_raises(TypeError, add_get) 543 | assert_false('get' in mapping) 544 | 545 | mapping['get'] = 'value' 546 | 547 | assert_not_equals(mapping.get, 'value') 548 | assert_equals(mapping['get'], 'value') 549 | assert_equals(mapping('get'), 'value') 550 | assert_equals(mapping.get('get'), 'value') 551 | 552 | # rewrite a value 553 | mapping.foo = 'manchu' 554 | 555 | assert_equals(mapping.foo, 'manchu') 556 | assert_equals(mapping['foo'], 'manchu') 557 | assert_equals(mapping('foo'), 'manchu') 558 | assert_equals(mapping.get('foo'), 'manchu') 559 | 560 | mapping['bar'] = 'bell' 561 | 562 | assert_equals(mapping.bar, 'bell') 563 | assert_equals(mapping['bar'], 'bell') 564 | assert_equals(mapping('bar'), 'bell') 565 | assert_equals(mapping.get('bar'), 'bell') 566 | 567 | if options.recursive: 568 | recursed = options.constructor({'foo': {'bar': 'baz'}}) 569 | 570 | recursed.foo.bar = 'qux' 571 | recursed.foo.alpha = 'bravo' 572 | 573 | assert_equals(recursed, {'foo': {'bar': 'qux', 'alpha': 'bravo'}}) 574 | 575 | 576 | def item_deletion(options): 577 | "Remove a key-value from to an {cls}" 578 | if not options.mutable: 579 | mapping = options.constructor({'foo': 'bar'}) 580 | 581 | # could be a TypeError or an AttributeError 582 | try: 583 | del mapping.foo 584 | except TypeError: 585 | pass 586 | except AttributeError: 587 | pass 588 | else: 589 | raise AssertionError('deletion should fail') 590 | 591 | def item(mapping): 592 | """ 593 | Attempt to del an item 594 | """ 595 | del mapping['foo'] 596 | 597 | assert_raises(TypeError, item, mapping) 598 | 599 | assert_equals(mapping, {'foo': 'bar'}) 600 | assert_equals(mapping.foo, 'bar') 601 | assert_equals(mapping['foo'], 'bar') 602 | else: 603 | mapping = options.constructor( 604 | {'foo': 'bar', 'lorem': 'ipsum', '_hidden': True, 'get': 'value'} 605 | ) 606 | 607 | del mapping.foo 608 | assert_false('foo' in mapping) 609 | 610 | del mapping['lorem'] 611 | assert_false('lorem' in mapping) 612 | 613 | def del_hidden(): 614 | "delete _hidden" 615 | del mapping._hidden 616 | 617 | try: 618 | del_hidden() 619 | except KeyError: 620 | pass 621 | except TypeError: 622 | pass 623 | else: 624 | assert_false("Test raised the appropriate exception") 625 | # assert_raises(TypeError, del_hidden) 626 | assert_true('_hidden' in mapping) 627 | 628 | del mapping['_hidden'] 629 | assert_false('hidden' in mapping) 630 | 631 | def del_get(): 632 | "delete get" 633 | del mapping.get 634 | 635 | assert_raises(TypeError, del_get) 636 | assert_true('get' in mapping) 637 | assert_true(mapping.get('get'), 'value') 638 | 639 | del mapping['get'] 640 | assert_false('get' in mapping) 641 | assert_true(mapping.get('get', 'banana'), 'banana') 642 | 643 | 644 | def sequence_typing(options): 645 | "Does {cls} respect sequence type?" 646 | data = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} 647 | 648 | tuple_mapping = options.constructor(data) 649 | 650 | assert_true(isinstance(tuple_mapping.list, tuple)) 651 | assert_equals(tuple_mapping.list[0].foo, 'bar') 652 | 653 | assert_true(isinstance(tuple_mapping.tuple, tuple)) 654 | assert_equals(tuple_mapping.tuple[0].foo, 'bar') 655 | 656 | list_mapping = options.constructor(data, sequence_type=list) 657 | 658 | assert_true(isinstance(list_mapping.list, list)) 659 | assert_equals(list_mapping.list[0].foo, 'bar') 660 | 661 | assert_true(isinstance(list_mapping.tuple, list)) 662 | assert_equals(list_mapping.tuple[0].foo, 'bar') 663 | 664 | none_mapping = options.constructor(data, sequence_type=None) 665 | 666 | assert_true(isinstance(none_mapping.list, list)) 667 | assert_raises(AttributeError, lambda: none_mapping.list[0].foo) 668 | 669 | assert_true(isinstance(none_mapping.tuple, tuple)) 670 | assert_raises(AttributeError, lambda: none_mapping.tuple[0].foo) 671 | 672 | 673 | def addition(options): 674 | "Adding {cls} to other mappings." 675 | left = { 676 | 'foo': 'bar', 677 | 'mismatch': False, 678 | 'sub': {'alpha': 'beta', 'a': 'b'}, 679 | } 680 | 681 | right = { 682 | 'lorem': 'ipsum', 683 | 'mismatch': True, 684 | 'sub': {'alpha': 'bravo', 'c': 'd'}, 685 | } 686 | 687 | merged = { 688 | 'foo': 'bar', 689 | 'lorem': 'ipsum', 690 | 'mismatch': True, 691 | 'sub': {'alpha': 'bravo', 'a': 'b', 'c': 'd'} 692 | } 693 | 694 | opposite = { 695 | 'foo': 'bar', 696 | 'lorem': 'ipsum', 697 | 'mismatch': False, 698 | 'sub': {'alpha': 'beta', 'a': 'b', 'c': 'd'} 699 | } 700 | 701 | constructor = options.constructor 702 | 703 | assert_raises(TypeError, lambda: constructor() + 1) 704 | assert_raises(TypeError, lambda: 1 + constructor()) 705 | 706 | assert_equals(constructor() + constructor(), {}) 707 | assert_equals(constructor() + {}, {}) 708 | assert_equals({} + constructor(), {}) 709 | 710 | assert_equals(constructor(left) + constructor(), left) 711 | assert_equals(constructor(left) + {}, left) 712 | assert_equals({} + constructor(left), left) 713 | 714 | assert_equals(constructor() + constructor(left), left) 715 | assert_equals(constructor() + left, left) 716 | assert_equals(left + constructor(), left) 717 | 718 | assert_equals(constructor(left) + constructor(right), merged) 719 | assert_equals(constructor(left) + right, merged) 720 | assert_equals(left + constructor(right), merged) 721 | 722 | assert_equals(constructor(right) + constructor(left), opposite) 723 | assert_equals(constructor(right) + left, opposite) 724 | assert_equals(right + constructor(left), opposite) 725 | 726 | # test sequence type changes 727 | data = {'sequence': [{'foo': 'bar'}]} 728 | 729 | assert_true(isinstance((constructor(data) + {}).sequence, tuple)) 730 | assert_true( 731 | isinstance((constructor(data) + constructor()).sequence, tuple) 732 | ) 733 | 734 | assert_true(isinstance((constructor(data, list) + {}).sequence, list)) 735 | # assert_true( 736 | # isinstance((constructor(data, list) + constructor()).sequence, tuple) 737 | # ) 738 | 739 | assert_true(isinstance((constructor(data, list) + {}).sequence, list)) 740 | assert_true( 741 | isinstance( 742 | (constructor(data, list) + constructor({}, list)).sequence, 743 | list 744 | ) 745 | ) 746 | 747 | 748 | def to_kwargs(options): 749 | "**{cls}" 750 | def return_results(**kwargs): 751 | "Return result passed into a function" 752 | return kwargs 753 | 754 | expected = {'foo': 1, 'bar': 2} 755 | 756 | assert_equals(return_results(**options.constructor()), {}) 757 | assert_equals(return_results(**options.constructor(expected)), expected) 758 | 759 | 760 | def check_pickle_roundtrip(source, options, **kwargs): 761 | """ 762 | serialize then deserialize a mapping, ensuring the result and initial 763 | objects are equivalent. 764 | """ 765 | source = options.constructor(source, **kwargs) 766 | data = pickle.dumps(source) 767 | loaded = pickle.loads(data) 768 | 769 | assert_true(isinstance(loaded, options.cls)) 770 | 771 | assert_equals(source, loaded) 772 | 773 | return loaded 774 | 775 | 776 | def pickling(options): 777 | "Pickle {cls}" 778 | 779 | empty = check_pickle_roundtrip(None, options) 780 | assert_equals(empty, {}) 781 | 782 | mapping = check_pickle_roundtrip({'foo': 'bar'}, options) 783 | assert_equals(mapping, {'foo': 'bar'}) 784 | 785 | # make sure sequence_type is preserved 786 | raw = {'list': [{'a': 'b'}], 'tuple': ({'a': 'b'},)} 787 | 788 | as_tuple = check_pickle_roundtrip(raw, options) 789 | assert_true(isinstance(as_tuple['list'], list)) 790 | assert_true(isinstance(as_tuple['tuple'], tuple)) 791 | assert_true(isinstance(as_tuple.list, tuple)) 792 | assert_true(isinstance(as_tuple.tuple, tuple)) 793 | 794 | as_list = check_pickle_roundtrip(raw, options, sequence_type=list) 795 | assert_true(isinstance(as_list['list'], list)) 796 | assert_true(isinstance(as_list['tuple'], tuple)) 797 | assert_true(isinstance(as_list.list, list)) 798 | assert_true(isinstance(as_list.tuple, list)) 799 | 800 | as_raw = check_pickle_roundtrip(raw, options, sequence_type=None) 801 | assert_true(isinstance(as_raw['list'], list)) 802 | assert_true(isinstance(as_raw['tuple'], tuple)) 803 | assert_true(isinstance(as_raw.list, list)) 804 | assert_true(isinstance(as_raw.tuple, tuple)) 805 | 806 | 807 | def pop(options): 808 | "Popping from {cls}" 809 | 810 | mapping = options.constructor({'foo': 'bar', 'baz': 'qux'}) 811 | 812 | assert_raises(KeyError, lambda: mapping.pop('lorem')) 813 | assert_equals(mapping.pop('lorem', 'ipsum'), 'ipsum') 814 | assert_equals(mapping, {'foo': 'bar', 'baz': 'qux'}) 815 | 816 | assert_equals(mapping.pop('baz'), 'qux') 817 | assert_false('baz' in mapping) 818 | assert_equals(mapping, {'foo': 'bar'}) 819 | 820 | assert_equals(mapping.pop('foo', 'qux'), 'bar') 821 | assert_false('foo' in mapping) 822 | assert_equals(mapping, {}) 823 | 824 | 825 | def popitem(options): 826 | "Popping items from {cls}" 827 | expected = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'beta'} 828 | actual = {} 829 | 830 | mapping = options.constructor(dict(expected)) 831 | 832 | for _ in range(3): 833 | key, value = mapping.popitem() 834 | 835 | assert_equals(expected[key], value) 836 | actual[key] = value 837 | 838 | assert_equals(expected, actual) 839 | 840 | assert_raises(AttributeError, lambda: mapping.foo) 841 | assert_raises(AttributeError, lambda: mapping.lorem) 842 | assert_raises(AttributeError, lambda: mapping.alpha) 843 | assert_raises(KeyError, mapping.popitem) 844 | 845 | 846 | def clear(options): 847 | "clear the {cls}" 848 | 849 | mapping = options.constructor( 850 | {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'beta'} 851 | ) 852 | 853 | mapping.clear() 854 | 855 | assert_equals(mapping, {}) 856 | 857 | assert_raises(AttributeError, lambda: mapping.foo) 858 | assert_raises(AttributeError, lambda: mapping.lorem) 859 | assert_raises(AttributeError, lambda: mapping.alpha) 860 | 861 | 862 | def update(options): 863 | "update a {cls}" 864 | 865 | mapping = options.constructor({'foo': 'bar', 'alpha': 'bravo'}) 866 | 867 | mapping.update(alpha='beta', lorem='ipsum') 868 | assert_equals(mapping, {'foo': 'bar', 'alpha': 'beta', 'lorem': 'ipsum'}) 869 | 870 | mapping.update({'foo': 'baz', 1: 'one'}) 871 | assert_equals( 872 | mapping, 873 | {'foo': 'baz', 'alpha': 'beta', 'lorem': 'ipsum', 1: 'one'} 874 | ) 875 | 876 | assert_equals(mapping.foo, 'baz') 877 | assert_equals(mapping.alpha, 'beta') 878 | assert_equals(mapping.lorem, 'ipsum') 879 | assert_equals(mapping(1), 'one') 880 | 881 | 882 | def setdefault(options): 883 | "{cls}.setdefault" 884 | 885 | mapping = options.constructor({'foo': 'bar'}) 886 | 887 | assert_equals(mapping.setdefault('foo', 'baz'), 'bar') 888 | assert_equals(mapping.foo, 'bar') 889 | 890 | assert_equals(mapping.setdefault('lorem', 'ipsum'), 'ipsum') 891 | assert_equals(mapping.lorem, 'ipsum') 892 | 893 | assert_true(mapping.setdefault('return_none') is None) 894 | assert_true(mapping.return_none is None) 895 | 896 | assert_equals(mapping.setdefault(1, 'one'), 'one') 897 | assert_equals(mapping[1], 'one') 898 | 899 | assert_equals(mapping.setdefault('_hidden', 'yes'), 'yes') 900 | assert_raises(AttributeError, lambda: mapping._hidden) 901 | assert_equals(mapping['_hidden'], 'yes') 902 | 903 | assert_equals(mapping.setdefault('get', 'value'), 'value') 904 | assert_not_equals(mapping.get, 'value') 905 | assert_equals(mapping['get'], 'value') 906 | 907 | 908 | def copying(options): 909 | "copying a {cls}" 910 | mapping_a = options.constructor({'foo': {'bar': 'baz'}}) 911 | mapping_b = copy.copy(mapping_a) 912 | mapping_c = mapping_b 913 | 914 | mapping_b.foo.lorem = 'ipsum' 915 | 916 | assert_equals(mapping_a, mapping_b) 917 | assert_equals(mapping_b, mapping_c) 918 | 919 | mapping_c.alpha = 'bravo' 920 | 921 | 922 | def deepcopying(options): 923 | "deepcopying a {cls}" 924 | mapping_a = options.constructor({'foo': {'bar': 'baz'}}) 925 | mapping_b = copy.deepcopy(mapping_a) 926 | mapping_c = mapping_b 927 | 928 | mapping_b['foo']['lorem'] = 'ipsum' 929 | 930 | assert_not_equals(mapping_a, mapping_b) 931 | assert_equals(mapping_b, mapping_c) 932 | 933 | mapping_c.alpha = 'bravo' 934 | 935 | assert_not_equals(mapping_a, mapping_b) 936 | assert_equals(mapping_b, mapping_c) 937 | 938 | assert_false('lorem' in mapping_a.foo) 939 | assert_equals(mapping_a.setdefault('alpha', 'beta'), 'beta') 940 | assert_equals(mapping_c.alpha, 'bravo') 941 | -------------------------------------------------------------------------------- /tests/test_depricated.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for depricated methods. 3 | """ 4 | from nose.tools import assert_true, assert_false 5 | from six import PY2 6 | 7 | 8 | if PY2: 9 | def test_has_key(): 10 | """ 11 | The now-depricated has_keys method 12 | """ 13 | from attrdict.dictionary import AttrDict 14 | 15 | mapping = AttrDict( 16 | {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} 17 | ) 18 | empty = AttrDict() 19 | 20 | assert_true(mapping.has_key('foo')) 21 | assert_false(empty.has_key('foo')) 22 | 23 | assert_true(mapping.has_key(frozenset((1, 2, 3)))) 24 | assert_false(empty.has_key(frozenset((1, 2, 3)))) 25 | 26 | assert_true(mapping.has_key(1)) 27 | assert_false(empty.has_key(1)) 28 | 29 | assert_false(mapping.has_key('banana')) 30 | assert_false(empty.has_key('banana')) 31 | -------------------------------------------------------------------------------- /tests/test_merge.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the merge function 3 | """ 4 | from nose.tools import assert_equals 5 | 6 | 7 | def test_merge(): 8 | """ 9 | merge function. 10 | """ 11 | from attrdict.merge import merge 12 | 13 | left = { 14 | 'baz': 'qux', 15 | 'mismatch': False, 16 | 'sub': {'alpha': 'beta', 1: 2}, 17 | } 18 | right = { 19 | 'lorem': 'ipsum', 20 | 'mismatch': True, 21 | 'sub': {'alpha': 'bravo', 3: 4}, 22 | } 23 | 24 | assert_equals(merge({}, {}), {}) 25 | assert_equals(merge(left, {}), left) 26 | assert_equals(merge({}, right), right) 27 | assert_equals( 28 | merge(left, right), 29 | { 30 | 'baz': 'qux', 31 | 'lorem': 'ipsum', 32 | 'mismatch': True, 33 | 'sub': {'alpha': 'bravo', 1: 2, 3: 4} 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /tests/test_mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the AttrDefault class. 3 | """ 4 | from nose.tools import assert_equals, assert_raises 5 | 6 | 7 | def test_invalid_attributes(): 8 | """ 9 | Tests how set/delattr handle invalid attributes. 10 | """ 11 | from attrdict.mapping import AttrMap 12 | 13 | mapping = AttrMap() 14 | 15 | # mapping currently has allow_invalid_attributes set to False 16 | def assign(): 17 | """ 18 | Assign to an invalid attribute. 19 | """ 20 | mapping._key = 'value' 21 | 22 | assert_raises(TypeError, assign) 23 | assert_raises(AttributeError, lambda: mapping._key) 24 | assert_equals(mapping, {}) 25 | 26 | mapping._setattr('_allow_invalid_attributes', True) 27 | 28 | assign() 29 | assert_equals(mapping._key, 'value') 30 | assert_equals(mapping, {}) 31 | 32 | # delete the attribute 33 | def delete(): 34 | """ 35 | Delete an invalid attribute. 36 | """ 37 | del mapping._key 38 | 39 | delete() 40 | assert_raises(AttributeError, lambda: mapping._key) 41 | assert_equals(mapping, {}) 42 | 43 | # now with disallowing invalid 44 | assign() 45 | mapping._setattr('_allow_invalid_attributes', False) 46 | 47 | assert_raises(TypeError, delete) 48 | assert_equals(mapping._key, 'value') 49 | assert_equals(mapping, {}) 50 | 51 | # force delete 52 | mapping._delattr('_key') 53 | assert_raises(AttributeError, lambda: mapping._key) 54 | assert_equals(mapping, {}) 55 | 56 | 57 | def test_constructor(): 58 | """ 59 | _constructor MUST be implemented. 60 | """ 61 | from attrdict.mixins import Attr 62 | 63 | class AttrImpl(Attr): 64 | """ 65 | An implementation of attr that doesn't implement _constructor. 66 | """ 67 | pass 68 | 69 | assert_raises(NotImplementedError, lambda: AttrImpl._constructor({}, ())) 70 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py37, pypy, pypy3, flake8 3 | 4 | [testenv] 5 | commands = python setup.py nosetests 6 | deps = -rrequirements-tests.txt 7 | 8 | [testenv:flake8] 9 | deps = flake8 10 | commands = flake8 attrdict tests 11 | 12 | [flake8] 13 | exclude = tests/test_depricated.py 14 | --------------------------------------------------------------------------------