Skip to content

Commit 8ac61b1

Browse files
committed
applied pre-commit corrections. Improved doc comments
1 parent 4c93bf1 commit 8ac61b1

File tree

8 files changed

+155
-62
lines changed

8 files changed

+155
-62
lines changed

automapper/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ class MappingError(Exception):
88

99
class CircularReferenceError(Exception):
1010
def __init__(self, *args: object) -> None:
11-
super().__init__("Mapper does not support objects with circular references yet", *args)
11+
super().__init__(
12+
"Mapper does not support objects with circular references yet", *args
13+
)

automapper/extensions/default.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ def __init_method_classifier__(target_cls: Type[T]) -> bool:
1111
return (
1212
hasattr(target_cls, "__init__")
1313
and hasattr(getattr(target_cls, "__init__"), "__annotations__")
14-
and isinstance(getattr(getattr(target_cls, "__init__"), "__annotations__"), dict)
14+
and isinstance(
15+
getattr(getattr(target_cls, "__init__"), "__annotations__"), dict
16+
)
1517
and getattr(getattr(target_cls, "__init__"), "__annotations__")
1618
)
1719

automapper/mapper.py

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@
3232
__PRIMITIVE_TYPES = {int, float, complex, str, bytes, bytearray, bool}
3333

3434

35-
def is_sequence(obj: Any) -> bool:
35+
def _is_sequence(obj: Any) -> bool:
3636
"""Check if object implements `__iter__` method"""
3737
return hasattr(obj, "__iter__")
3838

3939

40-
def is_subscriptable(obj: Any) -> bool:
40+
def _is_subscriptable(obj: Any) -> bool:
4141
"""Check if object implements `__get_item__` method"""
4242
return hasattr(obj, "__get_item__")
4343

4444

45-
def is_primitive(obj: Any) -> bool:
45+
def _is_primitive(obj: Any) -> bool:
4646
"""Check if object type is primitive"""
4747
return type(obj) in __PRIMITIVE_TYPES
4848

@@ -67,12 +67,26 @@ def map(
6767
fields_mapping: FieldsMap = None,
6868
deepcopy: bool = True,
6969
) -> T:
70-
"""Produces output object mapped from source object and custom arguments
70+
"""Produces output object mapped from source object and custom arguments.
7171
7272
Parameters:
7373
skip_none_values - do not map fields that has None value
7474
fields_mapping - mapping for fields with different names
7575
deepcopy - should we deepcopy all attributes? [default: True]
76+
77+
Args:
78+
obj (S): _description_
79+
skip_none_values (bool, optional): Skip None values when creating `target class` obj. Defaults to False.
80+
fields_mapping (FieldsMap, optional): Custom mapping.
81+
Specify dictionary in format {"field_name": value_object}. Defaults to None.
82+
deepcopy (bool, optional): Applies deepcopy to all child objects when copy into output instance.
83+
Defaults to True.
84+
85+
Raises:
86+
CircularReferenceError: Circular references in `source class` object are not allowed yet.
87+
88+
Returns:
89+
T: instance of `target class` with mapped values from `source class` or custom `fields_mapping` dictionary.
7690
"""
7791
return self.__mapper._map_common(
7892
obj,
@@ -97,22 +111,22 @@ def __init__(self) -> None:
97111
def add_spec(self, classifier: Type[T], spec_func: SpecFunction[T]) -> None:
98112
"""Add a spec function for all classes in inherited from base class.
99113
100-
Parameters:
101-
* classifier - base class to identify all descendant classes
102-
* spec_func - returns a list of fields (List[str]) for target class
103-
that are accepted in constructor
114+
Args:
115+
classifier (ClassifierFunction[T]): base class to identify all descendant classes.
116+
spec_func (SpecFunction[T]): get list of fields (List[str]) for `target class` to be passed in constructor.
104117
"""
105118
...
106119

107120
@overload
108-
def add_spec(self, classifier: ClassifierFunction[T], spec_func: SpecFunction[T]) -> None:
121+
def add_spec(
122+
self, classifier: ClassifierFunction[T], spec_func: SpecFunction[T]
123+
) -> None:
109124
"""Add a spec function for all classes identified by classifier function.
110125
111-
Parameters:
112-
* classifier - boolean predicate that identifies a group of classes
113-
by certain characteristics: if class has a specific method or a field, etc.
114-
* spec_func - returns a list of fields (List[str]) for target class
115-
that are accepted in constructor
126+
Args:
127+
classifier (ClassifierFunction[T]): boolean predicate that identifies a group of classes
128+
by certain characteristics: if class has a specific method or a field, etc.
129+
spec_func (SpecFunction[T]): get list of fields (List[str]) for `target class` to be passed in constructor.
116130
"""
117131
...
118132

@@ -146,16 +160,21 @@ def add(
146160
) -> None:
147161
"""Adds mapping between object of `source class` to an object of `target class`.
148162
149-
Parameters
150-
----------
151-
source_cls : Type
152-
Source class to map from
153-
target_cls : Type
154-
Target class to map to
155-
override : bool, optional
156-
Override existing `source class` mapping to use new `target class`
157-
deepcopy : bool, optional
158-
Should we deepcopy all attributes? [default: True]
163+
Args:
164+
source_cls (Type[S]): Source class to map from
165+
target_cls (Type[T]): Target class to map to
166+
override (bool, optional): Override existing `source class` mapping to use new `target class`.
167+
Defaults to False.
168+
fields_mapping (FieldsMap, optional): Custom mapping.
169+
Specify dictionary in format {"field_name": value_object}. Defaults to None.
170+
deepcopy (bool, optional): Applies deepcopy to all child objects when copy into output instance.
171+
Defaults to True.
172+
173+
Raises:
174+
DuplicatedRegistrationError: Same mapping for `source class` was added.
175+
Only one mapping per source class can exist at a time for now.
176+
You can specify target class manually using `mapper.to(target_cls)` method
177+
or use `override` argument to replace existing mapping.
159178
"""
160179
if source_cls in self._mappings and not override:
161180
raise DuplicatedRegistrationError(
@@ -169,22 +188,42 @@ def map(
169188
*,
170189
skip_none_values: bool = False,
171190
fields_mapping: FieldsMap = None,
172-
deepcopy: bool = None,
191+
deepcopy: bool = True,
173192
) -> T: # type: ignore [type-var]
174-
"""Produces output object mapped from source object and custom arguments"""
193+
"""Produces output object mapped from source object and custom arguments
194+
195+
Args:
196+
obj (object): Source object to map to `target class`.
197+
skip_none_values (bool, optional): Skip None values when creating `target class` obj. Defaults to False.
198+
fields_mapping (FieldsMap, optional): Custom mapping.
199+
Specify dictionary in format {"field_name": value_object}. Defaults to None.
200+
deepcopy (bool, optional): Applies deepcopy to all child objects when copy into output instance.
201+
Defaults to True.
202+
203+
Raises:
204+
MappingError: No `target class` specified to be mapped into.
205+
Register mappings using `mapped.add(...)` or specify `target class` using `mapper.to(target_cls).map()`.
206+
CircularReferenceError: Circular references in `source class` object are not allowed yet.
207+
208+
Returns:
209+
T: instance of `target class` with mapped values from `source class` or custom `fields_mapping` dictionary.
210+
"""
175211
obj_type = type(obj)
176212
if obj_type not in self._mappings:
177213
raise MappingError(f"Missing mapping type for input type {obj_type}")
178214
obj_type_preffix = f"{obj_type.__name__}."
179215

180-
target_cls, target_cls_field_mappings, target_deepcopy = self._mappings[obj_type]
216+
target_cls, target_cls_field_mappings, target_deepcopy = self._mappings[
217+
obj_type
218+
]
181219

182220
common_fields_mapping = fields_mapping
183221
if target_cls_field_mappings:
184222
# transform mapping if it's from source class field
185223
common_fields_mapping = {
186224
target_obj_field: getattr(obj, source_field[len(obj_type_preffix) :])
187-
if isinstance(source_field, str) and source_field.startswith(obj_type_preffix)
225+
if isinstance(source_field, str)
226+
and source_field.startswith(obj_type_preffix)
188227
else source_field
189228
for target_obj_field, source_field in target_cls_field_mappings.items()
190229
}
@@ -217,13 +256,15 @@ def _get_fields(self, target_cls: Type[T]) -> Iterable[str]:
217256
if classifier(target_cls):
218257
return self._classifier_specs[classifier](target_cls)
219258

220-
raise MappingError(f"No spec function is added for base class of {type(target_cls)}")
259+
raise MappingError(
260+
f"No spec function is added for base class of {type(target_cls)}"
261+
)
221262

222263
def _map_subobject(
223264
self, obj: S, _visited_stack: Set[int], skip_none_values: bool = False
224265
) -> Any:
225266
"""Maps subobjects recursively"""
226-
if is_primitive(obj):
267+
if _is_primitive(obj):
227268
return obj
228269

229270
obj_id = id(obj)
@@ -238,10 +279,12 @@ def _map_subobject(
238279
else:
239280
_visited_stack.add(obj_id)
240281

241-
if is_sequence(obj):
282+
if _is_sequence(obj):
242283
if isinstance(obj, dict):
243284
result = {
244-
k: self._map_subobject(v, _visited_stack, skip_none_values=skip_none_values)
285+
k: self._map_subobject(
286+
v, _visited_stack, skip_none_values=skip_none_values
287+
)
245288
for k, v in obj.items()
246289
}
247290
else:
@@ -269,12 +312,23 @@ def _map_common(
269312
fields_mapping: FieldsMap = None,
270313
deepcopy: bool = True,
271314
) -> T:
272-
"""Produces output object mapped from source object and custom arguments
273-
274-
Parameters:
275-
skip_none_values - do not map fields that has None value
276-
fields_mapping - fields mappings for fields with different names
277-
deepcopy - Should we deepcopy all attributes? [default: True]
315+
"""Produces output object mapped from source object and custom arguments.
316+
317+
Args:
318+
obj (S): Source object to map to `target class`.
319+
target_cls (Type[T]): Target class to map to.
320+
_visited_stack (Set[int]): Visited child objects. To avoid infinite recursive calls.
321+
skip_none_values (bool, optional): Skip None values when creating `target class` obj. Defaults to False.
322+
fields_mapping (FieldsMap, optional): Custom mapping.
323+
Specify dictionary in format {"field_name": value_object}. Defaults to None.
324+
deepcopy (bool, optional): Applies deepcopy to all child objects when copy into output instance.
325+
Defaults to True.
326+
327+
Raises:
328+
CircularReferenceError: Circular references in `source class` object are not allowed yet.
329+
330+
Returns:
331+
T: Instance of `target class` with mapped fields.
278332
"""
279333
obj_id = id(obj)
280334

@@ -285,7 +339,7 @@ def _map_common(
285339
target_cls_fields = self._get_fields(target_cls)
286340

287341
mapped_values: Dict[str, Any] = {}
288-
is_obj_subscriptable = is_subscriptable(obj)
342+
is_obj_subscriptable = _is_subscriptable(obj)
289343
for field_name in target_cls_fields:
290344
if (
291345
(fields_mapping and field_name in fields_mapping)
@@ -316,5 +370,12 @@ def _map_common(
316370
return cast(target_cls, target_cls(**mapped_values)) # type: ignore [valid-type]
317371

318372
def to(self, target_cls: Type[T]) -> MappingWrapper[T]:
319-
"""Specify target class to map source object to"""
373+
"""Specify `target class` to which map `source class` object.
374+
375+
Args:
376+
target_cls (Type[T]): Target class.
377+
378+
Returns:
379+
MappingWrapper[T]: Mapping wrapper. Use `map` method to perform mapping now.
380+
"""
320381
return MappingWrapper[T](self, target_cls)

tests/test_automapper.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def __init__(self, num: int, text: str, flag: bool) -> None:
2020

2121
@classmethod
2222
def fields(cls) -> Iterable[str]:
23-
return (field for field in cls.__init__.__annotations__.keys() if field != "return")
23+
return (
24+
field for field in cls.__init__.__annotations__.keys() if field != "return"
25+
)
2426

2527

2628
class AnotherClass:
@@ -66,12 +68,16 @@ def setUp(self):
6668
def test_add_spec__adds_to_internal_collection(self):
6769
self.mapper.add_spec(ParentClass, custom_spec_func)
6870
assert ParentClass in self.mapper._class_specs
69-
assert ["num", "text", "flag"] == self.mapper._class_specs[ParentClass](ChildClass)
71+
assert ["num", "text", "flag"] == self.mapper._class_specs[ParentClass](
72+
ChildClass
73+
)
7074

7175
def test_add_spec__error_on_adding_same_class_spec(self):
7276
self.mapper.add_spec(ParentClass, custom_spec_func)
7377
with pytest.raises(DuplicatedRegistrationError):
74-
self.mapper.add_spec(ParentClass, lambda concrete_type: ["field1", "field2"])
78+
self.mapper.add_spec(
79+
ParentClass, lambda concrete_type: ["field1", "field2"]
80+
)
7581

7682
def test_add_spec__adds_to_internal_collection_for_classifier(self):
7783
self.mapper.add_spec(classifier_func, spec_func)

tests/test_automapper_dict_field.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Dict
22
from unittest import TestCase
33

4-
from automapper import mapper, create_mapper
4+
from automapper import create_mapper, mapper
55

66

77
class Candy:
@@ -36,17 +36,27 @@ def setUp(self) -> None:
3636
def test_map__with_dict_field(self):
3737
public_info = mapper.to(ShopPublicInfo).map(self.shop)
3838

39-
self.assertEqual(public_info.products["magazines"], self.shop.products["magazines"])
39+
self.assertEqual(
40+
public_info.products["magazines"], self.shop.products["magazines"]
41+
)
4042
self.assertNotEqual(
4143
id(public_info.products["magazines"]), id(self.shop.products["magazines"])
4244
)
4345

44-
self.assertNotEqual(public_info.products["candies"], self.shop.products["candies"])
45-
self.assertNotEqual(public_info.products["candies"][0], self.shop.products["candies"][0])
46-
self.assertNotEqual(public_info.products["candies"][1], self.shop.products["candies"][1])
46+
self.assertNotEqual(
47+
public_info.products["candies"], self.shop.products["candies"]
48+
)
49+
self.assertNotEqual(
50+
public_info.products["candies"][0], self.shop.products["candies"][0]
51+
)
52+
self.assertNotEqual(
53+
public_info.products["candies"][1], self.shop.products["candies"][1]
54+
)
4755

4856
self.assertEqual(public_info.products["candies"][0].name, "Reese's cups")
49-
self.assertEqual(public_info.products["candies"][0].brand, "The Hershey Company")
57+
self.assertEqual(
58+
public_info.products["candies"][0].brand, "The Hershey Company"
59+
)
5060

5161
self.assertEqual(public_info.products["candies"][1].name, "Snickers")
5262
self.assertEqual(public_info.products["candies"][1].brand, "Mars, Incorporated")
@@ -56,20 +66,25 @@ def test_deepcopy_disabled(self):
5666
public_info = mapper.to(ShopPublicInfo).map(self.shop)
5767

5868
self.assertIsNot(public_info.products, self.shop.products)
59-
self.assertEqual(public_info.products["magazines"], self.shop.products["magazines"])
60-
self.assertNotEqual(public_info.products["magazines"], id(self.shop.products["magazines"]))
69+
self.assertEqual(
70+
public_info.products["magazines"], self.shop.products["magazines"]
71+
)
72+
self.assertNotEqual(
73+
public_info.products["magazines"], id(self.shop.products["magazines"])
74+
)
6175

6276
self.assertIs(public_info_deep.products, self.shop.products)
6377
self.assertEqual(
64-
id(public_info_deep.products["magazines"]), id(self.shop.products["magazines"])
78+
id(public_info_deep.products["magazines"]),
79+
id(self.shop.products["magazines"]),
6580
)
6681

6782
def test_deepcopy_disabled_in_add(self):
6883
self.mapper.add(Shop, ShopPublicInfo, deepcopy=False)
69-
public_info = self.mapper.map(self.shop)
84+
public_info: ShopPublicInfo = self.mapper.map(self.shop)
7085

7186
self.assertIs(public_info.products, self.shop.products)
7287

7388
# Manually enable deepcopy on .map()
74-
public_info = self.mapper.map(self.shop, deepcopy=True)
75-
self.assertIsNot(public_info.products, self.shop.products)
89+
public_info2: ShopPublicInfo = self.mapper.map(self.shop, deepcopy=True)
90+
self.assertIsNot(public_info2.products, self.shop.products)

tests/test_automapper_sample.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
23
from automapper import mapper
34

45

@@ -64,7 +65,9 @@ def test_map__field_with_different_name():
6465

6566
def test_map__field_with_different_name_register():
6667
try:
67-
mapper.add(UserInfo, PublicUserInfoDiff, fields_mapping={"full_name": "UserInfo.name"})
68+
mapper.add(
69+
UserInfo, PublicUserInfoDiff, fields_mapping={"full_name": "UserInfo.name"}
70+
)
6871

6972
user_info = UserInfo("John Malkovich", 35, "engineer")
7073
public_user_info: PublicUserInfoDiff = mapper.map(user_info)

0 commit comments

Comments
 (0)