Skip to content

Commit 6653f86

Browse files
authored
Merge pull request #16 from anikolaienko/bugfix/mapping_dict_to_obj
fixed mapping dict to obj, simplified code
2 parents b9ce4be + 161a6c9 commit 6653f86

File tree

8 files changed

+101
-38
lines changed

8 files changed

+101
-38
lines changed

.coveragerc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[run]
2+
branch = True
3+
omit =
4+
*/__init__.py
5+
tests/*
6+
7+
[report]
8+
show_missing = True
9+
fail_under = 94
10+
11+
[html]
12+
directory = docs/coverage

.github/workflows/run_code_checks.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ jobs:
3535
run: poetry run pre-commit run --all-files
3636

3737
- name: Run unit tests
38-
run: poetry run pytest tests/
38+
run: poetry run coverage run -m pytest tests/
39+
40+
- name: Check unit test coverage
41+
run: poetry run coverage report

.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,

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
1.2.1 - 2022/11/13
2+
* Fixed dictionary source mapping to target object.
3+
* Implemented CI checks
4+
15
1.2.0 - 2022/10/25
26
* [g-pichler] Ability to disable deepcopy on mapping: `use_deepcopy` flag in `map` method.
37
* [g-pichler] Improved error text when no spec function exists for `target class`.

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Table of Contents:
1313
- [About](#about)
1414
- [Contribute](#contribute)
1515
- [Usage](#usage)
16+
- [Installation](#installation)
17+
- [Get started](#get-started)
18+
- [Map dictionary source to target object](#map-dictionary-source-to-target-object)
1619
- [Different field names](#different-field-names)
1720
- [Overwrite field value in mapping](#overwrite-field-value-in-mapping)
1821
- [Disable Deepcopy](#disable-deepcopy)
@@ -37,25 +40,27 @@ The major advantage of py-automapper is its extensibility, that allows it to map
3740
Read [CONTRIBUTING.md](/CONTRIBUTING.md) guide.
3841

3942
# Usage
43+
## Installation
4044
Install package:
4145
```bash
4246
pip install py-automapper
4347
```
4448

45-
Let's say we have domain model `UserInfo` and its API representation `PublicUserInfo` with `age` field missing:
49+
## Get started
50+
Let's say we have domain model `UserInfo` and its API representation `PublicUserInfo` without exposing user `age`:
4651
```python
4752
class UserInfo:
48-
def __init__(self, name: str, age: int, profession: str):
53+
def __init__(self, name: str, profession: str, age: int):
4954
self.name = name
50-
self.age = age
5155
self.profession = profession
56+
self.age = age
5257

5358
class PublicUserInfo:
5459
def __init__(self, name: str, profession: str):
5560
self.name = name
5661
self.profession = profession
5762

58-
user_info = UserInfo("John Malkovich", 35, "engineer")
63+
user_info = UserInfo("John Malkovich", "engineer", 35)
5964
```
6065
To create `PublicUserInfo` object:
6166
```python
@@ -77,6 +82,19 @@ print(vars(public_user_info))
7782
# {'name': 'John Malkovich', 'profession': 'engineer'}
7883
```
7984

85+
## Map dictionary source to target object
86+
If source object is dictionary:
87+
```python
88+
source = {
89+
"name": "John Carter",
90+
"profession": "hero"
91+
}
92+
public_info = mapper.to(PublicUserInfo).map(source)
93+
94+
print(vars(public_info))
95+
# {'name': 'John Carter', 'profession': 'hero'}
96+
```
97+
8098
## Different field names
8199
If your target class field name is different from source class.
82100
```python

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "py-automapper"
3-
version = "1.2.0"
3+
version = "1.2.1"
44
description = "Library for automatically mapping one object to another"
55
authors = ["Andrii Nikolaienko <[email protected]>"]
66
license = "MIT"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from automapper import mapper
2+
3+
4+
class PublicUserInfo(object):
5+
def __init__(self, name: str, profession: str):
6+
self.name = name
7+
self.profession = profession
8+
9+
10+
def test_map__dict_to_object():
11+
original = {"name": "John Carter", "age": 35, "profession": "hero"}
12+
13+
public_info = mapper.to(PublicUserInfo).map(original)
14+
15+
assert hasattr(public_info, "name") and public_info.name == "John Carter"
16+
assert hasattr(public_info, "profession") and public_info.profession == "hero"
17+
assert not hasattr(public_info, "age")

0 commit comments

Comments
 (0)