Skip to content

Commit

Permalink
Update Property
Browse files Browse the repository at this point in the history
Implement get_value and get_value_exc fields to allow customization of data retrieving
  • Loading branch information
OkThought committed Aug 19, 2019
1 parent 72aa273 commit ad9d7cb
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 28 deletions.
2 changes: 1 addition & 1 deletion data_mapper/errors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Iterable


class PropertyNotFound(ValueError):
class PropertyNotFound(LookupError):
def __init__(self, sources: Iterable):
super().__init__(f'Property not found in sources: {sources}')

Expand Down
9 changes: 4 additions & 5 deletions data_mapper/mappers/mapper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable, Tuple, Any, Mapping
from typing import Iterable, Tuple, Any

from data_mapper.properties.abstract import AbstractProperty
from data_mapper.properties.compound import CompoundProperty
Expand All @@ -13,9 +13,8 @@ def __init__(
props_map = props_map or dict(self._get_class_props())
super().__init__(_options, **props_map)

@classmethod
def _get_class_props(cls) -> Iterable[Tuple[Any, AbstractProperty]]:
for key, prop in cls.__dict__.items():
def _get_class_props(self) -> Iterable[Tuple[Any, AbstractProperty]]:
for key, prop in self.__class__.__dict__.items():
if isinstance(prop, AbstractProperty):
cls.configure_prop_sources(prop, key)
self.configure_prop(prop, key)
yield (key, prop)
10 changes: 5 additions & 5 deletions data_mapper/properties/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ def __init__(
sources = _options.pop('sources')
super().__init__(sources_it=sources, **_options)
self.props_map = props_map
self.configure_props_map(props_map)
self.configure_props(props_map)

def configure_props_map(self, props_map):
def configure_props(self, props_map):
for key, prop in props_map.items():
self.configure_prop_sources(prop, key)
self.configure_prop(prop, key)

@staticmethod
def configure_prop_sources(prop, key):
def configure_prop(self, prop, key):
prop.parent = self
if getattr(prop, 'sources', 0) is None:
prop.sources = [key]

Expand Down
32 changes: 21 additions & 11 deletions data_mapper/properties/operations/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@


class Operation(Property):
star_func = None
func = None
default_sources = []

def __init__(
self,
Expand All @@ -18,16 +17,31 @@ def __init__(
func: Callable = None,
**kwargs,
):
assert star_func is None or func is None

if star_func is not None:
if not hasattr(self, 'star_func'):
self.star_func = star_func

if func is not None:
if not hasattr(self, 'func'):
self.func = func

assert self.star_func is None or self.func is None, \
'star_func and func exclusive'

super().__init__(*props, sources_it=props_it, **kwargs)

self.configure_props()

def configure_props(self):
for source in self.get_sources():
if isinstance(source, AbstractProperty):
self.configure_prop(source)
else:
for prop in source:
self.configure_prop(prop)

def configure_prop(self, prop):
if isinstance(prop, Property):
prop.parent = self

def apply(self, *args):
if self.star_func is not None:
return self.star_func(*args)
Expand All @@ -39,11 +53,7 @@ def get_raw(self, data, result=None):
return self.apply(*self.get_args(data, result))

def get_args(self, data, result=None):
sources = self.sources
if sources is None:
sources = []

for props in sources:
for props in self.get_sources():
if isinstance(props, AbstractProperty):
yield props.get(data, result)
else:
Expand Down
48 changes: 42 additions & 6 deletions data_mapper/properties/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@


class Property(AbstractProperty):
default_sources = [[]]
get_value_exc_default = (LookupError, AttributeError)

def __init__(
self,
*sources,
sources_it: Iterable = None,
default=NOT_SET,
required: bool = None,
transforms: Iterable[Callable[[str], str]] = None,
get_value: Callable = None,
get_value_exc=None,
**kwargs,
):
assert not sources or not sources_it, \
Expand All @@ -31,6 +36,9 @@ def __init__(
self.default = default
self.required = True if required is None else required
self.transforms = [] if transforms is None else transforms
self._get_value = get_value
self._get_value_exc = get_value_exc
self.parent = None

def get(self, data, result=None):
value = self.get_raw(data, result)
Expand All @@ -48,19 +56,17 @@ def get_transforms(self):
return self.transforms

def get_raw(self, data, result=None):
sources = self.sources
if sources is None:
sources = [[]]
sources = self.get_sources()

for source in sources:
value = data
try:
if isinstance(source, str) or not hasattr(source, '__iter__'):
value = value[source]
value = self.get_value(value, source)
else:
for sub_source in source:
value = value[sub_source]
except KeyError:
value = self.get_value(value, sub_source)
except self.get_value_exc:
continue
else:
break
Expand All @@ -74,6 +80,36 @@ def get_raw(self, data, result=None):

return value

def get_sources(self):
return self.default_sources if self.sources is None else self.sources

@property
def get_value(self):
get_value = self._get_value
if get_value is None and self.parent is not None:
get_value = self.parent.get_value
if get_value is None:
get_value = self.get_value_default
return get_value

@staticmethod
def get_value_default(d, k):
try:
return d[k]
except (LookupError, TypeError) as e:
if isinstance(k, str):
return getattr(d, k)
raise e

@property
def get_value_exc(self):
get_value_exc = self._get_value_exc
if get_value_exc is None and self.parent is not None:
get_value_exc = self.parent.get_value_exc
if get_value_exc is None:
get_value_exc = self.get_value_exc_default
return get_value_exc

def __str__(self):
if self.sources is None:
sources_str = str(self.sources)
Expand Down
14 changes: 14 additions & 0 deletions data_mapper/tests/compound_list_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,17 @@ def test__allowed_sizes__range(self):
self.assertEqual([1, 2, 3], prop.get(dict(x=1, y=2, z=3)))
self.assertEqual([1, 2], prop.get(dict(x=1, y=2)))
self.assertEqual([1, 2], prop.get(dict(x=1, z=2)))

def test__get_value__set_in_parent(self):
prop = CompoundListProperty(
Property('x'),
get_value=lambda *_: 'foo',
)
self.assertEqual(['foo'], prop.get(dict(x=5)))

def test__get_value__set_in_grand_parent(self):
prop = CompoundListProperty(
CompoundListProperty(Property('x')),
get_value=lambda *_: 'foo',
)
self.assertEqual([['foo']], prop.get(dict(x=5)))
17 changes: 17 additions & 0 deletions data_mapper/tests/compound_property.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest import TestCase

from data_mapper.properties import Property
from data_mapper.properties.compound import CompoundProperty
from data_mapper.properties.string import StringProperty

Expand Down Expand Up @@ -38,3 +39,19 @@ def test__properties_no_sources(self):
dict(first_name=first_name, last_name=last_name),
prop.get(data=dict(first_name=first_name, last_name=last_name)),
)

def test__get_value__set_in_parent(self):
prop = CompoundProperty(
dict(get_value=lambda *_: 'foo'),
x=Property(),
)
self.assertEqual('foo', prop.get(dict(x=5))['x'])

def test__get_value__set_in_grand_parent(self):
prop = CompoundProperty(
dict(get_value=lambda *_: 'foo'),
y=CompoundProperty(
x=Property(),
),
)
self.assertEqual('foo', prop.get(dict(x=5))['y']['x'])
16 changes: 16 additions & 0 deletions data_mapper/tests/examples.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import date
from unittest import TestCase

from data_mapper.mappers import Mapper
from data_mapper.mappers.object import ObjectMapper
from data_mapper.properties.compound import CompoundProperty
from data_mapper.properties.compound_list import CompoundListProperty
Expand Down Expand Up @@ -115,3 +117,17 @@ def test__object_mapper__init():
assert person.first_name == first
assert person.middle_name == middle
assert person.last_name == last

def test__object_to_dict(self):
mapper = Mapper(
year=IntegerProperty(),
month=IntegerProperty(),
day=IntegerProperty(),
)

d = date(year=1997, month=4, day=12)
value = mapper.get(d)
self.assertEqual(
dict(year=d.year, month=d.month, day=d.day),
dict(value),
)
17 changes: 17 additions & 0 deletions data_mapper/tests/mapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import TestCase

from data_mapper.mappers.mapper import Mapper
from data_mapper.properties import Property
from data_mapper.properties.string import StringProperty


Expand Down Expand Up @@ -37,3 +38,19 @@ class Person(Mapper):
),
)),
)

def test__get_value__set_in_parent(self):
prop = Mapper(
dict(get_value=lambda *_: 'foo'),
x=Property(),
)
self.assertEqual('foo', prop.get(dict(x=5))['x'])

def test__get_value__set_in_grand_parent(self):
prop = Mapper(
dict(get_value=lambda *_: 'foo'),
y=Mapper(
x=Property(),
),
)
self.assertEqual('foo', prop.get(dict(x=5))['y']['x'])
27 changes: 27 additions & 0 deletions data_mapper/tests/property.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from unittest import TestCase

from data_mapper.errors import PropertyNotFound
from data_mapper.properties.integer import IntegerProperty
from data_mapper.properties.operations.sum import Sum
from data_mapper.properties.property import Property
from data_mapper.properties.string import StringProperty
from data_mapper.tests.test_utils import Person


class PropertyTests(TestCase):
Expand Down Expand Up @@ -71,3 +73,28 @@ def test__sum(self):
self.assertEqual(6, prop.get(dict(x=1, y=2, z=3)))
self.assertEqual(3, prop.get(dict(x=1, y=2)))
self.assertEqual(3, prop.get(dict(x=1, y=2.5)))

def test__get_value__from_object(self):
first, middle, last = 'Alexander Sergeyevich Pushkin'.split()
pushkin = Person(1799, first, last, middle)

prop = IntegerProperty(
'id', get_value=getattr, get_value_exc=AttributeError)
self.assertEqual(1799, prop.get(pushkin))

prop = StringProperty(
'first_name', get_value=getattr, get_value_exc=AttributeError)
self.assertEqual(first, prop.get(pushkin))

prop = StringProperty(
'middle_name', get_value=getattr, get_value_exc=AttributeError)
self.assertEqual(middle, prop.get(pushkin))

prop = StringProperty(
'last_name', get_value=getattr, get_value_exc=AttributeError)
self.assertEqual(last, prop.get(pushkin))

prop = StringProperty(
'unknown', get_value=getattr, get_value_exc=AttributeError)
with self.assertRaises(PropertyNotFound):
prop.get(pushkin)
17 changes: 17 additions & 0 deletions data_mapper/tests/property_operation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest import TestCase

from data_mapper.properties import Property
from data_mapper.properties.operations.operation import Operation
from data_mapper.properties.string import StringProperty

Expand Down Expand Up @@ -32,3 +33,19 @@ def dot_join(args):
)
data = {'month': 8, 'day': 12}
self.assertEqual('2019.8.12', prop.get(data))

def test__get_value__set_in_parent(self):
prop = Operation(
Property('x'),
star_func=lambda x: x,
get_value=lambda *_: 'foo',
)
self.assertEqual('foo', prop.get_raw(dict(x=5)))

def test__get_value__set_in_grand_parent(self):
prop = Operation(
Operation(Property('x'), star_func=lambda x: x),
star_func=lambda x: x,
get_value=lambda *_: 'foo',
)
self.assertEqual('foo', prop.get_raw(dict(x=5)))

0 comments on commit ad9d7cb

Please sign in to comment.