diff --git a/.gitignore b/.gitignore index 1eb17a8..646cd4d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ MANIFEST build/* dist/* .tox +attrdict.egg-info/* diff --git a/.travis.yml b/.travis.yml index 0d93c90..92845bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,14 @@ language: python python: - "3.4" - "3.3" + - "3.2" - "2.7" - "2.6" - "pypy" + - "pypy3" install: - "pip install -r requirements-tests.txt" - "python setup.py install" -script: nosetests --with-coverage --cover-package attrdict -v +script: "python setup.py nosetests && flake8 attrdict tests" after_success: - coveralls diff --git a/CHANGES.txt b/CHANGES.txt index c0ea8b7..291406d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,3 +10,4 @@ v0.5.1, 2014/07/14 -- tox for local testing, README fix, 0.5.0 no longer from th v1.0.0, 2014/08/18 -- Development Status :: 5 - Production/Stable v1.1.0, 2014/10/29 -- has_key support to match python2 dicts (by Nikolaos-Digenis Karagiannis @Digenis) 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. +v2.0, 2015/04/09 -- Happy PyCon. An almost-complete rewrite. Hopefully in a good way diff --git a/README.rst b/README.rst index 803fd89..dc76c4c 100644 --- a/README.rst +++ b/README.rst @@ -6,9 +6,8 @@ AttrDict .. image:: https://coveralls.io/repos/bcj/AttrDict/badge.png?branch=master :target: https://coveralls.io/r/bcj/AttrDict?branch=master - -AttrDict is a 2.6, 2.7, 3-compatible dictionary that allows its elements -to be accessed both as keys and as attributes:: +AttrDict is an MIT-licensed library that provides mapping objects that allow +their elements to be accessed both as keys and as attributes:: > from attrdict import AttrDict > a = AttrDict({'foo': 'bar'}) @@ -17,18 +16,15 @@ to be accessed both as keys and as attributes:: > a['foo'] 'bar' -With this, you can easily create convenient, hierarchical settings -objects. - -:: +Attribute access makes it easy to create convenient, hierarchical settings +objects:: - with open('settings.yaml', 'r') as fileobj: + with open('settings.yaml') as fileobj: settings = AttrDict(yaml.safe_load(fileobj)) cursor = connect(**settings.db.credentials).cursor() - cursor.execute("SELECT column FROM table"); - + cursor.execute("SELECT column FROM table;") Installation ============ @@ -42,80 +38,110 @@ Or from Github:: $ cd AttrDict $ python setup.py install -Documentation -============= - -Documentation is available at https://github.com/bcj/AttrDict - -Usage -===== -Creation --------- -An empty AttrDict can be created with:: +Basic Usage +=========== +AttrDict comes with three different classes, `AttrMap`, `AttrDict`, and +`AttrDefault`. They are all fairly similar, as they all are MutableMappings ( +read: dictionaries) that allow creating, accessing, and deleting key-value +pairs as attributes. + +Valid Names +----------- +Any key can be used as an attribute as long as: + +#. The key represents a valid attribute (i.e., it is a string comprised only of + alphanumeric characters and underscores that doesn't start with a number) +#. The key represents a public attribute (i.e., it doesn't start with an + underscore). This is done (in part) so that implementation changes between + minor and micro versions don't force major version changes. +#. The key does not shadow a class attribute (e.g., get). + +Attributes vs. Keys +------------------- +There is a minor difference between accessing a value as an attribute vs. +accessing it as a key, is that when a dict is accessed as an attribute, it will +automatically be converted to an Attr object. This allows you to recursively +access keys:: + + > attr = AttrDict({'foo': {'bar': 'baz'}}) + > attr.foo.bar + 'baz' - a = AttrDict() +Relatedly, by default, sequence types that aren't `bytes`, `str`, or `unicode` +(e.g., lists, tuples) will automatically be converted to tuples, with any +mappings converted to Attrs:: -Or, you can pass an existing ``dict`` (or other type of ``Mapping`` object):: + > attr = AttrDict({'foo': [{'bar': 'baz'}, {'bar': 'qux'}]}) + > for sub_attr in attr.foo: + > print(subattr.foo) + 'baz' + 'qux' - a = AttrDict({'foo': 'bar'}) +To get this recursive functionality for keys that cannot be used as attributes, +you can replicate the behavior by calling the Attr object:: -NOTE: Unlike ``dict``, AttrDict will not clone on creation. AttrDict's -internal dictionary will be the same instance as the dict passed in. + > attr = AttrDict({1: {'two': 3}}) + > attr(1).two + 3 -Access ------- -AttrDict can be used *exactly* like a normal dict:: +Classes +------- +AttrDict comes with three different objects, `AttrMap`, `AttrDict`, and +`AttrDefault`. - > a = AttrDict() - > a['foo'] = 'bar' - > a['foo'] - 'bar' - > '{foo}'.format(**a) - 'bar' - > del a['foo'] - > a.get('foo', 'default') - 'default' +AttrMap +^^^^^^^ +The most basic implementation. Use this if you want to limit the number of +invalid keys, or otherwise cannot use `AttrDict` -AttrDict can also have it's keys manipulated as attributes to the object:: +AttrDict +^^^^^^^^ +An Attr object that subclasses `dict`. You should be able to use this +absolutely anywhere you can use a `dict`. While this is probably the class you +want to use, there are a few caveats that follow from this being a `dict` under +the hood. - > a = AttrDict() - > a.foo = 'bar' - > a.foo - 'bar' - > del a.foo +The `copy` method (which returns a shallow copy of the mapping) returns a +`dict` instead of an `AttrDict`. -Both methods operate on the same underlying object, so operations are -interchangeable. The only difference between the two methods is that -where dict-style access would return a dict, attribute-style access will -return an AttrDict. This allows recursive attribute-style access:: +Recursive attribute access results in a shallow copy, so recursive assignment +will fail (as you will be writing to a copy of that dictionary):: - > a = AttrDict({'foo': {'bar': 'baz'}}) - > a.foo.bar - 'baz' - > a['foo'].bar - AttributeError: 'dict' object has no attribute 'bar' + > attr = AttrDict('foo': {}) + > attr.foo.bar = 'baz' + > attr.foo + {} -There are some valid keys that cannot be accessed as attributes. To be -accessed as an attribute, a key must: +Assignment as keys will still work:: - * be a string + > attr = AttrDict('foo': {}) + > attr['foo']['bar'] = 'baz' + > attr.foo + {'bar': 'baz'} - * start with an alphabetic character +If either of these caveats are deal-breakers, or you don't need your object to +be a `dict`, consider using `AttrMap` instead. - * be comprised solely of alphanumeric characters and underscores +AttrDefault +^^^^^^^^^^^ +At Attr object that behaves like a `defaultdict`. This allows on-the-fly, +automatic key creation:: - * not map to an existing attribute name (e.g., get, items) + > attr = AttrDefault(int, {}) + > attr.foo += 1 + > attr.foo + 1 -To access these attributes while retaining an AttrDict wrapper (or to -dynamically access any key as an attribute):: +AttrDefault also has a `pass_key` option that passes the supplied key to the +`default_factory`:: - > a = AttrDict({'_foo': {'bar': 'baz'}}) - > a('_foo').bar - 'baz' + > attr = AttrDefault(sorted, {}, pass_key=True) + > attr.banana + ['a', 'a', 'a', 'b', 'n', 'n'] Merging ------- -AttrDicts can be merged with each other or other dict objects using the +All three Attr classes can be merged with eachother or other Mappings using the ``+`` operator. For conflicting keys, the right dict's value will be preferred, but in the case of two dictionary values, they will be recursively merged:: @@ -164,54 +190,6 @@ When merging an AttrDict with another mapping, this behavior will be disabled if at least one of the merged items is an AttrDict that has set ``recursive`` to ``False``. -DefaultDict -=========== - -AttrDict supports defaultdict-style automatic creation of attributes:: - - > adict = AttrDict(default_factory=list) - > adict.foo - [] - -Furthermore, if ``pass_key=True``, then the key will be passed to the function -used when creating the value:: - - > adict = AttrDict(default_factory=lambda value: value.upper(), pass_key=True) - > adict.foo - 'FOO' - -load -==== -A common usage for AttrDict is to use it in combination with settings files to -create hierarchical settings. attrdict comes with a load function to make this -easier:: - - from attrdict import load - - settings = load('settings.json') - -By default, ``load`` uses ``json.load`` to load the settings file, but this can -be overridden by passing ``load_function=YOUR_LOAD_FUNCTION``. - -``load`` supports loading from multiple files at once. This allows for -overriding of default settings, e.g.:: - - from attrdict import load - from yaml import safe_load - - # config.yaml = - # emergency: - # email: everyone@example.com - # message: Something went wrong - # - # user.yaml = - # emergency: - # email: user@example.com - settings = load('config.yaml', 'user.yaml', load_function=safe_load) - - assert settings.email == 'user@example.com' - assert settings.message == 'Something went wrong' - License ======= AttrDict is released under a MIT license. diff --git a/attrdict/__init__.py b/attrdict/__init__.py index 73ab6af..3a71a53 100644 --- a/attrdict/__init__.py +++ b/attrdict/__init__.py @@ -1,395 +1,10 @@ """ -AttrDict is a mapping object that allows access to its values both as -keys, and as attributes (whenever the key can be used as an attribute -name). +attrdict contains several mapping objects that allow access to their +keys as attributes. """ -from collections import Mapping, MutableMapping, Sequence -import json -import re -from sys import version_info +from attrdict.mapping import AttrMap +from attrdict.dictionary import AttrDict +from attrdict.default import AttrDefault -__all__ = ['AttrDict', 'merge'] - - -# Python 2 -PY2, STRING = (True, basestring) if version_info < (3,) else (False, str) - - -class AttrDict(MutableMapping): - """ - A mapping object that allows access to its values both as keys, and - as attributes (as long as the attribute name is valid). - """ - def __init__(self, mapping=None, recursive=True, - default_factory=None, pass_key=False): - """ - mapping: (optional, None) The mapping object to use for the - instance. Note that the mapping object itself is used, not a - copy. This means that you cannot clone an AttrDict using: - adict = AttrDict(adict) - recursive: (optional, True) Recursively convert mappings into - AttrDicts. - default_factory: (optional, Not Passed) If passed make AttrDict - behave like a default dict. - pass_key: (optional, False) If True, and default_factory is - given, then default_factory will be passed the key. - """ - if mapping is None: - mapping = {} - - if default_factory is None: - self.__setattr__('_default_factory', None, force=True) - self.__setattr__('_pass_key', False, force=True) - else: - self.__setattr__('_default_factory', default_factory, force=True) - self.__setattr__('_pass_key', pass_key, force=True) - - self.__setattr__('_recursive', recursive, force=True) - - self.__setattr__('_mapping', mapping, force=True) - - for key, value in mapping.iteritems() if PY2 else mapping.items(): - if self._valid_name(key): - setattr(self, key, value) - - def get(self, key, default=None): - """ - Get a value associated with a key. - - key: The key associated with the desired value. - default: (optional, None) The value to return if the key is not - found. - """ - return self._mapping.get(key, default) - - def items(self): - """ - In python 2.X returns a list of (key, value) pairs as 2-tuples. - In python 3.X returns an iterator over the (key, value) pairs. - """ - return self._mapping.items() - - def keys(self): - """ - In python 2.X returns a list of keys in the mapping. - In python 3.X returns an iterator over the mapping's keys. - """ - return self._mapping.keys() - - def values(self): - """ - In python 2.X returns a list of values in the mapping. - In python 3.X returns an iterator over the mapping's values. - """ - return self._mapping.values() - - def _set(self, key, value): - """ - Responsible for actually adding/changing a key-value pair. This - needs to be separated out so that setattr and setitem don't - clash. - """ - self._mapping[key] = value - - if self._valid_name(key): - super(AttrDict, self).__setattr__( - key, self._build(value, recursive=self._recursive)) - - def _delete(self, key): - """ - Responsible for actually deleting a key-value pair. This needs - to be separated out so that delattr and delitem don't clash. - """ - del self._mapping[key] - - if self._valid_name(key): - super(AttrDict, self).__delattr__(key) - - def __getattr__(self, key): - """ - value = adict.key - - Access a value associated with a key in the instance. - """ - if self._default_factory is None or key.startswith('_'): - raise AttributeError(key) - - return self.__missing__(key) - - def __call__(self, key): - """ - Access a value in the mapping as an attribute. This differs from - dict-style key access because it returns a new instance of an - AttrDict (if the value is a mapping object), not the underlying - type. This allows for dynamic attribute-style access. - - key: The key associated with the value being accessed. - """ - if key not in self._mapping: - if self._default_factory is not None: - self.__missing__(key) - else: - raise AttributeError( - "'{0}' instance has no attribute '{1}'".format( - self.__class__.__name__, key)) - - return self._build(self._mapping[key], recursive=self._recursive) - - def __setattr__(self, key, value, force=False): - """ - adict.key = value - - Add a key-value pair as an attribute - """ - if force: - super(AttrDict, self).__setattr__(key, value) - else: - if not self._valid_name(key): - raise TypeError("Invalid key: {0}".format(repr(key))) - - self._set(key, value) - - def __delattr__(self, key): - """ - del adict.key - - Remove a key-value pair as an attribute. - """ - if not self._valid_name(key) or key not in self._mapping: - raise TypeError("Invalid key: {0}".format(repr(key))) - - self._delete(key) - - def __setitem__(self, key, value): - """ - adict[key] = value - - Add a key-value pair to the instance. - """ - self._set(key, value) - - def __getitem__(self, key): - """ - value = adict[key] - - Access a value associated with a key in the instance. - """ - if key not in self._mapping and self._default_factory is not None: - self[key] = self.__missing__(key) - - return self._mapping[key] - - def __delitem__(self, key): - """ - del adict[key] - - Delete a key-value pair in the instance. - """ - self._delete(key) - - def __contains__(self, key): - """ - key in adict - - Check if a key is in the instance. - """ - return key in self._mapping - - def __len__(self): - """ - len(adict) - - Check the length of the instance. - """ - return len(self._mapping) - - def __iter__(self): - """ - (key for key in adict) - - iterate through all the keys. - """ - return self._mapping.__iter__() - - def __add__(self, other): - """ - adict + other - - Add a mapping to this AttrDict object. - - NOTE: AttrDict is not commutative. a + b != b + a. - """ - if not isinstance(other, Mapping): - return NotImplemented - - recursive = not hasattr(other, '_recursive') or other._recursive - - return merge(self, other, recursive=self._recursive and recursive) - - def __radd__(self, other): - """ - other + adict - - Add this AttrDict to a mapping object. - - NOTE: AttrDict is not commutative. a + b != b + a. - """ - if not isinstance(other, Mapping): - return NotImplemented - - return merge(other, self, recursive=self._recursive) - - def __repr__(self): - """ - Create a string representation of the AttrDict. - """ - return u"a{0}".format(repr(self._mapping)) - - def __missing__(self, key): - """ - Add a missing element. - """ - if self._pass_key: - self._mapping[key] = value = self._default_factory(key) - else: - self._mapping[key] = value = self._default_factory() - - return value - - def __getstate__(self): - """ - Handle proper serialization of the object. (used by pickle). - """ - return (self._mapping, self._recursive, self._default_factory, - self._pass_key) - - def __setstate__(self, state): - """ - Handle proper deserialization of the object. (used by pickle). - """ - mapping, recursive, default_factory, pass_key = state - self.__init__(mapping, recursive, default_factory, pass_key) - - @classmethod - def _build(cls, obj, recursive=True): - """ - Wrap an object in an AttrDict as necessary. Mappings are - wrapped, but all other objects are returned as is. - - obj: The object to (possibly) wrap. - recursive: (optional, True) Whether Sequences should have their - elements turned into attrdicts. - """ - if isinstance(obj, Mapping): - obj = cls(obj, recursive=recursive) - elif recursive: - if isinstance(obj, Sequence) and not isinstance(obj, STRING): - new = [cls._build(element, recursive=True) for element in obj] - - # This has to have a __class__. Only old-style classes - # don't, and none of them would subclass Sequence. - obj = obj.__class__(new) - - return obj - - @classmethod - def _valid_name(cls, name): - """ - Check whether a key name is a valid attribute name. A valid - name must start with an alphabetic character, and must only - contain alphanumeric characters and underscores. The name also - must not be an attribute of this class. - - NOTE: Names with leading underscores are considered invalid for - stylistic reasons. While this package is fairly un-Pythonic, I'm - going to stand strong on the fact that leading underscores - represent private attributes. Further, magic methods absolutely - need to be prevented so that crazy things don't happen. - """ - return (isinstance(name, STRING) and - re.match('^[A-Za-z][A-Za-z0-9_]*$', name) and - not hasattr(cls, name)) - - # Add missing iter methods in 2.X - if PY2: - def iteritems(self): - """ - Iterate over (key, value) 2-tuples in the mapping - """ - return self._mapping.iteritems() - - def iterkeys(self): - """ - Iterate over keys in the mapping - """ - return self._mapping.iterkeys() - - def itervalues(self): - """ - Iterate over values in the mapping - """ - return self._mapping.itervalues() - - def has_key(self, key): - """ - True if the mapping has key, False otherwise - """ - return key in self - - -def merge(left, right, recursive=True): - """ - merge to mappings objects into a new AttrDict. - - left: The left mapping object. - right: The right mapping object. - recursive: (optional, True) Whether Sequences should have their - elements turned into attrdicts. - - NOTE: This is not commutative. merge(a, b) != merge(b, a). - """ - merged = AttrDict(recursive=recursive) - - left_keys = set(left) - right_keys = set(right) - - # Items only in the left object - for key in (left_keys - right_keys): - merged[key] = left[key] - - # Items only in the right object - for key in (right_keys - left_keys): - merged[key] = right[key] - - # In both - for key in left_keys.intersection(right_keys): - if isinstance(left[key], Mapping) and isinstance(right[key], Mapping): - merged[key] = merge(left[key], right[key], recursive=recursive) - else: # different types, overwrite with the right value - merged[key] = right[key] - - return merged - - -# def load(*filenames, load_function=json.load) # once Python3-only -def load(*filenames, **kwargs): - """ - Returns a settings dict built from a list of settings files. - - filenames: The names of any number of settings files. - load_function: (optional, json.load) The function used to load the - settings into a Mapping object. - """ - load_function = kwargs.pop('load_function', json.load) - - if kwargs: - raise TypeError("unknown options: {0}".format(kwargs.keys())) - - settings = AttrDict() - - for filename in filenames: - with open(filename, 'r') as fileobj: - settings += load_function(fileobj) - - return settings +__all__ = ['AttrMap', 'AttrDict', 'AttrDefault'] diff --git a/attrdict/default.py b/attrdict/default.py new file mode 100644 index 0000000..a5e5bcb --- /dev/null +++ b/attrdict/default.py @@ -0,0 +1,130 @@ +""" +A subclass of MutableAttr that has defaultdict support. +""" +from collections import Mapping + +import six + +from attrdict.mixins import MutableAttr + + +__all__ = ['AttrDefault'] + + +class AttrDefault(MutableAttr): + """ + An implementation of MutableAttr with defaultdict support + """ + def __init__(self, default_factory=None, items=None, sequence_type=tuple, + pass_key=False): + if items is None: + items = {} + elif not isinstance(items, Mapping): + items = dict(items) + + self._setattr('_default_factory', default_factory) + self._setattr('_mapping', items) + self._setattr('_sequence_type', sequence_type) + self._setattr('_pass_key', pass_key) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for a AttrDefault instance + """ + return self._sequence_type, self._default_factory, self._pass_key + + def __getitem__(self, key): + """ + Access a value associated with a key. + + Note: values returned will not be wrapped, even if recursive + is True. + """ + if key in self._mapping: + return self._mapping[key] + elif self._default_factory is not None: + return self.__missing__(key) + + raise KeyError(key) + + def __setitem__(self, key, value): + """ + Add a key-value pair to the instance. + """ + self._mapping[key] = value + + def __delitem__(self, key): + """ + Delete a key-value pair + """ + del self._mapping[key] + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + Iterated through the keys. + """ + return iter(self._mapping) + + def __missing__(self, key): + """ + Add a missing element. + """ + if self._pass_key: + self[key] = value = self._default_factory(key) + else: + self[key] = value = self._default_factory() + + return value + + def __repr__(self): + """ + Return a string representation of the object. + """ + return six.u( + "AttrDefault({default_factory}, {pass_key}, {mapping})" + ).format( + default_factory=repr(self._default_factory), + pass_key=repr(self._pass_key), + mapping=repr(self._mapping), + ) + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self._default_factory, + self._mapping, + self._sequence_type, + self._pass_key, + self._allow_invalid_attributes, + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + (default_factory, mapping, sequence_type, pass_key, + allow_invalid_attributes) = state + + self._setattr('_default_factory', default_factory) + self._setattr('_mapping', mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_pass_key', pass_key) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + sequence_type, default_factory, pass_key = configuration + return cls(default_factory, mapping, sequence_type=sequence_type, + pass_key=pass_key) diff --git a/attrdict/dictionary.py b/attrdict/dictionary.py new file mode 100644 index 0000000..874e4a4 --- /dev/null +++ b/attrdict/dictionary.py @@ -0,0 +1,60 @@ +""" +A dict that implements MutableAttr. +""" +from attrdict.mixins import MutableAttr + +import six + + +__all__ = ['AttrDict'] + + +class AttrDict(dict, MutableAttr): + """ + A dict that implements MutableAttr. + """ + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + + self._setattr('_sequence_type', tuple) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for an attrmap instance. + """ + return self._sequence_type + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self.copy(), + self._sequence_type, + self._allow_invalid_attributes + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + mapping, sequence_type, allow_invalid_attributes = state + self.update(mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + def __repr__(self): + return six.u('AttrDict({contents})').format( + contents=super(AttrDict, self).__repr__() + ) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + attr = cls(mapping) + attr._setattr('_sequence_type', configuration) + + return attr diff --git a/attrdict/mapping.py b/attrdict/mapping.py new file mode 100644 index 0000000..02ebac8 --- /dev/null +++ b/attrdict/mapping.py @@ -0,0 +1,97 @@ +""" +An implementation of MutableAttr. +""" +from collections import Mapping + +import six + +from attrdict.mixins import MutableAttr + + +__all__ = ['AttrMap'] + + +class AttrMap(MutableAttr): + """ + An implementation of MutableAttr. + """ + def __init__(self, items=None, sequence_type=tuple): + if items is None: + items = {} + elif not isinstance(items, Mapping): + items = dict(items) + + self._setattr('_sequence_type', sequence_type) + self._setattr('_mapping', items) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for an attrmap instance. + """ + return self._sequence_type + + def __getitem__(self, key): + """ + Access a value associated with a key. + """ + return self._mapping[key] + + def __setitem__(self, key, value): + """ + Add a key-value pair to the instance. + """ + self._mapping[key] = value + + def __delitem__(self, key): + """ + Delete a key-value pair + """ + del self._mapping[key] + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + Iterated through the keys. + """ + return iter(self._mapping) + + def __repr__(self): + """ + Return a string representation of the object. + """ + # sequence type seems like more trouble than it is worth. + # If people want full serialization, they can pickle, and in + # 99% of cases, sequence_type won't change anyway + return six.u("AttrMap({mapping})").format(mapping=repr(self._mapping)) + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self._mapping, + self._sequence_type, + self._allow_invalid_attributes + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + mapping, sequence_type, allow_invalid_attributes = state + self._setattr('_mapping', mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + return cls(mapping, sequence_type=configuration) diff --git a/attrdict/merge.py b/attrdict/merge.py new file mode 100644 index 0000000..229596c --- /dev/null +++ b/attrdict/merge.py @@ -0,0 +1,44 @@ +""" +A right-favoring Mapping merge. +""" +from collections import Mapping + + +__all__ = ['merge'] + + +def merge(left, right): + """ + Merge two mappings objects together, combining overlapping Mappings, + and favoring right-values + + left: The left Mapping object. + right: The right (favored) Mapping object. + + NOTE: This is not commutative (merge(a,b) != merge(b,a)). + """ + merged = {} + + left_keys = frozenset(left) + right_keys = frozenset(right) + + # Items only in the left Mapping + for key in left_keys - right_keys: + merged[key] = left[key] + + # Items only in the right Mapping + for key in right_keys - left_keys: + merged[key] = right[key] + + # in both + for key in left_keys & right_keys: + left_value = left[key] + right_value = right[key] + + if (isinstance(left_value, Mapping) and + isinstance(right_value, Mapping)): # recursive merge + merged[key] = merge(left_value, right_value) + else: # overwrite with right value + merged[key] = right_value + + return merged diff --git a/attrdict/mixins.py b/attrdict/mixins.py new file mode 100644 index 0000000..cbe869f --- /dev/null +++ b/attrdict/mixins.py @@ -0,0 +1,209 @@ +""" +Mixin Classes for Attr-support. +""" +from abc import ABCMeta, abstractmethod +from collections import Mapping, MutableMapping, Sequence +import re + +import six + +from attrdict.merge import merge + + +__all__ = ['Attr', 'MutableAttr'] + + +@six.add_metaclass(ABCMeta) +class Attr(Mapping): + """ + A mixin class for a mapping that allows for attribute-style access + of values. + + A key may be used as an attribute if: + * It is a string + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) + * The key doesn't overlap with any class attributes (for Attr, + those would be 'get', 'items', 'keys', 'values', 'mro', and + 'register'). + + If a values which is accessed as an attribute is a Sequence-type + (and is not a string/bytes), it will be converted to a + _sequence_type with any mappings within it converted to Attrs. + + NOTE: This means that if _sequence_type is not None, then a + sequence accessed as an attribute will be a different object + than if accessed as an attribute than if it is accessed as an + item. + """ + @abstractmethod + def _configuration(self): + """ + All required state for building a new instance with the same + settings as the current object. + """ + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor used internally by Attr. + + mapping: A mapping of key-value pairs. It is HIGHLY recommended + that you use this as the internal key-value pair mapping, as + that will allow nested assignment (e.g., attr.foo.bar = baz) + configuration: The return value of Attr._configuration + """ + raise NotImplementedError("You need to implement this") + + def __call__(self, key): + """ + Dynamically access a key-value pair. + + key: A key associated with a value in the mapping. + + This differs from __getitem__, because it returns a new instance + of an Attr (if the value is a Mapping object). + """ + if key not in self: + raise AttributeError( + "'{cls} instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + return self._build(self[key]) + + def __getattr__(self, key): + """ + Access an item as an attribute. + """ + if key not in self or not self._valid_name(key): + raise AttributeError( + "'{cls}' instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + return self._build(self[key]) + + def __add__(self, other): + """ + Add a mapping to this Attr, creating a new, merged Attr. + + other: A mapping. + + NOTE: Addition is not commutative. a + b != b + a. + """ + if not isinstance(other, Mapping): + return NotImplemented + + return self._constructor(merge(self, other), self._configuration()) + + def __radd__(self, other): + """ + Add this Attr to a mapping, creating a new, merged Attr. + + other: A mapping. + + NOTE: Addition is not commutative. a + b != b + a. + """ + if not isinstance(other, Mapping): + return NotImplemented + + return self._constructor(merge(other, self), self._configuration()) + + def _build(self, obj): + """ + Conditionally convert an object to allow for recursive mapping + access. + + obj: An object that was a key-value pair in the mapping. If obj + is a mapping, self._constructor(obj, self._configuration()) + will be called. If obj is a non-string/bytes sequence, and + self._sequence_type is not None, the obj will be converted + to type _sequence_type and build will be called on its + elements. + """ + if isinstance(obj, Mapping): + obj = self._constructor(obj, self._configuration()) + elif (isinstance(obj, Sequence) and + not isinstance(obj, (six.string_types, six.binary_type))): + sequence_type = getattr(self, '_sequence_type', None) + + if sequence_type: + obj = sequence_type(self._build(element) for element in obj) + + return obj + + @classmethod + def _valid_name(cls, key): + """ + Check whether a key is a valid attribute name. + + A key may be used as an attribute if: + * It is a string + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) + * The key doesn't overlap with any class attributes (for Attr, + those would be 'get', 'items', 'keys', 'values', 'mro', and + 'register'). + """ + return ( + isinstance(key, six.string_types) and + re.match('^[A-Za-z][A-Za-z0-9_]*$', key) and + not hasattr(cls, key) + ) + + +@six.add_metaclass(ABCMeta) +class MutableAttr(Attr, MutableMapping): + """ + A mixin class for a mapping that allows for attribute-style access + of values. + """ + def _setattr(self, key, value): + """ + Add an attribute to the object, without attempting to add it as + a key to the mapping. + """ + super(MutableAttr, self).__setattr__(key, value) + + def __setattr__(self, key, value): + """ + Add an attribute. + + key: The name of the attribute + value: The attributes contents + """ + if self._valid_name(key): + self[key] = value + elif getattr(self, '_allow_invalid_attributes', True): + super(MutableAttr, self).__setattr__(key, value) + else: + raise TypeError( + "'{cls}' does not allow attribute creation.".format( + cls=self.__class__.__name__ + ) + ) + + def _delattr(self, key): + """ + Delete an attribute from the object, without attempting to + remove it from the mapping. + """ + super(MutableAttr, self).__delattr__(key) + + def __delattr__(self, key, force=False): + """ + Delete an attribute. + + key: The name of the attribute + """ + if self._valid_name(key): + del self[key] + elif getattr(self, '_allow_invalid_attributes', True): + super(MutableAttr, self).__delattr__(key) + else: + raise TypeError( + "'{cls}' does not allow attribute deletion.".format( + cls=self.__class__.__name__ + ) + ) diff --git a/requirements-tests.txt b/requirements-tests.txt index 6af23e1..dcdf54f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,3 +1,4 @@ coverage +flake8 nose python-coveralls diff --git a/setup.cfg b/setup.cfg index 2a9acf1..c3f5071 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,11 @@ -[bdist_wheel] +[nosetests] +verbosity=2 +detailed-errors=1 +with-coverage=1 +cover-package=attrdict + +[flake8] +exclude = tests/test_depricated.py + +[wheel] universal = 1 diff --git a/setup.py b/setup.py index a6aa1ac..b36051e 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,30 @@ +""" +To install AttrDict: + + python setup.py install +""" +from setuptools import setup + + +DESCRIPTION = "A dict with attribute-style access" + try: - from setuptools import setup -except ImportError: - from distutils.core import setup + LONG_DESCRIPTION = open('README.rst').read() +except: + LONG_DESCRIPTION = DESCRIPTION + setup( name="attrdict", - version="1.2.0", + version="2.0.0", author="Brendan Curran-Johnson", author_email="brendan@bcjbcj.ca", - packages=["attrdict"], + packages=("attrdict",), url="https://github.com/bcj/AttrDict", license="MIT License", - description="A dict with attribute-style access", - long_description=open('README.rst').read(), - classifiers=[ + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + classifiers=( "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", @@ -21,9 +32,18 @@ "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - ], + ), + install_requires=( + 'six', + ), + tests_require=( + 'nose>=1.0', + 'coverage', + ), + zip_safe=True, ) diff --git a/tests.py b/tests.py deleted file mode 100644 index 0cc8b23..0000000 --- a/tests.py +++ /dev/null @@ -1,873 +0,0 @@ -""" -A collection of unit tests for the AttrDict class. -""" -from __future__ import print_function - -import os -from sys import version_info -from tempfile import mkstemp -import pickle -import unittest - - -PY2 = version_info < (3,) - - -class TestAttrDict(unittest.TestCase): - """ - A collection of unit tests for the AttrDict class. - """ - def setUp(self): - self.tempfiles = [] - - def tearDown(self): - for tempfile in self.tempfiles: - os.remove(tempfile) - - def test_init(self): - """ - Make sure that keys are accessable both as keys and attributes - at initialization. - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar', 'alpha': {'beta': 2, 'bravo': {}}}) - - # as key - self.assertEqual(adict['foo'], 'bar') - - # as attribute - self.assertEqual(adict.foo, 'bar') - - # nested as key - self.assertEqual(adict['alpha'], {'beta': 2, 'bravo': {}}) - - # nested as attribute - self.assertEqual(adict.alpha, {'beta': 2, 'bravo': {}}) - self.assertEqual(adict.alpha.beta, 2) - self.assertEqual(adict.alpha.bravo, {}) - - def test_get(self): - """ - Test that attributes can be accessed (both as keys, and as - attributes). - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar'}) - - # found - self.assertEqual(adict.get('foo'), 'bar') - - # found, default given - self.assertEqual(adict.get('foo', 'baz'), 'bar') - - # not found - self.assertEqual(adict.get('bar'), None) - - # not found, default given - self.assertEqual(adict.get('bar', 'baz'), 'baz') - - def test_iteration_2(self): - """ - Test the iteration methods (items, keys, values[, iteritems, - iterkeys, itervalues]). - """ - if not PY2: # Python2.6 doesn't have skipif/skipunless - return - - from attrdict import AttrDict - - empty = AttrDict() - adict = AttrDict({'foo': 'bar', 'lorem': 'ipsum', 'alpha': { - 'beta': 1, 'bravo': empty}}) - - self.assertEqual(empty.items(), []) - self.assertEqual(empty.keys(), []) - self.assertEqual(empty.values(), []) - - items = adict.items() - self.assertEqual(len(items), 3) - self.assertTrue(('foo', 'bar') in items) - self.assertTrue(('lorem', 'ipsum') in items) - self.assertTrue(('alpha', {'beta': 1, 'bravo': empty}) in items) - - self.assertEqual(set(adict.keys()), set(['foo', 'lorem', 'alpha'])) - - values = adict.values() - self.assertEqual(len(values), 3) - self.assertTrue('bar' in values) - self.assertTrue('ipsum' in values) - self.assertTrue({'beta': 1, 'bravo': empty} in values) - - # Iterator methods - iterator = empty.iteritems() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = empty.iterkeys() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = empty.itervalues() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = adict.iteritems() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), adict.items()) - - iterator = adict.iterkeys() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), adict.keys()) - - iterator = adict.itervalues() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), adict.values()) - - def test_iteration_3(self): - """ - Test the iteration methods (items, keys, values[, iteritems, - iterkeys, itervalues]). - """ - if PY2: # Python2.6 doesn't have skipif/skipunless - return - - from attrdict import AttrDict - - empty = AttrDict() - adict = AttrDict({'foo': 'bar', 'lorem': 'ipsum', 'alpha': { - 'beta': 1, 'bravo': empty}}) - - iterator = empty.items() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = empty.keys() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = empty.values() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(list(iterator), []) - - iterator = adict.items() - self.assertFalse(isinstance(iterator, list)) - items = list(iterator) - self.assertEqual(len(items), 3) - self.assertTrue(('foo', 'bar') in items) - self.assertTrue(('lorem', 'ipsum') in items) - self.assertTrue(('alpha', {'beta': 1, 'bravo': empty}) in items) - - iterator = adict.keys() - self.assertFalse(isinstance(iterator, list)) - self.assertEqual(set(iterator), set(['foo', 'lorem', 'alpha'])) - - iterator = adict.values() - self.assertFalse(isinstance(iterator, list)) - values = list(iterator) - self.assertEqual(len(values), 3) - self.assertTrue('bar' in values) - self.assertTrue('ipsum' in values) - self.assertTrue({'beta': 1, 'bravo': empty} in values) - - # make sure 'iter' methods don't exist - self.assertFalse(hasattr(adict, 'iteritems')) - self.assertFalse(hasattr(adict, 'iterkeys')) - self.assertFalse(hasattr(adict, 'itervalues')) - - def test_call(self): - """ - Ensure that attributes can be dynamically accessed - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar', 'alpha': {'beta': 2, 'bravo': {}}}) - - self.assertEqual(adict('foo'), 'bar') - self.assertEqual(adict('alpha'), {'beta': 2, 'bravo': {}}) - self.assertEqual(adict('alpha').beta, 2) - self.assertEqual(adict('alpha').bravo, {}) - - # Make sure call failes correctly - # with self.assertRaises(AttributeError) - try: - adict('fake') - except AttributeError: - pass # this is what we want - else: - raise AssertionError("AttributeError not raised") - - def test_setattr(self): - """ - Test that key-value pairs can be added/changes as attributes - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar'}) - - adict.foo = 'baz' - self.assertEqual(adict.foo, 'baz') - self.assertEqual(adict['foo'], 'baz') - - adict.lorem = 'ipsum' - self.assertEqual(adict.lorem, 'ipsum') - self.assertEqual(adict['lorem'], 'ipsum') - - adict.alpha = {'beta': 1, 'bravo': 2} - self.assertEqual(adict.alpha, {'beta': 1, 'bravo': 2}) - self.assertEqual(adict.alpha.beta, 1) - self.assertEqual(adict['alpha'], {'beta': 1, 'bravo': 2}) - - # with self.assertRaises(TypeError): - try: - adict._no = "Won't work" - except TypeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - def test_delattr(self): - """ - Test that key-value pairs can be deleted as attributes. - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar', '_set': 'shadows', 'get': 'shadows'}) - - del adict.foo - - # with self.assertRaises(AttributeError): - try: - adict.foo - except AttributeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(KeyError): - try: - adict['foo'] - except KeyError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(TypeError): - try: - del adict.lorem - except TypeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(TypeError): - try: - del adict._set - except TypeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(TypeError): - try: - del adict.get - except TypeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # make sure things weren't deleted - self.assertNotEqual(adict._set, 'shadows') - self.assertEqual(adict.get('get'), 'shadows') - self.assertEqual(adict, {'_set': 'shadows', 'get': 'shadows'}) - - def test_setitem(self): - """ - Test that key-value pairs can be added/changes as keys - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar'}) - - adict['foo'] = 'baz' - self.assertEqual(adict.foo, 'baz') - self.assertEqual(adict['foo'], 'baz') - - adict['lorem'] = 'ipsum' - self.assertEqual(adict.lorem, 'ipsum') - self.assertEqual(adict['lorem'], 'ipsum') - - adict['alpha'] = {'beta': 1, 'bravo': 2} - self.assertEqual(adict.alpha, {'beta': 1, 'bravo': 2}) - self.assertEqual(adict.alpha.beta, 1) - self.assertEqual(adict['alpha'], {'beta': 1, 'bravo': 2}) - - def test_delitem(self): - """ - Test that key-value pairs can be deleted as keys. - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar', '_set': 'shadows', 'get': 'shadows'}) - - del adict['foo'] - - # with self.assertRaises(AttributeError): - try: - adict.foo - except AttributeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(AttributeError): - try: - adict.foo - except AttributeError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(KeyError): - try: - adict['foo'] - except KeyError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - # with self.assertRaises(KeyError): - try: - del adict['lorem'] - except KeyError: - pass # expected - else: - raise AssertionError("Exception not thrown") - - del adict['_set'] - del adict['get'] - - # make sure things weren't deleted - adict._set - self.assertEqual(adict.get('get', 'deleted'), 'deleted') - self.assertEqual(adict, {}) - - def test_getitem(self): - """ - Tests that getitem doesn't return an attrdict. - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': {'bar': {'baz': 'lorem'}}}) - - self.assertEqual(adict.foo.bar, {'baz': 'lorem'}) # works - self.assertRaises(AttributeError, lambda: adict['foo'].bar) - - self.assertEqual(adict.foo.bar.baz, 'lorem') # works - self.assertRaises(AttributeError, lambda: adict['foo']['bar'].baz) - - adict = AttrDict({'foo': [{'bar': 'baz'}]}) - - self.assertEqual(adict.foo[0].bar, 'baz') # works - self.assertRaises(AttributeError, lambda: adict['foo'][0].bar) - - def test_contains(self): - """ - Test that contains works properly. - """ - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar', '_set': 'shadows', 'get': 'shadows'}) - - self.assertTrue('foo' in adict) - self.assertTrue('_set' in adict) - self.assertTrue('get' in adict) - self.assertFalse('items' in adict) - - def test_has_key(self): - """ - Test has_key behavior in regard to this python - """ - import inspect - from attrdict import AttrDict - - adict = AttrDict({'foo': 'bar'}) - masked = AttrDict({'has_key': 'foobar'}) - - if PY2: - self.assertTrue(inspect.ismethod(adict.has_key)) - self.assertTrue(inspect.ismethod(masked.has_key)) - self.assertFalse(adict.has_key('has_key')) - self.assertTrue(masked.has_key('has_key')) - else: # Python3 dropped this method - self.assertFalse(inspect.ismethod(masked.has_key)) - self.assertRaises(AttributeError, getattr, adict, 'has_key') - self.assertEqual(masked.has_key, 'foobar') - - def test_len(self): - """ - Test that len works properly. - """ - from attrdict import AttrDict - - # empty - adict = AttrDict() - self.assertEqual(len(adict), 0) - - # added via key - adict['key'] = 1 - self.assertEqual(len(adict), 1) - - adict['key'] = 2 - self.assertEqual(len(adict), 1) - - # added via attribute - adict.attribute = 3 - self.assertEqual(len(adict), 2) - - adict.key = 3 - self.assertEqual(len(adict), 2) - - # deleted - del adict.key - self.assertEqual(len(adict), 1) - - def test_iter(self): - """ - Test that iter works properly. - """ - from attrdict import AttrDict - - # empty - for key in AttrDict(): - raise AssertionError("Nothing should be run right now") - - # non-empty - expected = {'alpha': 1, 'bravo': 2, 'charlie': 3} - actual = set() - - adict = AttrDict(expected) - - for key in adict: - actual.add(key) - - self.assertEqual(actual, set(expected.keys())) - - def test_add(self): - """ - Test that adding works. - """ - from attrdict import AttrDict - - a = {'alpha': {'beta': 1, 'a': 1}, 'lorem': 'ipsum'} - b = {'alpha': {'bravo': 1, 'a': 0}, 'foo': 'bar'} - - ab = { - 'alpha': { - 'beta': 1, - 'bravo': 1, - 'a': 0 - }, - 'lorem': 'ipsum', - 'foo': 'bar' - } - - ba = { - 'alpha': { - 'beta': 1, - 'bravo': 1, - 'a': 1 - }, - 'lorem': 'ipsum', - 'foo': 'bar' - } - - # Both AttrDicts - self.assertEqual(AttrDict(a) + AttrDict(b), ab) - self.assertEqual(AttrDict(b) + AttrDict(a), ba) - - # Left AttrDict - self.assertEqual(AttrDict(a) + b, ab) - self.assertEqual(AttrDict(b) + a, ba) - - # Right AttrDict - self.assertEqual(a + AttrDict(b), ab) - self.assertEqual(b + AttrDict(a), ba) - - # Defer on non-mappings - class NonMapping(object): - """ - A non-mapping object to test NotImplemented - """ - def __radd__(self, other): - return 'success' - - self.assertEqual(AttrDict(a) + NonMapping(), 'success') - - # with self.assertRaises(NotImplementedError) - try: - NonMapping + AttrDict(b) - except TypeError: - pass # what we want to happen - else: - raise AssertionError("NotImplementedError not thrown") - - def test_build(self): - """ - Test that build works. - """ - from attrdict import AttrDict - - self.assertTrue(isinstance(AttrDict._build({}), AttrDict)) - self.assertTrue(isinstance(AttrDict._build([]), list)) - self.assertTrue(isinstance(AttrDict._build(AttrDict()), AttrDict)) - self.assertTrue(isinstance(AttrDict._build(1), int)) - - def test_valid_name(self): - """ - Test that valid_name works. - """ - from attrdict import AttrDict - - self.assertTrue(AttrDict._valid_name('valid')) - self.assertFalse(AttrDict._valid_name('_invalid')) - self.assertFalse(AttrDict._valid_name('get')) - - def test_kwargs(self): - """ - Test that ** works - """ - def return_results(**kwargs): - """Return result passed into a function""" - return kwargs - - expected = {'foo': 1, 'bar': 2} - - from attrdict import AttrDict - - self.assertEqual(return_results(**AttrDict()), {}) - self.assertEqual(return_results(**AttrDict(expected)), expected) - - def test_sequences(self): - """ - Test that AttrDict handles Sequences properly. - """ - from attrdict import AttrDict - - adict = AttrDict({'lists': [{'value': 1}, {'value': 2}], - 'tuple': ({'value': 1}, {'value': 2})}) - - # lists - self.assertTrue(adict.lists, list) - - self.assertEqual(adict.lists[0].value, 1) - self.assertEqual(adict.lists[1].value, 2) - - self.assertEqual(({} + adict).lists[0].value, 1) - self.assertEqual((adict + {}).lists[1].value, 2) - - self.assertEqual((AttrDict(recursive=True) + adict).lists[0].value, 1) - self.assertEqual((adict + AttrDict(recursive=True)).lists[1].value, 2) - - self.assertEqual([element.value for element in adict.lists], [1, 2]) - - self.assertEqual(adict('lists')[0].value, 1) - - # tuple - self.assertTrue(adict.tuple, tuple) - - self.assertEqual(adict.tuple[0].value, 1) - self.assertEqual(adict.tuple[1].value, 2) - - self.assertTrue(adict.tuple, tuple) - - self.assertEqual(({} + adict).tuple[0].value, 1) - self.assertEqual((adict + {}).tuple[1].value, 2) - - self.assertTrue(({} + adict).tuple, tuple) - self.assertTrue((adict + {}).tuple, tuple) - - self.assertEqual((AttrDict(recursive=True) + adict).tuple[0].value, 1) - self.assertEqual((adict + AttrDict(recursive=True)).tuple[1].value, 2) - - self.assertEqual([element.value for element in adict.tuple], [1, 2]) - - # Not recursive - adict = AttrDict({'lists': [{'value': 1}, {'value': 2}], - 'tuple': ({'value': 1}, {'value': 2})}, - recursive=False) - - self.assertFalse(isinstance(adict.lists[0], AttrDict)) - - self.assertFalse(isinstance(({} + adict).lists[0], AttrDict)) - self.assertFalse(isinstance((adict + {}).lists[1], AttrDict)) - - self.assertFalse( - isinstance((AttrDict(recursive=True) + adict).lists[0], AttrDict)) - self.assertFalse( - isinstance((adict + AttrDict(recursive=True)).lists[1], AttrDict)) - - self.assertFalse(isinstance((adict + adict).lists[0], AttrDict)) - - for element in adict.lists: - self.assertFalse(isinstance(element, AttrDict)) - - self.assertFalse(isinstance(adict('lists')[0], AttrDict)) - - # Dict access shouldn't produce an attrdict - self.assertFalse(isinstance(adict['lists'][0], AttrDict)) - - self.assertFalse(isinstance(adict.tuple[0], AttrDict)) - - self.assertFalse(isinstance(({} + adict).tuple[0], AttrDict)) - self.assertFalse(isinstance((adict + {}).tuple[1], AttrDict)) - - self.assertFalse( - isinstance((AttrDict(recursive=True) + adict).tuple[0], AttrDict)) - self.assertFalse( - isinstance((adict + AttrDict(recursive=True)).tuple[1], AttrDict)) - - self.assertFalse(isinstance((adict + adict).tuple[0], AttrDict)) - - for element in adict.tuple: - self.assertFalse(isinstance(element, AttrDict)) - - self.assertFalse(isinstance(adict('tuple')[0], AttrDict)) - - # Dict access shouldn't produce an attrdict - self.assertFalse(isinstance(adict['tuple'][0], AttrDict)) - - def test_repr(self): - """ - Test that repr works appropriately. - """ - from attrdict import AttrDict - - self.assertEqual(repr(AttrDict()), 'a{}') - self.assertEqual(repr(AttrDict({'foo': 'bar'})), "a{'foo': 'bar'}") - self.assertEqual( - repr(AttrDict({'foo': {1: 2}})), "a{'foo': {1: 2}}") - self.assertEqual( - repr(AttrDict({'foo': AttrDict({1: 2})})), "a{'foo': a{1: 2}}") - - def test_copy(self): - """ - test that attrdict supports copy. - """ - from copy import copy - - from attrdict import AttrDict - - adict = AttrDict({'foo': {'bar': 'baz'}}) - bdict = copy(adict) - cdict = bdict - - bdict.foo.lorem = 'ipsum' - - self.assertEqual(adict, bdict) - self.assertEqual(bdict, cdict) - - def test_deepcopy(self): - """ - test that attrdict supports deepcopy. - """ - from copy import deepcopy - - from attrdict import AttrDict - - adict = AttrDict({'foo': {'bar': 'baz'}}) - bdict = deepcopy(adict) - cdict = bdict - - bdict.foo.lorem = 'ipsum' - - self.assertNotEqual(adict, bdict) - self.assertEqual(bdict, cdict) - - def test_default_dict(self): - """ - test attrdict's defaultdict support. - """ - from attrdict import AttrDict - - self.assertRaises(KeyError, lambda: AttrDict()['foo']) - self.assertRaises(AttributeError, lambda: AttrDict().foo) - - adict = AttrDict(default_factory=lambda: ('foo', 'bar', 'baz')) - - self.assertEqual(adict['foo'], ('foo', 'bar', 'baz')) - self.assertEqual(adict('bar'), ('foo', 'bar', 'baz')) - self.assertEqual(adict.baz, ('foo', 'bar', 'baz')) - self.assertEqual(adict.get('lorem'), None) - self.assertEqual(adict.get('ipsum', 'alpha'), 'alpha') - - # make sure this doesn't break access - adict.bravo = 'charlie' - - self.assertEqual(adict['bravo'], 'charlie') - self.assertEqual(adict('bravo'), 'charlie') - self.assertEqual(adict.bravo, 'charlie') - self.assertEqual(adict.get('bravo'), 'charlie') - self.assertEqual(adict.get('bravo', 'alpha'), 'charlie') - - def test_default_dict_pass_key(self): - """ - test attrdict's defaultdict support. - """ - from attrdict import AttrDict - - adict = AttrDict(default_factory=lambda foo: (foo, 'bar', 'baz'), - pass_key=True) - - self.assertEqual(adict['foo'], ('foo', 'bar', 'baz')) - self.assertEqual(adict('bar'), ('bar', 'bar', 'baz')) - self.assertEqual(adict.baz, ('baz', 'bar', 'baz')) - self.assertEqual(adict.get('lorem'), None) - self.assertEqual(adict.get('ipsum', 'alpha'), 'alpha') - - # make sure this doesn't break access - adict.bravo = 'charlie' - - self.assertEqual(adict['bravo'], 'charlie') - self.assertEqual(adict('bravo'), 'charlie') - self.assertEqual(adict.bravo, 'charlie') - self.assertEqual(adict.get('bravo'), 'charlie') - self.assertEqual(adict.get('bravo', 'alpha'), 'charlie') - - # make sure missing doesn't happen on hidden attributes - adict = AttrDict(default_factory=lambda: 'default') - - self.assertEqual(adict.works, 'default') - - self.assertRaises(AttributeError, lambda: adict._hidden) - self.assertEqual(adict.get('_hidden'), None) - - self.assertRaises(AttributeError, lambda: adict.__magic__) - self.assertEqual(adict.get('__magic__'), None) - - # but this should work - self.assertEqual(adict('_this_is_fine'), 'default') - - def test_load_bad_kwarg(self): - """ - Test that load TypeErrors on kwargs other than load_function - """ - from attrdict import load - - self.assertRaises(TypeError, load, foo='bar') - - def test_load_empty(self): - """ - Test that load TypeErrors on kwargs other than load_function - """ - from attrdict import load, AttrDict - - adict = load() - - self.assertTrue(isinstance(adict, AttrDict)) - self.assertFalse(adict) - - def test_load_one(self): - """ - test loading a single file - """ - from attrdict import load - - self.tempfiles.append(mkstemp()[1]) - - with open(self.tempfiles[0], 'w') as fileobj: - fileobj.write('{"foo": "bar", "baz": 1}') - - adict = load(self.tempfiles[0]) - - self.assertEqual(adict, {'foo': 'bar', 'baz': 1}) - - def test_load_many(self): - """ - test loading multiple files at once. - """ - from attrdict import load - - self.tempfiles.append(mkstemp()[1]) - - with open(self.tempfiles[0], 'w') as fileobj: - fileobj.write('{"foo": "bar", "baz": {"lorem": "ipsum"}}') - - self.tempfiles.append(mkstemp()[1]) - - with open(self.tempfiles[1], 'w') as fileobj: - fileobj.write('{"alpha": "bravo", "baz": {"charlie": "delta"}}') - - self.tempfiles.append(mkstemp()[1]) - - with open(self.tempfiles[2], 'w') as fileobj: - fileobj.write('{"alpha": "a", "baz": {"charlie": "delta"}}') - - adict = load(*self.tempfiles) - - self.assertEqual(adict, { - 'foo': 'bar', - 'alpha': 'a', - 'baz': {'lorem': 'ipsum', 'charlie': 'delta'}}) - - def test_load_load_function(self): - """ - test that load works with a custom load_function provided. - """ - from attrdict import load - - self.tempfiles.append(mkstemp()[1]) - - with open(self.tempfiles[0], 'w') as fileobj: - fileobj.write('{"foo": "bar", "baz": 1}') - - adict = load(self.tempfiles[0], load_function=lambda _: {'banana': 1}) - - self.assertEqual(adict, {'banana': 1}) - - def _check_pickle_roundtrip(self, source, **attrdict_kwargs): - """ - Serialize then Deserialize an attrdict, ensuring that the result - and initial object are equivalent. - """ - from attrdict import AttrDict - - source = AttrDict(source, **attrdict_kwargs) - data = pickle.dumps(source) - loaded = pickle.loads(data) - self.assertEqual(source, loaded) - self.assert_(isinstance(loaded, AttrDict), - '%s not instance of %s' % (type(loaded), AttrDict)) - return loaded - - def test_pickle_unpickle_simple(self): - """ - Test that AttrDict can handle pickling. - """ - from attrdict import AttrDict - - # simple - self._check_pickle_roundtrip({'a': 1}) - # nested - self._check_pickle_roundtrip({'a': 1, 'd': {'c': 1}}) - # default factory - loaded = self._check_pickle_roundtrip({'a': 5}, - default_factory=return1) - self.assertEqual(loaded.nonexistent, 1) - # recursive - loaded = self._check_pickle_roundtrip({'a': [{'c': 5}]}, - recursive=True) - self.assert_(isinstance(loaded.a[0], AttrDict), - '%s not instance of %s' % (type(loaded.a[0]), AttrDict)) - - -def return1(): - """ - function for testing pickling with default_factory. - """ - return 1 - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..83fc1d9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for the attrdict module +""" diff --git a/tests/test_attrdefault.py b/tests/test_attrdefault.py new file mode 100644 index 0000000..dec9fc1 --- /dev/null +++ b/tests/test_attrdefault.py @@ -0,0 +1,52 @@ +""" +Tests for the AttrDefault class. +""" +from nose.tools import assert_equals, assert_raises +from six import PY2 + + +def test_method_missing(): + """ + default values for AttrDefault + """ + from attrdict.default import AttrDefault + + default_none = AttrDefault() + default_list = AttrDefault(list, sequence_type=None) + default_double = AttrDefault(lambda value: value * 2, pass_key=True) + + assert_raises(AttributeError, lambda: default_none.foo) + assert_raises(KeyError, lambda: default_none['foo']) + assert_equals(default_none, {}) + + assert_equals(default_list.foo, []) + assert_equals(default_list['bar'], []) + assert_equals(default_list, {'foo': [], 'bar': []}) + + assert_equals(default_double.foo, 'foofoo') + assert_equals(default_double['bar'], 'barbar') + assert_equals(default_double, {'foo': 'foofoo', 'bar': 'barbar'}) + + +def test_repr(): + """ + repr(AttrDefault) + """ + from attrdict.default import AttrDefault + + assert_equals(repr(AttrDefault(None)), "AttrDefault(None, False, {})") + + # list's repr changes between python 2 and python 3 + type_or_class = 'type' if PY2 else 'class' + + assert_equals( + repr(AttrDefault(list)), + type_or_class.join(("AttrDefault(<", " 'list'>, False, {})")) + ) + + assert_equals( + repr(AttrDefault(list, {'foo': 'bar'}, pass_key=True)), + type_or_class.join( + ("AttrDefault(<", " 'list'>, True, {'foo': 'bar'})") + ) + ) diff --git a/tests/test_attrdict.py b/tests/test_attrdict.py new file mode 100644 index 0000000..b376765 --- /dev/null +++ b/tests/test_attrdict.py @@ -0,0 +1,125 @@ +# encoding: UTF-8 +""" +Tests for the AttrDict class. +""" +from nose.tools import assert_equals, assert_false +from six import PY2 + + +def test_init(): + """ + Create a new AttrDict. + """ + from attrdict.dictionary import AttrDict + + # empty + assert_equals(AttrDict(), {}) + assert_equals(AttrDict(()), {}) + assert_equals(AttrDict({}), {}) + + # with items + assert_equals(AttrDict({'foo': 'bar'}), {'foo': 'bar'}) + assert_equals(AttrDict((('foo', 'bar'),)), {'foo': 'bar'}) + assert_equals(AttrDict(foo='bar'), {'foo': 'bar'}) + + # non-overlapping + assert_equals(AttrDict({}, foo='bar'), {'foo': 'bar'}) + assert_equals(AttrDict((), foo='bar'), {'foo': 'bar'}) + + assert_equals( + AttrDict({'alpha': 'bravo'}, foo='bar'), + {'foo': 'bar', 'alpha': 'bravo'} + ) + + assert_equals( + AttrDict((('alpha', 'bravo'),), foo='bar'), + {'foo': 'bar', 'alpha': 'bravo'} + ) + + # updating + assert_equals( + AttrDict({'alpha': 'bravo'}, foo='bar', alpha='beta'), + {'foo': 'bar', 'alpha': 'beta'} + ) + + assert_equals( + AttrDict((('alpha', 'bravo'), ('alpha', 'beta')), foo='bar'), + {'foo': 'bar', 'alpha': 'beta'} + ) + + assert_equals( + AttrDict((('alpha', 'bravo'), ('alpha', 'beta')), alpha='bravo'), + {'alpha': 'bravo'} + ) + + +def test_copy(): + """ + Make a dict copy of an AttrDict. + """ + from attrdict.dictionary import AttrDict + + mapping_a = AttrDict({'foo': {'bar': 'baz'}}) + mapping_b = mapping_a.copy() + mapping_c = mapping_b + + mapping_b['foo']['lorem'] = 'ipsum' + + assert_equals(mapping_a, mapping_b) + assert_equals(mapping_b, mapping_c) + + +def test_fromkeys(): + """ + make a new sequence from a set of keys. + """ + from attrdict.dictionary import AttrDict + + # default value + assert_equals(AttrDict.fromkeys(()), {}) + assert_equals( + AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}), + {'foo': None, 'baz': None} + ) + assert_equals( + AttrDict.fromkeys(('foo', 'baz')), + {'foo': None, 'baz': None} + ) + + # custom value + assert_equals(AttrDict.fromkeys((), 0), {}) + assert_equals( + AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}, 0), + {'foo': 0, 'baz': 0} + ) + assert_equals( + AttrDict.fromkeys(('foo', 'baz'), 0), + {'foo': 0, 'baz': 0} + ) + + +def test_repr(): + """ + repr(AttrDict) + """ + from attrdict.dictionary import AttrDict + + assert_equals(repr(AttrDict()), "AttrDict({})") + assert_equals(repr(AttrDict({'foo': 'bar'})), "AttrDict({'foo': 'bar'})") + assert_equals( + repr(AttrDict({1: {'foo': 'bar'}})), "AttrDict({1: {'foo': 'bar'}})" + ) + assert_equals( + repr(AttrDict({1: AttrDict({'foo': 'bar'})})), + "AttrDict({1: AttrDict({'foo': 'bar'})})" + ) + + +if not PY2: + def test_has_key(): + """ + The now-depricated has_keys method + """ + from attrdict.dictionary import AttrDict + + assert_false(hasattr(AttrDict(), 'has_key')) diff --git a/tests/test_attrmap.py b/tests/test_attrmap.py new file mode 100644 index 0000000..c802f33 --- /dev/null +++ b/tests/test_attrmap.py @@ -0,0 +1,21 @@ +""" +Tests for the AttrMap class. +""" +from nose.tools import assert_equals + + +def test_repr(): + """ + repr(AttrMap) + """ + from attrdict.mapping import AttrMap + + assert_equals(repr(AttrMap()), "AttrMap({})") + assert_equals(repr(AttrMap({'foo': 'bar'})), "AttrMap({'foo': 'bar'})") + assert_equals( + repr(AttrMap({1: {'foo': 'bar'}})), "AttrMap({1: {'foo': 'bar'}})" + ) + assert_equals( + repr(AttrMap({1: AttrMap({'foo': 'bar'})})), + "AttrMap({1: AttrMap({'foo': 'bar'})})" + ) diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 0000000..ca02cac --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,940 @@ +# encoding: UTF-8 +""" +Common tests that apply to multiple Attr-derived classes. +""" +import copy +from collections import namedtuple, Mapping, ItemsView, KeysView, ValuesView +from itertools import chain +import pickle +from sys import version_info + +from nose.tools import (assert_equals, assert_not_equals, + assert_true, assert_false, assert_raises) +import six + +from attrdict.mixins import Attr + + +Options = namedtuple( + 'Options', + ('cls', 'constructor', 'mutable', 'iter_methods', 'view_methods', + 'recursive') +) + + +class AttrImpl(Attr): + """ + An implementation of Attr. + """ + def __init__(self, items=None, sequence_type=tuple): + if items is None: + items = {} + elif not isinstance(items, Mapping): + items = dict(items) + + self._mapping = items + self._sequence_type = sequence_type + + def _configuration(self): + """ + The configuration for an attrmap instance. + """ + return self._sequence_type + + def __getitem__(self, key): + """ + Access a value associated with a key. + """ + return self._mapping[key] + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + Iterated through the keys. + """ + return iter(self._mapping) + + def __getstate__(self): + """ + Serialize the object. + """ + return (self._mapping, self._sequence_type) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + mapping, sequence_type = state + self._mapping = mapping + self._sequence_type = sequence_type + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + return cls(mapping, sequence_type=configuration) + + +def test_attr(): + """ + Tests for an class that implements Attr. + """ + for test in common(AttrImpl, mutable=False): + yield test + + +def test_attrmap(): + """ + Run AttrMap against the common tests. + """ + from attrdict.mapping import AttrMap + + for test in common(AttrMap, mutable=True): + yield test + + +def test_attrdict(): + """ + Run AttrDict against the common tests. + """ + from attrdict.dictionary import AttrDict + + view_methods = (2, 7) <= version_info < (3,) + + def constructor(items=None, sequence_type=tuple): + """ + Build a new AttrDict. + """ + if items is None: + items = {} + + return AttrDict._constructor(items, sequence_type) + + for test in common(AttrDict, constructor=constructor, + mutable=True, iter_methods=True, + view_methods=view_methods, recursive=False): + yield test + + +def test_attrdefault(): + """ + Run AttrDefault against the common tests. + """ + from attrdict.default import AttrDefault + + def constructor(items=None, sequence_type=tuple): + """ + Build a new AttrDefault. + """ + if items is None: + items = {} + + return AttrDefault(None, items, sequence_type) + + for test in common(AttrDefault, constructor=constructor, mutable=True): + yield test + + +def common(cls, constructor=None, mutable=False, iter_methods=False, + view_methods=False, recursive=True): + """ + Iterates over tests common to multiple Attr-derived classes + + cls: The class that is being tested. + mutable: (optional, False) Whether the object is supposed to be + mutable. + iter_methods: (optional, False) Whether the class implements + iter under Python 2. + view_methods: (optional, False) Whether the class implements + view under Python 2. + recursive: (optional, True) Whether recursive assignment works. + """ + tests = ( + item_access, iteration, containment, length, equality, + item_creation, item_deletion, sequence_typing, addition, + to_kwargs, pickling, + ) + + mutable_tests = ( + pop, popitem, clear, update, setdefault, copying, deepcopying, + ) + + if constructor is None: + constructor = cls + + options = Options(cls, constructor, mutable, iter_methods, view_methods, + recursive) + + if mutable: + tests = chain(tests, mutable_tests) + + for test in tests: + test.description = test.__doc__.format(cls=cls.__name__) + + yield test, options + + +def item_access(options): + """Access items in {cls}.""" + mapping = options.constructor( + { + 'foo': 'bar', + '_lorem': 'ipsum', + six.u('👻'): 'boo', + 3: 'three', + 'get': 'not the function', + 'sub': {'alpha': 'bravo'}, + 'bytes': b'bytes', + 'tuple': ({'a': 'b'}, 'c'), + 'list': [{'a': 'b'}, {'c': 'd'}], + } + ) + + # key that can be an attribute + assert_equals(mapping['foo'], 'bar') + assert_equals(mapping.foo, 'bar') + assert_equals(mapping('foo'), 'bar') + assert_equals(mapping.get('foo'), 'bar') + + # key that cannot be an attribute + assert_equals(mapping[3], 'three') + assert_raises(TypeError, getattr, mapping, 3) + assert_equals(mapping(3), 'three') + assert_equals(mapping.get(3), 'three') + + # key that cannot be an attribute (sadly) + assert_equals(mapping[six.u('👻')], 'boo') + if six.PY2: + assert_raises(UnicodeEncodeError, getattr, mapping, six.u('👻')) + else: + assert_raises(AttributeError, getattr, mapping, six.u('👻')) + assert_equals(mapping(six.u('👻')), 'boo') + assert_equals(mapping.get(six.u('👻')), 'boo') + + # key that represents a hidden attribute + assert_equals(mapping['_lorem'], 'ipsum') + assert_raises(AttributeError, lambda: mapping._lorem) + assert_equals(mapping('_lorem'), 'ipsum') + assert_equals(mapping.get('_lorem'), 'ipsum') + + # key that represents an attribute that already exists + assert_equals(mapping['get'], 'not the function') + assert_not_equals(mapping.get, 'not the function') + assert_equals(mapping('get'), 'not the function') + assert_equals(mapping.get('get'), 'not the function') + + # does recursion work + assert_raises(AttributeError, lambda: mapping['sub'].alpha) + assert_equals(mapping.sub.alpha, 'bravo') + assert_equals(mapping('sub').alpha, 'bravo') + assert_raises(AttributeError, lambda: mapping.get('sub').alpha) + + # bytes + assert_equals(mapping['bytes'], b'bytes') + assert_equals(mapping.bytes, b'bytes') + assert_equals(mapping('bytes'), b'bytes') + assert_equals(mapping.get('bytes'), b'bytes') + + # tuple + assert_equals(mapping['tuple'], ({'a': 'b'}, 'c')) + assert_equals(mapping.tuple, ({'a': 'b'}, 'c')) + assert_equals(mapping('tuple'), ({'a': 'b'}, 'c')) + assert_equals(mapping.get('tuple'), ({'a': 'b'}, 'c')) + + assert_raises(AttributeError, lambda: mapping['tuple'][0].a) + assert_equals(mapping.tuple[0].a, 'b') + assert_equals(mapping('tuple')[0].a, 'b') + assert_raises(AttributeError, lambda: mapping.get('tuple')[0].a) + + assert_true(isinstance(mapping['tuple'], tuple)) + assert_true(isinstance(mapping.tuple, tuple)) + assert_true(isinstance(mapping('tuple'), tuple)) + assert_true(isinstance(mapping.get('tuple'), tuple)) + + assert_true(isinstance(mapping['tuple'][0], dict)) + assert_true(isinstance(mapping.tuple[0], options.cls)) + assert_true(isinstance(mapping('tuple')[0], options.cls)) + assert_true(isinstance(mapping.get('tuple')[0], dict)) + + assert_true(isinstance(mapping['tuple'][1], str)) + assert_true(isinstance(mapping.tuple[1], str)) + assert_true(isinstance(mapping('tuple')[1], str)) + assert_true(isinstance(mapping.get('tuple')[1], str)) + + # list + assert_equals(mapping['list'], [{'a': 'b'}, {'c': 'd'}]) + assert_equals(mapping.list, ({'a': 'b'}, {'c': 'd'})) + assert_equals(mapping('list'), ({'a': 'b'}, {'c': 'd'})) + assert_equals(mapping.get('list'), [{'a': 'b'}, {'c': 'd'}]) + + assert_raises(AttributeError, lambda: mapping['list'][0].a) + assert_equals(mapping.list[0].a, 'b') + assert_equals(mapping('list')[0].a, 'b') + assert_raises(AttributeError, lambda: mapping.get('list')[0].a) + + assert_true(isinstance(mapping['list'], list)) + assert_true(isinstance(mapping.list, tuple)) + assert_true(isinstance(mapping('list'), tuple)) + assert_true(isinstance(mapping.get('list'), list)) + + assert_true(isinstance(mapping['list'][0], dict)) + assert_true(isinstance(mapping.list[0], options.cls)) + assert_true(isinstance(mapping('list')[0], options.cls)) + assert_true(isinstance(mapping.get('list')[0], dict)) + + assert_true(isinstance(mapping['list'][1], dict)) + assert_true(isinstance(mapping.list[1], options.cls)) + assert_true(isinstance(mapping('list')[1], options.cls)) + assert_true(isinstance(mapping.get('list')[1], dict)) + + # Nonexistent key + assert_raises(KeyError, lambda: mapping['fake']) + assert_raises(AttributeError, lambda: mapping.fake) + assert_raises(AttributeError, lambda: mapping('fake')) + assert_equals(mapping.get('fake'), None) + assert_equals(mapping.get('fake', 'bake'), 'bake') + + +def iteration(options): + "Iterate over keys/values/items in {cls}" + raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} + + mapping = options.constructor(raw) + + expected_keys = frozenset(('foo', 'lorem', 'alpha')) + expected_values = frozenset(('bar', 'ipsum', 'bravo')) + expected_items = frozenset( + (('foo', 'bar'), ('lorem', 'ipsum'), ('alpha', 'bravo')) + ) + + assert_equals(set(iter(mapping)), expected_keys) + + actual_keys = mapping.keys() + actual_values = mapping.values() + actual_items = mapping.items() + + if six.PY2: + for collection in (actual_keys, actual_values, actual_items): + assert_true(isinstance(collection, list)) + + assert_equals(frozenset(actual_keys), expected_keys) + assert_equals(frozenset(actual_values), expected_values) + assert_equals(frozenset(actual_items), expected_items) + + if options.iter_methods: + actual_keys = mapping.iterkeys() + actual_values = mapping.itervalues() + actual_items = mapping.iteritems() + + for iterable in (actual_keys, actual_values, actual_items): + assert_false(isinstance(iterable, list)) + + assert_equals(frozenset(actual_keys), expected_keys) + assert_equals(frozenset(actual_values), expected_values) + assert_equals(frozenset(actual_items), expected_items) + + if options.view_methods: + actual_keys = mapping.viewkeys() + actual_values = mapping.viewvalues() + actual_items = mapping.viewitems() + + # These views don't actually extend from collections.*View + for iterable in (actual_keys, actual_values, actual_items): + assert_false(isinstance(iterable, list)) + + assert_equals(frozenset(actual_keys), expected_keys) + assert_equals(frozenset(actual_values), expected_values) + assert_equals(frozenset(actual_items), expected_items) + + # What happens if mapping isn't a dict + from attrdict.mapping import AttrMap + + mapping = options.constructor(AttrMap(raw)) + + actual_keys = mapping.viewkeys() + actual_values = mapping.viewvalues() + actual_items = mapping.viewitems() + + # These views don't actually extend from collections.*View + for iterable in (actual_keys, actual_values, actual_items): + assert_false(isinstance(iterable, list)) + + assert_equals(frozenset(actual_keys), expected_keys) + assert_equals(frozenset(actual_values), expected_values) + assert_equals(frozenset(actual_items), expected_items) + + else: # methods are actually views + assert_true(isinstance(actual_keys, KeysView)) + assert_equals(frozenset(actual_keys), expected_keys) + + assert_true(isinstance(actual_values, ValuesView)) + assert_equals(frozenset(actual_values), expected_values) + + assert_true(isinstance(actual_items, ItemsView)) + assert_equals(frozenset(actual_items), expected_items) + + # make sure empty iteration works + assert_equals(tuple(options.constructor().items()), ()) + + +def containment(options): + "Check whether {cls} contains keys" + mapping = options.constructor( + {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} + ) + empty = options.constructor() + + assert_true('foo' in mapping) + assert_false('foo' in empty) + + assert_true(frozenset((1, 2, 3)) in mapping) + assert_false(frozenset((1, 2, 3)) in empty) + + assert_true(1 in mapping) + assert_false(1 in empty) + + assert_false('banana' in mapping) + assert_false('banana' in empty) + + +def length(options): + "Get the length of an {cls} instance" + assert_equals(len(options.constructor()), 0) + assert_equals(len(options.constructor({'foo': 'bar'})), 1) + assert_equals(len(options.constructor({'foo': 'bar', 'baz': 'qux'})), 2) + + +def equality(options): + "Equality checks for {cls}" + empty = {} + mapping_a = {'foo': 'bar'} + mapping_b = {'lorem': 'ipsum'} + + constructor = options.constructor + + assert_true(constructor(empty) == empty) + assert_false(constructor(empty) != empty) + assert_false(constructor(empty) == mapping_a) + assert_true(constructor(empty) != mapping_a) + assert_false(constructor(empty) == mapping_b) + assert_true(constructor(empty) != mapping_b) + + assert_false(constructor(mapping_a) == empty) + assert_true(constructor(mapping_a) != empty) + assert_true(constructor(mapping_a) == mapping_a) + assert_false(constructor(mapping_a) != mapping_a) + assert_false(constructor(mapping_a) == mapping_b) + assert_true(constructor(mapping_a) != mapping_b) + + assert_false(constructor(mapping_b) == empty) + assert_true(constructor(mapping_b) != empty) + assert_false(constructor(mapping_b) == mapping_a) + assert_true(constructor(mapping_b) != mapping_a) + assert_true(constructor(mapping_b) == mapping_b) + assert_false(constructor(mapping_b) != mapping_b) + + assert_true(constructor(empty) == constructor(empty)) + assert_false(constructor(empty) != constructor(empty)) + assert_false(constructor(empty) == constructor(mapping_a)) + assert_true(constructor(empty) != constructor(mapping_a)) + assert_false(constructor(empty) == constructor(mapping_b)) + assert_true(constructor(empty) != constructor(mapping_b)) + + assert_false(constructor(mapping_a) == constructor(empty)) + assert_true(constructor(mapping_a) != constructor(empty)) + assert_true(constructor(mapping_a) == constructor(mapping_a)) + assert_false(constructor(mapping_a) != constructor(mapping_a)) + assert_false(constructor(mapping_a) == constructor(mapping_b)) + assert_true(constructor(mapping_a) != constructor(mapping_b)) + + assert_false(constructor(mapping_b) == constructor(empty)) + assert_true(constructor(mapping_b) != constructor(empty)) + assert_false(constructor(mapping_b) == constructor(mapping_a)) + assert_true(constructor(mapping_b) != constructor(mapping_a)) + assert_true(constructor(mapping_b) == constructor(mapping_b)) + assert_false(constructor(mapping_b) != constructor(mapping_b)) + + assert_true(constructor((('foo', 'bar'),)) == {'foo': 'bar'}) + + +def item_creation(options): + "Add a key-value pair to an {cls}" + + if not options.mutable: + # Assignment shouldn't add to the dict + mapping = options.constructor() + + try: + mapping.foo = 'bar' + except TypeError: + pass # may fail if setattr modified + else: + pass # may assign, but shouldn't assign to dict + + def item(): + """ + Attempt to add an item. + """ + mapping['foo'] = 'bar' + + assert_raises(TypeError, item) + + assert_false('foo' in mapping) + else: + mapping = options.constructor() + + # key that can be an attribute + mapping.foo = 'bar' + + assert_equals(mapping.foo, 'bar') + assert_equals(mapping['foo'], 'bar') + assert_equals(mapping('foo'), 'bar') + assert_equals(mapping.get('foo'), 'bar') + + mapping['baz'] = 'qux' + + assert_equals(mapping.baz, 'qux') + assert_equals(mapping['baz'], 'qux') + assert_equals(mapping('baz'), 'qux') + assert_equals(mapping.get('baz'), 'qux') + + # key that cannot be an attribute + assert_raises(TypeError, setattr, mapping, 1, 'one') + + assert_true(1 not in mapping) + + mapping[2] = 'two' + + assert_equals(mapping[2], 'two') + assert_equals(mapping(2), 'two') + assert_equals(mapping.get(2), 'two') + + # key that represents a hidden attribute + def add_foo(): + "add _foo to mapping" + mapping._foo = '_bar' + + assert_raises(TypeError, add_foo) + assert_false('_foo' in mapping) + + mapping['_baz'] = 'qux' + + def get_baz(): + "get the _foo attribute" + return mapping._baz + + assert_raises(AttributeError, get_baz) + assert_equals(mapping['_baz'], 'qux') + assert_equals(mapping('_baz'), 'qux') + assert_equals(mapping.get('_baz'), 'qux') + + # key that represents an attribute that already exists + def add_get(): + "add get to mapping" + mapping.get = 'attribute' + + assert_raises(TypeError, add_get) + assert_false('get' in mapping) + + mapping['get'] = 'value' + + assert_not_equals(mapping.get, 'value') + assert_equals(mapping['get'], 'value') + assert_equals(mapping('get'), 'value') + assert_equals(mapping.get('get'), 'value') + + # rewrite a value + mapping.foo = 'manchu' + + assert_equals(mapping.foo, 'manchu') + assert_equals(mapping['foo'], 'manchu') + assert_equals(mapping('foo'), 'manchu') + assert_equals(mapping.get('foo'), 'manchu') + + mapping['bar'] = 'bell' + + assert_equals(mapping.bar, 'bell') + assert_equals(mapping['bar'], 'bell') + assert_equals(mapping('bar'), 'bell') + assert_equals(mapping.get('bar'), 'bell') + + if options.recursive: + recursed = options.constructor({'foo': {'bar': 'baz'}}) + + recursed.foo.bar = 'qux' + recursed.foo.alpha = 'bravo' + + assert_equals(recursed, {'foo': {'bar': 'qux', 'alpha': 'bravo'}}) + + +def item_deletion(options): + "Remove a key-value from to an {cls}" + if not options.mutable: + mapping = options.constructor({'foo': 'bar'}) + + # could be a TypeError or an AttributeError + try: + del mapping.foo + except TypeError: + pass + except AttributeError: + pass + else: + raise AssertionError('deletion should fail') + + def item(mapping): + """ + Attempt to del an item + """ + del mapping['foo'] + + assert_raises(TypeError, item, mapping) + + assert_equals(mapping, {'foo': 'bar'}) + assert_equals(mapping.foo, 'bar') + assert_equals(mapping['foo'], 'bar') + else: + mapping = options.constructor( + {'foo': 'bar', 'lorem': 'ipsum', '_hidden': True, 'get': 'value'} + ) + + del mapping.foo + assert_false('foo' in mapping) + + del mapping['lorem'] + assert_false('lorem' in mapping) + + def del_hidden(): + "delete _hidden" + del mapping._hidden + + try: + del_hidden() + except KeyError: + pass + except TypeError: + pass + else: + assert_false("Test raised the appropriate exception") + # assert_raises(TypeError, del_hidden) + assert_true('_hidden' in mapping) + + del mapping['_hidden'] + assert_false('hidden' in mapping) + + def del_get(): + "delete get" + del mapping.get + + assert_raises(TypeError, del_get) + assert_true('get' in mapping) + assert_true(mapping.get('get'), 'value') + + del mapping['get'] + assert_false('get' in mapping) + assert_true(mapping.get('get', 'banana'), 'banana') + + +def sequence_typing(options): + "Does {cls} respect sequence type?" + data = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} + + tuple_mapping = options.constructor(data) + + assert_true(isinstance(tuple_mapping.list, tuple)) + assert_equals(tuple_mapping.list[0].foo, 'bar') + + assert_true(isinstance(tuple_mapping.tuple, tuple)) + assert_equals(tuple_mapping.tuple[0].foo, 'bar') + + list_mapping = options.constructor(data, sequence_type=list) + + assert_true(isinstance(list_mapping.list, list)) + assert_equals(list_mapping.list[0].foo, 'bar') + + assert_true(isinstance(list_mapping.tuple, list)) + assert_equals(list_mapping.tuple[0].foo, 'bar') + + none_mapping = options.constructor(data, sequence_type=None) + + assert_true(isinstance(none_mapping.list, list)) + assert_raises(AttributeError, lambda: none_mapping.list[0].foo) + + assert_true(isinstance(none_mapping.tuple, tuple)) + assert_raises(AttributeError, lambda: none_mapping.tuple[0].foo) + + +def addition(options): + "Adding {cls} to other mappings." + left = { + 'foo': 'bar', + 'mismatch': False, + 'sub': {'alpha': 'beta', 'a': 'b'}, + } + + right = { + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 'c': 'd'}, + } + + merged = { + 'foo': 'bar', + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 'a': 'b', 'c': 'd'} + } + + opposite = { + 'foo': 'bar', + 'lorem': 'ipsum', + 'mismatch': False, + 'sub': {'alpha': 'beta', 'a': 'b', 'c': 'd'} + } + + constructor = options.constructor + + assert_raises(TypeError, lambda: constructor() + 1) + assert_raises(TypeError, lambda: 1 + constructor()) + + assert_equals(constructor() + constructor(), {}) + assert_equals(constructor() + {}, {}) + assert_equals({} + constructor(), {}) + + assert_equals(constructor(left) + constructor(), left) + assert_equals(constructor(left) + {}, left) + assert_equals({} + constructor(left), left) + + assert_equals(constructor() + constructor(left), left) + assert_equals(constructor() + left, left) + assert_equals(left + constructor(), left) + + assert_equals(constructor(left) + constructor(right), merged) + assert_equals(constructor(left) + right, merged) + assert_equals(left + constructor(right), merged) + + assert_equals(constructor(right) + constructor(left), opposite) + assert_equals(constructor(right) + left, opposite) + assert_equals(right + constructor(left), opposite) + + # test sequence type changes + data = {'sequence': [{'foo': 'bar'}]} + + assert_true(isinstance((constructor(data) + {}).sequence, tuple)) + assert_true( + isinstance((constructor(data) + constructor()).sequence, tuple) + ) + + assert_true(isinstance((constructor(data, list) + {}).sequence, list)) + # assert_true( + # isinstance((constructor(data, list) + constructor()).sequence, tuple) + # ) + + assert_true(isinstance((constructor(data, list) + {}).sequence, list)) + assert_true( + isinstance( + (constructor(data, list) + constructor({}, list)).sequence, + list + ) + ) + + +def to_kwargs(options): + "**{cls}" + def return_results(**kwargs): + "Return result passed into a function" + return kwargs + + expected = {'foo': 1, 'bar': 2} + + assert_equals(return_results(**options.constructor()), {}) + assert_equals(return_results(**options.constructor(expected)), expected) + + +def check_pickle_roundtrip(source, options, **kwargs): + """ + serialize then deserialize a mapping, ensuring the result and initial + objects are equivalent. + """ + source = options.constructor(source, **kwargs) + data = pickle.dumps(source) + loaded = pickle.loads(data) + + assert_true(isinstance(loaded, options.cls)) + + assert_equals(source, loaded) + + return loaded + + +def pickling(options): + "Pickle {cls}" + + empty = check_pickle_roundtrip(None, options) + assert_equals(empty, {}) + + mapping = check_pickle_roundtrip({'foo': 'bar'}, options) + assert_equals(mapping, {'foo': 'bar'}) + + # make sure sequence_type is preserved + raw = {'list': [{'a': 'b'}], 'tuple': ({'a': 'b'},)} + + as_tuple = check_pickle_roundtrip(raw, options) + assert_true(isinstance(as_tuple['list'], list)) + assert_true(isinstance(as_tuple['tuple'], tuple)) + assert_true(isinstance(as_tuple.list, tuple)) + assert_true(isinstance(as_tuple.tuple, tuple)) + + as_list = check_pickle_roundtrip(raw, options, sequence_type=list) + assert_true(isinstance(as_list['list'], list)) + assert_true(isinstance(as_list['tuple'], tuple)) + assert_true(isinstance(as_list.list, list)) + assert_true(isinstance(as_list.tuple, list)) + + as_raw = check_pickle_roundtrip(raw, options, sequence_type=None) + assert_true(isinstance(as_raw['list'], list)) + assert_true(isinstance(as_raw['tuple'], tuple)) + assert_true(isinstance(as_raw.list, list)) + assert_true(isinstance(as_raw.tuple, tuple)) + + +def pop(options): + "Popping from {cls}" + + mapping = options.constructor({'foo': 'bar', 'baz': 'qux'}) + + assert_raises(KeyError, lambda: mapping.pop('lorem')) + assert_equals(mapping.pop('lorem', 'ipsum'), 'ipsum') + assert_equals(mapping, {'foo': 'bar', 'baz': 'qux'}) + + assert_equals(mapping.pop('baz'), 'qux') + assert_false('baz' in mapping) + assert_equals(mapping, {'foo': 'bar'}) + + assert_equals(mapping.pop('foo', 'qux'), 'bar') + assert_false('foo' in mapping) + assert_equals(mapping, {}) + + +def popitem(options): + "Popping items from {cls}" + expected = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'beta'} + actual = {} + + mapping = options.constructor(dict(expected)) + + for _ in range(3): + key, value = mapping.popitem() + + assert_equals(expected[key], value) + actual[key] = value + + assert_equals(expected, actual) + + assert_raises(AttributeError, lambda: mapping.foo) + assert_raises(AttributeError, lambda: mapping.lorem) + assert_raises(AttributeError, lambda: mapping.alpha) + assert_raises(KeyError, mapping.popitem) + + +def clear(options): + "clear the {cls}" + + mapping = options.constructor( + {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'beta'} + ) + + mapping.clear() + + assert_equals(mapping, {}) + + assert_raises(AttributeError, lambda: mapping.foo) + assert_raises(AttributeError, lambda: mapping.lorem) + assert_raises(AttributeError, lambda: mapping.alpha) + + +def update(options): + "update a {cls}" + + mapping = options.constructor({'foo': 'bar', 'alpha': 'bravo'}) + + mapping.update(alpha='beta', lorem='ipsum') + assert_equals(mapping, {'foo': 'bar', 'alpha': 'beta', 'lorem': 'ipsum'}) + + mapping.update({'foo': 'baz', 1: 'one'}) + assert_equals( + mapping, + {'foo': 'baz', 'alpha': 'beta', 'lorem': 'ipsum', 1: 'one'} + ) + + assert_equals(mapping.foo, 'baz') + assert_equals(mapping.alpha, 'beta') + assert_equals(mapping.lorem, 'ipsum') + assert_equals(mapping(1), 'one') + + +def setdefault(options): + "{cls}.setdefault" + + mapping = options.constructor({'foo': 'bar'}) + + assert_equals(mapping.setdefault('foo', 'baz'), 'bar') + assert_equals(mapping.foo, 'bar') + + assert_equals(mapping.setdefault('lorem', 'ipsum'), 'ipsum') + assert_equals(mapping.lorem, 'ipsum') + + assert_true(mapping.setdefault('return_none') is None) + assert_true(mapping.return_none is None) + + assert_equals(mapping.setdefault(1, 'one'), 'one') + assert_equals(mapping[1], 'one') + + assert_equals(mapping.setdefault('_hidden', 'yes'), 'yes') + assert_raises(AttributeError, lambda: mapping._hidden) + assert_equals(mapping['_hidden'], 'yes') + + assert_equals(mapping.setdefault('get', 'value'), 'value') + assert_not_equals(mapping.get, 'value') + assert_equals(mapping['get'], 'value') + + +def copying(options): + "copying a {cls}" + mapping_a = options.constructor({'foo': {'bar': 'baz'}}) + mapping_b = copy.copy(mapping_a) + mapping_c = mapping_b + + mapping_b.foo.lorem = 'ipsum' + + assert_equals(mapping_a, mapping_b) + assert_equals(mapping_b, mapping_c) + + mapping_c.alpha = 'bravo' + + +def deepcopying(options): + "deepcopying a {cls}" + mapping_a = options.constructor({'foo': {'bar': 'baz'}}) + mapping_b = copy.deepcopy(mapping_a) + mapping_c = mapping_b + + mapping_b['foo']['lorem'] = 'ipsum' + + assert_not_equals(mapping_a, mapping_b) + assert_equals(mapping_b, mapping_c) + + mapping_c.alpha = 'bravo' + + assert_not_equals(mapping_a, mapping_b) + assert_equals(mapping_b, mapping_c) + + assert_false('lorem' in mapping_a.foo) + assert_equals(mapping_a.setdefault('alpha', 'beta'), 'beta') + assert_equals(mapping_c.alpha, 'bravo') diff --git a/tests/test_depricated.py b/tests/test_depricated.py new file mode 100644 index 0000000..0a4a5b0 --- /dev/null +++ b/tests/test_depricated.py @@ -0,0 +1,30 @@ +""" +Tests for depricated methods. +""" +from nose.tools import assert_true, assert_false +from six import PY2 + + +if PY2: + def test_has_key(): + """ + The now-depricated has_keys method + """ + from attrdict.dictionary import AttrDict + + mapping = AttrDict( + {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} + ) + empty = AttrDict() + + assert_true(mapping.has_key('foo')) + assert_false(empty.has_key('foo')) + + assert_true(mapping.has_key(frozenset((1, 2, 3)))) + assert_false(empty.has_key(frozenset((1, 2, 3)))) + + assert_true(mapping.has_key(1)) + assert_false(empty.has_key(1)) + + assert_false(mapping.has_key('banana')) + assert_false(empty.has_key('banana')) diff --git a/tests/test_merge.py b/tests/test_merge.py new file mode 100644 index 0000000..7939e6d --- /dev/null +++ b/tests/test_merge.py @@ -0,0 +1,35 @@ +""" +Test the merge function +""" +from nose.tools import assert_equals + + +def test_merge(): + """ + merge function. + """ + from attrdict.merge import merge + + left = { + 'baz': 'qux', + 'mismatch': False, + 'sub': {'alpha': 'beta', 1: 2}, + } + right = { + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 3: 4}, + } + + assert_equals(merge({}, {}), {}) + assert_equals(merge(left, {}), left) + assert_equals(merge({}, right), right) + assert_equals( + merge(left, right), + { + 'baz': 'qux', + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 1: 2, 3: 4} + } + ) diff --git a/tests/test_mixins.py b/tests/test_mixins.py new file mode 100644 index 0000000..a05cc71 --- /dev/null +++ b/tests/test_mixins.py @@ -0,0 +1,69 @@ +""" +Tests for the AttrDefault class. +""" +from nose.tools import assert_equals, assert_raises + + +def test_invalid_attributes(): + """ + Tests how set/delattr handle invalid attributes. + """ + from attrdict.mapping import AttrMap + + mapping = AttrMap() + + # mapping currently has allow_invalid_attributes set to False + def assign(): + """ + Assign to an invalid attribute. + """ + mapping._key = 'value' + + assert_raises(TypeError, assign) + assert_raises(AttributeError, lambda: mapping._key) + assert_equals(mapping, {}) + + mapping._setattr('_allow_invalid_attributes', True) + + assign() + assert_equals(mapping._key, 'value') + assert_equals(mapping, {}) + + # delete the attribute + def delete(): + """ + Delete an invalid attribute. + """ + del mapping._key + + delete() + assert_raises(AttributeError, lambda: mapping._key) + assert_equals(mapping, {}) + + # now with disallowing invalid + assign() + mapping._setattr('_allow_invalid_attributes', False) + + assert_raises(TypeError, delete) + assert_equals(mapping._key, 'value') + assert_equals(mapping, {}) + + # force delete + mapping._delattr('_key') + assert_raises(AttributeError, lambda: mapping._key) + assert_equals(mapping, {}) + + +def test_constructor(): + """ + _constructor MUST be implemented. + """ + from attrdict.mixins import Attr + + class AttrImpl(Attr): + """ + An implementation of attr that doesn't implement _constructor. + """ + pass + + assert_raises(NotImplementedError, lambda: AttrImpl._constructor({}, ())) diff --git a/tox.ini b/tox.ini index 3b3a462..61f5748 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,13 @@ [tox] -envlist = py26, py27, py33, py34, pypy +envlist = py26, py27, py32, py33, py34, pypy, pypy3, flake8 [testenv] -commands = nosetests --with-coverage --cover-package attrdict -v +commands = python setup.py nosetests deps = -rrequirements-tests.txt + +[testenv:flake8] +deps = flake8 +commands = flake8 attrdict tests + +[flake8] +exclude = tests/test_depricated.py