Skip to content

Commit ef3e70f

Browse files
authored
Merge pull request #27 from anikolaienko/bugfix/issue-25
Fixing issue 25
2 parents 8a72f4c + e7e9659 commit ef3e70f

File tree

7 files changed

+227
-17
lines changed

7 files changed

+227
-17
lines changed

automapper/mapper.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
DuplicatedRegistrationError,
2222
MappingError,
2323
)
24-
from .utils import is_enum, is_primitive, is_sequence, object_contains
24+
from .utils import is_dictionary, is_enum, is_primitive, is_sequence, object_contains
2525

2626
# Custom Types
2727
S = TypeVar("S")
@@ -264,23 +264,24 @@ def _map_subobject(
264264
else:
265265
_visited_stack.add(obj_id)
266266

267-
if is_sequence(obj):
268-
if isinstance(obj, dict):
269-
result = {
267+
if is_dictionary(obj):
268+
result = type(obj)( # type: ignore [call-arg]
269+
{
270270
k: self._map_subobject(
271271
v, _visited_stack, skip_none_values=skip_none_values
272272
)
273-
for k, v in obj.items()
273+
for k, v in obj.items() # type: ignore [attr-defined]
274274
}
275-
else:
276-
result = type(obj)( # type: ignore [call-arg]
277-
[
278-
self._map_subobject(
279-
x, _visited_stack, skip_none_values=skip_none_values
280-
)
281-
for x in cast(Iterable[Any], obj)
282-
]
283-
)
275+
)
276+
elif is_sequence(obj):
277+
result = type(obj)( # type: ignore [call-arg]
278+
[
279+
self._map_subobject(
280+
x, _visited_stack, skip_none_values=skip_none_values
281+
)
282+
for x in cast(Iterable[Any], obj)
283+
]
284+
)
284285
else:
285286
result = deepcopy(obj)
286287

automapper/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
from enum import Enum
2-
from typing import Any
2+
from typing import Any, Dict, Sequence
33

44
__PRIMITIVE_TYPES = {int, float, complex, str, bytes, bytearray, bool}
55

66

77
def is_sequence(obj: Any) -> bool:
88
"""Check if object implements `__iter__` method"""
9-
return hasattr(obj, "__iter__")
9+
return isinstance(obj, Sequence)
10+
11+
12+
def is_dictionary(obj: Any) -> bool:
13+
"""Check is object is of type dictionary"""
14+
return isinstance(obj, Dict)
1015

1116

1217
def is_subscriptable(obj: Any) -> bool:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "py-automapper"
7-
version = "2.0.0"
7+
version = "2.1.0"
88
description = "Library for automatically mapping one object to another"
99
authors = [
1010
{name = "Andrii Nikolaienko", email = "[email protected]"}

tests/test_automapper_dict.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from collections import OrderedDict
2+
from dataclasses import dataclass
3+
from typing import Dict
4+
5+
from automapper import mapper
6+
7+
8+
@dataclass
9+
class Teacher:
10+
teacher: str
11+
12+
13+
class Student:
14+
def __init__(self, name: str, classes: Dict[str, Teacher]):
15+
self.name = name
16+
self.classes = classes
17+
self.ordered_classes = OrderedDict(classes)
18+
19+
20+
class PublicUserInfo:
21+
def __init__(
22+
self,
23+
name: str,
24+
classes: Dict[str, Teacher],
25+
ordered_classes: Dict[str, Teacher],
26+
):
27+
self.name = name
28+
self.classes = classes
29+
self.ordered_classes = ordered_classes
30+
31+
32+
def test_map__dict_and_ordereddict_are_mapped_correctly_to_same_types():
33+
classes = {"math": Teacher("Ms G"), "art": Teacher("Mr A")}
34+
student = Student("Tim", classes)
35+
36+
public_info = mapper.to(PublicUserInfo).map(student)
37+
38+
assert public_info.name is student.name
39+
40+
assert public_info.classes == student.classes
41+
assert public_info.classes is not student.classes
42+
assert isinstance(public_info.classes, Dict)
43+
44+
assert public_info.ordered_classes == student.ordered_classes
45+
assert public_info.ordered_classes is not student.ordered_classes
46+
assert isinstance(public_info.ordered_classes, OrderedDict)

tests/test_issue_25.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Optional
2+
3+
from automapper import mapper
4+
from pydantic import BaseModel
5+
6+
7+
class Address(BaseModel):
8+
street: Optional[str]
9+
number: Optional[int]
10+
zip_code: Optional[int]
11+
city: Optional[str]
12+
13+
14+
class PersonInfo(BaseModel):
15+
name: Optional[str] = None
16+
age: Optional[int] = None
17+
address: Optional[Address] = None
18+
19+
20+
class PublicPersonInfo(BaseModel):
21+
name: Optional[str] = None
22+
address: Optional[Address] = None
23+
24+
25+
def test_map__without_deepcopy_mapped_objects_should_be_the_same():
26+
address = Address(street="Main Street", number=1, zip_code=100001, city="Test City")
27+
info = PersonInfo(name="John Doe", age=35, address=address)
28+
29+
# default deepcopy behavior
30+
public_info = mapper.to(PublicPersonInfo).map(info)
31+
assert (
32+
public_info.address is not address
33+
), "Address mapped object is same as origin."
34+
35+
# disable deepcopy
36+
public_info = mapper.to(PublicPersonInfo).map(info, use_deepcopy=False)
37+
assert (
38+
public_info.address is info.address
39+
), "Address mapped object is not same as origin."

tests/test_try_get_field_values.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from dataclasses import dataclass
2+
3+
from automapper.mapper import _try_get_field_value
4+
5+
6+
@dataclass
7+
class TestClass:
8+
map_field: str
9+
10+
11+
def test_try_get_field_value__if_in_custom_mapping():
12+
is_found, mapped_value = _try_get_field_value("field1", None, {"field1": 123})
13+
14+
assert is_found
15+
assert mapped_value == 123
16+
17+
18+
def test_try_get_field_value__if_origin_has_same_field_attr():
19+
is_found, mapped_value = _try_get_field_value(
20+
"map_field", TestClass("Hello world"), None
21+
)
22+
23+
assert is_found
24+
assert mapped_value == "Hello world"
25+
26+
27+
def test_try_get_field_value__if_origin_contains_same_field_as_item():
28+
is_found, mapped_value = _try_get_field_value(
29+
"map_field", {"map_field": "Hello world. Again"}, None
30+
)
31+
32+
assert is_found
33+
assert mapped_value == "Hello world. Again"
34+
35+
36+
def test_try_get_field_value__if_field_not_found():
37+
is_found, mapped_value = _try_get_field_value("field1", None, None)
38+
39+
assert not is_found
40+
assert mapped_value is None

tests/test_utils.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from collections import OrderedDict
2+
from enum import Enum
3+
4+
from automapper.utils import (
5+
is_dictionary,
6+
is_enum,
7+
is_primitive,
8+
is_sequence,
9+
is_subscriptable,
10+
object_contains,
11+
)
12+
13+
14+
def test_is_sequence__list_is_sequence():
15+
assert is_sequence([1, 2])
16+
17+
18+
def test_is_sequence__tuple_is_sequence():
19+
assert is_sequence((1, 2, 3))
20+
21+
22+
def test_is_sequence__dict_is_not_a_sequence():
23+
assert not is_sequence({"a": 1})
24+
25+
26+
def test_is_dictionary__dict_is_of_type_dictionary():
27+
assert is_dictionary({"a1": 1})
28+
29+
30+
def test_is_dictionary__ordered_dict_is_of_type_dictionary():
31+
assert is_dictionary(OrderedDict({"a1": 1}))
32+
33+
34+
def test_is_subscriptable__dict_is_subscriptable():
35+
assert is_subscriptable({"a": 1})
36+
37+
38+
def test_is_subscriptable__custom_class_can_be_subscriptable():
39+
class A:
40+
def __getitem__(self):
41+
yield 1
42+
43+
assert is_subscriptable(A())
44+
45+
46+
def test_object_contains__dict_contains_field():
47+
assert object_contains({"a1": 1, "b2": 2}, "a1")
48+
49+
50+
def test_object_contains__dict_does_not_contain_field():
51+
assert not object_contains({"a1": 1, "b2": 2}, "c3")
52+
53+
54+
def test_is_primitive__int_is_primitive():
55+
assert is_primitive(1)
56+
57+
58+
def test_is_primitive__float_is_primitive():
59+
assert is_primitive(1.2)
60+
61+
62+
def test_is_primitive__str_is_primitive():
63+
assert is_primitive("hello")
64+
65+
66+
def test_is_primitive__bool_is_primitive():
67+
assert is_primitive(False)
68+
69+
70+
def test_is_enum__object_is_enum():
71+
class EnumValue(Enum):
72+
A = "A"
73+
B = "B"
74+
75+
assert is_enum(EnumValue("A"))
76+
77+
78+
def test_is_enum__dict_is_not_enum():
79+
assert not is_enum({"A": 1, "B": 2})

0 commit comments

Comments
 (0)