3
3
Copyright (C) 2015, marazt. All rights reserved.
4
4
"""
5
5
from inspect import getmembers , isroutine
6
+ from datetime import date , datetime
6
7
7
8
from mapper .casedict import CaseDict
8
9
from mapper .object_mapper_exception import ObjectMapperException
@@ -14,6 +15,8 @@ class ObjectMapper(object):
14
15
Supports mapping conversions too
15
16
"""
16
17
18
+ primitive_types = { int , str , bool , date , datetime }
19
+
17
20
def __init__ (self ):
18
21
"""Constructor
19
22
@@ -60,7 +63,7 @@ def __init__(self):
60
63
mapper = ObjectMapper()
61
64
mapper.create_map(A, B, {'last_name': None})
62
65
63
- instance_b = mapper.map(A(), B )
66
+ instance_b = mapper.map(A())
64
67
65
68
In this case, value of A.name will be copied into B.name automatically by the attribute name 'name'.
66
69
Attribute A.last_name will be not mapped thanks the suppression (lambda function is None).
@@ -72,18 +75,22 @@ class 'B' with attributes 'name' and 'age' and we want to map 'A' to 'B' in a wa
72
75
Initialization of the ObjectMapper will be:
73
76
mapper = ObjectMapper()
74
77
mapper.create_map(A, B)
75
- instance_b = mapper.map(A(), B, ignore_case=True)
78
+ instance_b = mapper.map(A(), ignore_case=True)
76
79
77
80
In this case, the value of A.Name will be copied into B.name and
78
81
the value of A.Age will be copied into B.age.
79
82
80
83
:return: Instance of the ObjectMapper
81
84
"""
82
- # self.to_type = to_type
85
+
86
+ # mapping is a 2-layer dict keyed by source type then by dest type, and stores two things in a tuple:
87
+ # - the destination type class
88
+ # - custom mapping functions, if any
83
89
self .mappings = {}
84
90
pass
85
91
86
92
def create_map (self , type_from , type_to , mapping = None ):
93
+ # type: (type, type, Dict) -> None
87
94
"""Method for adding mapping definitions
88
95
89
96
:param type_from: source type
@@ -93,32 +100,43 @@ def create_map(self, type_from, type_to, mapping=None):
93
100
94
101
:return: None
95
102
"""
103
+
104
+ if (type (type_from ) is not type ):
105
+ raise ObjectMapperException ("type_from must be a type" )
106
+
107
+ if (type (type_to ) is not type ):
108
+ raise ObjectMapperException ("type_to must be a type" )
109
+
110
+ if (mapping is not None and not isinstance (mapping , dict )):
111
+ raise ObjectMapperException ("mapping, if provided, must be a Dict type" )
112
+
96
113
key_from = type_from
97
114
key_to = type_to
98
115
99
- if mapping is None :
100
- mapping = {}
101
-
102
116
if key_from in self .mappings :
103
117
inner_map = self .mappings [key_from ]
104
118
if key_to in inner_map :
105
119
raise ObjectMapperException (
106
120
"Mapping for {0}.{1} -> {2}.{3} already exists" .format (key_from .__module__ , key_from .__name__ ,
107
121
key_to .__module__ , key_to .__name__ ))
108
122
else :
109
- inner_map [key_to ] = mapping
123
+ inner_map [key_to ] = ( type_to , mapping )
110
124
else :
111
125
self .mappings [key_from ] = {}
112
- self .mappings [key_from ][key_to ] = mapping
126
+ self .mappings [key_from ][key_to ] = (type_to , mapping )
127
+
113
128
114
- def map (self , from_obj , to_type , ignore_case = False , allow_none = False , excluded = None ):
129
+ def map (self , from_obj , to_type = type (None ), ignore_case = False , allow_none = False , excluded = None , included = None , allow_unmapped = False ):
130
+ # type: (object, type, bool, bool, List[str], List[str], bool) -> object
115
131
"""Method for creating target object instance
116
132
117
133
:param from_obj: source object to be mapped from
118
134
:param to_type: target type
119
135
:param ignore_case: if set to true, ignores attribute case when performing the mapping
120
136
:param allow_none: if set to true, returns None if the source object is None; otherwise throws an exception
121
137
:param excluded: A list of fields to exclude when performing the mapping
138
+ :param included: A list of fields to force inclusion when performing the mapping
139
+ :param allow_unmapped: if set to true, copy over the non-primitive object that didn't have a mapping defined; otherwise exception
122
140
123
141
:return: Instance of the target class with mapped attributes
124
142
"""
@@ -128,22 +146,45 @@ def map(self, from_obj, to_type, ignore_case=False, allow_none=False, excluded=N
128
146
# one of the tests is explicitly checking for an attribute error on __dict__ if it's not set
129
147
from_obj .__dict__
130
148
131
- inst = to_type ()
132
149
key_from = from_obj .__class__
133
- key_to = to_type
150
+
151
+ if key_from not in self .mappings :
152
+ raise ObjectMapperException ("No mapping defined for {0}.{1}"
153
+ .format (key_from .__module__ , key_from .__name__ ))
154
+
155
+ if to_type is None or to_type is type (None ):
156
+ # automatically infer to to_type
157
+ # if this is a nested call and we do not currently support more than one to_types
158
+ assert (len (self .mappings [key_from ]) > 0 )
159
+ if len (self .mappings [key_from ]) > 1 :
160
+ raise ObjectMapperException ("Ambiguous type mapping exists for {0}.{1}, must specifiy to_type explicitly"
161
+ .format (key_from .__module__ , key_from .__name__ ))
162
+ key_to = next (iter (self .mappings [key_from ]))
163
+ else :
164
+ if to_type not in self .mappings [key_from ]:
165
+ raise ObjectMapperException ("No mapping defined for {0}.{1} -> {2}.{3}"
166
+ .format (key_from .__module__ , key_from .__name__ , to_type .__module__ , to_type .__name__ ))
167
+ key_to = to_type
168
+ custom_mappings = self .mappings [key_from ][key_to ][1 ]
169
+
170
+ # Currently, all target class data members need to have default value
171
+ # Object with __init__ that carries required non-default arguments are not supported
172
+ inst = key_to ()
134
173
135
174
def not_private (s ):
136
175
return not s .startswith ('_' )
137
176
138
177
def not_excluded (s ):
139
178
return not (excluded and s in excluded )
140
179
180
+ def is_included (s , mapping ):
181
+ return (included and s in included ) or (mapping and s in mapping )
182
+
141
183
from_obj_attributes = getmembers (from_obj , lambda a : not isroutine (a ))
142
- from_obj_dict = {k : v for k , v in from_obj_attributes
143
- if not_private (k ) and not_excluded (k )}
184
+ from_obj_dict = {k : v for k , v in from_obj_attributes }
144
185
145
186
to_obj_attributes = getmembers (inst , lambda a : not isroutine (a ))
146
- to_obj_dict = {k : v for k , v in to_obj_attributes if not_private (k )}
187
+ to_obj_dict = {k : v for k , v in to_obj_attributes if not_excluded ( k ) and ( not_private (k ) or is_included ( k , custom_mappings ) )}
147
188
148
189
if ignore_case :
149
190
from_props = CaseDict (from_obj_dict )
@@ -152,29 +193,54 @@ def not_excluded(s):
152
193
from_props = from_obj_dict
153
194
to_props = to_obj_dict
154
195
155
- for prop in to_props :
156
- if self .mappings is not None \
157
- and key_from in self .mappings \
158
- and key_to in self .mappings [key_from ]:
159
- if prop in self .mappings [key_from ][key_to ]:
160
- # take mapping function
161
- try :
162
- fnc = self .mappings [key_from ][key_to ][prop ]
163
- if fnc is not None :
164
- setattr (inst , prop , fnc (from_obj ))
165
- # none suppress mapping
166
- except Exception :
167
- raise ObjectMapperException ("Invalid mapping function while setting property {0}.{1}" .
168
- format (inst .__class__ .__name__ , prop ))
196
+ def map_obj (o , allow_unmapped ):
197
+ if o is not None :
198
+ key_from_child = o .__class__
199
+ if (key_from_child in self .mappings ):
200
+ # if key_to has a mapping defined, nests the map() call
201
+ return self .map (o , type (None ), ignore_case , allow_none , excluded , included , allow_unmapped )
202
+ elif (key_from_child in ObjectMapper .primitive_types ):
203
+ # allow primitive types without mapping
204
+ return o
205
+ else :
206
+ # fail complex type conversion if mapping was not defined, unless explicitly allowed
207
+ if allow_unmapped :
208
+ return o
209
+ else :
210
+ raise ObjectMapperException ("No mapping defined for {0}.{1}"
211
+ .format (key_from_child .__module__ , key_from_child .__name__ ))
212
+ else :
213
+ return None
169
214
215
+ for prop in to_props :
216
+
217
+ val = None
218
+ suppress_mapping = False
219
+
220
+ # mapping function take precedence over complex type mapping
221
+ if custom_mappings is not None and prop in custom_mappings :
222
+ try :
223
+ fnc = custom_mappings [prop ]
224
+ if fnc is not None :
225
+ val = fnc (from_obj )
226
+ else :
227
+ suppress_mapping = True
228
+ except Exception :
229
+ raise ObjectMapperException ("Invalid mapping function while setting property {0}.{1}" .
230
+ format (inst .__class__ .__name__ , prop ))
231
+
232
+ elif prop in from_props :
233
+ # try find property with the same name in the source
234
+ from_obj_child = from_props [prop ]
235
+ if isinstance (from_obj_child , list ):
236
+ val = [map_obj (from_obj_child_i , allow_unmapped ) for from_obj_child_i in from_obj_child ]
170
237
else :
171
- # try find property with the same name in the source
172
- if prop in from_props :
173
- setattr (inst , prop , from_props [prop ])
174
- # case when target attribute is not mapped (can be extended)
238
+ val = map_obj (from_obj_child , allow_unmapped )
239
+
175
240
else :
176
- raise ObjectMapperException (
177
- "No mapping defined for {0}.{1} -> {2}.{3}" .format (key_from .__module__ , key_from .__name__ ,
178
- key_to .__module__ , key_to .__name__ ))
241
+ suppress_mapping = True
242
+
243
+ if not suppress_mapping :
244
+ setattr (inst , prop , val )
179
245
180
246
return inst
0 commit comments