Skip to content

Commit 6e92202

Browse files
authored
Merge pull request #18 from direbearform/master
support for mapping nested objects
2 parents f60f58f + d407cd8 commit 6e92202

File tree

7 files changed

+251
-53
lines changed

7 files changed

+251
-53
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ target/
5858
# Idea
5959
.idea
6060
.python-version
61+
62+
# VSCode
63+
.vscode

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ matrix:
99
os: windows # Windows 10.0.17134 N/A Build 17134
1010
language: shell # 'language: python' is an error on Travis CI Windows
1111
before_install:
12-
- choco install python3
12+
- choco install python3 --version 3.7.3
1313
- pip install virtualenv
1414
- virtualenv $HOME/venv
1515
- source $HOME/venv/Scripts/activate

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Object Mapper
44

55
**Version**
6-
1.0.7
6+
1.1.0
77

88
**Author**
99
marazt
@@ -15,7 +15,7 @@ marazt
1515
The MIT License (MIT)
1616

1717
**Last updated**
18-
19 June 2019
18+
13 July 2019
1919

2020
**Package Download**
2121
https://pypi.python.org/pypi/object-mapper
@@ -27,6 +27,10 @@ https://pypi.python.org/pypi/object-mapper
2727

2828
## Versions
2929

30+
**1.1.0 - 2019/07/13**
31+
32+
* Add basic support for nested object, thanks [@direbearform](https://github.com/direbearform)
33+
3034
**1.0.7 - 2019/06/19**
3135

3236
* Fix type name inside mapper dict to avoid collision, thanks [@renanvieira](https://github.com/renanvieira)

README.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
Object Mapper
22
=============
33

4-
**Version** 1.0.7
4+
**Version** 1.1.0
55

66
**Author** marazt
77

88
**Copyright** marazt
99

1010
**License** The MIT License (MIT)
1111

12-
**Last updated** 19 June 2019
12+
**Last updated** 13 July 2019
13+
1314

1415
**Package Download** https://pypi.python.org/pypi/object-mapper ---
1516

1617
Versions
1718
--------
1819

20+
**1.1.0 - 2019/07/13**
21+
22+
- Add basic support for nested object, thanks [@direbearform](https://github.com/direbearform)
23+
1924
**1.0.7 - 2019/06/19**
2025

2126
- Fix type name inside mapper dict to avoid collision, thanks [@renanvieira](https://github.com/renanvieira)

mapper/object_mapper.py

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Copyright (C) 2015, marazt. All rights reserved.
44
"""
55
from inspect import getmembers, isroutine
6+
from datetime import date, datetime
67

78
from mapper.casedict import CaseDict
89
from mapper.object_mapper_exception import ObjectMapperException
@@ -14,6 +15,8 @@ class ObjectMapper(object):
1415
Supports mapping conversions too
1516
"""
1617

18+
primitive_types = { int, str, bool, date, datetime }
19+
1720
def __init__(self):
1821
"""Constructor
1922
@@ -60,7 +63,7 @@ def __init__(self):
6063
mapper = ObjectMapper()
6164
mapper.create_map(A, B, {'last_name': None})
6265
63-
instance_b = mapper.map(A(), B)
66+
instance_b = mapper.map(A())
6467
6568
In this case, value of A.name will be copied into B.name automatically by the attribute name 'name'.
6669
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
7275
Initialization of the ObjectMapper will be:
7376
mapper = ObjectMapper()
7477
mapper.create_map(A, B)
75-
instance_b = mapper.map(A(), B, ignore_case=True)
78+
instance_b = mapper.map(A(), ignore_case=True)
7679
7780
In this case, the value of A.Name will be copied into B.name and
7881
the value of A.Age will be copied into B.age.
7982
8083
:return: Instance of the ObjectMapper
8184
"""
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
8389
self.mappings = {}
8490
pass
8591

8692
def create_map(self, type_from, type_to, mapping=None):
93+
# type: (type, type, Dict) -> None
8794
"""Method for adding mapping definitions
8895
8996
:param type_from: source type
@@ -93,32 +100,43 @@ def create_map(self, type_from, type_to, mapping=None):
93100
94101
:return: None
95102
"""
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+
96113
key_from = type_from
97114
key_to = type_to
98115

99-
if mapping is None:
100-
mapping = {}
101-
102116
if key_from in self.mappings:
103117
inner_map = self.mappings[key_from]
104118
if key_to in inner_map:
105119
raise ObjectMapperException(
106120
"Mapping for {0}.{1} -> {2}.{3} already exists".format(key_from.__module__, key_from.__name__,
107121
key_to.__module__, key_to.__name__))
108122
else:
109-
inner_map[key_to] = mapping
123+
inner_map[key_to] = (type_to, mapping)
110124
else:
111125
self.mappings[key_from] = {}
112-
self.mappings[key_from][key_to] = mapping
126+
self.mappings[key_from][key_to] = (type_to, mapping)
127+
113128

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
115131
"""Method for creating target object instance
116132
117133
:param from_obj: source object to be mapped from
118134
:param to_type: target type
119135
:param ignore_case: if set to true, ignores attribute case when performing the mapping
120136
:param allow_none: if set to true, returns None if the source object is None; otherwise throws an exception
121137
: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
122140
123141
:return: Instance of the target class with mapped attributes
124142
"""
@@ -128,22 +146,45 @@ def map(self, from_obj, to_type, ignore_case=False, allow_none=False, excluded=N
128146
# one of the tests is explicitly checking for an attribute error on __dict__ if it's not set
129147
from_obj.__dict__
130148

131-
inst = to_type()
132149
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()
134173

135174
def not_private(s):
136175
return not s.startswith('_')
137176

138177
def not_excluded(s):
139178
return not (excluded and s in excluded)
140179

180+
def is_included(s, mapping):
181+
return (included and s in included) or (mapping and s in mapping)
182+
141183
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}
144185

145186
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))}
147188

148189
if ignore_case:
149190
from_props = CaseDict(from_obj_dict)
@@ -152,29 +193,54 @@ def not_excluded(s):
152193
from_props = from_obj_dict
153194
to_props = to_obj_dict
154195

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
169214

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]
170237
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+
175240
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)
179245

180246
return inst

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# Versions should comply with PEP440. For a discussion on single-sourcing
2626
# the version across setup.py and the project code, see
2727
# https://packaging.python.org/en/latest/single_source_version.html
28-
version='1.0.7',
28+
version='1.1.0',
2929

3030
description="ObjectMapper is a class for automatic object mapping. It helps you to create objects between\
3131
project layers (data layer, service layer, view) in a simple, transparent way.",

0 commit comments

Comments
 (0)