Skip to content

Commit

Permalink
Merge 97bb072 into b4ae960
Browse files Browse the repository at this point in the history
  • Loading branch information
jmvrbanac committed Aug 27, 2017
2 parents b4ae960 + 97bb072 commit 8b37de5
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 50 deletions.
2 changes: 1 addition & 1 deletion alchemize/__init__.py
@@ -1,2 +1,2 @@
from .mapping import Attr, JsonMappedModel # NOQA
from .transmute import AbstractBaseTransmuter, JsonTransmuter # NOQA
from .transmute import AlchemizeError, AbstractBaseTransmuter, JsonTransmuter # NOQA
5 changes: 3 additions & 2 deletions alchemize/mapping.py
Expand Up @@ -21,12 +21,13 @@ class Attr(object):
:param name: Python attribute name
:param type: Attribute type (e.g str, int, dict, etc)
:param serialize: Determines if the attribute can be serialized
:param required: Forces attribute to be defined
"""
def __init__(self, attr_name, attr_type, serialize=True):

def __init__(self, attr_name, attr_type, serialize=True, required=False):
self.name = attr_name
self.type = attr_type
self.serialize = serialize
self.required = required


class BaseMappedModel(object):
Expand Down
99 changes: 52 additions & 47 deletions alchemize/transmute.py
Expand Up @@ -17,7 +17,7 @@
import six
from abc import ABCMeta, abstractmethod

from alchemize.mapping import JsonMappedModel, Attr
from alchemize.mapping import JsonMappedModel, get_normalized_map


NON_CONVERSION_TYPES = [
Expand All @@ -33,13 +33,28 @@
]


class UnsupportedMappedModelError(Exception):
class AlchemizeError(Exception):
"""Base Exception for all Alchemize errors."""
pass


class UnsupportedMappedModelError(AlchemizeError):
"""Exception that is raised when attempting to transmute a model that
is not supported by the specified transmuter.
"""
pass


class RequiredAttributeError(AlchemizeError):
"""Exception that is raised when attempting to retrieve/apply an
attribute that isn't available.
"""
def __init__(self, attribute_name):
super(RequiredAttributeError, self).__init__(
'Attribute "{}" is required'.format(attribute_name)
)


class AbstractBaseTransmuter(object):
"""The abtract base class from which all Transmuters are built."""
__metaclass__ = ABCMeta
Expand Down Expand Up @@ -113,40 +128,37 @@ def transmute_to(cls, mapped_model, to_string=True, assign_all=False,
super(JsonTransmuter, cls).transmute_to(mapped_model)
result = {}

for json_key, map_obj in mapped_model.__get_full_mapping__().items():

# For backwards compatibility
if not isinstance(map_obj, Attr):
map_obj = Attr(map_obj[0], map_obj[1])
for name, attr in get_normalized_map(mapped_model).items():
attr_value = None

# Make we ignore values that shouldn't be serialized
if not serialize_all and not map_obj.serialize:
if not serialize_all and not attr.serialize:
continue

attr_name, attr_type = map_obj.name, map_obj.type
attr_value = None

if hasattr(mapped_model, attr_name):
current_value = getattr(mapped_model, attr_name)
if hasattr(mapped_model, attr.name):
current_value = getattr(mapped_model, attr.name)
# Convert a single mapped object
if cls._check_supported_mapping(attr_type, True):
if cls._check_supported_mapping(attr.type, True):
attr_value = cls.transmute_to(current_value, False)

# Converts lists of mapped objects
elif (cls.is_list_of_mapping_types(attr_type)
elif (cls.is_list_of_mapping_types(attr.type)
and isinstance(current_value, list)):
attr_value = [cls.transmute_to(child, False)
for child in current_value]

# Converts all other objects (if possible)
elif attr_type in NON_CONVERSION_TYPES:
elif attr.type in NON_CONVERSION_TYPES:
attr_value = current_value

if coerce_values:
attr_value = attr_type(attr_value)
attr_value = attr.type(attr_value)

if assign_all or attr_value is not None:
result[json_key] = attr_value
result[name] = attr_value

elif attr.required:
raise RequiredAttributeError(attr.name)

# Support Attribute Wrapping
if mapped_model.__wrapped_attr_name__:
Expand Down Expand Up @@ -176,40 +188,33 @@ def transmute_from(cls, data, mapped_model_type, coerce_values=False):
if mapped_obj.__wrapped_attr_name__:
json_dict = json_dict.get(mapped_obj.__wrapped_attr_name__)

for key, val in json_dict.items():
map_obj = mapped_model_type.__get_full_mapping__().get(key)
if map_obj:
# For backwards compatibility
if not isinstance(map_obj, Attr):
map_obj = Attr(map_obj[0], map_obj[1])
for name, attr in get_normalized_map(mapped_model_type).items():
val = json_dict.get(name)
attr_value = None

attr_name, attr_type = map_obj.name, map_obj.type
attr_value = None
if attr.required and val is None:
raise RequiredAttributeError(name)

# Convert a single mapped object
if cls._check_supported_mapping(attr_type, True):
attr_value = cls.transmute_from(
val,
attr_type,
coerce_values
)
# Convert a single mapped object
if cls._check_supported_mapping(attr.type, True):
attr_value = cls.transmute_from(val, attr.type, coerce_values)

# Converts lists of mapped objects
elif (cls.is_list_of_mapping_types(attr_type)
and isinstance(val, list)):
attr_value = [
cls.transmute_from(child, attr_type[0], coerce_values)
for child in val
]
# Converts lists of mapped objects
elif (cls.is_list_of_mapping_types(attr.type)
and isinstance(val, list)):
attr_value = [
cls.transmute_from(child, attr.type[0], coerce_values)
for child in val
]

# Converts all other objects (if possible)
elif attr_type in NON_CONVERSION_TYPES:
attr_value = val
# Converts all other objects (if possible)
elif attr.type in NON_CONVERSION_TYPES:
attr_value = val

if coerce_values:
attr_value = attr_type(attr_value)
if coerce_values:
attr_value = attr.type(attr_value)

# Add mapped value to the new mapped_obj is possible
setattr(mapped_obj, attr_name, attr_value)
# Add mapped value to the new mapped_obj is possible
setattr(mapped_obj, name, attr_value)

return mapped_obj
4 changes: 4 additions & 0 deletions docs/api.rst
Expand Up @@ -28,4 +28,8 @@ Mapped Models
Exceptions
------------

.. autoclass:: alchemize.AlchemizeError

.. autoclass:: alchemize.transmute.RequiredAttributeError

.. autoclass:: alchemize.transmute.UnsupportedMappedModelError
49 changes: 49 additions & 0 deletions spec/json_transmuter.py
Expand Up @@ -3,6 +3,7 @@

from specter import Spec, DataSpec, expect, require
from alchemize import JsonTransmuter, JsonMappedModel, Attr
from alchemize.transmute import RequiredAttributeError


class TestWrappedModel(JsonMappedModel):
Expand Down Expand Up @@ -36,7 +37,18 @@ class TestExtendedModel(TestMappedModel):
}


class TestRequiredMappedModel(JsonMappedModel):
__mapping__ = {
'test': Attr('test', int),
'other': Attr('other', int, required=True),
}


TRANSMUTE_COMMON_TYPES_DATASET = {
'bool': {
'attr_type': bool,
'attr_data': 'true', 'attr_result': True
},
'str': {
'attr_type': str,
'attr_data': '"data"', 'attr_result': 'data'
Expand Down Expand Up @@ -308,3 +320,40 @@ class TestMappedModel(JsonMappedModel):

expect(result.nope.test).to.equal(1)
expect(result.nope.test).to.be_an_instance_of(int)

def transmute_from_with_missing_required_attr_raises(self):
expect(
JsonTransmuter.transmute_from,
[
'{"test": 1}',
TestRequiredMappedModel
]
).to.raise_a(RequiredAttributeError)

def transmute_from_with_all_required_attrs(self):
result = JsonTransmuter.transmute_from(
'{"test": 1, "other": 2}',
TestRequiredMappedModel,
)

expect(result.test).to.equal(1)
expect(result.other).to.equal(2)

def transmute_to_missing_required_attr_raises(self):
model = TestRequiredMappedModel()
model.test = 1

expect(
JsonTransmuter.transmute_to,
[model]
).to.raise_a(RequiredAttributeError)

def transmute_to_with_all_required_attrs(self):
model = TestRequiredMappedModel()
model.test = 1
model.other = 2

result = JsonTransmuter.transmute_to(model, to_string=False)

expect(result['test']).to.equal(1)
expect(result['other']).to.equal(2)

0 comments on commit 8b37de5

Please sign in to comment.