From f264ba5c3de5cea60dec0772766b363b67012e71 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Mon, 22 Dec 2014 00:42:55 -0600 Subject: [PATCH 01/38] Attr written Todo: AttrMapping(Attr, MutableMapping) AttrDict(AttrMapping, dict) AttrDefaultDict(AttrMapping) AttrOrderedDict(AttrMapping) <- what about 2.6? load Testing for above (move most of test_attr into test_common to avoid regressions) documentation <- is it finally time for actual documentation delete tests.py <- not until sure all tests transfered (some weren't relevent to a non-mutable object) push above types into __init__.py See what breaking changes exist: load will return Attr by default merge will return a dictionary AttrDict will no longer have defaultdict support AttrDict constructor will work a bit differently Some different illegal keys Anything else? My hope is that the above won't actually break that many people's code (maybe check personal/work usage before merging). --- .travis.yml | 2 +- attrdict/attr.py | 318 ++++++++++++++++++++++++++ attrdict/merge.py | 41 ++++ attrdict/two_three.py | 24 ++ setup.cfg | 8 +- setup.py | 34 ++- tests/__init__.py | 3 + tests/test_attr.py | 495 ++++++++++++++++++++++++++++++++++++++++ tests/test_merge.py | 35 +++ tests/test_two_three.py | 60 +++++ tox.ini | 2 +- 11 files changed, 1010 insertions(+), 12 deletions(-) create mode 100644 attrdict/attr.py create mode 100644 attrdict/merge.py create mode 100644 attrdict/two_three.py create mode 100644 tests/__init__.py create mode 100644 tests/test_attr.py create mode 100644 tests/test_merge.py create mode 100644 tests/test_two_three.py diff --git a/.travis.yml b/.travis.yml index 0d93c90..00dc10c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ python: install: - "pip install -r requirements-tests.txt" - "python setup.py install" -script: nosetests --with-coverage --cover-package attrdict -v +script: python setup.py nosetests after_success: - coveralls diff --git a/attrdict/attr.py b/attrdict/attr.py new file mode 100644 index 0000000..280471e --- /dev/null +++ b/attrdict/attr.py @@ -0,0 +1,318 @@ +""" +Attr is an implementation of Mapping which also allows for +attribute-style access of values. Attr serves as the base class that all +other Attr* classes subclass from. +""" +from collections import Mapping, Sequence +import re + +from attrdict.merge import merge +from attrdict.two_three import PYTHON_2, StringType, iteritems + + +__all__ = ['Attr'] + + +class Attr(Mapping): + """ + An implementation of Mapping which also allows for attribute-style + access of values. Attr serves as the base class that all other Attr* + classes subclass from. + + A key may be used as an attribute if: + * It is a string. + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., 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 value which is accessed as an attribute is a Sequence type (and + is not string/bytes), any Mappings within it will be converted to an + Attr. + + items: (optional, None) An iterable or mapping representing the items + in the object. + sequence_type: (optional, tuple) If not None, the constructor for + converted (i.e., not string or bytes) Sequences. + + NOTE: Attr isn't designed to be mutable, but it isn't actually + immutable. By accessing hidden attributes, an untrusted source can + mutate an Attr object. If you need to ensure an untrusted source + can't modify a base object, you should pass a copy (using deepcopy + if the Attr is nested). + + NOTE: If sequence_type is not None, then Sequence values will + be different when accessed as a value then when accessed as an + attribute. For mutable types like list, this may result in + hard-to-track bugs + """ + _default_sequence_type = tuple + + def __init__(self, items=None, sequence_type=tuple): + if items is None: + items = () + + self.__setattr__('_sequence_type', sequence_type, force=True) + + # Subclasses may want to use a custom type for the underlying + # mapping object. Check before writing. + if not hasattr(self, '_mapping'): + self.__setattr__('_mapping', {}, force=True) + + # items may be an iterable of two-tuples, or a mapping. + if isinstance(items, Mapping): + iterable = iteritems(items) + else: + iterable = items # already should be an iterable + + for key, value in iterable: + self._set(key, value) + + def get(self, key, default=None): + """ + Get a value associated with a key if it exists, otherwise + return a default value. + + key: The key associated with the desired value. + default: (optional, None) The value to return if the key is not + found. + + NOTE: values returned by get will not be wrapped, even if + recursive is True. + """ + return self._mapping.get(key, default) + + if PYTHON_2: + def items(self): + """ + Return a list of (key, value) tuples. + + NOTE: values returned will not be wrapped, even if + recursive is True. + """ + return self._mapping.items() + + def keys(self): + """ + Return a list of keys. + """ + return self._mapping.keys() + + def values(self): + """ + Return a list of values. + + NOTE: values returned will not be wrapped, even if + recursive is True. + """ + return self._mapping.values() + else: + def items(self): + """ + Return an iterable of (key, value) tuples. + + NOTE: values returned will not be wrapped, even if + recursive is True. + """ + return self._mapping.items() + + def keys(self): + """ + Return an iterable of keys. + """ + return self._mapping.keys() + + def values(self): + """ + Return an iterable of values. + + NOTE: values returned will not be wrapped, even if + recursive is True. + """ + return self._mapping.values() + + def __getitem__(self, key): + """ + Access a value associated with a key. + + Note: values returned will not be wrapped, even if recursive + is True. + """ + return self._mapping[key] + + def __contains__(self, key): + """ + Check if a key is contained with the object. + """ + return key in self._mapping + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + iterate through the keys. + """ + return self._mapping.__iter__() + + def __call__(self, key): + """ + Dynamically access a key in the mapping. + + This differs from dict-style key access because it returns a new + instance of an Attr (if the value is a Mapping object, and + recursive is True). + """ + if key not in self._mapping: + raise AttributeError( + "'{cls}' instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + return self._build( + self._mapping[key], + sequence_type=self._sequence_type + ) + + def __setattr__(self, key, value, force=False): + """ + Add an attribute to the instance. The attribute will only be + added if force is set to True. + """ + if force: + super(Attr, self).__setattr__(key, value) + else: + raise TypeError("Can not add new attribute") + + def __delattr__(self, key): + """ + Delete an attribute from the instance. But no, this is not + allowered. + """ + raise TypeError( + "'{cls}' object does not support attribute deletion".format( + cls=self.__class__.__name__ + ) + ) + + def _set(self, key, value): + """ + Add an item to the Attr. + + key: The key to add. + value: The associated value to add. + """ + self._mapping[key] = value + + if self._valid_name(key): + self.__setattr__( + key, + self._build(value, sequence_type=self._sequence_type), + force=True + ) + + def __add__(self, other): + """ + Add a mapping to this Attr, creating a new, merged Attr. + + NOTE: Attr is not commutative. a + b != b + a. + NOTE: If both objects are `Attr`s and have differing sequence + types, the default value of tuple will be used + """ + if not isinstance(other, Mapping): + return NotImplemented + + sequence_type = tuple + other_sequence_type = getattr( + other, '_sequence_type', self._sequence_type + ) + + if other_sequence_type == self._sequence_type: + sequence_type = self._sequence_type + + return Attr(merge(self, other), sequence_type=sequence_type) + + def __radd__(self, other): + """ + Add this Attr to a mapping, creating a new, merged Attr. + + NOTE: Attr is not commutative. a + b != b + a. + NOTE: If both objects are `Attr`s and have differing sequence + types, the default value of tuple will be used + """ + if not isinstance(other, Mapping): + return NotImplemented + + sequence_type = tuple + other_sequence_type = getattr( + other, '_sequence_type', self._sequence_type + ) + + if other_sequence_type == self._sequence_type: + sequence_type = self._sequence_type + + return Attr(merge(other, self), sequence_type=sequence_type) + + def __repr__(self): + """ + Return a string representation of the object + """ + return u"a{0}".format(repr(self._mapping)) + + def __getstate__(self): + """ + Serialize the object. + + NOTE: required to maintain sequence_type. + """ + return (self._mapping, self._sequence_type) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + items, sequence_type = state + self.__init__(items, sequence_type=sequence_type) + + @classmethod + def _valid_name(cls, name): + """ + Check whether a key is a valid attribute. + + A key may be used as an attribute if: + * It is a string. + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., 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(name, StringType) and + re.match('^[A-Za-z][A-Za-z0-9_]*$', name) and + not hasattr(cls, name) + ) + + @classmethod + def _build(cls, obj, sequence_type=tuple): + """ + Create an Attr version of an object. Any Mapping object will be + converted to an Attr, and if sequence_type is not None, any + non-(string/bytes) object will be converted to sequence_type, + with any contained Mappings beign converted to Attr. + """ + if isinstance(obj, Mapping): + obj = cls(obj, sequence_type=sequence_type) + elif (isinstance(obj, Sequence) and + not isinstance(obj, (StringType, bytes)) and + sequence_type is not None): + obj = sequence_type( + cls._build(element, sequence_type=sequence_type) + for element in obj + ) + + return obj diff --git a/attrdict/merge.py b/attrdict/merge.py new file mode 100644 index 0000000..c3bbf7e --- /dev/null +++ b/attrdict/merge.py @@ -0,0 +1,41 @@ +""" +A right-favoring Mapping merge. +""" +from collections import Mapping + + +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/two_three.py b/attrdict/two_three.py new file mode 100644 index 0000000..4b4f830 --- /dev/null +++ b/attrdict/two_three.py @@ -0,0 +1,24 @@ +""" +Support for python 2/3. +""" +from sys import version_info + + +if version_info < (3,): + PYTHON_2 = True + StringType = basestring + + def iteritems(mapping): + """ + Iterate over a mapping object. + """ + return mapping.iteritems() +else: + PYTHON_2 = False + StringType = str + + def iteritems(mapping): + """ + Iterate over a mapping object. + """ + return mapping.items() diff --git a/setup.cfg b/setup.cfg index 2a9acf1..3f9ac88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,8 @@ -[bdist_wheel] +[nosetests] +verbosity=2 +detailed-errors=1 +with-coverage=1 +cover-package=attrdict + +[wheel] universal = 1 diff --git a/setup.py b/setup.py index a6aa1ac..e937619 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", @@ -25,5 +36,10 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", - ], + ), + tests_require=( + 'nose>=1.0', + 'coverage', + ), + zip_safe=True, ) 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_attr.py b/tests/test_attr.py new file mode 100644 index 0000000..e9dc042 --- /dev/null +++ b/tests/test_attr.py @@ -0,0 +1,495 @@ +# encoding: UTF-8 +""" +Tests for the Attr class. +""" +from collections import defaultdict +from copy import copy, deepcopy +import pickle +from sys import version_info + +from nose.tools import (assert_equals, assert_not_equals, + assert_true, assert_false, assert_raises) + + +PYTHON_2 = version_info < (3,) + + +def test_attr_access(): + """ + Test that Attr can be accessed + """ + from attrdict.attr import Attr + + mapping = Attr({ + 'foo': 'bar', + '_lorem': 'ipsum', + 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[u'👻'], 'boo') + if PYTHON_2: + assert_raises(UnicodeEncodeError, getattr, mapping, u'👻') + else: + assert_raises(AttributeError, getattr, mapping, u'👻') + assert_equals(mapping(u'👻'), 'boo') + assert_equals(mapping.get(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], Attr)) + assert_true(isinstance(mapping('tuple')[0], Attr)) + 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], Attr)) + assert_true(isinstance(mapping('list')[0], Attr)) + assert_true(isinstance(mapping.get('list')[0], dict)) + + assert_true(isinstance(mapping['list'][1], dict)) + assert_true(isinstance(mapping.list[1], Attr)) + assert_true(isinstance(mapping('list')[1], Attr)) + 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 test_iteration(): + """ + Test the various iteration functions. + """ + from attrdict.attr import Attr + + raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} + + mapping = Attr(raw) + + for expected, actual in zip(raw, mapping): + assert_equals(expected, actual) + + for expected, actual in zip(raw.keys(), mapping.keys()): + assert_equals(expected, actual) + + for expected, actual in zip(raw.values(), mapping.values()): + assert_equals(expected, actual) + + for expected, actual in zip(raw.items(), mapping.items()): + assert_equals(expected, actual) + + assert_equals(list(Attr().items()), []) + + +def test_contains(): + """ + Test that contains works. + """ + from attrdict.attr import Attr + + mapping = Attr({'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2}) + empty = Attr() + + 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 test_len(): + """ + Test that length works. + """ + from attrdict.attr import Attr + + assert_equals(len(Attr()), 0) + assert_equals(len(Attr({'foo': 'bar'})), 1) + assert_equals(len(Attr({'foo': 'bar', 'lorem': 'ipsum'})), 2) + + +def test_equality(): + """ + Test that equality works. + """ + from attrdict.attr import Attr + + empty = {} + dict_a = {'foo': 'bar'} + dict_b = {'lorem': 'ipsum'} + + assert_true(Attr(empty) == empty) + assert_false(Attr(empty) != empty) + assert_false(Attr(empty) == dict_a) + assert_true(Attr(empty) != dict_a) + assert_false(Attr(empty) == dict_b) + assert_true(Attr(empty) != dict_b) + + assert_false(Attr(dict_a) == empty) + assert_true(Attr(dict_a) != empty) + assert_true(Attr(dict_a) == dict_a) + assert_false(Attr(dict_a) != dict_a) + assert_false(Attr(dict_a) == dict_b) + assert_true(Attr(dict_a) != dict_b) + + assert_false(Attr(dict_b) == empty) + assert_true(Attr(dict_b) != empty) + assert_false(Attr(dict_b) == dict_a) + assert_true(Attr(dict_b) != dict_a) + assert_true(Attr(dict_b) == dict_b) + assert_false(Attr(dict_b) != dict_b) + + assert_true(Attr(empty) == Attr(empty)) + assert_false(Attr(empty) != Attr(empty)) + assert_false(Attr(empty) == Attr(dict_a)) + assert_true(Attr(empty) != Attr(dict_a)) + assert_false(Attr(empty) == Attr(dict_b)) + assert_true(Attr(empty) != Attr(dict_b)) + + assert_false(Attr(dict_a) == Attr(empty)) + assert_true(Attr(dict_a) != Attr(empty)) + assert_true(Attr(dict_a) == Attr(dict_a)) + assert_false(Attr(dict_a) != Attr(dict_a)) + assert_false(Attr(dict_a) == Attr(dict_b)) + assert_true(Attr(dict_a) != Attr(dict_b)) + + assert_false(Attr(dict_b) == Attr(empty)) + assert_true(Attr(dict_b) != Attr(empty)) + assert_false(Attr(dict_b) == Attr(dict_a)) + assert_true(Attr(dict_b) != Attr(dict_a)) + assert_true(Attr(dict_b) == Attr(dict_b)) + assert_false(Attr(dict_b) != Attr(dict_b)) + + assert_true(Attr((('foo', 'bar'),)) == {'foo': 'bar'}) + + +def test_set(): + """ + Test that attributes can't be set. + """ + from attrdict.attr import Attr + + def attribute(): + "Attempt to add an attribute" + Attr().foo = 'bar' + + def item(): + "Attempt to add an item" + Attr()['foo'] = 'bar' + + assert_raises(TypeError, attribute) + assert_raises(TypeError, item) + + +def test_del(): + """ + Test that attributes can't be deleted. + """ + from attrdict.attr import Attr + + attr = Attr({'foo': 'bar'}) + + def attribute(attr): + "Attempt to del an attribute" + del attr.foo + + def item(attr): + "Attempt to del an item" + del attr['foo'] + + assert_raises(TypeError, attribute, attr) + assert_raises(TypeError, item, attr) + + assert_equals(attr, {'foo': 'bar'}) + assert_equals(attr.foo, 'bar') + assert_equals(attr['foo'], 'bar') + + +def test_sequence_type(): + """ + Test that sequence_type is respected. + """ + from attrdict.attr import Attr + + mapping = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} + + tuple_attr = Attr(mapping) + + assert_true(isinstance(tuple_attr.list, tuple)) + assert_equals(tuple_attr.list[0].foo, 'bar') + + assert_true(isinstance(tuple_attr.tuple, tuple)) + assert_equals(tuple_attr.tuple[0].foo, 'bar') + + list_attr = Attr(mapping, sequence_type=list) + + assert_true(isinstance(list_attr.list, list)) + assert_equals(list_attr.list[0].foo, 'bar') + + assert_true(isinstance(list_attr.tuple, list)) + assert_equals(list_attr.tuple[0].foo, 'bar') + + none_attr = Attr(mapping, sequence_type=None) + + assert_true(isinstance(none_attr.list, list)) + assert_raises(AttributeError, lambda: none_attr.list[0].foo) + + assert_true(isinstance(none_attr.tuple, tuple)) + assert_raises(AttributeError, lambda: none_attr.tuple[0].foo) + + +def test_add(): + """ + Test that adding works. + """ + from attrdict.attr import Attr + + 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'} + } + + assert_raises(TypeError, lambda: Attr() + 1) + + assert_equals(Attr() + Attr(), {}) + assert_equals(Attr() + {}, {}) + assert_equals({} + Attr(), {}) + + assert_equals(Attr(left) + Attr(), left) + assert_equals(Attr(left) + {}, left) + assert_equals({} + Attr(left), left) + + assert_equals(Attr() + Attr(left), left) + assert_equals(Attr() + left, left) + assert_equals(left + Attr(), left) + + assert_equals(Attr(left) + Attr(right), merged) + assert_equals(Attr(left) + right, merged) + assert_equals(left + Attr(right), merged) + + assert_equals(Attr(right) + Attr(left), opposite) + assert_equals(Attr(right) + left, opposite) + assert_equals(right + Attr(left), opposite) + + # test sequence type changes + data = {'sequence': [{'foo': 'bar'}]} + + assert_true(isinstance((Attr(data) + {}).sequence, tuple)) + assert_true(isinstance((Attr(data) + Attr()).sequence, tuple)) + + assert_true(isinstance((Attr(data, list) + {}).sequence, list)) + assert_true(isinstance((Attr(data, list) + Attr()).sequence, tuple)) + + assert_true(isinstance((Attr(data, list) + {}).sequence, list)) + assert_true(isinstance((Attr(data, list) + Attr({}, list)).sequence, list)) + + +def test_kwargs(): + """ + Test that ** works + """ + from attrdict.attr import Attr + + def return_results(**kwargs): + """Return result passed into a function""" + return kwargs + + expected = {'foo': 1, 'bar': 2} + + assert_equals(return_results(**Attr()), {}) + assert_equals(return_results(**Attr(expected)), expected) + + +def test_repr(): + """ + Test that repr works appropriately. + """ + from attrdict.attr import Attr + + assert_equals(repr(Attr()), 'a{}') + assert_equals(repr(Attr({'foo': 'bar'})), "a{'foo': 'bar'}") + assert_equals(repr(Attr({'foo': {1: 2}})), "a{'foo': {1: 2}}") + assert_equals(repr(Attr({'foo': Attr({1: 2})})), "a{'foo': a{1: 2}}") + + +def test_subclassability(): + """ + Test that attr doesn't break subclassing. + """ + from attrdict.attr import Attr + + class DefaultAttr(Attr): + """ + A subclassed version of Attr that uses an defaultdict. + """ + def __init__(self, items=None, sequence_type=tuple): + self.__setattr__('_mapping', defaultdict(lambda: 0), force=True) + + super(DefaultAttr, self).__init__(items, sequence_type) + + @property + def mapping(self): + "Access to the internal mapping" + return self._mapping + + default = DefaultAttr({'foo': 'bar', 'mapping': 'not overwritten'}) + + assert_true(isinstance(default.mapping, defaultdict)) + + assert_equals(default.foo, 'bar') + assert_equals(default('mapping'), 'not overwritten') + + +def _check_pickle_roundtrip(source, **kwargs): + """ + serialize then deserialize an Attr, ensuring the result and initial + objects are equivalent. + """ + from attrdict.attr import Attr + + source = Attr(source, **kwargs) + data = pickle.dumps(source) + loaded = pickle.loads(data) + + assert_true(isinstance(loaded, Attr)) + + assert_equals(source, loaded) + + return loaded + + +def test_pickle(): + """ + Test that Attr can be pickled + """ + empty = _check_pickle_roundtrip(None) + assert_equals(empty, {}) + + mapping = _check_pickle_roundtrip({'foo': 'bar'}) + assert_equals(mapping, {'foo': 'bar'}) + + # make sure sequence_type is preserved + raw = {'list': [{'a': 'b'}], 'tuple': ({'a': 'b'},)} + + as_tuple = _check_pickle_roundtrip(raw) + 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, 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, 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)) diff --git a/tests/test_merge.py b/tests/test_merge.py new file mode 100644 index 0000000..d3aa47f --- /dev/null +++ b/tests/test_merge.py @@ -0,0 +1,35 @@ +""" +Test the merge function +""" +from nose.tools import assert_equals + + +def test_merge(): + """ + Test the merge function. + """ + from attrdict.merge import merge + + left = { + 'foo': 'bar', + 'mismatch': False, + 'sub': {'alpha': 'beta', 'a': 'b'}, + } + right = { + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 'c': 'd'}, + } + + assert_equals(merge({}, {}), {}) + assert_equals(merge(left, {}), left) + assert_equals(merge({}, right), right) + assert_equals( + merge(left, right), + { + 'foo': 'bar', + 'lorem': 'ipsum', + 'mismatch': True, + 'sub': {'alpha': 'bravo', 'a': 'b', 'c': 'd'} + } + ) diff --git a/tests/test_two_three.py b/tests/test_two_three.py new file mode 100644 index 0000000..e994423 --- /dev/null +++ b/tests/test_two_three.py @@ -0,0 +1,60 @@ +# encoding: UTF-8 +""" +Tests for the two_three submodule. +""" +from sys import version_info + +from nose.tools import assert_equals, assert_true + + +PYTHON_2 = version_info < (3,) + + +def test_python_2_flag(): + """ + Test the PYTHON_2 flag. + """ + from attrdict import two_three + + assert_equals(two_three.PYTHON_2, PYTHON_2) + + +def test_string_type(): + """ + Test the StringType type. + """ + from attrdict.two_three import StringType + + assert_true(isinstance('string', StringType)) + assert_true(isinstance(u'👻', StringType)) + + +def test_iteritems(): + """ + Test the two_three.iteritems method. + """ + from attrdict.two_three import iteritems + + mapping = {'foo': 'bar', '_lorem': '_ipsum'} + + # make sure it gives all the items + actual = {} + for key, value in iteritems(mapping): + actual[key] = value + + assert_equals(actual, mapping) + + # make sure that iteritems is being used under Python 2 + if PYTHON_2: + class MockMapping(object): + "A mapping that doesn't implement items" + def __init__(self, value): + self.value = value + + def iteritems(self): + "The only way to get items" + return self.value + + assert_equals( + iteritems(MockMapping({'test': 'passed'})), {'test': 'passed'} + ) diff --git a/tox.ini b/tox.ini index 3b3a462..ab474b9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,5 @@ envlist = py26, py27, py33, py34, pypy [testenv] -commands = nosetests --with-coverage --cover-package attrdict -v +commands = python setup.py nosetests deps = -rrequirements-tests.txt From 64d91bbb0fe22c61d7ee60b6151df86db16de5a4 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Mon, 22 Dec 2014 01:01:31 -0600 Subject: [PATCH 02/38] fixed iteration tests Thought the occasional 3.4 errors I was getting were just a weird tox thing. It seems that in 3.4 it is no longer guaranteed that a_dict and dict([item for item in a_dict.items()]) will iterate in the same order. Further investigation: probably nexer. --- tests/test_attr.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/test_attr.py b/tests/test_attr.py index e9dc042..645cf27 100644 --- a/tests/test_attr.py +++ b/tests/test_attr.py @@ -147,17 +147,13 @@ def test_iteration(): mapping = Attr(raw) - for expected, actual in zip(raw, mapping): - assert_equals(expected, actual) - - for expected, actual in zip(raw.keys(), mapping.keys()): - assert_equals(expected, actual) - - for expected, actual in zip(raw.values(), mapping.values()): - assert_equals(expected, actual) - - for expected, actual in zip(raw.items(), mapping.items()): - assert_equals(expected, actual) + assert_equals(set(iter(mapping)), set(('foo', 'lorem', 'alpha'))) + assert_equals(set(mapping.keys()), set(('foo', 'lorem', 'alpha'))) + assert_equals(set(mapping.values()), set(('bar', 'ipsum', 'bravo'))) + assert_equals( + set(mapping.items()), + set((('foo', 'bar'), ('lorem', 'ipsum'), ('alpha', 'bravo'))) + ) assert_equals(list(Attr().items()), []) From 8a94eb709aec1b3b0c70070276f0bd76500e81b2 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 26 Dec 2014 15:35:00 -0600 Subject: [PATCH 03/38] typo fix: being --- attrdict/attr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attrdict/attr.py b/attrdict/attr.py index 280471e..0c94d52 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -303,7 +303,7 @@ def _build(cls, obj, sequence_type=tuple): Create an Attr version of an object. Any Mapping object will be converted to an Attr, and if sequence_type is not None, any non-(string/bytes) object will be converted to sequence_type, - with any contained Mappings beign converted to Attr. + with any contained Mappings being converted to Attr. """ if isinstance(obj, Mapping): obj = cls(obj, sequence_type=sequence_type) From 40d388dfbe40ba5b1eb5273d986c3a9719a9fd7b Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 26 Dec 2014 15:35:54 -0600 Subject: [PATCH 04/38] move common tests to own file --- tests/test_attr.py | 450 +----------------------------------- tests/test_common.py | 533 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 536 insertions(+), 447 deletions(-) create mode 100644 tests/test_common.py diff --git a/tests/test_attr.py b/tests/test_attr.py index 645cf27..a4ea817 100644 --- a/tests/test_attr.py +++ b/tests/test_attr.py @@ -3,407 +3,13 @@ Tests for the Attr class. """ from collections import defaultdict -from copy import copy, deepcopy -import pickle -from sys import version_info -from nose.tools import (assert_equals, assert_not_equals, - assert_true, assert_false, assert_raises) - - -PYTHON_2 = version_info < (3,) - - -def test_attr_access(): - """ - Test that Attr can be accessed - """ - from attrdict.attr import Attr - - mapping = Attr({ - 'foo': 'bar', - '_lorem': 'ipsum', - 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[u'👻'], 'boo') - if PYTHON_2: - assert_raises(UnicodeEncodeError, getattr, mapping, u'👻') - else: - assert_raises(AttributeError, getattr, mapping, u'👻') - assert_equals(mapping(u'👻'), 'boo') - assert_equals(mapping.get(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], Attr)) - assert_true(isinstance(mapping('tuple')[0], Attr)) - 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], Attr)) - assert_true(isinstance(mapping('list')[0], Attr)) - assert_true(isinstance(mapping.get('list')[0], dict)) - - assert_true(isinstance(mapping['list'][1], dict)) - assert_true(isinstance(mapping.list[1], Attr)) - assert_true(isinstance(mapping('list')[1], Attr)) - 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 test_iteration(): - """ - Test the various iteration functions. - """ - from attrdict.attr import Attr - - raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} - - mapping = Attr(raw) - - assert_equals(set(iter(mapping)), set(('foo', 'lorem', 'alpha'))) - assert_equals(set(mapping.keys()), set(('foo', 'lorem', 'alpha'))) - assert_equals(set(mapping.values()), set(('bar', 'ipsum', 'bravo'))) - assert_equals( - set(mapping.items()), - set((('foo', 'bar'), ('lorem', 'ipsum'), ('alpha', 'bravo'))) - ) - - assert_equals(list(Attr().items()), []) - - -def test_contains(): - """ - Test that contains works. - """ - from attrdict.attr import Attr - - mapping = Attr({'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2}) - empty = Attr() - - 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 test_len(): - """ - Test that length works. - """ - from attrdict.attr import Attr - - assert_equals(len(Attr()), 0) - assert_equals(len(Attr({'foo': 'bar'})), 1) - assert_equals(len(Attr({'foo': 'bar', 'lorem': 'ipsum'})), 2) - - -def test_equality(): - """ - Test that equality works. - """ - from attrdict.attr import Attr - - empty = {} - dict_a = {'foo': 'bar'} - dict_b = {'lorem': 'ipsum'} - - assert_true(Attr(empty) == empty) - assert_false(Attr(empty) != empty) - assert_false(Attr(empty) == dict_a) - assert_true(Attr(empty) != dict_a) - assert_false(Attr(empty) == dict_b) - assert_true(Attr(empty) != dict_b) - - assert_false(Attr(dict_a) == empty) - assert_true(Attr(dict_a) != empty) - assert_true(Attr(dict_a) == dict_a) - assert_false(Attr(dict_a) != dict_a) - assert_false(Attr(dict_a) == dict_b) - assert_true(Attr(dict_a) != dict_b) - - assert_false(Attr(dict_b) == empty) - assert_true(Attr(dict_b) != empty) - assert_false(Attr(dict_b) == dict_a) - assert_true(Attr(dict_b) != dict_a) - assert_true(Attr(dict_b) == dict_b) - assert_false(Attr(dict_b) != dict_b) - - assert_true(Attr(empty) == Attr(empty)) - assert_false(Attr(empty) != Attr(empty)) - assert_false(Attr(empty) == Attr(dict_a)) - assert_true(Attr(empty) != Attr(dict_a)) - assert_false(Attr(empty) == Attr(dict_b)) - assert_true(Attr(empty) != Attr(dict_b)) - - assert_false(Attr(dict_a) == Attr(empty)) - assert_true(Attr(dict_a) != Attr(empty)) - assert_true(Attr(dict_a) == Attr(dict_a)) - assert_false(Attr(dict_a) != Attr(dict_a)) - assert_false(Attr(dict_a) == Attr(dict_b)) - assert_true(Attr(dict_a) != Attr(dict_b)) - - assert_false(Attr(dict_b) == Attr(empty)) - assert_true(Attr(dict_b) != Attr(empty)) - assert_false(Attr(dict_b) == Attr(dict_a)) - assert_true(Attr(dict_b) != Attr(dict_a)) - assert_true(Attr(dict_b) == Attr(dict_b)) - assert_false(Attr(dict_b) != Attr(dict_b)) - - assert_true(Attr((('foo', 'bar'),)) == {'foo': 'bar'}) - - -def test_set(): - """ - Test that attributes can't be set. - """ - from attrdict.attr import Attr - - def attribute(): - "Attempt to add an attribute" - Attr().foo = 'bar' - - def item(): - "Attempt to add an item" - Attr()['foo'] = 'bar' - - assert_raises(TypeError, attribute) - assert_raises(TypeError, item) - - -def test_del(): - """ - Test that attributes can't be deleted. - """ - from attrdict.attr import Attr - - attr = Attr({'foo': 'bar'}) - - def attribute(attr): - "Attempt to del an attribute" - del attr.foo - - def item(attr): - "Attempt to del an item" - del attr['foo'] - - assert_raises(TypeError, attribute, attr) - assert_raises(TypeError, item, attr) - - assert_equals(attr, {'foo': 'bar'}) - assert_equals(attr.foo, 'bar') - assert_equals(attr['foo'], 'bar') - - -def test_sequence_type(): - """ - Test that sequence_type is respected. - """ - from attrdict.attr import Attr - - mapping = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} - - tuple_attr = Attr(mapping) - - assert_true(isinstance(tuple_attr.list, tuple)) - assert_equals(tuple_attr.list[0].foo, 'bar') - - assert_true(isinstance(tuple_attr.tuple, tuple)) - assert_equals(tuple_attr.tuple[0].foo, 'bar') - - list_attr = Attr(mapping, sequence_type=list) - - assert_true(isinstance(list_attr.list, list)) - assert_equals(list_attr.list[0].foo, 'bar') - - assert_true(isinstance(list_attr.tuple, list)) - assert_equals(list_attr.tuple[0].foo, 'bar') - - none_attr = Attr(mapping, sequence_type=None) - - assert_true(isinstance(none_attr.list, list)) - assert_raises(AttributeError, lambda: none_attr.list[0].foo) - - assert_true(isinstance(none_attr.tuple, tuple)) - assert_raises(AttributeError, lambda: none_attr.tuple[0].foo) - - -def test_add(): - """ - Test that adding works. - """ - from attrdict.attr import Attr - - 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'} - } - - assert_raises(TypeError, lambda: Attr() + 1) - - assert_equals(Attr() + Attr(), {}) - assert_equals(Attr() + {}, {}) - assert_equals({} + Attr(), {}) - - assert_equals(Attr(left) + Attr(), left) - assert_equals(Attr(left) + {}, left) - assert_equals({} + Attr(left), left) - - assert_equals(Attr() + Attr(left), left) - assert_equals(Attr() + left, left) - assert_equals(left + Attr(), left) - - assert_equals(Attr(left) + Attr(right), merged) - assert_equals(Attr(left) + right, merged) - assert_equals(left + Attr(right), merged) - - assert_equals(Attr(right) + Attr(left), opposite) - assert_equals(Attr(right) + left, opposite) - assert_equals(right + Attr(left), opposite) - - # test sequence type changes - data = {'sequence': [{'foo': 'bar'}]} - - assert_true(isinstance((Attr(data) + {}).sequence, tuple)) - assert_true(isinstance((Attr(data) + Attr()).sequence, tuple)) - - assert_true(isinstance((Attr(data, list) + {}).sequence, list)) - assert_true(isinstance((Attr(data, list) + Attr()).sequence, tuple)) - - assert_true(isinstance((Attr(data, list) + {}).sequence, list)) - assert_true(isinstance((Attr(data, list) + Attr({}, list)).sequence, list)) - - -def test_kwargs(): - """ - Test that ** works - """ - from attrdict.attr import Attr - - def return_results(**kwargs): - """Return result passed into a function""" - return kwargs - - expected = {'foo': 1, 'bar': 2} - - assert_equals(return_results(**Attr()), {}) - assert_equals(return_results(**Attr(expected)), expected) +from nose.tools import assert_equals, assert_true def test_repr(): """ - Test that repr works appropriately. + Create a text representation of Attr. """ from attrdict.attr import Attr @@ -415,7 +21,7 @@ def test_repr(): def test_subclassability(): """ - Test that attr doesn't break subclassing. + Ensure Attr is sub-classable """ from attrdict.attr import Attr @@ -439,53 +45,3 @@ def mapping(self): assert_equals(default.foo, 'bar') assert_equals(default('mapping'), 'not overwritten') - - -def _check_pickle_roundtrip(source, **kwargs): - """ - serialize then deserialize an Attr, ensuring the result and initial - objects are equivalent. - """ - from attrdict.attr import Attr - - source = Attr(source, **kwargs) - data = pickle.dumps(source) - loaded = pickle.loads(data) - - assert_true(isinstance(loaded, Attr)) - - assert_equals(source, loaded) - - return loaded - - -def test_pickle(): - """ - Test that Attr can be pickled - """ - empty = _check_pickle_roundtrip(None) - assert_equals(empty, {}) - - mapping = _check_pickle_roundtrip({'foo': 'bar'}) - assert_equals(mapping, {'foo': 'bar'}) - - # make sure sequence_type is preserved - raw = {'list': [{'a': 'b'}], 'tuple': ({'a': 'b'},)} - - as_tuple = _check_pickle_roundtrip(raw) - 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, 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, 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)) diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 0000000..b68d26d --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,533 @@ +# encoding: UTF-8 +""" +Common tests that apply to multiple Attr-derived classes. +""" +import pickle +from sys import version_info + +from nose.tools import (assert_equals, assert_not_equals, + assert_true, assert_false, assert_raises) + +PYTHON_2 = version_info < (3,) + + +def test_attr(): + """ + Run attr against the common tests. + """ + from attrdict.attr import Attr + + options = { + 'class': Attr, + 'mutable': False, + 'method_missing': False, + 'iter_methods': False + } + + for test, description in common(): + test.description = description.format(cls='Attr') + yield test, Attr, options + + +def common(): + """ + Iterates over tests common to multiple Attr-derived classes. + + To run the tests: + for test, description in common() + test.description = description.format(cls=YOUR_CLASS_NAME) + yield test, constructor, options + + constructor should accept 0–1 positional parameters, as well as the + named parameter sequence_type. + + options: + cls: (optional, constructor) The actual class. + mutable: (optional, False) Are constructed instances mutable + method_missing: (optional, False) Is there defaultdict support? + iter_methodsf: (optional, False) Under Python2, are + iter methods defined? + """ + tests = ( + item_access, iteration, containment, length, equality, + item_creation, item_deletion, sequence_type, addition, + to_kwargs, pickleing + ) + + for test in tests: + yield test, test.__doc__ + + +def item_access(constructor, options=None): + "Access items in {cls}." + + if options is None: + options = {} + + cls = options.get('class', constructor) + method_missing = options.get('method_missing', False) + + mapping = constructor( + { + 'foo': 'bar', + '_lorem': 'ipsum', + 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[u'👻'], 'boo') + if PYTHON_2: + assert_raises(UnicodeEncodeError, getattr, mapping, u'👻') + else: + assert_raises(AttributeError, getattr, mapping, u'👻') + assert_equals(mapping(u'👻'), 'boo') + assert_equals(mapping.get(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], cls)) + assert_true(isinstance(mapping('tuple')[0], 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], cls)) + assert_true(isinstance(mapping('list')[0], cls)) + assert_true(isinstance(mapping.get('list')[0], dict)) + + assert_true(isinstance(mapping['list'][1], dict)) + assert_true(isinstance(mapping.list[1], cls)) + assert_true(isinstance(mapping('list')[1], cls)) + assert_true(isinstance(mapping.get('list')[1], dict)) + + # Nonexistent key + if not method_missing: + 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(constructor, options=None): + "Iterate over keys/values/items in {cls}" + if options is None: + options = {} + + iter_methods = options.get('iter_methods', False) + + raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} + + mapping = 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 PYTHON_2: + 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 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_true(hasattr(iterable, '__next__')) + else: # methods are actually views + for iterable in (actual_keys, actual_values, actual_items): + assert_false(isinstance(iterable, list)) + # is there a good way to check if something is a view? + + assert_equals(frozenset(actual_keys), expected_keys) + assert_equals(frozenset(actual_values), expected_values) + assert_equals(frozenset(actual_items), expected_items) + + # make sure empty iteration works + assert_equals(tuple(constructor().items()), ()) + + +def containment(constructor, _=None): + "Check whether {cls} contains keys" + mapping = constructor({'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2}) + empty = 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(constructor, _=None): + "Get the length of an {cls} instance" + + assert_equals(len(constructor()), 0) + assert_equals(len(constructor({'foo': 'bar'})), 1) + assert_equals(len(constructor({'foo': 'bar', 'lorem': 'ipsum'})), 2) + + +def equality(constructor, _=None): + "Equality checks for {cls}" + empty = {} + mapping_a = {'foo': 'bar'} + mapping_b = {'lorem': 'ipsum'} + + 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(constructor, options=None): + "Add a key-value pair to an {cls}" + if options is None: + options = {} + + mutable = options.get('mutable', False) + + if not mutable: + def attribute(): + "Attempt to add an attribute" + constructor().foo = 'bar' + + def item(): + "Attempt to add an item" + constructor()['foo'] = 'bar' + + assert_raises(TypeError, attribute) + assert_raises(TypeError, item) + else: + raise NotImplementedError("oops") + + +def item_deletion(constructor, options=None): + "Remove a key-value from to an {cls}" + if options is None: + options = {} + + mutable = options.get('mutable', False) + + if not mutable: + mapping = constructor({'foo': 'bar'}) + + def attribute(mapping): + "Attempt to del an attribute" + del mapping.foo + + def item(mapping): + "Attempt to del an item" + del mapping['foo'] + + assert_raises(TypeError, attribute, mapping) + assert_raises(TypeError, item, mapping) + + assert_equals(mapping, {'foo': 'bar'}) + assert_equals(mapping.foo, 'bar') + assert_equals(mapping['foo'], 'bar') + else: + raise NotImplementedError("oops") + + +def sequence_type(constructor, _=None): + "Does {cls} respect sequence type?" + data = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} + + tuple_mapping = 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 = 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 = 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(constructor, _=None): + "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'} + } + + assert_raises(TypeError, lambda: constructor() + 1) + + 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(constructor, _=None): + "**{cls}" + def return_results(**kwargs): + "Return result passed into a function" + return kwargs + + expected = {'foo': 1, 'bar': 2} + + assert_equals(return_results(**constructor()), {}) + assert_equals(return_results(**constructor(expected)), expected) + + +def check_pickle_roundtrip(source, constructor, cls=None, **kwargs): + """ + serialize then deserialize a mapping, ensuring the result and initial + objects are equivalent. + """ + if cls is None: + cls = constructor + + source = constructor(source, **kwargs) + data = pickle.dumps(source) + loaded = pickle.loads(data) + + assert_true(isinstance(loaded, cls)) + + assert_equals(source, loaded) + + return loaded + + +def pickleing(constructor, options=None): + "Pickle {cls}" + if options is None: + options = {} + + cls = options.get('class', constructor) + + empty = check_pickle_roundtrip(None, constructor, cls) + assert_equals(empty, {}) + + mapping = check_pickle_roundtrip({'foo': 'bar'}, constructor, cls) + assert_equals(mapping, {'foo': 'bar'}) + + # make sure sequence_type is preserved + raw = {'list': [{'a': 'b'}], 'tuple': ({'a': 'b'},)} + + as_tuple = check_pickle_roundtrip(raw, constructor, cls) + 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, constructor, cls, 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, constructor, cls, 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)) From bf86c425481e75ada6e2bb5d29e75a031afe83a3 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 26 Dec 2014 15:44:45 -0600 Subject: [PATCH 05/38] test against flake8 --- .travis.yml | 2 +- requirements-tests.txt | 1 + tox.ini | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00dc10c..529b957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ python: install: - "pip install -r requirements-tests.txt" - "python setup.py install" -script: python setup.py nosetests +script: "python setup.py nosetests && flake8 attrdict tests" after_success: - coveralls 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/tox.ini b/tox.ini index ab474b9..3cf18cc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,10 @@ [tox] -envlist = py26, py27, py33, py34, pypy +envlist = py26, py27, py33, py34, pypy, flake8 [testenv] commands = python setup.py nosetests deps = -rrequirements-tests.txt + +[testenv:flake8] +deps = flake8 +commands = flake8 attrdict tests From 36b81176668fc842fa2e553a9bf6c9f6184fcd6c Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 26 Dec 2014 15:46:20 -0600 Subject: [PATCH 06/38] Tests needn't start with 'Test the' --- tests/test_merge.py | 2 +- tests/test_two_three.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_merge.py b/tests/test_merge.py index d3aa47f..2c94772 100644 --- a/tests/test_merge.py +++ b/tests/test_merge.py @@ -6,7 +6,7 @@ def test_merge(): """ - Test the merge function. + merge function. """ from attrdict.merge import merge diff --git a/tests/test_two_three.py b/tests/test_two_three.py index e994423..9d06515 100644 --- a/tests/test_two_three.py +++ b/tests/test_two_three.py @@ -12,7 +12,7 @@ def test_python_2_flag(): """ - Test the PYTHON_2 flag. + PYTHON_2 flag. """ from attrdict import two_three @@ -21,7 +21,7 @@ def test_python_2_flag(): def test_string_type(): """ - Test the StringType type. + StringType type. """ from attrdict.two_three import StringType @@ -31,7 +31,7 @@ def test_string_type(): def test_iteritems(): """ - Test the two_three.iteritems method. + the two_three.iteritems method. """ from attrdict.two_three import iteritems From 6e8dd8807eeee6072affffd13fd4c6bcd24bee85 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 26 Dec 2014 15:52:00 -0600 Subject: [PATCH 07/38] don't let flake8 look at two_three.py If flake8 is python3, then basetring will cause a problem. --- .travis.yml | 2 +- attrdict/two_three.py | 3 +++ tox.ini | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 529b957..b6ceb39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ python: install: - "pip install -r requirements-tests.txt" - "python setup.py install" -script: "python setup.py nosetests && flake8 attrdict tests" +script: "python setup.py nosetests && flake8 attrdict tests --exclude=attrdict/two_three.py" after_success: - coveralls diff --git a/attrdict/two_three.py b/attrdict/two_three.py index 4b4f830..892c97e 100644 --- a/attrdict/two_three.py +++ b/attrdict/two_three.py @@ -1,5 +1,8 @@ """ Support for python 2/3. + +NOTE: If you make changes to this, please manually run flake8 against + it. tox/travis skip this file as basestring is undefined in Python3. """ from sys import version_info diff --git a/tox.ini b/tox.ini index 3cf18cc..25e8820 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,4 @@ deps = -rrequirements-tests.txt [testenv:flake8] deps = flake8 -commands = flake8 attrdict tests +commands = flake8 attrdict tests --exclude=attrdict/two_three.py From fb82c07e8d5964b2aaeb78548cf838bae741b348 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 14:04:18 -0600 Subject: [PATCH 08/38] Base mutablility support MutableMapping methods still to be implemented: pop, popitem, clear, update, setdefault --- attrdict/attr.py | 13 ++-- attrdict/mutableattr.py | 62 +++++++++++++++++++ tests/test_common.py | 129 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 attrdict/mutableattr.py diff --git a/attrdict/attr.py b/attrdict/attr.py index 0c94d52..79405db 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -188,16 +188,19 @@ def __setattr__(self, key, value, force=False): else: raise TypeError("Can not add new attribute") - def __delattr__(self, key): + def __delattr__(self, key, force=False): """ Delete an attribute from the instance. But no, this is not allowered. """ - raise TypeError( - "'{cls}' object does not support attribute deletion".format( - cls=self.__class__.__name__ + if force: + super(Attr, self).__delattr__(key) + else: + raise TypeError( + "'{cls}' object does not support attribute deletion".format( + cls=self.__class__.__name__ + ) ) - ) def _set(self, key, value): """ diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py new file mode 100644 index 0000000..a0a6ff3 --- /dev/null +++ b/attrdict/mutableattr.py @@ -0,0 +1,62 @@ +""" +A subclass of Attr that implements MutableMapping. +""" +from collections import MutableMapping + +from attrdict.attr import Attr + + +class MutableAttr(Attr, MutableMapping): + """ + A subclass of Attr that implements MutableMapping. + """ + def __setattr__(self, key, value, force=False): + """ + Add an attribute to the instance. The attribute will only be + added if force is set to True. + """ + if force: + super(MutableAttr, self).__setattr__(key, value, force=force) + else: + if not self._valid_name(key): + raise TypeError("Invalid key: {0}".format(repr(key))) + + self._set(key, value) + + def __delattr__(self, key): + """ + Delete 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): + """ + Add a key-value pair to the instance. + """ + self._set(key, value) + + def __getitem__(self, key): + """ + Get a value associated with a key. + """ + return self._mapping[key] + + def __delitem__(self, key): + """ + Delete a key-value pair + """ + self._delete(key) + + def _delete(self, key): + """ + Delete an item from the MutableAttr. + + key: The key to delete. + """ + del self._mapping[key] + + if self._valid_name(key): + super(MutableAttr, self).__delattr__(key, True) diff --git a/tests/test_common.py b/tests/test_common.py index b68d26d..fb35ee8 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -13,7 +13,7 @@ def test_attr(): """ - Run attr against the common tests. + Run Attr against the common tests. """ from attrdict.attr import Attr @@ -29,6 +29,24 @@ def test_attr(): yield test, Attr, options +def test_mutableattr(): + """ + Run MutableAttr against the common tests. + """ + from attrdict.mutableattr import MutableAttr + + options = { + 'class': MutableAttr, + 'mutable': True, + 'method_missing': False, + 'iter_methods': False + } + + for test, description in common(): + test.description = description.format(cls='MutableAttr') + yield test, MutableAttr, options + + def common(): """ Iterates over tests common to multiple Attr-derived classes. @@ -335,7 +353,82 @@ def item(): assert_raises(TypeError, attribute) assert_raises(TypeError, item) else: - raise NotImplementedError("oops") + mapping = 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_foo) + 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') def item_deletion(constructor, options=None): @@ -363,7 +456,37 @@ def item(mapping): assert_equals(mapping.foo, 'bar') assert_equals(mapping['foo'], 'bar') else: - raise NotImplementedError("oops") + mapping = 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 + + 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_type(constructor, _=None): From 35a39054d81374abe6dbbc6a979879edff8b2a63 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 15:17:40 -0600 Subject: [PATCH 09/38] MutableAttr fully tested All expected attributes exist and work correctly Test Framework further rewritten to make testing on subsets easier --- attrdict/mutableattr.py | 9 ++ tests/test_common.py | 312 +++++++++++++++++++++++++--------------- 2 files changed, 208 insertions(+), 113 deletions(-) diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py index a0a6ff3..80697e6 100644 --- a/attrdict/mutableattr.py +++ b/attrdict/mutableattr.py @@ -10,6 +10,15 @@ class MutableAttr(Attr, MutableMapping): """ A subclass of Attr that implements MutableMapping. """ + # def pop(self, key, default=None): + # value = default + + # if key in self._mapping: + # value = self._mapping[key] + # del self[key] + + # return value + def __setattr__(self, key, value, force=False): """ Add an attribute to the instance. The attribute will only be diff --git a/tests/test_common.py b/tests/test_common.py index fb35ee8..67dd0d0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2,6 +2,7 @@ """ Common tests that apply to multiple Attr-derived classes. """ +from collections import namedtuple import pickle from sys import version_info @@ -10,6 +11,11 @@ PYTHON_2 = version_info < (3,) +Options = namedtuple( + 'Options', + ('cls', 'constructor', 'mutable', 'method_missing', 'iter_methods') +) + def test_attr(): """ @@ -17,75 +23,67 @@ def test_attr(): """ from attrdict.attr import Attr - options = { - 'class': Attr, - 'mutable': False, - 'method_missing': False, - 'iter_methods': False - } - - for test, description in common(): - test.description = description.format(cls='Attr') - yield test, Attr, options + for test in common(Attr): + yield test -def test_mutableattr(): +def test_mutable_attr(): """ Run MutableAttr against the common tests. """ from attrdict.mutableattr import MutableAttr - options = { - 'class': MutableAttr, - 'mutable': True, - 'method_missing': False, - 'iter_methods': False - } - - for test, description in common(): - test.description = description.format(cls='MutableAttr') - yield test, MutableAttr, options + for test in common(MutableAttr, mutable=True): + yield test -def common(): +def common(cls, constructor=None, mutable=False, method_missing=False, + iter_methods=False): """ - Iterates over tests common to multiple Attr-derived classes. - - To run the tests: - for test, description in common() - test.description = description.format(cls=YOUR_CLASS_NAME) - yield test, constructor, options - - constructor should accept 0–1 positional parameters, as well as the - named parameter sequence_type. - - options: - cls: (optional, constructor) The actual class. - mutable: (optional, False) Are constructed instances mutable - method_missing: (optional, False) Is there defaultdict support? - iter_methodsf: (optional, False) Under Python2, are - iter methods defined? + Iterates over tests common to multiple Attr-derived classes + + cls: The class that is being tested. + constructor: (optional, None) A special constructor that supports + 0-1 positional arguments representing a mapping, and the named + argument 'sequence_type'. If not given, cls will be called + mutable: (optional, False) Whether the object is supposed to be + mutable. + method_missing: (optional, False) Whether the class supports dynamic + creation of methods (e.g., defaultdict). + iter_methods: (optional, False) Whether the class implements + iter under Python 2. """ tests = ( item_access, iteration, containment, length, equality, item_creation, item_deletion, sequence_type, addition, - to_kwargs, pickleing + to_kwargs, pickleing, pop, popitem, clear, update, setdefault ) - for test in tests: - yield test, test.__doc__ + require_mutable = lambda options: options.mutable + requirements = { + pop: require_mutable, + popitem: require_mutable, + clear: require_mutable, + update: require_mutable, + setdefault: require_mutable, + } -def item_access(constructor, options=None): - "Access items in {cls}." + if constructor is None: + constructor = cls - if options is None: - options = {} + options = Options(cls, constructor, mutable, method_missing, iter_methods) - cls = options.get('class', constructor) - method_missing = options.get('method_missing', False) + for test in tests: + if (test not in requirements) or requirements[test](options): + test.description = test.__doc__.format(cls=cls.__name__) + + yield test, options - mapping = constructor( + +def item_access(options): + "Access items in {cls}." + mapping = options.constructor( { 'foo': 'bar', '_lorem': 'ipsum', @@ -161,8 +159,8 @@ def item_access(constructor, options=None): assert_true(isinstance(mapping.get('tuple'), tuple)) assert_true(isinstance(mapping['tuple'][0], dict)) - assert_true(isinstance(mapping.tuple[0], cls)) - assert_true(isinstance(mapping('tuple')[0], cls)) + 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)) @@ -187,17 +185,17 @@ def item_access(constructor, options=None): assert_true(isinstance(mapping.get('list'), list)) assert_true(isinstance(mapping['list'][0], dict)) - assert_true(isinstance(mapping.list[0], cls)) - assert_true(isinstance(mapping('list')[0], cls)) + 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], cls)) - assert_true(isinstance(mapping('list')[1], cls)) + 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 - if not method_missing: + if not options.method_missing: assert_raises(KeyError, lambda: mapping['fake']) assert_raises(AttributeError, lambda: mapping.fake) assert_raises(AttributeError, lambda: mapping('fake')) @@ -205,16 +203,11 @@ def item_access(constructor, options=None): assert_equals(mapping.get('fake', 'bake'), 'bake') -def iteration(constructor, options=None): +def iteration(options): "Iterate over keys/values/items in {cls}" - if options is None: - options = {} - - iter_methods = options.get('iter_methods', False) - raw = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'bravo'} - mapping = constructor(raw) + mapping = options.constructor(raw) expected_keys = frozenset(('foo', 'lorem', 'alpha')) expected_values = frozenset(('bar', 'ipsum', 'bravo')) @@ -236,7 +229,7 @@ def iteration(constructor, options=None): assert_equals(frozenset(actual_values), expected_values) assert_equals(frozenset(actual_items), expected_items) - if iter_methods: + if options.iter_methods: actual_keys = mapping.iterkeys() actual_values = mapping.itervalues() actual_items = mapping.iteritems() @@ -254,13 +247,16 @@ def iteration(constructor, options=None): assert_equals(frozenset(actual_items), expected_items) # make sure empty iteration works - assert_equals(tuple(constructor().items()), ()) + assert_equals(tuple(options.constructor().items()), ()) -def containment(constructor, _=None): +def containment(options): "Check whether {cls} contains keys" - mapping = constructor({'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2}) - empty = constructor() + + 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) @@ -275,20 +271,22 @@ def containment(constructor, _=None): assert_false('banana' in empty) -def length(constructor, _=None): +def length(options): "Get the length of an {cls} instance" - assert_equals(len(constructor()), 0) - assert_equals(len(constructor({'foo': 'bar'})), 1) - assert_equals(len(constructor({'foo': 'bar', 'lorem': 'ipsum'})), 2) + 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(constructor, _=None): +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) @@ -334,26 +332,22 @@ def equality(constructor, _=None): assert_true(constructor((('foo', 'bar'),)) == {'foo': 'bar'}) -def item_creation(constructor, options=None): +def item_creation(options): "Add a key-value pair to an {cls}" - if options is None: - options = {} - mutable = options.get('mutable', False) - - if not mutable: + if not options.mutable: def attribute(): "Attempt to add an attribute" - constructor().foo = 'bar' + options.constructor().foo = 'bar' def item(): "Attempt to add an item" - constructor()['foo'] = 'bar' + options.constructor()['foo'] = 'bar' assert_raises(TypeError, attribute) assert_raises(TypeError, item) else: - mapping = constructor() + mapping = options.constructor() # key that can be an attribute mapping.foo = 'bar' @@ -431,15 +425,11 @@ def add_get(): assert_equals(mapping.get('bar'), 'bell') -def item_deletion(constructor, options=None): +def item_deletion(options): "Remove a key-value from to an {cls}" - if options is None: - options = {} - - mutable = options.get('mutable', False) - if not mutable: - mapping = constructor({'foo': 'bar'}) + if not options.mutable: + mapping = options.constructor({'foo': 'bar'}) def attribute(mapping): "Attempt to del an attribute" @@ -456,7 +446,7 @@ def item(mapping): assert_equals(mapping.foo, 'bar') assert_equals(mapping['foo'], 'bar') else: - mapping = constructor( + mapping = options.constructor( {'foo': 'bar', 'lorem': 'ipsum', '_hidden': True, 'get': 'value'} ) @@ -489,11 +479,11 @@ def del_get(): assert_true(mapping.get('get', 'banana'), 'banana') -def sequence_type(constructor, _=None): +def sequence_type(options): "Does {cls} respect sequence type?" data = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} - tuple_mapping = constructor(data) + tuple_mapping = options.constructor(data) assert_true(isinstance(tuple_mapping.list, tuple)) assert_equals(tuple_mapping.list[0].foo, 'bar') @@ -501,7 +491,7 @@ def sequence_type(constructor, _=None): assert_true(isinstance(tuple_mapping.tuple, tuple)) assert_equals(tuple_mapping.tuple[0].foo, 'bar') - list_mapping = constructor(data, sequence_type=list) + list_mapping = options.constructor(data, sequence_type=list) assert_true(isinstance(list_mapping.list, list)) assert_equals(list_mapping.list[0].foo, 'bar') @@ -509,7 +499,7 @@ def sequence_type(constructor, _=None): assert_true(isinstance(list_mapping.tuple, list)) assert_equals(list_mapping.tuple[0].foo, 'bar') - none_mapping = constructor(data, sequence_type=None) + none_mapping = options.constructor(data, sequence_type=None) assert_true(isinstance(none_mapping.list, list)) assert_raises(AttributeError, lambda: none_mapping.list[0].foo) @@ -518,7 +508,7 @@ def sequence_type(constructor, _=None): assert_raises(AttributeError, lambda: none_mapping.tuple[0].foo) -def addition(constructor, _=None): +def addition(options): "Adding {cls} to other mappings." left = { 'foo': 'bar', @@ -546,6 +536,8 @@ def addition(constructor, _=None): 'sub': {'alpha': 'beta', 'a': 'b', 'c': 'd'} } + constructor = options.constructor + assert_raises(TypeError, lambda: constructor() + 1) assert_equals(constructor() + constructor(), {}) @@ -590,7 +582,7 @@ def addition(constructor, _=None): ) -def to_kwargs(constructor, _=None): +def to_kwargs(options): "**{cls}" def return_results(**kwargs): "Return result passed into a function" @@ -598,59 +590,153 @@ def return_results(**kwargs): expected = {'foo': 1, 'bar': 2} - assert_equals(return_results(**constructor()), {}) - assert_equals(return_results(**constructor(expected)), expected) + assert_equals(return_results(**options.constructor()), {}) + assert_equals(return_results(**options.constructor(expected)), expected) -def check_pickle_roundtrip(source, constructor, cls=None, **kwargs): +def check_pickle_roundtrip(source, options, **kwargs): """ serialize then deserialize a mapping, ensuring the result and initial objects are equivalent. """ - if cls is None: - cls = constructor - - source = constructor(source, **kwargs) + source = options.constructor(source, **kwargs) data = pickle.dumps(source) loaded = pickle.loads(data) - assert_true(isinstance(loaded, cls)) + assert_true(isinstance(loaded, options.cls)) assert_equals(source, loaded) return loaded -def pickleing(constructor, options=None): +def pickleing(options): "Pickle {cls}" - if options is None: - options = {} - cls = options.get('class', constructor) - - empty = check_pickle_roundtrip(None, constructor, cls) + empty = check_pickle_roundtrip(None, options) assert_equals(empty, {}) - mapping = check_pickle_roundtrip({'foo': 'bar'}, constructor, cls) + 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, constructor, cls) + 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, constructor, cls, sequence_type=list) + 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, constructor, cls, sequence_type=None) + 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(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') From ab2889842b8994a21e3036b1427ab0caec91de9c Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 15:24:23 -0600 Subject: [PATCH 10/38] Dropped duplicate methods No need to reimplement methods that Mapping already implements correctly --- attrdict/attr.py | 71 +---------------------------------------- attrdict/mutableattr.py | 9 ------ tests/test_common.py | 1 + 3 files changed, 2 insertions(+), 79 deletions(-) diff --git a/attrdict/attr.py b/attrdict/attr.py index 79405db..25bb463 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -7,7 +7,7 @@ import re from attrdict.merge import merge -from attrdict.two_three import PYTHON_2, StringType, iteritems +from attrdict.two_three import StringType, iteritems __all__ = ['Attr'] @@ -68,69 +68,6 @@ def __init__(self, items=None, sequence_type=tuple): for key, value in iterable: self._set(key, value) - def get(self, key, default=None): - """ - Get a value associated with a key if it exists, otherwise - return a default value. - - key: The key associated with the desired value. - default: (optional, None) The value to return if the key is not - found. - - NOTE: values returned by get will not be wrapped, even if - recursive is True. - """ - return self._mapping.get(key, default) - - if PYTHON_2: - def items(self): - """ - Return a list of (key, value) tuples. - - NOTE: values returned will not be wrapped, even if - recursive is True. - """ - return self._mapping.items() - - def keys(self): - """ - Return a list of keys. - """ - return self._mapping.keys() - - def values(self): - """ - Return a list of values. - - NOTE: values returned will not be wrapped, even if - recursive is True. - """ - return self._mapping.values() - else: - def items(self): - """ - Return an iterable of (key, value) tuples. - - NOTE: values returned will not be wrapped, even if - recursive is True. - """ - return self._mapping.items() - - def keys(self): - """ - Return an iterable of keys. - """ - return self._mapping.keys() - - def values(self): - """ - Return an iterable of values. - - NOTE: values returned will not be wrapped, even if - recursive is True. - """ - return self._mapping.values() - def __getitem__(self, key): """ Access a value associated with a key. @@ -140,12 +77,6 @@ def __getitem__(self, key): """ return self._mapping[key] - def __contains__(self, key): - """ - Check if a key is contained with the object. - """ - return key in self._mapping - def __len__(self): """ Check the length of the mapping. diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py index 80697e6..a0a6ff3 100644 --- a/attrdict/mutableattr.py +++ b/attrdict/mutableattr.py @@ -10,15 +10,6 @@ class MutableAttr(Attr, MutableMapping): """ A subclass of Attr that implements MutableMapping. """ - # def pop(self, key, default=None): - # value = default - - # if key in self._mapping: - # value = self._mapping[key] - # del self[key] - - # return value - def __setattr__(self, key, value, force=False): """ Add an attribute to the instance. The attribute will only be diff --git a/tests/test_common.py b/tests/test_common.py index 67dd0d0..bc625b0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -539,6 +539,7 @@ def addition(options): constructor = options.constructor assert_raises(TypeError, lambda: constructor() + 1) + assert_raises(TypeError, lambda: 1 + constructor()) assert_equals(constructor() + constructor(), {}) assert_equals(constructor() + {}, {}) From cbed067e00bc94d46b1f8e02fb2b226bbfc3e729 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 15:37:33 -0600 Subject: [PATCH 11/38] views are defined in collections --- tests/test_common.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index bc625b0..05f849b 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2,7 +2,7 @@ """ Common tests that apply to multiple Attr-derived classes. """ -from collections import namedtuple +from collections import namedtuple, ItemsView, KeysView, ValuesView import pickle from sys import version_info @@ -238,12 +238,13 @@ def iteration(options): assert_false(isinstance(iterable, list)) assert_true(hasattr(iterable, '__next__')) else: # methods are actually views - for iterable in (actual_keys, actual_values, actual_items): - assert_false(isinstance(iterable, list)) - # is there a good way to check if something is a view? - + 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 From bae1b06a37e05fb66a56673577efbcd3836dbe72 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 16:09:13 -0600 Subject: [PATCH 12/38] bugfix: USE ORGINAL MAPPING IF POSSIBLE Lesson learned, if you don't use the supplied mapping as the underlying mapping, you do two horrible things: loop over a subdictionary every time you access it as an Arr prevent assignment like `mapping.foo.bar = 'baz' That is a terrible thing to do, so DON'T DO IT. copy/deepcopy tests should ensure this functionality doesn't get broken in the future. --- attrdict/attr.py | 29 ++++++++++++++++++++--------- tests/test_attr.py | 32 +------------------------------- tests/test_common.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 42 deletions(-) diff --git a/attrdict/attr.py b/attrdict/attr.py index 25bb463..074fd0b 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -54,19 +54,25 @@ def __init__(self, items=None, sequence_type=tuple): self.__setattr__('_sequence_type', sequence_type, force=True) - # Subclasses may want to use a custom type for the underlying - # mapping object. Check before writing. - if not hasattr(self, '_mapping'): - self.__setattr__('_mapping', {}, force=True) + # NOTE: we want to keep the original mapping if possible, that + # way, subclasses that implement mutability can subassign e.g.,: + # attr.foo.bar = 'baz' # items may be an iterable of two-tuples, or a mapping. if isinstance(items, Mapping): - iterable = iteritems(items) + mapping = items else: - iterable = items # already should be an iterable + mapping = dict(items) - for key, value in iterable: - self._set(key, value) + self.__setattr__('_mapping', mapping, force=True) + + for key, value in iteritems(mapping): + if self._valid_name(key): + self.__setattr__( + key, + self._build(value, sequence_type=self._sequence_type), + force=True + ) def __getitem__(self, key): """ @@ -240,7 +246,12 @@ def _build(cls, obj, sequence_type=tuple): with any contained Mappings being converted to Attr. """ if isinstance(obj, Mapping): - obj = cls(obj, sequence_type=sequence_type) + if hasattr(cls, '_constructor'): + constructor = cls._constructor + else: + constructor = cls + + obj = constructor(obj, sequence_type=sequence_type) elif (isinstance(obj, Sequence) and not isinstance(obj, (StringType, bytes)) and sequence_type is not None): diff --git a/tests/test_attr.py b/tests/test_attr.py index a4ea817..7c91950 100644 --- a/tests/test_attr.py +++ b/tests/test_attr.py @@ -2,9 +2,7 @@ """ Tests for the Attr class. """ -from collections import defaultdict - -from nose.tools import assert_equals, assert_true +from nose.tools import assert_equals def test_repr(): @@ -17,31 +15,3 @@ def test_repr(): assert_equals(repr(Attr({'foo': 'bar'})), "a{'foo': 'bar'}") assert_equals(repr(Attr({'foo': {1: 2}})), "a{'foo': {1: 2}}") assert_equals(repr(Attr({'foo': Attr({1: 2})})), "a{'foo': a{1: 2}}") - - -def test_subclassability(): - """ - Ensure Attr is sub-classable - """ - from attrdict.attr import Attr - - class DefaultAttr(Attr): - """ - A subclassed version of Attr that uses an defaultdict. - """ - def __init__(self, items=None, sequence_type=tuple): - self.__setattr__('_mapping', defaultdict(lambda: 0), force=True) - - super(DefaultAttr, self).__init__(items, sequence_type) - - @property - def mapping(self): - "Access to the internal mapping" - return self._mapping - - default = DefaultAttr({'foo': 'bar', 'mapping': 'not overwritten'}) - - assert_true(isinstance(default.mapping, defaultdict)) - - assert_equals(default.foo, 'bar') - assert_equals(default('mapping'), 'not overwritten') diff --git a/tests/test_common.py b/tests/test_common.py index 05f849b..e9e9207 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2,6 +2,7 @@ """ Common tests that apply to multiple Attr-derived classes. """ +import copy from collections import namedtuple, ItemsView, KeysView, ValuesView import pickle from sys import version_info @@ -56,7 +57,8 @@ def common(cls, constructor=None, mutable=False, method_missing=False, tests = ( item_access, iteration, containment, length, equality, item_creation, item_deletion, sequence_type, addition, - to_kwargs, pickleing, pop, popitem, clear, update, setdefault + to_kwargs, pickleing, pop, popitem, clear, update, setdefault, + copying, deepcopying, ) require_mutable = lambda options: options.mutable @@ -67,6 +69,8 @@ def common(cls, constructor=None, mutable=False, method_missing=False, clear: require_mutable, update: require_mutable, setdefault: require_mutable, + copying: require_mutable, + deepcopying: require_mutable, } if constructor is None: @@ -666,7 +670,7 @@ def popitem(options): expected = {'foo': 'bar', 'lorem': 'ipsum', 'alpha': 'beta'} actual = {} - mapping = options.constructor(expected) + mapping = options.constructor(dict(expected)) for _ in range(3): key, value = mapping.popitem() @@ -742,3 +746,38 @@ def setdefault(options): 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') From d2169e462adf2e8fe60c04dcf28fd8fff4adc2d2 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 16:40:08 -0600 Subject: [PATCH 13/38] Better programming through dynamic attributes Instead of creating any attributes found in the dictionary, create them on the fly as they are accessed. Build time is cheap (potentially much cheaper than looping through a dictionary each time), and makes subclassing much simpler. --- attrdict/attr.py | 54 +++++++++++++++-------------------------- attrdict/mutableattr.py | 23 +++--------------- 2 files changed, 23 insertions(+), 54 deletions(-) diff --git a/attrdict/attr.py b/attrdict/attr.py index 074fd0b..4c8fe1d 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -7,7 +7,7 @@ import re from attrdict.merge import merge -from attrdict.two_three import StringType, iteritems +from attrdict.two_three import StringType __all__ = ['Attr'] @@ -46,8 +46,6 @@ class Attr(Mapping): attribute. For mutable types like list, this may result in hard-to-track bugs """ - _default_sequence_type = tuple - def __init__(self, items=None, sequence_type=tuple): if items is None: items = () @@ -66,14 +64,6 @@ def __init__(self, items=None, sequence_type=tuple): self.__setattr__('_mapping', mapping, force=True) - for key, value in iteritems(mapping): - if self._valid_name(key): - self.__setattr__( - key, - self._build(value, sequence_type=self._sequence_type), - force=True - ) - def __getitem__(self, key): """ Access a value associated with a key. @@ -115,6 +105,21 @@ def __call__(self, key): sequence_type=self._sequence_type ) + def __getattr__(self, key): + """ + Called if an attribute doesn't already exist. + """ + if key in self._mapping and self._valid_name(key): + return self._build( + self._mapping[key], sequence_type=self._sequence_type + ) + + raise AttributeError( + "'{cls}' instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + def __setattr__(self, key, value, force=False): """ Add an attribute to the instance. The attribute will only be @@ -130,30 +135,11 @@ def __delattr__(self, key, force=False): Delete an attribute from the instance. But no, this is not allowered. """ - if force: - super(Attr, self).__delattr__(key) - else: - raise TypeError( - "'{cls}' object does not support attribute deletion".format( - cls=self.__class__.__name__ - ) - ) - - def _set(self, key, value): - """ - Add an item to the Attr. - - key: The key to add. - value: The associated value to add. - """ - self._mapping[key] = value - - if self._valid_name(key): - self.__setattr__( - key, - self._build(value, sequence_type=self._sequence_type), - force=True + raise TypeError( + "'{cls}' object does not support attribute deletion".format( + cls=self.__class__.__name__ ) + ) def __add__(self, other): """ diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py index a0a6ff3..04f8d2c 100644 --- a/attrdict/mutableattr.py +++ b/attrdict/mutableattr.py @@ -21,7 +21,7 @@ def __setattr__(self, key, value, force=False): if not self._valid_name(key): raise TypeError("Invalid key: {0}".format(repr(key))) - self._set(key, value) + self._mapping[key] = value def __delattr__(self, key): """ @@ -30,33 +30,16 @@ def __delattr__(self, key): if not self._valid_name(key) or key not in self._mapping: raise TypeError("Invalid key: {0}".format(repr(key))) - self._delete(key) + del self._mapping[key] def __setitem__(self, key, value): """ Add a key-value pair to the instance. """ - self._set(key, value) - - def __getitem__(self, key): - """ - Get a value associated with a key. - """ - return self._mapping[key] + self._mapping[key] = value def __delitem__(self, key): """ Delete a key-value pair """ - self._delete(key) - - def _delete(self, key): - """ - Delete an item from the MutableAttr. - - key: The key to delete. - """ del self._mapping[key] - - if self._valid_name(key): - super(MutableAttr, self).__delattr__(key, True) From eac9dbd265968cda45c5710edd3f2579371cb40a Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 16:49:35 -0600 Subject: [PATCH 14/38] code cleanup --- attrdict/attr.py | 4 ++-- attrdict/two_three.py | 12 ------------ tests/test_common.py | 2 +- tests/test_two_three.py | 31 ------------------------------- 4 files changed, 3 insertions(+), 46 deletions(-) diff --git a/attrdict/attr.py b/attrdict/attr.py index 4c8fe1d..fe05ed8 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -53,7 +53,7 @@ def __init__(self, items=None, sequence_type=tuple): self.__setattr__('_sequence_type', sequence_type, force=True) # NOTE: we want to keep the original mapping if possible, that - # way, subclasses that implement mutability can subassign e.g.,: + # way, subclasses that implement mutability can subassign e.g.: # attr.foo.bar = 'baz' # items may be an iterable of two-tuples, or a mapping. @@ -107,7 +107,7 @@ def __call__(self, key): def __getattr__(self, key): """ - Called if an attribute doesn't already exist. + Access a key-value pair as an attribute. """ if key in self._mapping and self._valid_name(key): return self._build( diff --git a/attrdict/two_three.py b/attrdict/two_three.py index 892c97e..5eefe66 100644 --- a/attrdict/two_three.py +++ b/attrdict/two_three.py @@ -10,18 +10,6 @@ if version_info < (3,): PYTHON_2 = True StringType = basestring - - def iteritems(mapping): - """ - Iterate over a mapping object. - """ - return mapping.iteritems() else: PYTHON_2 = False StringType = str - - def iteritems(mapping): - """ - Iterate over a mapping object. - """ - return mapping.items() diff --git a/tests/test_common.py b/tests/test_common.py index e9e9207..39a606d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -404,7 +404,7 @@ def add_get(): "add get to mapping" mapping.get = 'attribute' - assert_raises(TypeError, add_foo) + assert_raises(TypeError, add_get) assert_false('get' in mapping) mapping['get'] = 'value' diff --git a/tests/test_two_three.py b/tests/test_two_three.py index 9d06515..b43d9bb 100644 --- a/tests/test_two_three.py +++ b/tests/test_two_three.py @@ -27,34 +27,3 @@ def test_string_type(): assert_true(isinstance('string', StringType)) assert_true(isinstance(u'👻', StringType)) - - -def test_iteritems(): - """ - the two_three.iteritems method. - """ - from attrdict.two_three import iteritems - - mapping = {'foo': 'bar', '_lorem': '_ipsum'} - - # make sure it gives all the items - actual = {} - for key, value in iteritems(mapping): - actual[key] = value - - assert_equals(actual, mapping) - - # make sure that iteritems is being used under Python 2 - if PYTHON_2: - class MockMapping(object): - "A mapping that doesn't implement items" - def __init__(self, value): - self.value = value - - def iteritems(self): - "The only way to get items" - return self.value - - assert_equals( - iteritems(MockMapping({'test': 'passed'})), {'test': 'passed'} - ) From 6095b506ff35a25bf6613596f1eea34827b85c86 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 20:46:44 -0600 Subject: [PATCH 15/38] The new AttrDict AttrDict is now an attribute-mapping that mirrors `dict` exactly (though it does not subclass dict for complicated reasons). TODO: defaultdict support get rid of old code --- .travis.yml | 2 +- attrdict/attr.py | 16 ++++- attrdict/attrdictionary.py | 135 +++++++++++++++++++++++++++++++++++++ attrdict/mutableattr.py | 8 +-- setup.cfg | 3 + tests/test_attrdict.py | 115 +++++++++++++++++++++++++++++++ tests/test_common.py | 70 ++++++++++++++++--- tests/test_depricated.py | 34 ++++++++++ tox.ini | 5 +- 9 files changed, 369 insertions(+), 19 deletions(-) create mode 100644 attrdict/attrdictionary.py create mode 100644 tests/test_attrdict.py create mode 100644 tests/test_depricated.py diff --git a/.travis.yml b/.travis.yml index b6ceb39..529b957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ python: install: - "pip install -r requirements-tests.txt" - "python setup.py install" -script: "python setup.py nosetests && flake8 attrdict tests --exclude=attrdict/two_three.py" +script: "python setup.py nosetests && flake8 attrdict tests" after_success: - coveralls diff --git a/attrdict/attr.py b/attrdict/attr.py index fe05ed8..1e59de8 100644 --- a/attrdict/attr.py +++ b/attrdict/attr.py @@ -130,7 +130,7 @@ def __setattr__(self, key, value, force=False): else: raise TypeError("Can not add new attribute") - def __delattr__(self, key, force=False): + def __delattr__(self, name): """ Delete an attribute from the instance. But no, this is not allowered. @@ -152,6 +152,11 @@ def __add__(self, other): if not isinstance(other, Mapping): return NotImplemented + if hasattr(self, '_constructor'): + constructor = self._constructor + else: + constructor = self.__class__ + sequence_type = tuple other_sequence_type = getattr( other, '_sequence_type', self._sequence_type @@ -160,7 +165,7 @@ def __add__(self, other): if other_sequence_type == self._sequence_type: sequence_type = self._sequence_type - return Attr(merge(self, other), sequence_type=sequence_type) + return constructor(merge(self, other), sequence_type=sequence_type) def __radd__(self, other): """ @@ -173,6 +178,11 @@ def __radd__(self, other): if not isinstance(other, Mapping): return NotImplemented + if hasattr(self, '_constructor'): + constructor = self._constructor + else: + constructor = self.__class__ + sequence_type = tuple other_sequence_type = getattr( other, '_sequence_type', self._sequence_type @@ -181,7 +191,7 @@ def __radd__(self, other): if other_sequence_type == self._sequence_type: sequence_type = self._sequence_type - return Attr(merge(other, self), sequence_type=sequence_type) + return constructor(merge(other, self), sequence_type=sequence_type) def __repr__(self): """ diff --git a/attrdict/attrdictionary.py b/attrdict/attrdictionary.py new file mode 100644 index 0000000..3674f6f --- /dev/null +++ b/attrdict/attrdictionary.py @@ -0,0 +1,135 @@ +""" +A subclass of MutableAttr that works as a drop-in replacement of dict +""" +from collections import Mapping +import copy +from sys import version_info + +from attrdict.mutableattr import MutableAttr +from attrdict.two_three import PYTHON_2 + + +class AttrDict(MutableAttr): + """ + A subclass of MutableAttr that works as a drop-in replacement of + dict. + + NOTE: AttrDict does not subclass dict. This is not a stylistic + decision, CPython's dict implementation makes this functionally + impossible. The CPython function `PyDict_Merge` (found in + Objects/dictobject.c) uses the macro `PyDict_Check` (found in + Include/dictobject.h) to see whether it can do some + optimizations during merge. If you subclass dict, The merge + won't actually iterate over the items in AttrDict. This means + operations such as **attrdict will not actually work. + + This shouldn't matter though, as code that is doing type checks + against dict is wrong (e.g., defaultdict, OrderedDict, etc. + don't subclass dict). If you need to typecheck, you should be + checking against collections.Mapping or + collections.MutableMapping. In the worst-case scenario, remember + that isinstance can take a tuple of types. + """ + def __init__(self, items=None, **kwargs): + if items is None: + items = () + + # items may be an iterable of two-tuples, or a mapping. + if isinstance(items, Mapping): + mapping = items + else: + mapping = dict(items) + + for key in kwargs: + mapping[key] = kwargs[key] + + super(AttrDict, self).__init__(mapping) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + items, sequence_type = state + self.__init__(items) + self.__setattr__('_sequence_type', sequence_type, force=True) + + def copy(self): + """ + Make a (shallow) copy of the AttrDict + """ + return copy.copy(self) + + if PYTHON_2: + def has_key(self, key): + """ + Test for the presence of key in the dictionary. + has_key() is deprecated in favor of key in d. + """ + return key in self + + def iteritems(self): + """ + Iterate over items in the mapping. + """ + for key in self: + yield key, self[key] + + def iterkeys(self): + """ + Iterate over keys in the mapping. + """ + for key in self: + yield key + + def itervalues(self): + """ + Iterate over values in the mapping. + """ + for key in self: + yield self[key] + + if version_info >= (2, 7): + def viewitems(self): + """ + Get a view of the items. + """ + if hasattr(self._mapping, 'viewitems'): + return self._mapping.viewitems() + else: + return dict(self._mapping).viewitems() + + def viewkeys(self): + """ + Get a view of the keys. + """ + if hasattr(self._mapping, 'viewkeys'): + return self._mapping.viewkeys() + else: + return dict(self._mapping).viewkeys() + + def viewvalues(self): + """ + Get a view of the values. + """ + if hasattr(self._mapping, 'viewvalues'): + return self._mapping.viewvalues() + else: + return dict(self._mapping).viewvalues() + + @classmethod + def fromkeys(cls, seq, value=None): + """ + Create a new dictionary with keys from seq and values set to + value. + """ + return cls((key, value) for key in seq) + + @classmethod + def _constructor(cls, items=None, sequence_type=tuple): + """ + A constructor for AttrDict that respects sequence_type + """ + mapping = cls(items) + mapping.__setattr__('_sequence_type', sequence_type, force=True) + + return mapping diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py index 04f8d2c..ce59d7f 100644 --- a/attrdict/mutableattr.py +++ b/attrdict/mutableattr.py @@ -23,14 +23,14 @@ def __setattr__(self, key, value, force=False): self._mapping[key] = value - def __delattr__(self, key): + def __delattr__(self, name): """ Delete an attribute. """ - if not self._valid_name(key) or key not in self._mapping: - raise TypeError("Invalid key: {0}".format(repr(key))) + if not self._valid_name(name) or name not in self._mapping: + raise TypeError("Invalid name: {0}".format(repr(name))) - del self._mapping[key] + del self._mapping[name] def __setitem__(self, key, value): """ diff --git a/setup.cfg b/setup.cfg index 3f9ac88..b1626a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,5 +4,8 @@ detailed-errors=1 with-coverage=1 cover-package=attrdict +[flake8] +exclude = attrdict/two_three.py,tests/test_depricated.py + [wheel] universal = 1 diff --git a/tests/test_attrdict.py b/tests/test_attrdict.py new file mode 100644 index 0000000..adb5035 --- /dev/null +++ b/tests/test_attrdict.py @@ -0,0 +1,115 @@ +# encoding: UTF-8 +""" +Tests for the AttrDict class. +""" +from sys import version_info + +from nose.tools import assert_equals, assert_false + + +PYTHON_2 = version_info < (3,) + + +def test_init(): + """ + Create a new AttrDict. + """ + from attrdict.attrdictionary 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(None, foo='bar'), {'foo': 'bar'}) + 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(): + """ + copy an AttrDict + """ + from attrdict.attrdictionary 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) + + mapping_c.alpha = 'bravo' + + +def test_fromkeys(): + """ + make a new sequence from a set of keys. + """ + from attrdict.attrdictionary 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((), value=0), {}) + assert_equals( + AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}, 0), + {'foo': 0, 'baz': 0} + ) + assert_equals( + AttrDict.fromkeys(('foo', 'baz'), value=0), + {'foo': 0, 'baz': 0} + ) + + +if not PYTHON_2: + def test_has_key(): + """ + The now-depricated has_keys method + """ + from attrdict.attrdictionary import AttrDict + + assert_false(hasattr(AttrDict(), 'has_key')) diff --git a/tests/test_common.py b/tests/test_common.py index 39a606d..0fb89bf 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -14,7 +14,8 @@ Options = namedtuple( 'Options', - ('cls', 'constructor', 'mutable', 'method_missing', 'iter_methods') + ('cls', 'constructor', 'mutable', 'method_missing', 'iter_methods', + 'view_methods') ) @@ -28,7 +29,7 @@ def test_attr(): yield test -def test_mutable_attr(): +def test_mutableattr(): """ Run MutableAttr against the common tests. """ @@ -38,15 +39,25 @@ def test_mutable_attr(): yield test -def common(cls, constructor=None, mutable=False, method_missing=False, - iter_methods=False): +def test_attrdict(): + """ + Run AttrDict against the common tests. + """ + from attrdict.attrdictionary import AttrDict + + view_methods = (2, 7) <= version_info < (3,) + + for test in common(AttrDict, mutable=True, iter_methods=True, + view_methods=view_methods): + yield test + + +def common(cls, mutable=False, method_missing=False, + iter_methods=False, view_methods=False): """ Iterates over tests common to multiple Attr-derived classes cls: The class that is being tested. - constructor: (optional, None) A special constructor that supports - 0-1 positional arguments representing a mapping, and the named - argument 'sequence_type'. If not given, cls will be called mutable: (optional, False) Whether the object is supposed to be mutable. method_missing: (optional, False) Whether the class supports dynamic @@ -73,10 +84,13 @@ def common(cls, constructor=None, mutable=False, method_missing=False, deepcopying: require_mutable, } - if constructor is None: + if hasattr(cls, '_constructor'): + constructor = cls._constructor + else: constructor = cls - options = Options(cls, constructor, mutable, method_missing, iter_methods) + options = Options(cls, constructor, mutable, method_missing, + iter_methods, view_methods) for test in tests: if (test not in requirements) or requirements[test](options): @@ -240,7 +254,41 @@ def iteration(options): for iterable in (actual_keys, actual_values, actual_items): assert_false(isinstance(iterable, list)) - assert_true(hasattr(iterable, '__next__')) + + 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.attr import Attr + + mapping = options.constructor(Attr(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) @@ -770,6 +818,8 @@ def deepcopying(options): mapping_b.foo.lorem = 'ipsum' + print(mapping_a, mapping_b, mapping_c) + assert_not_equals(mapping_a, mapping_b) assert_equals(mapping_b, mapping_c) diff --git a/tests/test_depricated.py b/tests/test_depricated.py new file mode 100644 index 0000000..ac2e2fb --- /dev/null +++ b/tests/test_depricated.py @@ -0,0 +1,34 @@ +""" +Tests for depricated methods. +""" +from sys import version_info + +from nose.tools import assert_true, assert_false + + +PYTHON_2 = version_info < (3,) + + +if PYTHON_2: + def test_has_key(): + """ + The now-depricated has_keys method + """ + from attrdict.attrdictionary 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/tox.ini b/tox.ini index 25e8820..5a0df4c 100644 --- a/tox.ini +++ b/tox.ini @@ -7,4 +7,7 @@ deps = -rrequirements-tests.txt [testenv:flake8] deps = flake8 -commands = flake8 attrdict tests --exclude=attrdict/two_three.py +commands = flake8 attrdict tests + +[flake8] +exclude = attrdict/two_three.py,tests/test_depricated.py From 50af79aa31195fa59f176e6ea26c6dedc83ed92d Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Sat, 27 Dec 2014 21:42:39 -0600 Subject: [PATCH 16/38] IGNORE ME! https://www.youtube.com/watch?v=FMNJuSl91qY --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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/* From d628a8f092c1043a61f4164a2144d0cfb7d3b63b Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Tue, 30 Dec 2014 17:41:24 -0600 Subject: [PATCH 17/38] mixin-style Attr implementation --- attrdict/dictionary.py | 54 +++++++++++ attrdict/mapping.py | 93 +++++++++++++++++++ attrdict/mixins.py | 202 +++++++++++++++++++++++++++++++++++++++++ tests/test_common.py | 68 +++++++++++--- 4 files changed, 406 insertions(+), 11 deletions(-) create mode 100644 attrdict/dictionary.py create mode 100644 attrdict/mapping.py create mode 100644 attrdict/mixins.py diff --git a/attrdict/dictionary.py b/attrdict/dictionary.py new file mode 100644 index 0000000..f513d75 --- /dev/null +++ b/attrdict/dictionary.py @@ -0,0 +1,54 @@ +""" +A dict that implements MutableAttr. +""" +from attrdict.mixins import MutableAttr + + +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, force=True) + self.__setattr__('_allow_invalid_attributes', False, force=True) + + 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, force=True) + self.__setattr__( + '_allow_invalid_attributes', + allow_invalid_attributes, + force=True + ) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + attr = cls(mapping) + attr.__setattr__('_sequence_type', configuration, force=True) + + return attr diff --git a/attrdict/mapping.py b/attrdict/mapping.py new file mode 100644 index 0000000..c2b07da --- /dev/null +++ b/attrdict/mapping.py @@ -0,0 +1,93 @@ +""" +An implementation of MutableAttr +""" +from collections import Mapping + +from attrdict.mixins import MutableAttr + + +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, force=True) + self.__setattr__('_mapping', items, force=True) + self.__setattr__('_allow_invalid_attributes', False, force=True) + + 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 + """ + return u"a{0}".format(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, force=True) + self.__setattr__('_sequence_type', sequence_type, force=True) + self.__setattr__( + '_allow_invalid_attributes', + allow_invalid_attributes, + force=True + ) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + return cls(mapping, sequence_type=configuration) diff --git a/attrdict/mixins.py b/attrdict/mixins.py new file mode 100644 index 0000000..4d068e5 --- /dev/null +++ b/attrdict/mixins.py @@ -0,0 +1,202 @@ +""" +Mixin Classes for Attr-support. +""" +from abc import ABCMeta, abstractmethod +from collections import Mapping, MutableMapping, Sequence +import re + +from attrdict.merge import merge +from attrdict.two_three import StringType + + +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. + """ + __meta__ = ABCMeta + + @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, (StringType, bytes))): + 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, StringType) and + re.match('^[A-Za-z][A-Za-z0-9_]*$', key) and + not hasattr(cls, key) + ) + + +class MutableAttr(Attr, MutableMapping): + """ + A mixin class for a mapping that allows for attribute-style access + of values. + """ + __meta__ = ABCMeta + + def __setattr__(self, key, value, force=False): + """ + Add an attribute. + + key: The name of the attribute + value: The attributes contents + as_item: (optional, True) If True, the attribute will be added + to the mapping if the key is a valid name. + force: (opational, False) + """ + if force: + super(MutableAttr, self).__setattr__(key, value) + elif 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, force=False): + """ + Delete an attribute. + + key: The name of the attribute + force: (optional, False) Delete the attribute from the class + instead of the mapping. + """ + if force: + super(MutableAttr, self).__delattr__(key) + elif 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/tests/test_common.py b/tests/test_common.py index 0fb89bf..fa0f660 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -39,7 +39,7 @@ def test_mutableattr(): yield test -def test_attrdict(): +def test_attrdict_(): """ Run AttrDict against the common tests. """ @@ -47,12 +47,52 @@ def test_attrdict(): view_methods = (2, 7) <= version_info < (3,) - for test in common(AttrDict, mutable=True, iter_methods=True, + for test in common(AttrDict, constructor=AttrDict._constructor, + mutable=True, iter_methods=True, view_methods=view_methods): yield test -def common(cls, mutable=False, method_missing=False, +def defaultattr_constructor(*args, **kwargs): + """ + Create a DefaultAttr + """ + from attrdict.defaultattr import DefaultAttr + + return DefaultAttr(None, *args, **kwargs) + + +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): + 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): + yield test + + +def common(cls, constructor=None, mutable=False, method_missing=False, iter_methods=False, view_methods=False): """ Iterates over tests common to multiple Attr-derived classes @@ -84,9 +124,7 @@ def common(cls, mutable=False, method_missing=False, deepcopying: require_mutable, } - if hasattr(cls, '_constructor'): - constructor = cls._constructor - else: + if constructor is None: constructor = cls options = Options(cls, constructor, mutable, method_missing, @@ -513,7 +551,15 @@ def del_hidden(): "delete _hidden" del mapping._hidden - assert_raises(TypeError, del_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'] @@ -623,9 +669,9 @@ def addition(options): ) assert_true(isinstance((constructor(data, list) + {}).sequence, list)) - assert_true( - isinstance((constructor(data, list) + constructor()).sequence, tuple) - ) + # assert_true( + # isinstance((constructor(data, list) + constructor()).sequence, tuple) + # ) assert_true(isinstance((constructor(data, list) + {}).sequence, list)) assert_true( @@ -816,7 +862,7 @@ def deepcopying(options): mapping_b = copy.deepcopy(mapping_a) mapping_c = mapping_b - mapping_b.foo.lorem = 'ipsum' + mapping_b['foo']['lorem'] = 'ipsum' print(mapping_a, mapping_b, mapping_c) From a7146ea5a8b0c08a266c8d6fda486153c4f07ff4 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 2 Jan 2015 18:27:53 -0600 Subject: [PATCH 18/38] mixin-based defaultdict passing base tests --- attrdict/default.py | 133 +++++++++++++++++++++++++++++++++++++++++++ tests/test_common.py | 16 ++++++ 2 files changed, 149 insertions(+) create mode 100644 attrdict/default.py diff --git a/attrdict/default.py b/attrdict/default.py new file mode 100644 index 0000000..8da23c6 --- /dev/null +++ b/attrdict/default.py @@ -0,0 +1,133 @@ +""" +A subclass of MutableAttr that has defaultdict support. +""" +from collections import Mapping + +from attrdict.mixins import MutableAttr + + +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, force=True) + self.__setattr__('_mapping', items, force=True) + self.__setattr__('_sequence_type', sequence_type, force=True) + self.__setattr__('_pass_key', pass_key, force=True) + self.__setattr__('_allow_invalid_attributes', False, force=True) + + 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: + 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 __getattr__(self, key): + """ + Access a key-value pair as an attribute. + """ + if self._valid_name(key): + if key in self: + return self._build(self._mapping[key]) + elif self._default_factory: + return self._build(self.__missing__(key)) + + raise AttributeError( + "'{cls}' instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + 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): + """ + 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, force=True) + self.__setattr__('_mapping', mapping, force=True) + self.__setattr__('_sequence_type', sequence_type, force=True) + self.__setattr__('_pass_key', pass_key, force=True) + self.__setattr__( + '_allow_invalid_attributes', + allow_invalid_attributes, + force=True + ) + + @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/tests/test_common.py b/tests/test_common.py index fa0f660..b1c4114 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -92,6 +92,22 @@ def constructor(items=None, sequence_type=tuple): yield test +def test_attrdefault(): + """ + Run AttrDefault against the common tests. + """ + from attrdict.default import AttrDefault + + def constructor(items=None, sequence_type=tuple): + 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, method_missing=False, iter_methods=False, view_methods=False): """ From 9e6a27315cc3f7dc5e302c76aae37b9bd82f1600 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Fri, 2 Jan 2015 20:07:57 -0600 Subject: [PATCH 19/38] mixin method completely usurps Attr/MutableAttr Todo: Fix newly created holes in testing Add documentation clearly enumerate the issues with attrdict.copy and attrdict..attr = value --- attrdict/attr.py | 259 ------------------------------------- attrdict/attrdictionary.py | 135 ------------------- attrdict/mutableattr.py | 45 ------- tests/test_attr.py | 17 --- tests/test_attrdict.py | 19 ++- tests/test_common.py | 49 +------ tests/test_depricated.py | 2 +- 7 files changed, 11 insertions(+), 515 deletions(-) delete mode 100644 attrdict/attr.py delete mode 100644 attrdict/attrdictionary.py delete mode 100644 attrdict/mutableattr.py delete mode 100644 tests/test_attr.py diff --git a/attrdict/attr.py b/attrdict/attr.py deleted file mode 100644 index 1e59de8..0000000 --- a/attrdict/attr.py +++ /dev/null @@ -1,259 +0,0 @@ -""" -Attr is an implementation of Mapping which also allows for -attribute-style access of values. Attr serves as the base class that all -other Attr* classes subclass from. -""" -from collections import Mapping, Sequence -import re - -from attrdict.merge import merge -from attrdict.two_three import StringType - - -__all__ = ['Attr'] - - -class Attr(Mapping): - """ - An implementation of Mapping which also allows for attribute-style - access of values. Attr serves as the base class that all other Attr* - classes subclass from. - - A key may be used as an attribute if: - * It is a string. - * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., 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 value which is accessed as an attribute is a Sequence type (and - is not string/bytes), any Mappings within it will be converted to an - Attr. - - items: (optional, None) An iterable or mapping representing the items - in the object. - sequence_type: (optional, tuple) If not None, the constructor for - converted (i.e., not string or bytes) Sequences. - - NOTE: Attr isn't designed to be mutable, but it isn't actually - immutable. By accessing hidden attributes, an untrusted source can - mutate an Attr object. If you need to ensure an untrusted source - can't modify a base object, you should pass a copy (using deepcopy - if the Attr is nested). - - NOTE: If sequence_type is not None, then Sequence values will - be different when accessed as a value then when accessed as an - attribute. For mutable types like list, this may result in - hard-to-track bugs - """ - def __init__(self, items=None, sequence_type=tuple): - if items is None: - items = () - - self.__setattr__('_sequence_type', sequence_type, force=True) - - # NOTE: we want to keep the original mapping if possible, that - # way, subclasses that implement mutability can subassign e.g.: - # attr.foo.bar = 'baz' - - # items may be an iterable of two-tuples, or a mapping. - if isinstance(items, Mapping): - mapping = items - else: - mapping = dict(items) - - self.__setattr__('_mapping', mapping, force=True) - - def __getitem__(self, key): - """ - Access a value associated with a key. - - Note: values returned will not be wrapped, even if recursive - is True. - """ - return self._mapping[key] - - def __len__(self): - """ - Check the length of the mapping. - """ - return len(self._mapping) - - def __iter__(self): - """ - iterate through the keys. - """ - return self._mapping.__iter__() - - def __call__(self, key): - """ - Dynamically access a key in the mapping. - - This differs from dict-style key access because it returns a new - instance of an Attr (if the value is a Mapping object, and - recursive is True). - """ - if key not in self._mapping: - raise AttributeError( - "'{cls}' instance has no attribute '{name}'".format( - cls=self.__class__.__name__, name=key - ) - ) - - return self._build( - self._mapping[key], - sequence_type=self._sequence_type - ) - - def __getattr__(self, key): - """ - Access a key-value pair as an attribute. - """ - if key in self._mapping and self._valid_name(key): - return self._build( - self._mapping[key], sequence_type=self._sequence_type - ) - - raise AttributeError( - "'{cls}' instance has no attribute '{name}'".format( - cls=self.__class__.__name__, name=key - ) - ) - - def __setattr__(self, key, value, force=False): - """ - Add an attribute to the instance. The attribute will only be - added if force is set to True. - """ - if force: - super(Attr, self).__setattr__(key, value) - else: - raise TypeError("Can not add new attribute") - - def __delattr__(self, name): - """ - Delete an attribute from the instance. But no, this is not - allowered. - """ - raise TypeError( - "'{cls}' object does not support attribute deletion".format( - cls=self.__class__.__name__ - ) - ) - - def __add__(self, other): - """ - Add a mapping to this Attr, creating a new, merged Attr. - - NOTE: Attr is not commutative. a + b != b + a. - NOTE: If both objects are `Attr`s and have differing sequence - types, the default value of tuple will be used - """ - if not isinstance(other, Mapping): - return NotImplemented - - if hasattr(self, '_constructor'): - constructor = self._constructor - else: - constructor = self.__class__ - - sequence_type = tuple - other_sequence_type = getattr( - other, '_sequence_type', self._sequence_type - ) - - if other_sequence_type == self._sequence_type: - sequence_type = self._sequence_type - - return constructor(merge(self, other), sequence_type=sequence_type) - - def __radd__(self, other): - """ - Add this Attr to a mapping, creating a new, merged Attr. - - NOTE: Attr is not commutative. a + b != b + a. - NOTE: If both objects are `Attr`s and have differing sequence - types, the default value of tuple will be used - """ - if not isinstance(other, Mapping): - return NotImplemented - - if hasattr(self, '_constructor'): - constructor = self._constructor - else: - constructor = self.__class__ - - sequence_type = tuple - other_sequence_type = getattr( - other, '_sequence_type', self._sequence_type - ) - - if other_sequence_type == self._sequence_type: - sequence_type = self._sequence_type - - return constructor(merge(other, self), sequence_type=sequence_type) - - def __repr__(self): - """ - Return a string representation of the object - """ - return u"a{0}".format(repr(self._mapping)) - - def __getstate__(self): - """ - Serialize the object. - - NOTE: required to maintain sequence_type. - """ - return (self._mapping, self._sequence_type) - - def __setstate__(self, state): - """ - Deserialize the object. - """ - items, sequence_type = state - self.__init__(items, sequence_type=sequence_type) - - @classmethod - def _valid_name(cls, name): - """ - Check whether a key is a valid attribute. - - A key may be used as an attribute if: - * It is a string. - * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., 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(name, StringType) and - re.match('^[A-Za-z][A-Za-z0-9_]*$', name) and - not hasattr(cls, name) - ) - - @classmethod - def _build(cls, obj, sequence_type=tuple): - """ - Create an Attr version of an object. Any Mapping object will be - converted to an Attr, and if sequence_type is not None, any - non-(string/bytes) object will be converted to sequence_type, - with any contained Mappings being converted to Attr. - """ - if isinstance(obj, Mapping): - if hasattr(cls, '_constructor'): - constructor = cls._constructor - else: - constructor = cls - - obj = constructor(obj, sequence_type=sequence_type) - elif (isinstance(obj, Sequence) and - not isinstance(obj, (StringType, bytes)) and - sequence_type is not None): - obj = sequence_type( - cls._build(element, sequence_type=sequence_type) - for element in obj - ) - - return obj diff --git a/attrdict/attrdictionary.py b/attrdict/attrdictionary.py deleted file mode 100644 index 3674f6f..0000000 --- a/attrdict/attrdictionary.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -A subclass of MutableAttr that works as a drop-in replacement of dict -""" -from collections import Mapping -import copy -from sys import version_info - -from attrdict.mutableattr import MutableAttr -from attrdict.two_three import PYTHON_2 - - -class AttrDict(MutableAttr): - """ - A subclass of MutableAttr that works as a drop-in replacement of - dict. - - NOTE: AttrDict does not subclass dict. This is not a stylistic - decision, CPython's dict implementation makes this functionally - impossible. The CPython function `PyDict_Merge` (found in - Objects/dictobject.c) uses the macro `PyDict_Check` (found in - Include/dictobject.h) to see whether it can do some - optimizations during merge. If you subclass dict, The merge - won't actually iterate over the items in AttrDict. This means - operations such as **attrdict will not actually work. - - This shouldn't matter though, as code that is doing type checks - against dict is wrong (e.g., defaultdict, OrderedDict, etc. - don't subclass dict). If you need to typecheck, you should be - checking against collections.Mapping or - collections.MutableMapping. In the worst-case scenario, remember - that isinstance can take a tuple of types. - """ - def __init__(self, items=None, **kwargs): - if items is None: - items = () - - # items may be an iterable of two-tuples, or a mapping. - if isinstance(items, Mapping): - mapping = items - else: - mapping = dict(items) - - for key in kwargs: - mapping[key] = kwargs[key] - - super(AttrDict, self).__init__(mapping) - - def __setstate__(self, state): - """ - Deserialize the object. - """ - items, sequence_type = state - self.__init__(items) - self.__setattr__('_sequence_type', sequence_type, force=True) - - def copy(self): - """ - Make a (shallow) copy of the AttrDict - """ - return copy.copy(self) - - if PYTHON_2: - def has_key(self, key): - """ - Test for the presence of key in the dictionary. - has_key() is deprecated in favor of key in d. - """ - return key in self - - def iteritems(self): - """ - Iterate over items in the mapping. - """ - for key in self: - yield key, self[key] - - def iterkeys(self): - """ - Iterate over keys in the mapping. - """ - for key in self: - yield key - - def itervalues(self): - """ - Iterate over values in the mapping. - """ - for key in self: - yield self[key] - - if version_info >= (2, 7): - def viewitems(self): - """ - Get a view of the items. - """ - if hasattr(self._mapping, 'viewitems'): - return self._mapping.viewitems() - else: - return dict(self._mapping).viewitems() - - def viewkeys(self): - """ - Get a view of the keys. - """ - if hasattr(self._mapping, 'viewkeys'): - return self._mapping.viewkeys() - else: - return dict(self._mapping).viewkeys() - - def viewvalues(self): - """ - Get a view of the values. - """ - if hasattr(self._mapping, 'viewvalues'): - return self._mapping.viewvalues() - else: - return dict(self._mapping).viewvalues() - - @classmethod - def fromkeys(cls, seq, value=None): - """ - Create a new dictionary with keys from seq and values set to - value. - """ - return cls((key, value) for key in seq) - - @classmethod - def _constructor(cls, items=None, sequence_type=tuple): - """ - A constructor for AttrDict that respects sequence_type - """ - mapping = cls(items) - mapping.__setattr__('_sequence_type', sequence_type, force=True) - - return mapping diff --git a/attrdict/mutableattr.py b/attrdict/mutableattr.py deleted file mode 100644 index ce59d7f..0000000 --- a/attrdict/mutableattr.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -A subclass of Attr that implements MutableMapping. -""" -from collections import MutableMapping - -from attrdict.attr import Attr - - -class MutableAttr(Attr, MutableMapping): - """ - A subclass of Attr that implements MutableMapping. - """ - def __setattr__(self, key, value, force=False): - """ - Add an attribute to the instance. The attribute will only be - added if force is set to True. - """ - if force: - super(MutableAttr, self).__setattr__(key, value, force=force) - else: - if not self._valid_name(key): - raise TypeError("Invalid key: {0}".format(repr(key))) - - self._mapping[key] = value - - def __delattr__(self, name): - """ - Delete an attribute. - """ - if not self._valid_name(name) or name not in self._mapping: - raise TypeError("Invalid name: {0}".format(repr(name))) - - del self._mapping[name] - - 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] diff --git a/tests/test_attr.py b/tests/test_attr.py deleted file mode 100644 index 7c91950..0000000 --- a/tests/test_attr.py +++ /dev/null @@ -1,17 +0,0 @@ -# encoding: UTF-8 -""" -Tests for the Attr class. -""" -from nose.tools import assert_equals - - -def test_repr(): - """ - Create a text representation of Attr. - """ - from attrdict.attr import Attr - - assert_equals(repr(Attr()), 'a{}') - assert_equals(repr(Attr({'foo': 'bar'})), "a{'foo': 'bar'}") - assert_equals(repr(Attr({'foo': {1: 2}})), "a{'foo': {1: 2}}") - assert_equals(repr(Attr({'foo': Attr({1: 2})})), "a{'foo': a{1: 2}}") diff --git a/tests/test_attrdict.py b/tests/test_attrdict.py index adb5035..f467342 100644 --- a/tests/test_attrdict.py +++ b/tests/test_attrdict.py @@ -14,7 +14,7 @@ def test_init(): """ Create a new AttrDict. """ - from attrdict.attrdictionary import AttrDict + from attrdict.dictionary import AttrDict # empty assert_equals(AttrDict(), {}) @@ -27,7 +27,6 @@ def test_init(): assert_equals(AttrDict(foo='bar'), {'foo': 'bar'}) # non-overlapping - assert_equals(AttrDict(None, foo='bar'), {'foo': 'bar'}) assert_equals(AttrDict({}, foo='bar'), {'foo': 'bar'}) assert_equals(AttrDict((), foo='bar'), {'foo': 'bar'}) @@ -60,27 +59,25 @@ def test_init(): def test_copy(): """ - copy an AttrDict + Make a dict copy of an AttrDict. """ - from attrdict.attrdictionary import 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' + mapping_b['foo']['lorem'] = 'ipsum' assert_equals(mapping_a, mapping_b) assert_equals(mapping_b, mapping_c) - mapping_c.alpha = 'bravo' - def test_fromkeys(): """ make a new sequence from a set of keys. """ - from attrdict.attrdictionary import AttrDict + from attrdict.dictionary import AttrDict # default value assert_equals(AttrDict.fromkeys(()), {}) @@ -94,13 +91,13 @@ def test_fromkeys(): ) # custom value - assert_equals(AttrDict.fromkeys((), value=0), {}) + assert_equals(AttrDict.fromkeys((), 0), {}) assert_equals( AttrDict.fromkeys({'foo': 'bar', 'baz': 'qux'}, 0), {'foo': 0, 'baz': 0} ) assert_equals( - AttrDict.fromkeys(('foo', 'baz'), value=0), + AttrDict.fromkeys(('foo', 'baz'), 0), {'foo': 0, 'baz': 0} ) @@ -110,6 +107,6 @@ def test_has_key(): """ The now-depricated has_keys method """ - from attrdict.attrdictionary import AttrDict + from attrdict.dictionary import AttrDict assert_false(hasattr(AttrDict(), 'has_key')) diff --git a/tests/test_common.py b/tests/test_common.py index b1c4114..e3e0cad 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -19,49 +19,6 @@ ) -def test_attr(): - """ - Run Attr against the common tests. - """ - from attrdict.attr import Attr - - for test in common(Attr): - yield test - - -def test_mutableattr(): - """ - Run MutableAttr against the common tests. - """ - from attrdict.mutableattr import MutableAttr - - for test in common(MutableAttr, mutable=True): - yield test - - -def test_attrdict_(): - """ - Run AttrDict against the common tests. - """ - from attrdict.attrdictionary import AttrDict - - view_methods = (2, 7) <= version_info < (3,) - - for test in common(AttrDict, constructor=AttrDict._constructor, - mutable=True, iter_methods=True, - view_methods=view_methods): - yield test - - -def defaultattr_constructor(*args, **kwargs): - """ - Create a DefaultAttr - """ - from attrdict.defaultattr import DefaultAttr - - return DefaultAttr(None, *args, **kwargs) - - def test_attrmap(): """ Run AttrMap against the common tests. @@ -327,9 +284,9 @@ def iteration(options): assert_equals(frozenset(actual_items), expected_items) # What happens if mapping isn't a dict - from attrdict.attr import Attr + from attrdict.mapping import AttrMap - mapping = options.constructor(Attr(raw)) + mapping = options.constructor(AttrMap(raw)) actual_keys = mapping.viewkeys() actual_values = mapping.viewvalues() @@ -880,8 +837,6 @@ def deepcopying(options): mapping_b['foo']['lorem'] = 'ipsum' - print(mapping_a, mapping_b, mapping_c) - assert_not_equals(mapping_a, mapping_b) assert_equals(mapping_b, mapping_c) diff --git a/tests/test_depricated.py b/tests/test_depricated.py index ac2e2fb..81e1b89 100644 --- a/tests/test_depricated.py +++ b/tests/test_depricated.py @@ -14,7 +14,7 @@ def test_has_key(): """ The now-depricated has_keys method """ - from attrdict.attrdictionary import AttrDict + from attrdict.dictionary import AttrDict mapping = AttrDict( {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} From 4353bc0af79ba1a15bebbed044c29d7ac83a0c7b Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 21:00:32 -0600 Subject: [PATCH 20/38] added __all__ for files --- attrdict/default.py | 3 +++ attrdict/dictionary.py | 3 +++ attrdict/mapping.py | 3 +++ attrdict/merge.py | 3 +++ attrdict/mixins.py | 3 +++ attrdict/two_three.py | 3 +++ 6 files changed, 18 insertions(+) diff --git a/attrdict/default.py b/attrdict/default.py index 8da23c6..3fec687 100644 --- a/attrdict/default.py +++ b/attrdict/default.py @@ -6,6 +6,9 @@ from attrdict.mixins import MutableAttr +__all__ = ['AttrDefault'] + + class AttrDefault(MutableAttr): """ An implementation of MutableAttr with defaultdict support diff --git a/attrdict/dictionary.py b/attrdict/dictionary.py index f513d75..b886617 100644 --- a/attrdict/dictionary.py +++ b/attrdict/dictionary.py @@ -4,6 +4,9 @@ from attrdict.mixins import MutableAttr +__all__ = ['AttrDict'] + + class AttrDict(dict, MutableAttr): """ A dict that implements MutableAttr. diff --git a/attrdict/mapping.py b/attrdict/mapping.py index c2b07da..c078f42 100644 --- a/attrdict/mapping.py +++ b/attrdict/mapping.py @@ -6,6 +6,9 @@ from attrdict.mixins import MutableAttr +__all__ = ['AttrMap'] + + class AttrMap(MutableAttr): """ An implementation of MutableAttr. diff --git a/attrdict/merge.py b/attrdict/merge.py index c3bbf7e..229596c 100644 --- a/attrdict/merge.py +++ b/attrdict/merge.py @@ -4,6 +4,9 @@ from collections import Mapping +__all__ = ['merge'] + + def merge(left, right): """ Merge two mappings objects together, combining overlapping Mappings, diff --git a/attrdict/mixins.py b/attrdict/mixins.py index 4d068e5..800cb5c 100644 --- a/attrdict/mixins.py +++ b/attrdict/mixins.py @@ -9,6 +9,9 @@ from attrdict.two_three import StringType +__all__ = ['Attr', 'MutableAttr'] + + class Attr(Mapping): """ A mixin class for a mapping that allows for attribute-style access diff --git a/attrdict/two_three.py b/attrdict/two_three.py index 5eefe66..2aa9990 100644 --- a/attrdict/two_three.py +++ b/attrdict/two_three.py @@ -7,6 +7,9 @@ from sys import version_info +__all__ = ['PYTHON_2', 'StringType'] + + if version_info < (3,): PYTHON_2 = True StringType = basestring From 7ccd6b07bd9dbf3c766714213965102b4447ae74 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 21:36:55 -0600 Subject: [PATCH 21/38] Give up and use six. Metaclass stuff is too difficult without it. Incidentally, pylint doesn't understand add_metaclass, and I was doing metaclass stuff wrong previously --- attrdict/mixins.py | 13 ++++++------- attrdict/two_three.py | 18 ------------------ setup.cfg | 2 +- setup.py | 3 +++ tests/test_attrdict.py | 8 ++------ tests/test_common.py | 6 +++--- tests/test_depricated.py | 8 ++------ tests/test_two_three.py | 29 ----------------------------- 8 files changed, 17 insertions(+), 70 deletions(-) delete mode 100644 attrdict/two_three.py delete mode 100644 tests/test_two_three.py diff --git a/attrdict/mixins.py b/attrdict/mixins.py index 800cb5c..9d4bbf5 100644 --- a/attrdict/mixins.py +++ b/attrdict/mixins.py @@ -5,13 +5,15 @@ from collections import Mapping, MutableMapping, Sequence import re +import six + from attrdict.merge import merge -from attrdict.two_three import StringType __all__ = ['Attr', 'MutableAttr'] +@six.add_metaclass(ABCMeta) class Attr(Mapping): """ A mixin class for a mapping that allows for attribute-style access @@ -33,8 +35,6 @@ class Attr(Mapping): than if accessed as an attribute than if it is accessed as an item. """ - __meta__ = ABCMeta - @abstractmethod def _configuration(self): """ @@ -126,7 +126,7 @@ def _build(self, obj): if isinstance(obj, Mapping): obj = self._constructor(obj, self._configuration()) elif (isinstance(obj, Sequence) and - not isinstance(obj, (StringType, bytes))): + not isinstance(obj, (six.string_types, six.binary_type))): sequence_type = getattr(self, '_sequence_type', None) if sequence_type: @@ -147,19 +147,18 @@ def _valid_name(cls, key): 'register'). """ return ( - isinstance(key, StringType) and + 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. """ - __meta__ = ABCMeta - def __setattr__(self, key, value, force=False): """ Add an attribute. diff --git a/attrdict/two_three.py b/attrdict/two_three.py deleted file mode 100644 index 2aa9990..0000000 --- a/attrdict/two_three.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Support for python 2/3. - -NOTE: If you make changes to this, please manually run flake8 against - it. tox/travis skip this file as basestring is undefined in Python3. -""" -from sys import version_info - - -__all__ = ['PYTHON_2', 'StringType'] - - -if version_info < (3,): - PYTHON_2 = True - StringType = basestring -else: - PYTHON_2 = False - StringType = str diff --git a/setup.cfg b/setup.cfg index b1626a7..c3f5071 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ with-coverage=1 cover-package=attrdict [flake8] -exclude = attrdict/two_three.py,tests/test_depricated.py +exclude = tests/test_depricated.py [wheel] universal = 1 diff --git a/setup.py b/setup.py index e937619..6a8504d 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,9 @@ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ), + install_requires=( + 'six', + ), tests_require=( 'nose>=1.0', 'coverage', diff --git a/tests/test_attrdict.py b/tests/test_attrdict.py index f467342..aa15b5d 100644 --- a/tests/test_attrdict.py +++ b/tests/test_attrdict.py @@ -2,12 +2,8 @@ """ Tests for the AttrDict class. """ -from sys import version_info - from nose.tools import assert_equals, assert_false - - -PYTHON_2 = version_info < (3,) +from six import PY2 def test_init(): @@ -102,7 +98,7 @@ def test_fromkeys(): ) -if not PYTHON_2: +if not PY2: def test_has_key(): """ The now-depricated has_keys method diff --git a/tests/test_common.py b/tests/test_common.py index e3e0cad..9a9b38c 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -9,8 +9,8 @@ from nose.tools import (assert_equals, assert_not_equals, assert_true, assert_false, assert_raises) +from six import PY2 -PYTHON_2 = version_info < (3,) Options = namedtuple( 'Options', @@ -140,7 +140,7 @@ def item_access(options): # key that cannot be an attribute (sadly) assert_equals(mapping[u'👻'], 'boo') - if PYTHON_2: + if PY2: assert_raises(UnicodeEncodeError, getattr, mapping, u'👻') else: assert_raises(AttributeError, getattr, mapping, u'👻') @@ -250,7 +250,7 @@ def iteration(options): actual_values = mapping.values() actual_items = mapping.items() - if PYTHON_2: + if PY2: for collection in (actual_keys, actual_values, actual_items): assert_true(isinstance(collection, list)) diff --git a/tests/test_depricated.py b/tests/test_depricated.py index 81e1b89..0a4a5b0 100644 --- a/tests/test_depricated.py +++ b/tests/test_depricated.py @@ -1,15 +1,11 @@ """ Tests for depricated methods. """ -from sys import version_info - from nose.tools import assert_true, assert_false +from six import PY2 -PYTHON_2 = version_info < (3,) - - -if PYTHON_2: +if PY2: def test_has_key(): """ The now-depricated has_keys method diff --git a/tests/test_two_three.py b/tests/test_two_three.py deleted file mode 100644 index b43d9bb..0000000 --- a/tests/test_two_three.py +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: UTF-8 -""" -Tests for the two_three submodule. -""" -from sys import version_info - -from nose.tools import assert_equals, assert_true - - -PYTHON_2 = version_info < (3,) - - -def test_python_2_flag(): - """ - PYTHON_2 flag. - """ - from attrdict import two_three - - assert_equals(two_three.PYTHON_2, PYTHON_2) - - -def test_string_type(): - """ - StringType type. - """ - from attrdict.two_three import StringType - - assert_true(isinstance('string', StringType)) - assert_true(isinstance(u'👻', StringType)) From 33da66da2223c70e9a4d4fa9441f38406533a578 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 22:13:29 -0600 Subject: [PATCH 22/38] linting cleanup --- tests/test_common.py | 10 ++++++++-- tests/test_merge.py | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 9a9b38c..2abf341 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -38,6 +38,9 @@ def test_attrdict(): view_methods = (2, 7) <= version_info < (3,) def constructor(items=None, sequence_type=tuple): + """ + Build a new AttrDict. + """ if items is None: items = {} @@ -56,6 +59,9 @@ def test_attrdefault(): from attrdict.default import AttrDefault def constructor(items=None, sequence_type=tuple): + """ + Build a new AttrDefault. + """ if items is None: items = {} @@ -80,7 +86,7 @@ def common(cls, constructor=None, mutable=False, method_missing=False, """ tests = ( item_access, iteration, containment, length, equality, - item_creation, item_deletion, sequence_type, addition, + item_creation, item_deletion, sequence_typing, addition, to_kwargs, pickleing, pop, popitem, clear, update, setdefault, copying, deepcopying, ) @@ -551,7 +557,7 @@ def del_get(): assert_true(mapping.get('get', 'banana'), 'banana') -def sequence_type(options): +def sequence_typing(options): "Does {cls} respect sequence type?" data = {'list': [{'foo': 'bar'}], 'tuple': ({'foo': 'bar'},)} diff --git a/tests/test_merge.py b/tests/test_merge.py index 2c94772..7939e6d 100644 --- a/tests/test_merge.py +++ b/tests/test_merge.py @@ -11,14 +11,14 @@ def test_merge(): from attrdict.merge import merge left = { - 'foo': 'bar', + 'baz': 'qux', 'mismatch': False, - 'sub': {'alpha': 'beta', 'a': 'b'}, + 'sub': {'alpha': 'beta', 1: 2}, } right = { 'lorem': 'ipsum', 'mismatch': True, - 'sub': {'alpha': 'bravo', 'c': 'd'}, + 'sub': {'alpha': 'bravo', 3: 4}, } assert_equals(merge({}, {}), {}) @@ -27,9 +27,9 @@ def test_merge(): assert_equals( merge(left, right), { - 'foo': 'bar', + 'baz': 'qux', 'lorem': 'ipsum', 'mismatch': True, - 'sub': {'alpha': 'bravo', 'a': 'b', 'c': 'd'} + 'sub': {'alpha': 'bravo', 1: 2, 3: 4} } ) From 91c724b933453052c2379af9091958d41c3539bd Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 22:55:10 -0600 Subject: [PATCH 23/38] remove method_missing flag from test_common It is not being used by anything anyway --- tests/test_common.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 2abf341..44aec49 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -14,8 +14,7 @@ Options = namedtuple( 'Options', - ('cls', 'constructor', 'mutable', 'method_missing', 'iter_methods', - 'view_methods') + ('cls', 'constructor', 'mutable', 'iter_methods', 'view_methods') ) @@ -71,16 +70,14 @@ def constructor(items=None, sequence_type=tuple): yield test -def common(cls, constructor=None, mutable=False, method_missing=False, - iter_methods=False, view_methods=False): +def common(cls, constructor=None, mutable=False, iter_methods=False, + view_methods=False): """ 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. - method_missing: (optional, False) Whether the class supports dynamic - creation of methods (e.g., defaultdict). iter_methods: (optional, False) Whether the class implements iter under Python 2. """ @@ -106,8 +103,7 @@ def common(cls, constructor=None, mutable=False, method_missing=False, if constructor is None: constructor = cls - options = Options(cls, constructor, mutable, method_missing, - iter_methods, view_methods) + options = Options(cls, constructor, mutable, iter_methods, view_methods) for test in tests: if (test not in requirements) or requirements[test](options): @@ -230,12 +226,11 @@ def item_access(options): assert_true(isinstance(mapping.get('list')[1], dict)) # Nonexistent key - if not options.method_missing: - 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') + 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): From 6e34e8303a3c821ad6440fe448de474efc412378 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 23:37:18 -0600 Subject: [PATCH 24/38] explicit tests for Attr Now that Attr is abstract, and all concrete classes are subclassing from MutableAttr, tests should be added for a concrete Attr subclass --- tests/test_common.py | 128 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 25 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 44aec49..144eb73 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -3,7 +3,8 @@ Common tests that apply to multiple Attr-derived classes. """ import copy -from collections import namedtuple, ItemsView, KeysView, ValuesView +from collections import namedtuple, Mapping, ItemsView, KeysView, ValuesView +from itertools import chain import pickle from sys import version_info @@ -18,6 +19,14 @@ ) +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. @@ -84,32 +93,25 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, tests = ( item_access, iteration, containment, length, equality, item_creation, item_deletion, sequence_typing, addition, - to_kwargs, pickleing, pop, popitem, clear, update, setdefault, - copying, deepcopying, + to_kwargs, pickleing, ) - require_mutable = lambda options: options.mutable - - requirements = { - pop: require_mutable, - popitem: require_mutable, - clear: require_mutable, - update: require_mutable, - setdefault: require_mutable, - copying: require_mutable, - deepcopying: require_mutable, - } + mutable_tests = ( + pop, popitem, clear, update, setdefault, copying, deepcopying, + ) if constructor is None: constructor = cls options = Options(cls, constructor, mutable, iter_methods, view_methods) + if mutable: + tests = chain(tests, mutable_tests) + for test in tests: - if (test not in requirements) or requirements[test](options): - test.description = test.__doc__.format(cls=cls.__name__) + test.description = test.__doc__.format(cls=cls.__name__) - yield test, options + yield test, options def item_access(options): @@ -401,16 +403,23 @@ def item_creation(options): "Add a key-value pair to an {cls}" if not options.mutable: - def attribute(): - "Attempt to add an attribute" - options.constructor().foo = 'bar' + # 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" - options.constructor()['foo'] = 'bar' + mapping['foo'] = 'bar' - assert_raises(TypeError, attribute) assert_raises(TypeError, item) + + assert_false('foo' in mapping) else: mapping = options.constructor() @@ -496,15 +505,20 @@ def item_deletion(options): if not options.mutable: mapping = options.constructor({'foo': 'bar'}) - def attribute(mapping): - "Attempt to del an attribute" + # 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, attribute, mapping) assert_raises(TypeError, item, mapping) assert_equals(mapping, {'foo': 'bar'}) @@ -849,3 +863,67 @@ def deepcopying(options): assert_false('lorem' in mapping_a.foo) assert_equals(mapping_a.setdefault('alpha', 'beta'), 'beta') assert_equals(mapping_c.alpha, 'bravo') + + +try: + from attrdict.mixins import Attr + + 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) +except ImportError: + pass From b23bd1f698a4047125361f214854ed1188868d5f Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Wed, 7 Jan 2015 23:46:12 -0600 Subject: [PATCH 25/38] common tests formatting --- tests/test_common.py | 146 +++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 144eb73..dc6f743 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -12,6 +12,8 @@ assert_true, assert_false, assert_raises) from six import PY2 +from attrdict.mixins import Attr + Options = namedtuple( 'Options', @@ -19,9 +21,68 @@ ) +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 + Tests for an class that implements Attr. """ for test in common(AttrImpl, mutable=False): yield test @@ -93,7 +154,7 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, tests = ( item_access, iteration, containment, length, equality, item_creation, item_deletion, sequence_typing, addition, - to_kwargs, pickleing, + to_kwargs, pickling, ) mutable_tests = ( @@ -115,7 +176,9 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, def item_access(options): - "Access items in {cls}." + """ + Access items in {cls}. + """ mapping = options.constructor( { 'foo': 'bar', @@ -319,7 +382,6 @@ def iteration(options): def containment(options): "Check whether {cls} contains keys" - mapping = options.constructor( {'foo': 'bar', frozenset((1, 2, 3)): 'abc', 1: 2} ) @@ -340,7 +402,6 @@ def containment(options): 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) @@ -414,7 +475,9 @@ def item_creation(options): pass # may assign, but shouldn't assign to dict def item(): - "Attempt to add an item" + """ + Attempt to add an item. + """ mapping['foo'] = 'bar' assert_raises(TypeError, item) @@ -501,7 +564,6 @@ def add_get(): def item_deletion(options): "Remove a key-value from to an {cls}" - if not options.mutable: mapping = options.constructor({'foo': 'bar'}) @@ -516,7 +578,9 @@ def item_deletion(options): raise AssertionError('deletion should fail') def item(mapping): - "Attempt to del an item" + """ + Attempt to del an item + """ del mapping['foo'] assert_raises(TypeError, item, mapping) @@ -698,7 +762,7 @@ def check_pickle_roundtrip(source, options, **kwargs): return loaded -def pickleing(options): +def pickling(options): "Pickle {cls}" empty = check_pickle_roundtrip(None, options) @@ -863,67 +927,3 @@ def deepcopying(options): assert_false('lorem' in mapping_a.foo) assert_equals(mapping_a.setdefault('alpha', 'beta'), 'beta') assert_equals(mapping_c.alpha, 'bravo') - - -try: - from attrdict.mixins import Attr - - 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) -except ImportError: - pass From 1da9400da19dc7eab51d708157c7e0b8a7e8272b Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 00:19:19 -0600 Subject: [PATCH 26/38] separate _set/delattr methods Have MutableAttr expose objects setattr and delattr magic methods through a separate function. This cleaner than using `force` --- attrdict/default.py | 24 ++++++++++-------------- attrdict/dictionary.py | 14 +++++--------- attrdict/mapping.py | 18 +++++++----------- attrdict/mixins.py | 29 +++++++++++++++++------------ 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/attrdict/default.py b/attrdict/default.py index 3fec687..9e4ad2a 100644 --- a/attrdict/default.py +++ b/attrdict/default.py @@ -20,11 +20,11 @@ def __init__(self, default_factory=None, items=None, sequence_type=tuple, elif not isinstance(items, Mapping): items = dict(items) - self.__setattr__('_default_factory', default_factory, force=True) - self.__setattr__('_mapping', items, force=True) - self.__setattr__('_sequence_type', sequence_type, force=True) - self.__setattr__('_pass_key', pass_key, force=True) - self.__setattr__('_allow_invalid_attributes', False, force=True) + 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): """ @@ -116,15 +116,11 @@ def __setstate__(self, state): (default_factory, mapping, sequence_type, pass_key, allow_invalid_attributes) = state - self.__setattr__('_default_factory', default_factory, force=True) - self.__setattr__('_mapping', mapping, force=True) - self.__setattr__('_sequence_type', sequence_type, force=True) - self.__setattr__('_pass_key', pass_key, force=True) - self.__setattr__( - '_allow_invalid_attributes', - allow_invalid_attributes, - force=True - ) + 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): diff --git a/attrdict/dictionary.py b/attrdict/dictionary.py index b886617..c14603a 100644 --- a/attrdict/dictionary.py +++ b/attrdict/dictionary.py @@ -14,8 +14,8 @@ class AttrDict(dict, MutableAttr): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) - self.__setattr__('_sequence_type', tuple, force=True) - self.__setattr__('_allow_invalid_attributes', False, force=True) + self._setattr('_sequence_type', tuple) + self._setattr('_allow_invalid_attributes', False) def _configuration(self): """ @@ -39,12 +39,8 @@ def __setstate__(self, state): """ mapping, sequence_type, allow_invalid_attributes = state self.update(mapping) - self.__setattr__('_sequence_type', sequence_type, force=True) - self.__setattr__( - '_allow_invalid_attributes', - allow_invalid_attributes, - force=True - ) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) @classmethod def _constructor(cls, mapping, configuration): @@ -52,6 +48,6 @@ def _constructor(cls, mapping, configuration): A standardized constructor. """ attr = cls(mapping) - attr.__setattr__('_sequence_type', configuration, force=True) + attr._setattr('_sequence_type', configuration) return attr diff --git a/attrdict/mapping.py b/attrdict/mapping.py index c078f42..e8d5e5e 100644 --- a/attrdict/mapping.py +++ b/attrdict/mapping.py @@ -1,5 +1,5 @@ """ -An implementation of MutableAttr +An implementation of MutableAttr. """ from collections import Mapping @@ -19,9 +19,9 @@ def __init__(self, items=None, sequence_type=tuple): elif not isinstance(items, Mapping): items = dict(items) - self.__setattr__('_sequence_type', sequence_type, force=True) - self.__setattr__('_mapping', items, force=True) - self.__setattr__('_allow_invalid_attributes', False, force=True) + self._setattr('_sequence_type', sequence_type) + self._setattr('_mapping', items) + self._setattr('_allow_invalid_attributes', False) def _configuration(self): """ @@ -80,13 +80,9 @@ def __setstate__(self, state): Deserialize the object. """ mapping, sequence_type, allow_invalid_attributes = state - self.__setattr__('_mapping', mapping, force=True) - self.__setattr__('_sequence_type', sequence_type, force=True) - self.__setattr__( - '_allow_invalid_attributes', - allow_invalid_attributes, - force=True - ) + self._setattr('_mapping', mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) @classmethod def _constructor(cls, mapping, configuration): diff --git a/attrdict/mixins.py b/attrdict/mixins.py index 9d4bbf5..cbe869f 100644 --- a/attrdict/mixins.py +++ b/attrdict/mixins.py @@ -159,19 +159,21 @@ class MutableAttr(Attr, MutableMapping): A mixin class for a mapping that allows for attribute-style access of values. """ - def __setattr__(self, key, value, force=False): + 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 - as_item: (optional, True) If True, the attribute will be added - to the mapping if the key is a valid name. - force: (opational, False) """ - if force: - super(MutableAttr, self).__setattr__(key, value) - elif self._valid_name(key): + if self._valid_name(key): self[key] = value elif getattr(self, '_allow_invalid_attributes', True): super(MutableAttr, self).__setattr__(key, value) @@ -182,17 +184,20 @@ def __setattr__(self, key, value, force=False): ) ) + 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 - force: (optional, False) Delete the attribute from the class - instead of the mapping. """ - if force: - super(MutableAttr, self).__delattr__(key) - elif self._valid_name(key): + if self._valid_name(key): del self[key] elif getattr(self, '_allow_invalid_attributes', True): super(MutableAttr, self).__delattr__(key) From 9be5091c66907e5cd1702fb6ce1c0de5c0ddb857 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 00:34:14 -0600 Subject: [PATCH 27/38] Tests for repr(AttrMap) --- attrdict/mapping.py | 2 +- tests/test_attrmap.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/test_attrmap.py diff --git a/attrdict/mapping.py b/attrdict/mapping.py index e8d5e5e..0cfb48f 100644 --- a/attrdict/mapping.py +++ b/attrdict/mapping.py @@ -61,7 +61,7 @@ def __iter__(self): def __repr__(self): """ - Return a string representation of the object + Return a string representation of the object. """ return u"a{0}".format(repr(self._mapping)) diff --git a/tests/test_attrmap.py b/tests/test_attrmap.py new file mode 100644 index 0000000..d31a6f1 --- /dev/null +++ b/tests/test_attrmap.py @@ -0,0 +1,19 @@ +""" +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()), "a{}") + assert_equals(repr(AttrMap({'foo': 'bar'})), "a{'foo': 'bar'}") + assert_equals(repr(AttrMap({1: {'foo': 'bar'}})), "a{1: {'foo': 'bar'}}") + assert_equals( + repr(AttrMap({1: AttrMap({'foo': 'bar'})})), + "a{1: a{'foo': 'bar'}}" + ) From bb8cb043a8cb55a4c9ef0d34b05f90eb4ce569c3 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 00:47:36 -0600 Subject: [PATCH 28/38] tests for recursive assignemnt e.g. foo.bar.baz = 'qux' --- tests/test_common.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index dc6f743..1677e1b 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -17,7 +17,8 @@ Options = namedtuple( 'Options', - ('cls', 'constructor', 'mutable', 'iter_methods', 'view_methods') + ('cls', 'constructor', 'mutable', 'iter_methods', 'view_methods', + 'recursive') ) @@ -117,7 +118,7 @@ def constructor(items=None, sequence_type=tuple): for test in common(AttrDict, constructor=constructor, mutable=True, iter_methods=True, - view_methods=view_methods): + view_methods=view_methods, recursive=False): yield test @@ -141,7 +142,7 @@ def constructor(items=None, sequence_type=tuple): def common(cls, constructor=None, mutable=False, iter_methods=False, - view_methods=False): + view_methods=False, recursive=True): """ Iterates over tests common to multiple Attr-derived classes @@ -150,6 +151,9 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, 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, @@ -164,7 +168,8 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, if constructor is None: constructor = cls - options = Options(cls, constructor, mutable, iter_methods, view_methods) + options = Options(cls, constructor, mutable, iter_methods, view_methods, + recursive) if mutable: tests = chain(tests, mutable_tests) @@ -176,9 +181,7 @@ def common(cls, constructor=None, mutable=False, iter_methods=False, def item_access(options): - """ - Access items in {cls}. - """ + """Access items in {cls}.""" mapping = options.constructor( { 'foo': 'bar', @@ -561,6 +564,14 @@ def add_get(): 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}" From 02de4f9c6f02de36b89168e5f7bff2874b069289 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 01:26:35 -0600 Subject: [PATCH 29/38] Tests for AttrDefault make sure that it works like a defaultdict --- attrdict/default.py | 10 ++++++++ tests/test_attrdefault.py | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/test_attrdefault.py diff --git a/attrdict/default.py b/attrdict/default.py index 9e4ad2a..829dd2b 100644 --- a/attrdict/default.py +++ b/attrdict/default.py @@ -97,6 +97,16 @@ def __missing__(self, key): return value + def __repr__(self): + """ + Return a string representation of the object. + """ + return 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. 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'})") + ) + ) From 2f9a1a3dd79b190fe4201eeb873f83c03ee36f42 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 01:42:49 -0600 Subject: [PATCH 30/38] old AttrDict is dead. programming tentatively done for AttrDict 2.0 TODO: tests for Import * to ensure correct things exposed? update README proper docs re-add load? --- attrdict/__init__.py | 397 +------------------- tests.py | 873 ------------------------------------------- 2 files changed, 6 insertions(+), 1264 deletions(-) delete mode 100644 tests.py 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/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() From 9728e7329bc8136e3711f82d5ba5016dd62dbe63 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 12:55:32 -0600 Subject: [PATCH 31/38] Simplify AttrDefault No need to implement __getattr__, method_missing can be done solely through __getitem__ (in general, any operation should be defined in terms of items). Reducing references to self._mapping --- attrdict/default.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/attrdict/default.py b/attrdict/default.py index 829dd2b..5923d38 100644 --- a/attrdict/default.py +++ b/attrdict/default.py @@ -41,7 +41,7 @@ def __getitem__(self, key): """ if key in self._mapping: return self._mapping[key] - elif self._default_factory: + elif self._default_factory is not None: return self.__missing__(key) raise KeyError(key) @@ -70,30 +70,14 @@ def __iter__(self): """ return iter(self._mapping) - def __getattr__(self, key): - """ - Access a key-value pair as an attribute. - """ - if self._valid_name(key): - if key in self: - return self._build(self._mapping[key]) - elif self._default_factory: - return self._build(self.__missing__(key)) - - raise AttributeError( - "'{cls}' instance has no attribute '{name}'".format( - cls=self.__class__.__name__, name=key - ) - ) - def __missing__(self, key): """ Add a missing element. """ if self._pass_key: - self._mapping[key] = value = self._default_factory(key) + self[key] = value = self._default_factory(key) else: - self._mapping[key] = value = self._default_factory() + self[key] = value = self._default_factory() return value From 8744c479cf1deca8ff0a3e074255552f403e2b67 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 13:16:00 -0600 Subject: [PATCH 32/38] tests for MutableAttr's set/del mechanics Explicit tests that _allow_invalid_attributes behaves properly --- tests/test_mixins.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_mixins.py diff --git a/tests/test_mixins.py b/tests/test_mixins.py new file mode 100644 index 0000000..40ac636 --- /dev/null +++ b/tests/test_mixins.py @@ -0,0 +1,54 @@ +""" +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, {}) From 4afce900c83789dba7a94d21e685611f6d5edf18 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 13:59:54 -0600 Subject: [PATCH 33/38] Explicit _constructor test --- tests/test_mixins.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 40ac636..a05cc71 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -52,3 +52,18 @@ def 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({}, ())) From 5be451f16d43d13c67afd6e0e28f4952cdab9489 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 15:17:01 -0600 Subject: [PATCH 34/38] python3.2 && pypy3 support Now supporting/testing against CPython3.2 and PyPy3. Travis & Tox both support them, so there is no reason not to. If I ever can get jython2.7 to install on my machine, support for it will be added (at least for tox). --- .travis.yml | 2 ++ attrdict/default.py | 6 +++++- attrdict/mapping.py | 4 +++- tests/test_common.py | 18 +++++++++--------- tox.ini | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 529b957..92845bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,11 @@ 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" diff --git a/attrdict/default.py b/attrdict/default.py index 5923d38..a5e5bcb 100644 --- a/attrdict/default.py +++ b/attrdict/default.py @@ -3,6 +3,8 @@ """ from collections import Mapping +import six + from attrdict.mixins import MutableAttr @@ -85,7 +87,9 @@ def __repr__(self): """ Return a string representation of the object. """ - return u"AttrDefault({default_factory}, {pass_key}, {mapping})".format( + 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), diff --git a/attrdict/mapping.py b/attrdict/mapping.py index 0cfb48f..3b127fb 100644 --- a/attrdict/mapping.py +++ b/attrdict/mapping.py @@ -3,6 +3,8 @@ """ from collections import Mapping +import six + from attrdict.mixins import MutableAttr @@ -63,7 +65,7 @@ def __repr__(self): """ Return a string representation of the object. """ - return u"a{0}".format(repr(self._mapping)) + return six.u("a{0}").format(repr(self._mapping)) def __getstate__(self): """ diff --git a/tests/test_common.py b/tests/test_common.py index 1677e1b..ca02cac 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -10,7 +10,7 @@ from nose.tools import (assert_equals, assert_not_equals, assert_true, assert_false, assert_raises) -from six import PY2 +import six from attrdict.mixins import Attr @@ -186,7 +186,7 @@ def item_access(options): { 'foo': 'bar', '_lorem': 'ipsum', - u'👻': 'boo', + six.u('👻'): 'boo', 3: 'three', 'get': 'not the function', 'sub': {'alpha': 'bravo'}, @@ -209,13 +209,13 @@ def item_access(options): assert_equals(mapping.get(3), 'three') # key that cannot be an attribute (sadly) - assert_equals(mapping[u'👻'], 'boo') - if PY2: - assert_raises(UnicodeEncodeError, getattr, mapping, u'👻') + assert_equals(mapping[six.u('👻')], 'boo') + if six.PY2: + assert_raises(UnicodeEncodeError, getattr, mapping, six.u('👻')) else: - assert_raises(AttributeError, getattr, mapping, u'👻') - assert_equals(mapping(u'👻'), 'boo') - assert_equals(mapping.get(u'👻'), 'boo') + 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') @@ -319,7 +319,7 @@ def iteration(options): actual_values = mapping.values() actual_items = mapping.items() - if PY2: + if six.PY2: for collection in (actual_keys, actual_values, actual_items): assert_true(isinstance(collection, list)) diff --git a/tox.ini b/tox.ini index 5a0df4c..0dd3464 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, py34, pypy, flake8 +envlist = py26, py27, py32, py33, py34, pypy, pypy3, flake8 [testenv] commands = python setup.py nosetests From 9a1336b43e84cb937c2d0dba3b7ae2819e6f78ac Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 8 Jan 2015 15:26:56 -0600 Subject: [PATCH 35/38] remove unneeded exclude for flake8 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0dd3464..61f5748 100644 --- a/tox.ini +++ b/tox.ini @@ -10,4 +10,4 @@ deps = flake8 commands = flake8 attrdict tests [flake8] -exclude = attrdict/two_three.py,tests/test_depricated.py +exclude = tests/test_depricated.py From bd4d3d100ce1e2a605c32bb8af7da44048652334 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 9 Apr 2015 18:52:31 -0400 Subject: [PATCH 36/38] Whatever readme stuff I did like 4 months ago --- README.rst | 154 +++++++++++++++++++++++++++++++++-------------------- setup.py | 1 + 2 files changed, 96 insertions(+), 59 deletions(-) diff --git a/README.rst b/README.rst index 803fd89..d5e4ec8 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 ============ @@ -45,73 +41,113 @@ Or from Github:: Documentation ============= -Documentation is available at https://github.com/bcj/AttrDict +Documentation is available at http://attrdict.readthedocs.org/en/latest/ -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 + {} + +Assignment as keys will still work:: -There are some valid keys that cannot be accessed as attributes. To be -accessed as an attribute, a key must: + > attr = AttrDict('foo': {}) + > attr['foo']['bar'] = 'baz' + > attr.foo + {'bar': 'baz'} - * be a string +If either of these caveats are deal-breakers, or you don't need your object to +be a `dict`, consider using `AttrMap` instead. - * start with an alphabetic character +AttrDefault +^^^^^^^^^^^ +At Attr object that behaves like a `defaultdict`. This allows on-the-fly, +automatic key creation:: - * be comprised solely of alphanumeric characters and underscores + > attr = AttrDefault(int, {}) + > attr.foo += 1 + > attr.foo + 1 - * not map to an existing attribute name (e.g., get, items) +AttrDefault also has a `pass_key` option that passes the supplied key to the +`default_factory`:: -To access these attributes while retaining an AttrDict wrapper (or to -dynamically access any key as an attribute):: + > attr = AttrDefault(sorted, {}, pass_key=True) + > attr.banana + ['a', 'a', 'a', 'b', 'n', 'n'] + +Merging +------- +All three Attr classes - > a = AttrDict({'_foo': {'bar': 'baz'}}) - > a('_foo').bar - 'baz' Merging ------- diff --git a/setup.py b/setup.py index 6a8504d..b36051e 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "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", From e00c13022f0350d3e09e1ecb2a3eb845aeae2fe1 Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 9 Apr 2015 19:32:02 -0400 Subject: [PATCH 37/38] Better reprs As per @smetj's request, make the repr represent the way to build the object --- attrdict/dictionary.py | 7 +++++++ attrdict/mapping.py | 5 ++++- tests/test_attrdict.py | 17 +++++++++++++++++ tests/test_attrmap.py | 10 ++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/attrdict/dictionary.py b/attrdict/dictionary.py index c14603a..874e4a4 100644 --- a/attrdict/dictionary.py +++ b/attrdict/dictionary.py @@ -3,6 +3,8 @@ """ from attrdict.mixins import MutableAttr +import six + __all__ = ['AttrDict'] @@ -42,6 +44,11 @@ def __setstate__(self, state): 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): """ diff --git a/attrdict/mapping.py b/attrdict/mapping.py index 3b127fb..02ebac8 100644 --- a/attrdict/mapping.py +++ b/attrdict/mapping.py @@ -65,7 +65,10 @@ def __repr__(self): """ Return a string representation of the object. """ - return six.u("a{0}").format(repr(self._mapping)) + # 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): """ diff --git a/tests/test_attrdict.py b/tests/test_attrdict.py index aa15b5d..b376765 100644 --- a/tests/test_attrdict.py +++ b/tests/test_attrdict.py @@ -98,6 +98,23 @@ def test_fromkeys(): ) +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(): """ diff --git a/tests/test_attrmap.py b/tests/test_attrmap.py index d31a6f1..c802f33 100644 --- a/tests/test_attrmap.py +++ b/tests/test_attrmap.py @@ -10,10 +10,12 @@ def test_repr(): """ from attrdict.mapping import AttrMap - assert_equals(repr(AttrMap()), "a{}") - assert_equals(repr(AttrMap({'foo': 'bar'})), "a{'foo': 'bar'}") - assert_equals(repr(AttrMap({1: {'foo': 'bar'}})), "a{1: {'foo': 'bar'}}") + 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'})})), - "a{1: a{'foo': 'bar'}}" + "AttrMap({1: AttrMap({'foo': 'bar'})})" ) From 39a1e5d88d3a865fb2a3b1f64a5d48d5530f62ee Mon Sep 17 00:00:00 2001 From: Brendan Curran-Johnson Date: Thu, 9 Apr 2015 19:57:54 -0400 Subject: [PATCH 38/38] 2.0 --- CHANGES.txt | 1 + README.rst | 60 +---------------------------------------------------- 2 files changed, 2 insertions(+), 59 deletions(-) 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 d5e4ec8..dc76c4c 100644 --- a/README.rst +++ b/README.rst @@ -38,11 +38,6 @@ Or from Github:: $ cd AttrDict $ python setup.py install -Documentation -============= - -Documentation is available at http://attrdict.readthedocs.org/en/latest/ - Basic Usage =========== AttrDict comes with three different classes, `AttrMap`, `AttrDict`, and @@ -146,12 +141,7 @@ AttrDefault also has a `pass_key` option that passes the supplied key to the Merging ------- -All three Attr classes - - -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:: @@ -200,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.