Skip to content

Commit

Permalink
Merge pull request #47 from maximkulkin/descriptions
Browse files Browse the repository at this point in the history
Add 'name' and 'description' attributes to all types
  • Loading branch information
maximkulkin authored Mar 7, 2017
2 parents 06226a1 + 1656c3f commit 52c0cbf
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 53 deletions.
76 changes: 39 additions & 37 deletions lollipop/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ class Type(ErrorMessagesMixin, object):
'required': 'Value is required',
}

def __init__(self, validate=None, *args, **kwargs):
def __init__(self, name=None, description=None, validate=None, *args, **kwargs):
super(Type, self).__init__(*args, **kwargs)
if validate is None:
validate = []
elif callable(validate):
validate = [validate]

self.name = name
self.description = description
self._validators = ValidatorCollection(validate)

def validate(self, data, context=None):
Expand Down Expand Up @@ -1298,8 +1300,36 @@ def __repr__(self):
)


class Optional(Type):
"""A wrapper type which makes values optional: if value is missing or None,
class Modifier(Type):
"""Base class for modifiers - a wrapper for types that modify
how those types work. Also, it tries to be as transparent as possible
in regard to inner type, so it proxies all unknown attributes to inner type.
:param Type inner_type: Actual type that should be optional.
"""
def __init__(self, inner_type, **kwargs):
super(Modifier, self).__init__(
**dict({'name': inner_type.name,
'description': inner_type.description},
**kwargs)
)
self.inner_type = inner_type

def __hasattr__(self, name):
return hasattr(self.inner_type, name)

def __getattr__(self, name):
return getattr(self.inner_type, name)

def __repr__(self):
return '<{klass} {inner_type}>'.format(
klass=self.__class__.__name__,
inner_type=repr(self.inner_type),
)


class Optional(Modifier):
"""A modifier which makes values optional: if value is missing or None,
it will not transform it with an inner type but instead will return None
(or any other configured value).
Expand All @@ -1326,8 +1356,7 @@ class Optional(Type):
def __init__(self, inner_type,
load_default=None, dump_default=None,
**kwargs):
super(Optional, self).__init__(**kwargs)
self.inner_type = inner_type
super(Optional, self).__init__(inner_type, **kwargs)
if not callable(load_default):
load_default = constant(load_default)
if not callable(dump_default):
Expand Down Expand Up @@ -1358,7 +1387,7 @@ def __repr__(self):
)


class LoadOnly(Type):
class LoadOnly(Modifier):
"""A wrapper type which proxies loading to inner type but always returns
:obj:`MISSING` on dump.
Expand All @@ -1371,24 +1400,14 @@ class LoadOnly(Type):
:param Type inner_type: Data type.
"""
def __init__(self, inner_type):
super(LoadOnly, self).__init__()
self.inner_type = inner_type

def load(self, data, *args, **kwargs):
return self.inner_type.load(data, *args, **kwargs)

def dump(self, data, context=None):
def dump(self, data, *args, **kwargs):
return MISSING

def __repr__(self):
return '<{klass} {inner_type}>'.format(
klass=self.__class__.__name__,
inner_type=repr(self.inner_type),
)


class DumpOnly(Type):
class DumpOnly(Modifier):
"""A wrapper type which proxies dumping to inner type but always returns
:obj:`MISSING` on load.
Expand All @@ -1401,24 +1420,14 @@ class DumpOnly(Type):
:param Type inner_type: Data type.
"""
def __init__(self, inner_type):
super(DumpOnly, self).__init__()
self.inner_type = inner_type

def load(self, data, *args, **kwargs):
return MISSING

def dump(self, data, *args, **kwargs):
return self.inner_type.dump(data, *args, **kwargs)

def __repr__(self):
return '<{klass} {inner_type}>'.format(
klass=self.__class__.__name__,
inner_type=repr(self.inner_type),
)


class Transform(Type):
class Transform(Modifier):
"""A wrapper type which allows us to convert data structures to an inner type,
then loaded or dumped with a customized format.
Expand Down Expand Up @@ -1455,8 +1464,7 @@ class Transform(Type):
def __init__(self, inner_type,
pre_load=identity, post_load=identity,
pre_dump=identity, post_dump=identity):
super(Transform, self).__init__()
self.inner_type = inner_type
super(Transform, self).__init__(inner_type)
self.pre_load = make_context_aware(pre_load, 1)
self.post_load = make_context_aware(post_load, 1)
self.pre_dump = make_context_aware(pre_dump, 1)
Expand All @@ -1480,12 +1488,6 @@ def dump(self, value, context=None):
context,
)

def __repr__(self):
return '<{klass} {inner_type}>'.format(
klass=self.__class__.__name__,
inner_type=repr(self.inner_type),
)


def validated_type(base_type, name=None, validate=None):
"""Convenient way to create a new type by adding validation to existing type.
Expand Down
65 changes: 49 additions & 16 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def __call__(self, value, context=None):

class SpyType(Type):
def __init__(self, load_result=None, dump_result=None):
super(Type, self).__init__()
super(SpyType, self).__init__()
self.loaded = None
self.load_called = False
self.load_context = None
Expand Down Expand Up @@ -93,9 +93,21 @@ def load_into(self, obj, data, context=None, *args, **kwargs):
return self.load_into_result or data


class NameDescriptionTestsMixin(object):
"""Mixin that adds tests for adding name and description for type.
Host class should define `tested_type` properties.
"""
def test_name(self):
assert self.tested_type(name='foo').name == 'foo'

def test_description(self):
assert self.tested_type(description='Just a description').description \
== 'Just a description'


class RequiredTestsMixin:
"""Mixin that adds tests for reacting to missing/None values during load/dump.
Host class should define `tested_type` properties.
Host class should define `tested_type` property.
"""
def test_loading_missing_value_raises_required_error(self):
with pytest.raises(ValidationError) as exc_info:
Expand Down Expand Up @@ -169,7 +181,7 @@ def test_validation_returns_combined_errors_if_multiple_validators_fail(self):
.validate(self.valid_data) == [message1, message2]


class TestString(RequiredTestsMixin, ValidationTestsMixin):
class TestString(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = String
valid_data = 'foo'
valid_value = 'foo'
Expand All @@ -191,7 +203,7 @@ def test_dumping_non_string_value_raises_ValidationError(self):
assert exc_info.value.messages == String.default_error_messages['invalid']


class TestNumber(RequiredTestsMixin, ValidationTestsMixin):
class TestNumber(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = Number
valid_data = 1.23
valid_value = 1.23
Expand Down Expand Up @@ -257,7 +269,7 @@ def test_dumping_non_numeric_value_raises_ValidationError(self):
assert exc_info.value.messages == Float.default_error_messages['invalid']


class TestBoolean(RequiredTestsMixin, ValidationTestsMixin):
class TestBoolean(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = Boolean
valid_data = True
valid_value = True
Expand All @@ -281,7 +293,7 @@ def test_dumping_non_boolean_value_raises_ValidationError(self):
assert exc_info.value.messages == Boolean.default_error_messages['invalid']


class TestDateTime(RequiredTestsMixin, ValidationTestsMixin):
class TestDateTime(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = DateTime
valid_data = '2016-07-28T11:22:33UTC'
valid_value = datetime.datetime(2016, 7, 28, 11, 22, 33)
Expand Down Expand Up @@ -357,7 +369,7 @@ def test_customizing_error_message_if_value_is_not_string(self):
assert exc_info.value.messages == 'Data 123 should be string'


class TestDate(RequiredTestsMixin, ValidationTestsMixin):
class TestDate(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = Date
valid_data = '2016-07-28'
valid_value = datetime.date(2016, 7, 28)
Expand Down Expand Up @@ -425,7 +437,7 @@ def test_customizing_error_message_if_value_is_not_string(self):
assert exc_info.value.messages == 'Data 123 should be string'


class TestTime(RequiredTestsMixin, ValidationTestsMixin):
class TestTime(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = Time
valid_data = '11:22:33'
valid_value = datetime.time(11, 22, 33)
Expand Down Expand Up @@ -487,7 +499,7 @@ def test_customizing_error_message_if_value_is_not_string(self):
assert exc_info.value.messages == 'Data 123 should be string'


class TestList(RequiredTestsMixin, ValidationTestsMixin):
class TestList(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = partial(List, String())
valid_data = ['foo', 'bar']
valid_value = ['foo', 'bar']
Expand Down Expand Up @@ -548,7 +560,7 @@ def test_dumping_passes_context_to_inner_type_dump(self):
assert inner_type.dump_context == context


class TestTuple(RequiredTestsMixin, ValidationTestsMixin):
class TestTuple(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = partial(Tuple, [Integer(), Integer()])
valid_data = [123, 456]
valid_value = [123, 456]
Expand Down Expand Up @@ -604,7 +616,7 @@ def test_dumping_tuple_passes_context_to_inner_type_dump(self):
assert inner_type.dump_context == context


class TestDict(RequiredTestsMixin, ValidationTestsMixin):
class TestDict(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = partial(Dict, Integer())
valid_data = {'foo': 123, 'bar': 456}
valid_value = {'foo': 123, 'bar': 456}
Expand Down Expand Up @@ -1308,7 +1320,9 @@ def test_dumping_passes_context_to_field_type_dump(self):
assert field_type.dump_context == context


class TestConstant:
class TestConstant(NameDescriptionTestsMixin):
tested_type = partial(Constant, 42)

def test_loading_always_returns_missing(self):
assert Constant(42).load(42) == MISSING

Expand Down Expand Up @@ -1379,7 +1393,7 @@ def load_into(self, obj, name, data, inplace=True, context=None):
self.load_into_context = context


class TestObject(RequiredTestsMixin, ValidationTestsMixin):
class TestObject(NameDescriptionTestsMixin, RequiredTestsMixin, ValidationTestsMixin):
tested_type = partial(Object, {'foo': String(), 'bar': Integer()})
valid_data = {'foo': 'hello', 'bar': 123}
valid_value = {'foo': 'hello', 'bar': 123}
Expand Down Expand Up @@ -2077,7 +2091,22 @@ def generate_value(self, context):
assert spy.context is context


class TestLoadOnly:
class ProxyNameDescriptionTestsMixin(object):
"""Mixin that adds tests for proxying name and description attributes
to an inner type.
Host class should define `tested_type` properties.
"""
def test_name(self):
assert self.tested_type(Type(name='foo')).name == 'foo'

def test_description(self):
assert self.tested_type(Type(description='Just a description')).description \
== 'Just a description'


class TestLoadOnly(ProxyNameDescriptionTestsMixin):
tested_type = LoadOnly

def test_loading_returns_inner_type_load_result(self):
inner_type = SpyType(load_result='bar')
assert LoadOnly(inner_type).load('foo') == 'bar'
Expand All @@ -2098,7 +2127,9 @@ def test_dumping_does_not_call_inner_type_dump(self):
assert not inner_type.dump_called


class TestDumpOnly:
class TestDumpOnly(ProxyNameDescriptionTestsMixin):
tested_type = DumpOnly

def test_loading_always_returns_missing(self):
assert DumpOnly(Any()).load('foo') == MISSING

Expand All @@ -2119,7 +2150,9 @@ def test_dumping_passes_context_to_inner_type_dump(self):
assert inner_type.dump_context == context


class TestTransform:
class TestTransform(ProxyNameDescriptionTestsMixin):
tested_type = Transform

def test_loading_calls_pre_load_with_original_value(self):
class Callbacks():
def pre_load(self, data):
Expand Down

0 comments on commit 52c0cbf

Please sign in to comment.