Skip to content

Commit 1f4a276

Browse files
committed
fixed mapping dict to obj, simplified code
1 parent b9ce4be commit 1f4a276

File tree

3 files changed

+61
-32
lines changed

3 files changed

+61
-32
lines changed

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"hasattr",
55
"isclass",
66
"multimap",
7-
"pyautomapper"
7+
"pyautomapper",
8+
"subobject",
9+
"subobjects"
810
],
911
"python.testing.pytestArgs": [],
1012
"python.testing.unittestEnabled": false,

automapper/mapper.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,31 @@ def _is_sequence(obj: Any) -> bool:
3838

3939

4040
def _is_subscriptable(obj: Any) -> bool:
41-
"""Check if object implements `__get_item__` method"""
42-
return hasattr(obj, "__get_item__")
41+
"""Check if object implements `__getitem__` method"""
42+
return hasattr(obj, "__getitem__")
43+
44+
45+
def _object_contains(obj: Any, field_name: str) -> bool:
46+
return _is_subscriptable(obj) and field_name in obj
4347

4448

4549
def _is_primitive(obj: Any) -> bool:
4650
"""Check if object type is primitive"""
4751
return type(obj) in __PRIMITIVE_TYPES
4852

4953

54+
def _try_get_field_value(
55+
field_name: str, original_obj: Any, custom_mapping: FieldsMap
56+
) -> Tuple[bool, Any]:
57+
if field_name in (custom_mapping or {}):
58+
return True, custom_mapping[field_name] # type: ignore [index]
59+
if hasattr(original_obj, field_name):
60+
return True, getattr(original_obj, field_name)
61+
if _object_contains(original_obj, field_name):
62+
return True, original_obj[field_name]
63+
return False, None
64+
65+
5066
class MappingWrapper(Generic[T]):
5167
"""Internal wrapper for supporting syntax:
5268
```
@@ -88,7 +104,7 @@ def map(
88104
self.__target_cls,
89105
set(),
90106
skip_none_values=skip_none_values,
91-
fields_mapping=fields_mapping,
107+
custom_mapping=fields_mapping,
92108
use_deepcopy=use_deepcopy,
93109
)
94110

@@ -203,17 +219,17 @@ def map(
203219
obj_type = type(obj)
204220
if obj_type not in self._mappings:
205221
raise MappingError(f"Missing mapping type for input type {obj_type}")
206-
obj_type_preffix = f"{obj_type.__name__}."
222+
obj_type_prefix = f"{obj_type.__name__}."
207223

208224
target_cls, target_cls_field_mappings = self._mappings[obj_type]
209225

210226
common_fields_mapping = fields_mapping
211227
if target_cls_field_mappings:
212228
# transform mapping if it's from source class field
213229
common_fields_mapping = {
214-
target_obj_field: getattr(obj, source_field[len(obj_type_preffix) :])
230+
target_obj_field: getattr(obj, source_field[len(obj_type_prefix) :])
215231
if isinstance(source_field, str)
216-
and source_field.startswith(obj_type_preffix)
232+
and source_field.startswith(obj_type_prefix)
217233
else source_field
218234
for target_obj_field, source_field in target_cls_field_mappings.items()
219235
}
@@ -228,7 +244,7 @@ def map(
228244
target_cls,
229245
set(),
230246
skip_none_values=skip_none_values,
231-
fields_mapping=common_fields_mapping,
247+
custom_mapping=common_fields_mapping,
232248
use_deepcopy=use_deepcopy,
233249
)
234250

@@ -296,7 +312,7 @@ def _map_common(
296312
target_cls: Type[T],
297313
_visited_stack: Set[int],
298314
skip_none_values: bool = False,
299-
fields_mapping: FieldsMap = None,
315+
custom_mapping: FieldsMap = None,
300316
use_deepcopy: bool = True,
301317
) -> T:
302318
"""Produces output object mapped from source object and custom arguments.
@@ -306,7 +322,7 @@ def _map_common(
306322
target_cls (Type[T]): Target class to map to.
307323
_visited_stack (Set[int]): Visited child objects. To avoid infinite recursive calls.
308324
skip_none_values (bool, optional): Skip None values when creating `target class` obj. Defaults to False.
309-
fields_mapping (FieldsMap, optional): Custom mapping.
325+
custom_mapping (FieldsMap, optional): Custom mapping.
310326
Specify dictionary in format {"field_name": value_object}. Defaults to None.
311327
use_deepcopy (bool, optional): Apply deepcopy to all child objects when copy from source to target object.
312328
Defaults to True.
@@ -326,29 +342,20 @@ def _map_common(
326342
target_cls_fields = self._get_fields(target_cls)
327343

328344
mapped_values: Dict[str, Any] = {}
329-
is_obj_subscriptable = _is_subscriptable(obj)
330345
for field_name in target_cls_fields:
331-
if (
332-
(fields_mapping and field_name in fields_mapping)
333-
or hasattr(obj, field_name)
334-
or (is_obj_subscriptable and field_name in obj) # type: ignore [operator]
335-
):
336-
if fields_mapping and field_name in fields_mapping:
337-
value = fields_mapping[field_name]
338-
elif hasattr(obj, field_name):
339-
value = getattr(obj, field_name)
340-
else:
341-
value = obj[field_name] # type: ignore [index]
342-
343-
if value is not None:
344-
if use_deepcopy:
345-
mapped_values[field_name] = self._map_subobject(
346-
value, _visited_stack, skip_none_values
347-
)
348-
else: # if use_deepcopy is False, simply assign value to target obj.
349-
mapped_values[field_name] = value
350-
elif not skip_none_values:
351-
mapped_values[field_name] = None
346+
value_found, value = _try_get_field_value(field_name, obj, custom_mapping)
347+
if not value_found:
348+
continue
349+
350+
if value is not None:
351+
if use_deepcopy:
352+
mapped_values[field_name] = self._map_subobject(
353+
value, _visited_stack, skip_none_values
354+
)
355+
else: # if use_deepcopy is False, simply assign value to target obj.
356+
mapped_values[field_name] = value
357+
elif not skip_none_values:
358+
mapped_values[field_name] = None
352359

353360
_visited_stack.remove(obj_id)
354361

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from automapper import mapper
2+
3+
class PublicUserInfo(object):
4+
def __init__(self, name: str, profession: str):
5+
self.name = name
6+
self.profession = profession
7+
8+
9+
def test_map__dict_to_object():
10+
original = {
11+
"name": "John Carter",
12+
"age": 35,
13+
"profession": "hero"
14+
}
15+
16+
public_info = mapper.to(PublicUserInfo).map(original)
17+
18+
assert hasattr(public_info, "name") and public_info.name == "John Carter"
19+
assert hasattr(public_info, "profession") and public_info.profession == "hero"
20+
assert not hasattr(public_info, "age")

0 commit comments

Comments
 (0)