Skip to content

Commit

Permalink
Merge pull request #50 from maximkulkin/openstruct-default-object-con…
Browse files Browse the repository at this point in the history
…structor

OpenStruct default object constructor
  • Loading branch information
maximkulkin authored May 10, 2017
2 parents 7725a9f + eda66e6 commit edf22d2
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 4 deletions.
5 changes: 5 additions & 0 deletions lollipop/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@

if PY26:
from .ordereddict import OrderedDict
from UserDict import DictMixin
else:
from collections import OrderedDict
try:
from collections import MutableMapping as DictMixin
except ImportError:
from collections.abc import MutableMapping as DictMixin
9 changes: 6 additions & 3 deletions lollipop/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from lollipop.errors import ValidationError, ValidationErrorBuilder, \
ErrorMessagesMixin, merge_errors
from lollipop.utils import is_list, is_dict, make_context_aware, constant, identity
from lollipop.utils import is_list, is_dict, make_context_aware, \
constant, identity, OpenStruct
from lollipop.compat import string_types, int_types, iteritems, OrderedDict
import datetime

Expand Down Expand Up @@ -1186,8 +1187,10 @@ def load(self, data, *args, **kwargs):
errors_builder.raise_errors()

result = super(Object, self).load(result, *args, **kwargs)
if self.constructor:
result = self.constructor(**result)

result = self.constructor(**result) \
if self.constructor else OpenStruct(result)

return result

def load_into(self, obj, data, inplace=True, *args, **kwargs):
Expand Down
59 changes: 59 additions & 0 deletions lollipop/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import inspect
import re
from lollipop.compat import DictMixin, iterkeys


def identity(value):
Expand Down Expand Up @@ -66,3 +67,61 @@ def to_snake_case(s):
def to_camel_case(s):
"""Converts snake-case identifiers to camel-case."""
return re.sub('_([a-z])', lambda m: m.group(1).upper(), s)


class OpenStruct(DictMixin):
"""A dictionary that also allows accessing values through object attributes."""
def __init__(self, data=None):
self.__dict__.update({'_data': data or {}})

def __getitem__(self, key):
return self._data[key]

def __setitem__(self, key, value):
self._data[key] = value

def __delitem__(self, key):
del self._data[key]

def __iter__(self):
for key in self._data:
yield key

def __len__(self):
return len(self._data)

def __contains__(self, key):
return key in self._data

def keys(self):
return self._data.keys()

def iterkeys(self):
for k in iterkeys(self._data):
yield k

def iteritems(self):
for k, v in self._data.iteritems():
yield k, v

def __hasattr__(self, name):
return name in self._data

def __getattr__(self, name):
if name not in self._data:
raise AttributeError(name)
return self._data[name]

def __setattr__(self, name, value):
self._data[name] = value

def __delattr__(self, name):
if name not in self._data:
raise AttributeError(name)
del self._data[name]

def __repr__(self):
return '<%s %s>' % (
self.__class__.__name__,
' '.join('%s=%s' % (k, repr(v)) for k, v in self._data.iteritems()),
)
7 changes: 7 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,13 @@ def test_loading_passes_context_to_inner_type_load(self):
assert foo_type.load_context == context
assert bar_type.load_context == context

def test_constructing_objects_with_default_constructor_on_load(self):
result = Object({'foo': String(), 'bar': Integer()})\
.load({'foo': 'hello', 'bar': 123})

assert result.foo == 'hello'
assert result.bar == 123

def test_constructing_custom_objects_on_load(self):
MyData = namedtuple('MyData', ['foo', 'bar'])
assert Object({'foo': String(), 'bar': Integer()}, constructor=MyData)\
Expand Down
172 changes: 171 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from lollipop.compat import iterkeys, itervalues, iteritems
from lollipop.utils import call_with_context, to_camel_case, to_snake_case, \
constant, identity
constant, identity, OpenStruct
import pytest


class ObjMethodDummy:
Expand Down Expand Up @@ -115,3 +117,171 @@ def test_constant_returns_function_that_takes_any_arguments_and_always_returns_g
assert f(456) == 123
assert f(1, 3, 5) == 123
assert f(1, foo='bar') == 123


class TestOpenStruct:
def test_getting_values(self):
o = OpenStruct({'foo': 'hello', 'bar': 123})
assert o['foo'] == 'hello'
assert o['bar'] == 123

def test_getting_nonexisting_values_raises_KeyError(self):
o = OpenStruct({'foo': 'hello', 'bar': 123})

with pytest.raises(KeyError):
x = o['baz']

def test_setting_values(self):
o = OpenStruct({'foo': 'hello'})
o['foo'] = 'goodbye'
o['bar'] = 111

assert o['foo'] == 'goodbye'
assert o['bar'] == 111

def test_contains(self):
o = OpenStruct({'foo': 'hello'})

assert 'foo' in o
assert 'bar' not in o

o['bar'] = 123

assert 'foo' in o
assert 'bar' in o

def test_deleting_values(self):
o = OpenStruct({'foo': 'hello'})

del o['foo']
assert 'foo' not in o

o['bar'] = 123
del o['bar']
assert 'bar' not in o

def test_deleting_nonexisting_values_raises_KeyError(self):
o = OpenStruct({'foo': 'hello'})

with pytest.raises(KeyError):
del o['bar']

def test_keys(self):
assert sorted(OpenStruct().keys()) == []

o = OpenStruct({'foo': 1, 'bar': 2})
assert sorted(o.keys()) == sorted(['foo', 'bar'])

o['baz'] = 2
assert sorted(o.keys()) == sorted(['foo', 'bar', 'baz'])

def test_values(self):
assert sorted(OpenStruct().values()) == []

o = OpenStruct({'foo': 123})
assert sorted(o.values()) == sorted([123])

o['bar'] = 456
assert sorted(o.values()) == sorted([123, 456])

def test_items(self):
assert sorted(OpenStruct().items()) == []

o = OpenStruct({'foo': 'hello'})
assert sorted(o.items()) == sorted([('foo', 'hello')])

o['bar'] = 123
assert sorted(o.items()) == sorted([('foo', 'hello'), ('bar', 123)])

def test_iterkeys(self):
assert sorted(iterkeys(OpenStruct())) == []

o = OpenStruct({'foo': 123})
assert sorted(iterkeys(o)) == sorted(o.keys())

o['bar'] = 456
assert sorted(iterkeys(o)) == sorted(o.keys())

def test_itervalues(self):
assert sorted(itervalues(OpenStruct())) == []

o = OpenStruct({'foo': 'hello'})
assert sorted(itervalues(o)) == sorted(o.values())

o['bar'] = 'howdy'
assert sorted(itervalues(o)) == sorted(o.values())

def test_iteritems(self):
assert sorted(iteritems(OpenStruct())) == []

o = OpenStruct({'foo': 'hello'})
assert sorted(iteritems(o)) == sorted(o.items())

o['bar'] = 123
assert sorted(iteritems(o)) == sorted(o.items())

def test_length(self):
assert len(OpenStruct()) == 0

o = OpenStruct({'foo': 'hello'})
assert len(o) == 1

o['bar'] = 123
assert len(o) == 2

def test_iter(self):
assert [x for x in OpenStruct()] == []

o = OpenStruct({'foo': 'hello'})
assert sorted([x for x in o]) == sorted(o.keys())

o['bar'] = 123
assert sorted([x for x in o]) == sorted(o.keys())

def test_hasattr(self):
o = OpenStruct({'foo': 'hello'})
o['bar'] = 123

assert hasattr(o, 'foo')
assert hasattr(o, 'bar')
assert not hasattr(o, 'baz')

def test_getattr(self):
o = OpenStruct({'foo': 'hello'})
o['bar'] = 123

assert o.foo == 'hello'
assert o.bar == 123

def test_getattr_on_nonexisting_key_raises_AttributeError(self):
o = OpenStruct({'foo': 'hello'})

with pytest.raises(AttributeError):
x = o.baz

def test_setattr(self):
o = OpenStruct({'foo': 'hello'})

o.foo = 'goodbye'
o.bar = 123

assert o.foo == 'goodbye'
assert o.bar == 123
assert o['foo'] == o.foo
assert o['bar'] == o.bar

def test_delattr(self):
o = OpenStruct({'foo': 'hello'})
o['bar'] = 123

del o.foo
assert not hasattr(o, 'foo')

del o.bar
assert not hasattr(o, 'bar')

def test_delattr_on_nonexisting_key_raises_AttributeError(self):
o = OpenStruct()

with pytest.raises(AttributeError):
del o.foo

0 comments on commit edf22d2

Please sign in to comment.