├── .gitignore
├── .travis.yml
├── CHANGES.py
├── CHANGES.txt
├── LICENSE
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── README.rst
├── create_package.bat
├── create_package.sh
├── logo.png
├── mapper
├── __init__.py
├── casedict.py
├── object_mapper.py
└── object_mapper_exception.py
├── myconfig.cfg
├── object-mapper.iml
├── pypi_publish.bat
├── pypi_publish.sh
├── requirements.txt
├── run.py
├── run_tests.bat
├── run_tests.sh
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
└── test_object_mapper.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | venv/
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .cache
41 | nosetests.xml
42 | coverage.xml
43 | TEST-results.xml
44 |
45 | # Translations
46 | *.mo
47 | *.pot
48 |
49 | # Django stuff:
50 | *.log
51 |
52 | # Sphinx documentation
53 | docs/_build/
54 |
55 | # PyBuilder
56 | target/
57 |
58 | # Idea
59 | .idea
60 | .python-version
61 |
62 | # VSCode
63 | .vscode
64 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python # this works for Linux but is an error on macOS or Windows
2 | matrix:
3 | include:
4 | - name: "Python 3.7.1 on Xenial Linux"
5 | python: 3.7 # this works for Linux but is ignored on macOS or Windows
6 | dist: xenial # required for Python >= 3.7
7 | script: bash run_tests.sh
8 | - name: "Python 3.7.3 on Windows "
9 | os: windows # Windows 10.0.17134 N/A Build 17134
10 | language: shell # 'language: python' is an error on Travis CI Windows
11 | before_install:
12 | - choco install python3 --version 3.7.3
13 | - pip install virtualenv
14 | - virtualenv $HOME/venv
15 | - source $HOME/venv/Scripts/activate
16 | - pip install -r requirements.txt
17 | env: PATH=/c/Python37:/c/Python37/Scripts:$PATH
18 | script: nose2 tests --plugin nose2.plugins.junitxml --config myconfig.cfg
--------------------------------------------------------------------------------
/CHANGES.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (C) 2018, marazt. All rights reserved.
4 | """
5 |
--------------------------------------------------------------------------------
/CHANGES.txt:
--------------------------------------------------------------------------------
1 | 1.1.0 - 2019/07/13
2 | * Add basic support for nested object, thanks [@direbearform](https://github.com/direbearform)
3 |
4 | 1.0.7 - 2019/06/19
5 | * Fix type name inside mapper dict to avoid collision, thanks [@renanvieira](https://github.com/renanvieira)
6 |
7 | 1.0.6 - 2018/10/28
8 | * Added ability to specify excluded fields, thanks [@uralov](https://github.com/uralov)
9 |
10 | 1.0.5 - 2018/02/21
11 | * Support for dynamic properties
12 |
13 | 1.0.4 - 2017/11/03
14 | * Migration to new Pypi.org deployment
15 |
16 | 1.0.3 - 2015/05/15
17 | * Added support for None mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
18 |
19 | 1.0.2 - 2015/05/06
20 | * Added support for case insensitivity mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
21 |
22 | 1.0.1 - 2015/02/19
23 | * Fix of the package information
24 |
25 | 1.0.0 - 2015/02/19
26 | * Initial version
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Marek Polak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Marek Polak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # Include the license file
2 | include LICENSE.txt
3 |
4 | # Include the data files
5 | recursive-include data *
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Object Mapper
4 |
5 | **Version**
6 | 1.1.0
7 |
8 | **Author**
9 | marazt
10 |
11 | **Copyright**
12 | marazt
13 |
14 | **License**
15 | The MIT License (MIT)
16 |
17 | **Last updated**
18 | 13 July 2019
19 |
20 | **Package Download**
21 | https://pypi.python.org/pypi/object-mapper
22 |
23 | **Build Status**
24 | [](https://travis-ci.com/marazt/object-mapper)
25 |
26 | ---
27 |
28 | ## Versions
29 |
30 | **1.1.0 - 2019/07/13**
31 |
32 | * Add basic support for nested object, thanks [@direbearform](https://github.com/direbearform)
33 |
34 | **1.0.7 - 2019/06/19**
35 |
36 | * Fix type name inside mapper dict to avoid collision, thanks [@renanvieira](https://github.com/renanvieira)
37 |
38 | **1.0.6 - 2018/10/28**
39 |
40 | * Added ability to specify excluded fields, thanks [@uralov](https://github.com/uralov)
41 |
42 | **1.0.5 - 2018/02/21**
43 |
44 | * Support for dynamic properties [@nijm](https://github.com/nijm)
45 |
46 | **1.0.4 - 2017/11/03**
47 |
48 | * Migration to new Pypi.org deployment
49 |
50 | **1.0.3 - 2015/05/15**
51 |
52 | * Added support for None mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
53 |
54 |
55 | **1.0.2 - 2015/05/06**
56 |
57 | * Added support for case insensitivity mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
58 |
59 |
60 | **1.0.1 - 2015/02/19**
61 |
62 | * Fix of the package information
63 |
64 |
65 | **1.0.0 - 2015/02/19**
66 |
67 | * Initial version
68 |
69 |
70 | ## About
71 |
72 | **ObjectMapper** is a class for automatic object mapping inspired by .NET **AutoMapper**.
73 | It helps you to create objects between project layers (data layer, service layer, view) in a simple, transparent way.
74 |
75 | ## Example
76 |
77 | 1. **Mapping of the properties without mapping definition**
78 |
79 | In this case are mapped only these properties of the target class which
80 | are in target and source classes. Other properties are not mapped.
81 | Suppose we have class `A` with attributes `name` and `last_name`
82 | and class `B` with attribute `name`.
83 | Initialization of the ObjectMapper will be:
84 |
85 | ```python
86 | mapper = ObjectMapper()
87 | mapper.create_map(A, B)
88 | instance_b = mapper.map(A(), B)
89 | ```
90 |
91 | In this case, value of A.name will be copied into B.name.
92 |
93 | 2. **Mapping with defined mapping functions**
94 |
95 | Suppose we have class `A` with attributes `first_name` and `last_name`
96 | , class `B` with attribute `full_name` and class `C` with attribute reverse_name.
97 | And want to map it in a way `B.full_name = A.first_name + A.last_name` and
98 | `C.reverse_name = A.last_name + A.first_name`
99 | Initialization of the ObjectMapper will be:
100 |
101 | ```python
102 | mapper = ObjectMapper()
103 | mapper.create_map(A, B, {'name': lambda a : a.first_name + " " + a.last_name})
104 | mapper.create_map(A, C, {'name': lambda a : a.last_name + " " + a.first_name})
105 |
106 | instance_b = mapper.map(A(), B)
107 | instance_c = mapper.map(A(), C)
108 | ```
109 |
110 | In this case, to the `B.name` will be mapped `A.first_name + " " + A.last_name`
111 | In this case, to the `C.name` will be mapped `A.last_name + " " + A.first_name`
112 |
113 | 3. **Mapping suppression**
114 |
115 | For some purposes, it can be needed to suppress some mapping.
116 | Suppose we have class `A` with attributes `name` and `last_name`
117 | and class `B` with attributes `name` and `last_name`.
118 | And we want to map only the `A.name` into `B.name`, but not `A.last_name` to
119 | `B.last_name`
120 | Initialization of the ObjectMapper will be:
121 |
122 | ```python
123 | mapper = ObjectMapper()
124 | mapper.create_map(A, B, {'last_name': None})
125 |
126 | instance_b = mapper.map(A(), B)
127 | ```
128 |
129 | In this case, value of A.name will be copied into `B.name` automatically by the attribute name `name`.
130 | Attribute `A.last_name` will be not mapped thanks the suppression (lambda function is None).
131 |
132 | 4. **Case insensitive mapping**
133 |
134 | Suppose we have class `A` with attributes `Name` and `Age` and
135 | class `B` with attributes `name` and `age` and we want to map `A` to `B` in a way
136 | `B.name` = `A.Name` and `B.age` = `A.Age`
137 | Initialization of the ObjectMapper will be:
138 |
139 | ```python
140 | mapper = ObjectMapper()
141 | mapper.create_map(A, B)
142 | instance_b = mapper.map(A(), B, ignore_case=True)
143 | ```
144 |
145 | In this case, the value of A.Name will be copied into B.name and
146 | the value of A.Age will be copied into B.age.
147 |
148 | **Note:** You can find more examples in tests package
149 |
150 | ## Installation
151 |
152 | * Download this project
153 | * Download from Pypi: https://pypi.python.org/pypi/object-mapper
154 |
155 | ### ENJOY IT!
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Object Mapper
2 | =============
3 |
4 | **Version** 1.1.0
5 |
6 | **Author** marazt
7 |
8 | **Copyright** marazt
9 |
10 | **License** The MIT License (MIT)
11 |
12 | **Last updated** 13 July 2019
13 |
14 |
15 | **Package Download** https://pypi.python.org/pypi/object-mapper ---
16 |
17 | Versions
18 | --------
19 |
20 | **1.1.0 - 2019/07/13**
21 |
22 | - Add basic support for nested object, thanks [@direbearform](https://github.com/direbearform)
23 |
24 | **1.0.7 - 2019/06/19**
25 |
26 | - Fix type name inside mapper dict to avoid collision, thanks [@renanvieira](https://github.com/renanvieira)
27 |
28 | **1.0.6 - 2018/10/28**
29 |
30 | - Added ability to specify excluded fields, thanks [@uralov](https://github.com/uralov)
31 |
32 | **1.0.5 - 2018/02/21**
33 |
34 | - Support for dynamic properties [@nijm](https://github.com/nijm)
35 |
36 | **1.0.4 - 2017/11/03**
37 |
38 | - Migration to new Pypi.org deployment
39 |
40 | **1.0.3 - 2015/05/15**
41 |
42 | - Added support for None mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
43 |
44 | **1.0.2 - 2015/05/06**
45 |
46 | - Added support for case insensitivity mapping [@ramiabughazaleh](https://github.com/ramiabughazaleh)
47 |
48 | **1.0.1 - 2015/02/19**
49 |
50 | - Fix of the package information
51 |
52 | **1.0.0 - 2015/02/19**
53 |
54 | - Initial version
55 |
56 | About
57 | -----
58 |
59 | **ObjectMapper** is a class for automatic object mapping inspired by .NET **AutoMapper**. It helps you to create objects between project layers (data layer, service layer, view) in a simple, transparent way.
60 |
61 | Example
62 | -------
63 |
64 | 1. **Mapping of the properties without mapping definition**
65 |
66 | In this case are mapped only these properties of the target class which are in target and source classes. Other properties are not mapped. Suppose we have class ``A`` with attributes ``name`` and ``last_name`` and class ``B`` with attribute ``name``. Initialization of the ObjectMapper will be:
67 |
68 | ``python mapper = ObjectMapper() mapper.create_map(A, B) instance_b = mapper.map(A(), B)``
69 |
70 | In this case, value of A.name will be copied into B.name.
71 |
72 | 2. **Mapping with defined mapping functions**
73 |
74 | Suppose we have class ``A`` with attributes ``first_name`` and ``last_name`` , class ``B`` with attribute ``full_name`` and class ``C`` with attribute reverse\_name. And want to map it in a way ``B.full_name = A.first_name + A.last_name`` and ``C.reverse_name = A.last_name + A.first_name`` Initialization of the ObjectMapper will be:
75 |
76 | \`\`\`python mapper = ObjectMapper() mapper.create\_map(A, B, {'name': lambda a : a.first\_name + " " + a.last\_name}) mapper.create\_map(A, C, {'name': lambda a : a.last\_name + " " + a.first\_name})
77 |
78 | instance\_b = mapper.map(A(), B) instance\_c = mapper.map(A(), C) \`\`\`
79 |
80 | In this case, to the ``B.name`` will be mapped ``A.first_name + " " + A.last_name`` In this case, to the ``C.name`` will be mapped ``A.last_name + " " + A.first_name``
81 |
82 | 3. **Mapping suppression**
83 |
84 | For some purposes, it can be needed to suppress some mapping. Suppose we have class ``A`` with attributes ``name`` and ``last_name`` and class ``B`` with attributes ``name`` and ``last_name``. And we want to map only the ``A.name`` into ``B.name``, but not ``A.last_name`` to ``B.last_name`` Initialization of the ObjectMapper will be:
85 |
86 | \`\`\`python mapper = ObjectMapper() mapper.create\_map(A, B, {'last\_name': None})
87 |
88 | instance\_b = mapper.map(A(), B) \`\`\`
89 |
90 | In this case, value of A.name will be copied into ``B.name`` automatically by the attribute name ``name``. Attribute ``A.last_name`` will be not mapped thanks the suppression (lambda function is None).
91 |
92 | 4. **Case insensitive mapping**
93 |
94 | Suppose we have class ``A`` with attributes ``Name`` and ``Age`` and class ``B`` with attributes ``name`` and ``age`` and we want to map ``A`` to ``B`` in a way ``B.name`` = ``A.Name`` and ``B.age`` = ``A.Age`` Initialization of the ObjectMapper will be:
95 |
96 | ``python mapper = ObjectMapper() mapper.create_map(A, B) instance_b = mapper.map(A(), B, ignore_case=True)``
97 |
98 | In this case, the value of A.Name will be copied into B.name and the value of A.Age will be copied into B.age.
99 |
100 | **Note:** You can find more examples in tests package
101 |
102 | Installation
103 | ------------
104 |
105 | - Download this project
106 | - Download from Pypi: https://pypi.python.org/pypi/object-mapper
107 |
108 | ENJOY IT!
109 | ~~~~~~~~~
110 |
111 |
--------------------------------------------------------------------------------
/create_package.bat:
--------------------------------------------------------------------------------
1 | python setup.py sdist
2 | pause
--------------------------------------------------------------------------------
/create_package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python setup.py sdist
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marazt/object-mapper/9d62e9bf00cd78afdaccd6e950a6bb4a1dd1cc06/logo.png
--------------------------------------------------------------------------------
/mapper/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2015, marazt. All rights reserved.
2 |
--------------------------------------------------------------------------------
/mapper/casedict.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (c) 2013 Optiflows
4 | https://bitbucket.org/optiflowsrd/obelus/src/tip/obelus/casedict.py
5 | """
6 |
7 | from collections import MutableMapping
8 |
9 |
10 | _sentinel = object()
11 |
12 |
13 | class CaseDict(MutableMapping):
14 | """
15 | A case-insensitive dictionary.
16 | """
17 |
18 | __slots__ = ('_data',)
19 |
20 | def __init__(self, __dict=None, **kwargs):
21 | # lower-cased => (original casing, value)
22 | self._data = {}
23 | if __dict is not None:
24 | self.update(__dict)
25 | if kwargs:
26 | self.update(kwargs)
27 |
28 | # Minimum set of methods required for MutableMapping
29 |
30 | def __len__(self):
31 | return len(self._data)
32 |
33 | def __iter__(self):
34 | return (v[0] for v in self._data.values())
35 |
36 | def __getitem__(self, key):
37 | return self._data[key.lower()][1]
38 |
39 | def __setitem__(self, key, value):
40 | self._data[key.lower()] = (key, value)
41 |
42 | def __delitem__(self, key):
43 | del self._data[key.lower()]
44 |
45 | # Methods overriden to mitigate the performance overhead.
46 |
47 | def __contains__(self, key):
48 | return key.lower() in self._data
49 |
50 | def clear(self):
51 | """
52 | Removes all items from dictionary
53 | """
54 | self._data.clear()
55 |
56 | def get(self, key, default=_sentinel):
57 | """
58 | Gets the value from the key.
59 | If the key doesn't exist, the default value is returned, otherwise None.
60 |
61 | :param key: The key
62 | :param default: The default value
63 | :return: The value
64 | """
65 | tup = self._data.get(key.lower())
66 | if tup is not None:
67 | return tup[1]
68 | elif default is not _sentinel:
69 | return default
70 | else:
71 | return None
72 |
73 | def pop(self, key, default=_sentinel):
74 | """
75 | Removes the specified key and returns the corresponding value.
76 | If key is not found, the default is returned if given, otherwise KeyError is raised.
77 |
78 | :param key: The key
79 | :param default: The default value
80 | :return: The value
81 | """
82 | if default is not _sentinel:
83 | tup = self._data.pop(key.lower(), default)
84 | else:
85 | tup = self._data.pop(key.lower())
86 | if tup is not default:
87 | return tup[1]
88 | else:
89 | return default
90 |
91 | # Other methods
92 |
93 | def __repr__(self):
94 | if self._data:
95 | return '%s(%r)' % (self.__class__.__name__, dict(self))
96 | else:
97 | return '%s()' % self.__class__.__name__
98 |
--------------------------------------------------------------------------------
/mapper/object_mapper.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (C) 2015, marazt. All rights reserved.
4 | """
5 | from inspect import getmembers, isroutine
6 | from datetime import date, datetime
7 |
8 | from mapper.casedict import CaseDict
9 | from mapper.object_mapper_exception import ObjectMapperException
10 |
11 |
12 | class ObjectMapper(object):
13 | """
14 | Base class for mapping class attributes from one class to another one
15 | Supports mapping conversions too
16 | """
17 |
18 | primitive_types = { int, str, bool, date, datetime }
19 |
20 | def __init__(self):
21 | """Constructor
22 |
23 | Args:
24 | mappings: dictionary of the attribute conversions
25 |
26 | Examples:
27 |
28 | 1. Mapping of the properties without mapping definition
29 | In this case are mapped only these properties of the target class which
30 | are in target and source classes. Other properties are not mapped.
31 | Suppose we have class 'A' with attributes 'name' and 'last_name'
32 | and class 'B' with attribute 'name'.
33 | Initialization of the ObjectMapper will be:
34 | mapper = ObjectMapper()
35 | mapper.create_map(A, B)
36 | instance_b = mapper.map(A(), B)
37 |
38 | In this case, value of A.name will be copied into B.name.
39 |
40 | 2. Mapping with defined mapping functions
41 | Suppose we have class 'A' with attributes 'first_name' and 'last_name'
42 | , class 'B' with attribute 'full_name' and class 'C' with attribute reverse_name.
43 | And want to map it in a way 'B.full_name' = 'A.first_name' + 'A.last_name' and
44 | 'C.reverse_name' = 'A.last_name' + 'A.first_name'
45 | Initialization of the ObjectMapper will be:
46 | mapper = ObjectMapper()
47 | mapper.create_map(A, B, {'name': lambda a : a.first_name + " " + a.last_name})
48 | mapper.create_map(A, C, {'name': lambda a : a.last_name + " " + a.first_name})
49 |
50 | instance_b = mapper.map(A(), B)
51 | instance_c = mapper.map(A(), C)
52 |
53 | In this case, to the B.name will be mapped A.first_name + " " + A.last_name
54 | In this case, to the C.name will be mapped A.last_name + " " + A.first_name
55 |
56 | 3. Mapping suppression
57 | For some purposes, it can be needed to suppress some mapping.
58 | Suppose we have class 'A' with attributes 'name' and 'last_name'
59 | and class 'B' with attributes 'name' and 'last_name'.
60 | And we want to map only the A.name into B.name, but not A.last_name to
61 | b.last_name
62 | Initialization of the ObjectMapper will be:
63 | mapper = ObjectMapper()
64 | mapper.create_map(A, B, {'last_name': None})
65 |
66 | instance_b = mapper.map(A())
67 |
68 | In this case, value of A.name will be copied into B.name automatically by the attribute name 'name'.
69 | Attribute A.last_name will be not mapped thanks the suppression (lambda function is None).
70 |
71 | 4. Case insensitive mapping
72 | Suppose we have class 'A' with attributes 'Name' and 'Age' and
73 | class 'B' with attributes 'name' and 'age' and we want to map 'A' to 'B' in a way
74 | 'A.Name' = 'B.name' and 'A.Age' = 'B.age'
75 | Initialization of the ObjectMapper will be:
76 | mapper = ObjectMapper()
77 | mapper.create_map(A, B)
78 | instance_b = mapper.map(A(), ignore_case=True)
79 |
80 | In this case, the value of A.Name will be copied into B.name and
81 | the value of A.Age will be copied into B.age.
82 |
83 | :return: Instance of the ObjectMapper
84 | """
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
89 | self.mappings = {}
90 | pass
91 |
92 | def create_map(self, type_from, type_to, mapping=None):
93 | # type: (type, type, Dict) -> None
94 | """Method for adding mapping definitions
95 |
96 | :param type_from: source type
97 | :param type_to: target type
98 | :param mapping: dictionary of mapping definitions in a form {'target_property_name',
99 | lambda function from rhe source}
100 |
101 | :return: None
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 |
113 | key_from = type_from
114 | key_to = type_to
115 |
116 | if key_from in self.mappings:
117 | inner_map = self.mappings[key_from]
118 | if key_to in inner_map:
119 | raise ObjectMapperException(
120 | "Mapping for {0}.{1} -> {2}.{3} already exists".format(key_from.__module__, key_from.__name__,
121 | key_to.__module__, key_to.__name__))
122 | else:
123 | inner_map[key_to] = (type_to, mapping)
124 | else:
125 | self.mappings[key_from] = {}
126 | self.mappings[key_from][key_to] = (type_to, mapping)
127 |
128 |
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
131 | """Method for creating target object instance
132 |
133 | :param from_obj: source object to be mapped from
134 | :param to_type: target type
135 | :param ignore_case: if set to true, ignores attribute case when performing the mapping
136 | :param allow_none: if set to true, returns None if the source object is None; otherwise throws an exception
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
140 |
141 | :return: Instance of the target class with mapped attributes
142 | """
143 | if (from_obj is None) and allow_none:
144 | return None
145 | else:
146 | # one of the tests is explicitly checking for an attribute error on __dict__ if it's not set
147 | from_obj.__dict__
148 |
149 | key_from = from_obj.__class__
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()
173 |
174 | def not_private(s):
175 | return not s.startswith('_')
176 |
177 | def not_excluded(s):
178 | return not (excluded and s in excluded)
179 |
180 | def is_included(s, mapping):
181 | return (included and s in included) or (mapping and s in mapping)
182 |
183 | from_obj_attributes = getmembers(from_obj, lambda a: not isroutine(a))
184 | from_obj_dict = {k: v for k, v in from_obj_attributes}
185 |
186 | to_obj_attributes = getmembers(inst, lambda a: not isroutine(a))
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))}
188 |
189 | if ignore_case:
190 | from_props = CaseDict(from_obj_dict)
191 | to_props = CaseDict(to_obj_dict)
192 | else:
193 | from_props = from_obj_dict
194 | to_props = to_obj_dict
195 |
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
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]
237 | else:
238 | val = map_obj(from_obj_child, allow_unmapped)
239 |
240 | else:
241 | suppress_mapping = True
242 |
243 | if not suppress_mapping:
244 | setattr(inst, prop, val)
245 |
246 | return inst
247 |
--------------------------------------------------------------------------------
/mapper/object_mapper_exception.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (C) 2015, marazt. All rights reserved.
4 | """
5 |
6 |
7 | class ObjectMapperException(Exception):
8 | """
9 | Object Mapper exception
10 | """
11 | pass
12 |
--------------------------------------------------------------------------------
/myconfig.cfg:
--------------------------------------------------------------------------------
1 | [junit-xml]
2 | always-on = True
3 | path = TEST-results.xml
--------------------------------------------------------------------------------
/object-mapper.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pypi_publish.bat:
--------------------------------------------------------------------------------
1 | REM Install Universal Wheels
2 | pip install wheel
3 | python setup.py bdist_wheel --universal
4 | REM Upload package by Twine
5 | pip install twine
6 | twine upload dist/*
7 | pause
8 |
9 | REM if you want to upload without everytime registration, use .pypirc file in you home dir (~/.pypirc or C:\Users\YOU\.pypirc)
--------------------------------------------------------------------------------
/pypi_publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Install Universal Wheels
3 | pip install wheel
4 | python setup.py bdist_wheel --universal
5 | # Upload package by Twine
6 | pip install twine
7 | twine upload dist/*
8 |
9 | # if you want to upload without everytime registration, use .pypirc file in you home dir (~/.pypirc or C:\Users\YOU\.pypirc)
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | nose2==0.9.1
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (C) 2015, marazt. All rights reserved.
4 | """
5 |
--------------------------------------------------------------------------------
/run_tests.bat:
--------------------------------------------------------------------------------
1 | REM prerequisites: pip, virtualenv in path
2 | virtualenv venv
3 | cd venv/Scripts/
4 | activate.bat & cd ../.. & pip install -r requirements.txt & nose2 tests --plugin nose2.plugins.junitxml --config myconfig.cfg
--------------------------------------------------------------------------------
/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!bin/bash
2 | # prerequisites: pip, virtualenv in path
3 | pip install virtualenv
4 | virtualenv venv
5 | source venv/bin/activate
6 | pip install -r requirements.txt
7 | nose2 tests --plugin nose2.plugins.junitxml --config myconfig.cfg
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | # This flag says that the code is written to work on both Python 2 and Python
3 | # 3. If at all possible, it is good practice to do this. If you cannot, you
4 | # will need to generate wheels for each Python version that you support.
5 | universal=1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | """A setuptools based setup module.
4 |
5 | See:
6 | https://packaging.python.org/en/latest/distributing.html
7 | https://github.com/pypa/sampleproject
8 | """
9 |
10 | # Always prefer setuptools over distutils
11 | from setuptools import setup, find_packages
12 | # To use a consistent encoding
13 | from codecs import open
14 | from os import path
15 |
16 | here = path.abspath(path.dirname(__file__))
17 |
18 | # Get the long description from the README file
19 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
20 | long_description = f.read()
21 |
22 | setup(
23 | name='object-mapper',
24 |
25 | # Versions should comply with PEP440. For a discussion on single-sourcing
26 | # the version across setup.py and the project code, see
27 | # https://packaging.python.org/en/latest/single_source_version.html
28 | version='1.1.0',
29 |
30 | description="ObjectMapper is a class for automatic object mapping. It helps you to create objects between\
31 | project layers (data layer, service layer, view) in a simple, transparent way.",
32 | long_description=long_description,
33 |
34 | # The project's main homepage.
35 | url="https://github.com/marazt/object-mapper",
36 |
37 | # Author details
38 | author="marazt",
39 | author_email="marazt@gmail.com",
40 |
41 | # Choose your license
42 | license='MIT',
43 |
44 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
45 | classifiers=[
46 | # How mature is this project? Common values are
47 | # 3 - Alpha
48 | # 4 - Beta
49 | # 5 - Production/Stable
50 | 'Development Status :: 5 - Production/Stable',
51 |
52 | # Indicate who your project is intended for
53 | 'Intended Audience :: Developers',
54 | 'Topic :: Software Development :: Build Tools',
55 |
56 | # Pick your license as you wish (should match "license" above)
57 | 'License :: OSI Approved :: MIT License',
58 |
59 | # Specify the Python versions you support here. In particular, ensure
60 | # that you indicate whether you support Python 2, Python 3 or both.
61 | 'Programming Language :: Python :: 2',
62 | 'Programming Language :: Python :: 2.7',
63 | 'Programming Language :: Python :: 3',
64 | 'Programming Language :: Python :: 3.3',
65 | 'Programming Language :: Python :: 3.4',
66 | 'Programming Language :: Python :: 3.5',
67 | 'Programming Language :: Python :: 3.6',
68 | ],
69 |
70 | # What does your project relate to?
71 | keywords='utils dto object-mapper mapping development',
72 |
73 | # You can just specify the packages manually here if your project is
74 | # simple. Or you can use find_packages().
75 | packages=[
76 | "mapper",
77 | "tests",
78 | ],
79 |
80 | # Alternatively, if you want to distribute just a my_module.py, uncomment
81 | # this:
82 | # py_modules=["my_module"],
83 |
84 | # List run-time dependencies here. These will be installed by pip when
85 | # your project is installed. For an analysis of "install_requires" vs pip's
86 | # requirements files see:
87 | # https://packaging.python.org/en/latest/requirements.html
88 | install_requires=[
89 | "datetime",
90 | "nose",
91 | ],
92 |
93 | # List additional groups of dependencies here (e.g. development
94 | # dependencies). You can install these using the following syntax,
95 | # for example:
96 | # $ pip install -e .[dev,test]
97 | extras_require={
98 | 'dev': [],
99 | 'test': ["nose"],
100 | },
101 |
102 | # If there are data files included in your packages that need to be
103 | # installed, specify them here. If using Python 2.6 or less, then these
104 | # have to be included in MANIFEST.in as well.
105 | package_data={
106 | 'sample': [],
107 | },
108 |
109 | # Although 'package_data' is the preferred approach, in some case you may
110 | # need to place data files outside of your packages. See:
111 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa
112 | # In this case, 'data_file' will be installed into '/my_data'
113 | data_files=[],
114 |
115 | # To provide executable scripts, use entry points in preference to the
116 | # "scripts" keyword. Entry points provide cross-platform support and allow
117 | # pip to create the appropriate form of executable for the target platform.
118 | entry_points={},
119 | )
120 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2015, marazt. All rights reserved.
2 |
--------------------------------------------------------------------------------
/tests/test_object_mapper.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | """
3 | Copyright (C) 2015, marazt. All rights reserved.
4 | """
5 | import unittest
6 | from datetime import datetime
7 |
8 | from mapper.object_mapper import ObjectMapper
9 | from mapper.object_mapper_exception import ObjectMapperException
10 |
11 | NO_MAPPING_FOUND_EXCEPTION_MESSAGE = "No mapping defined for {0}.{1}"
12 | NO_MAPPING_PAIR_FOUND_EXCEPTION_MESSAGE = "No mapping defined for {0}.{1} -> {2}.{3}"
13 | MAPPING_ALREADY_EXISTS_EXCEPTION_MESSAGE = "Mapping for {0}.{1} -> {2}.{3} already exists"
14 |
15 |
16 | class ToTestClass(object):
17 | """ To Test Class """
18 |
19 | def __init__(self):
20 | self.name = ""
21 | self.date = ""
22 | self._actor_name = ""
23 | pass
24 |
25 |
26 | class ToTestClassTwo(object):
27 | """ To Test Class Two """
28 |
29 | def __init__(self):
30 | self.all = ""
31 | pass
32 |
33 |
34 | class ToTestClassEmpty(object):
35 | """ To Test Class Empty """
36 |
37 | def __init__(self):
38 | pass
39 |
40 |
41 | class ToTestComplexClass(object):
42 | """ To Test Class """
43 | def __init__(self):
44 | self.name = ""
45 | self.date = ""
46 | self.student = None
47 | self.knows = None
48 | pass
49 |
50 |
51 | class ToTestComplexChildClass(object):
52 | """ To Test Class """
53 | def __init__(self):
54 | self.full_name = ""
55 | pass
56 |
57 |
58 | class FromTestClass(object):
59 | """ From Test Class """
60 |
61 | def __init__(self):
62 | self.name = "Igor"
63 | self.surname = "Hnizdo"
64 | self.date = datetime(2015, 1, 1)
65 | self._actor_name = "Jan Triska"
66 | pass
67 |
68 |
69 | class FromTestComplexChildClass(object):
70 | """ From Test Class """
71 | def __init__(self, full_name="Eda Soucek"):
72 | self.full_name = full_name
73 | pass
74 |
75 |
76 | class FromTestComplexClass(object):
77 | """ From Test Class """
78 | def __init__(self):
79 | self.name = "Igor"
80 | self.surname = "Hnizdo"
81 | self.date = datetime(2015, 1, 1)
82 | self.student = FromTestComplexChildClass()
83 | self.knows = [FromTestComplexChildClass('Mrs. Souckova'), FromTestComplexChildClass('The schoolmaster')]
84 | pass
85 |
86 |
87 |
88 | class ObjectMapperTest(unittest.TestCase):
89 | """
90 | Unit tests for the `ObjectMapper` module.
91 | """
92 |
93 | def test_mapping_creation_without_mappings_correct(self):
94 | """ Test mapping creation without mappings """
95 |
96 | # Arrange
97 | from_class = FromTestClass()
98 | mapper = ObjectMapper()
99 | mapper.create_map(FromTestClass, ToTestClass)
100 |
101 | # Act
102 | result = mapper.map(FromTestClass())
103 |
104 | # Assert
105 | self.assertTrue(isinstance(result, ToTestClass), "Target types must be same")
106 | self.assertEqual(result.name, from_class.name, "Name mapping must be equal")
107 | self.assertEqual(result.date, from_class.date, "Date mapping must be equal")
108 | self.assertEqual(result._actor_name, "", "Private should not be copied by default")
109 | self.assertNotIn("surname", result.__dict__, "To class must not contain surname")
110 |
111 | def test_mapping_creation_with_mappings_correct(self):
112 | """ Test mapping creation with mappings """
113 |
114 | # Arrange
115 | from_class = FromTestClass()
116 | mapper = ObjectMapper()
117 | mapper.create_map(FromTestClass, ToTestClass,
118 | {"name": lambda x: "{0} {1}".format(x.name, x.surname),
119 | "date": lambda x: "{0} Hi!".format(str(x.date))})
120 | mapper.create_map(FromTestClass, ToTestClassTwo,
121 | {"all": lambda x: "{0}{1}{2}".format(x.name, x.surname, x.date)})
122 | mapper.create_map(ToTestClassTwo, ToTestClassEmpty)
123 |
124 | # Act
125 | result1 = mapper.map(from_class, ToTestClass)
126 | result2 = mapper.map(from_class, ToTestClassTwo)
127 | result3 = mapper.map(result2, ToTestClassEmpty)
128 |
129 | # Assert
130 | self.assertTrue(isinstance(result1, ToTestClass), "Type must be ToTestClass")
131 | self.assertEqual(result1.name, "{0} {1}".format(from_class.name, from_class.surname),
132 | "Name mapping must be equal")
133 | self.assertEqual(result1.date, "{0} Hi!".format(from_class.date), "Date mapping must be equal")
134 | self.assertNotIn("surname", result1.__dict__, "To class must not contain surname")
135 |
136 | self.assertTrue(isinstance(result2, ToTestClassTwo), "Type must be ToTestClassTwo")
137 | self.assertEqual(result2.all,
138 | "{0}{1}{2}".format(from_class.name, from_class.surname, from_class.date),
139 | "There must be concatenated all properties of fromTestClass")
140 | self.assertNotIn("name", result2.__dict__, "To class must not contain name")
141 | self.assertNotIn("surname", result2.__dict__, "To class must not contain surname")
142 | self.assertNotIn("date", result2.__dict__, "To class must not contain date")
143 |
144 | self.assertTrue(isinstance(result3, ToTestClassEmpty), "Type must be ToTestClassEmpty")
145 | self.assertTrue(len(result3.__dict__) == 0, "There must be no attributes")
146 |
147 | def test_mapping_creation_duplicate_mapping(self):
148 | """ Test mapping creation with duplicate mappings """
149 |
150 | # Arrange
151 | exc = False
152 | msg = MAPPING_ALREADY_EXISTS_EXCEPTION_MESSAGE.format(FromTestClass.__module__, FromTestClass.__name__,
153 | ToTestClass.__module__, ToTestClass.__name__)
154 | mapper = ObjectMapper()
155 |
156 | mapper.create_map(FromTestClass, ToTestClass)
157 |
158 | # Act
159 | try:
160 | mapper.create_map(FromTestClass, ToTestClass, {})
161 | except ObjectMapperException as ex:
162 | self.assertEqual(str(ex), msg, "Exception message must be correct")
163 | exc = True
164 |
165 | # Assert
166 | self.assertTrue(exc, "Exception must be thrown")
167 |
168 | def test_mapping_creation_invalid_mapping_function(self):
169 | """ Test mapping creation with invalid mapping function """
170 |
171 | # Arrange
172 | exc = False
173 | msg = "Invalid mapping function while setting property ToTestClass.date"
174 | mapper = ObjectMapper()
175 | mapper.create_map(FromTestClass, ToTestClass, {"date": lambda x: x.be + x.de})
176 |
177 | # Act
178 | try:
179 | mapper.map(FromTestClass())
180 | except ObjectMapperException as ex:
181 | self.assertEqual(str(ex), msg, "Exception message must be correct")
182 | exc = True
183 |
184 | # Assert
185 | self.assertTrue(exc, "Exception must be thrown")
186 |
187 | def test_mapping_creation_none_target(self):
188 | """ Test mapping creation with none target """
189 |
190 | # Arrange
191 | exc = None
192 | from_class = None
193 | mappings = \
194 | {
195 | "name": lambda x: x.name + " " + x.surname,
196 | "date": lambda x: str(x.date) + " Happy new year!"
197 | }
198 |
199 | mapper = ObjectMapper()
200 | mapper.create_map(FromTestClass, ToTestClass, mappings)
201 |
202 | # Act
203 | try:
204 | mapper.map(from_class)
205 | except AttributeError as ex:
206 | exc = ex
207 |
208 | # Assert
209 | self.assertIsNotNone(exc, "AttributeError must be thrown")
210 | self.assertEqual("'NoneType' object has no attribute '__dict__'", str(exc))
211 |
212 | def test_mapping_with_none_source_and_allow_none_returns_none(self):
213 | """ Test mapping with none source and allow none returns none """
214 |
215 | # Arrange
216 | from_class = None
217 | mappings = \
218 | {
219 | "name": lambda x: x.name + " " + x.surname,
220 | "date": lambda x: str(x.date) + " Happy new year!"
221 | }
222 |
223 | mapper = ObjectMapper()
224 | mapper.create_map(FromTestClass, ToTestClass, mappings)
225 |
226 | # Act
227 | result = mapper.map(from_class, allow_none=True)
228 |
229 | # Assert
230 | self.assertEqual(None, result)
231 |
232 | def test_mapping_creation_no_mapping_defined(self):
233 | """ Test mapping creation with no mapping defined """
234 |
235 | # Arrange
236 | exc = False
237 | from_class = FromTestClass()
238 | msg = NO_MAPPING_FOUND_EXCEPTION_MESSAGE.format(from_class.__module__, from_class.__class__.__name__)
239 | mapper = ObjectMapper()
240 |
241 | # Act
242 | try:
243 | mapper.map(from_class)
244 | except ObjectMapperException as ex:
245 | self.assertEqual(str(ex), msg, "Exception message must be correct")
246 | exc = True
247 |
248 | # Assert
249 | self.assertTrue(exc, "Exception must be thrown")
250 |
251 | def test_mapping_creation_no_mapping_pair_defined(self):
252 | """ Test mapping creation with no mapping defined for a from -> to pair"""
253 |
254 | # Arrange
255 | exc = False
256 | from_class = FromTestClass()
257 | to_class = ToTestClass()
258 | msg = NO_MAPPING_PAIR_FOUND_EXCEPTION_MESSAGE.format(from_class.__module__, from_class.__class__.__name__,
259 | to_class.__module__, to_class.__class__.__name__)
260 | mapper = ObjectMapper()
261 | mapper.create_map(FromTestClass, ToTestClassTwo, {})
262 |
263 | # Act
264 | try:
265 | mapper.map(from_class, ToTestClass)
266 | except ObjectMapperException as ex:
267 | self.assertEqual(str(ex), msg, "Exception message must be correct")
268 | exc = True
269 |
270 | # Assert
271 | self.assertTrue(exc, "Exception must be thrown")
272 |
273 | def test_mapping_creation_with_mapping_suppression(self):
274 | """ Test mapping creation with mapping suppression """
275 |
276 | # Arrange
277 | from_class = FromTestClass()
278 | mapper = ObjectMapper()
279 | mapper.create_map(FromTestClass, ToTestClass,
280 | {"name": None})
281 |
282 | # Act
283 | result1 = mapper.map(from_class)
284 |
285 | # Assert
286 | self.assertTrue(isinstance(result1, ToTestClass), "Type must be ToTestClass")
287 | self.assertEqual(result1.name, "", "Name must not be mapped")
288 | self.assertEqual(result1.date, from_class.date, "Date is set by property name")
289 | self.assertNotIn("surname", result1.__dict__, "To class must not contain surname")
290 |
291 | def test_mapping_with_case_insensitivity(self):
292 | """ Test mapping with case insensitivity """
293 |
294 | # Arrange
295 | class ToTestClass2(object):
296 | """ To Test Class 2 """
297 |
298 | def __init__(self):
299 | self.name = ""
300 |
301 | class FromTestClass2(object):
302 | """ From Test Class 2 """
303 |
304 | def __init__(self):
305 | self.Name = "Name"
306 |
307 | from_class = FromTestClass2()
308 | mapper = ObjectMapper()
309 | mapper.create_map(FromTestClass2, ToTestClass2)
310 |
311 | # Act
312 | result = mapper.map(FromTestClass2(), ToTestClass2, ignore_case=True)
313 |
314 | # Assert
315 | self.assertEqual(result.name, from_class.Name, "Name mapping must be equal")
316 |
317 | def test_mapping_creation_with_partial_mapping_correct(self):
318 | """ Test mapping creation with partial mapping """
319 |
320 | # Arrange
321 | from_class = FromTestClass()
322 | mapper = ObjectMapper()
323 | mapper.create_map(FromTestClass, ToTestClass,
324 | {"name": lambda x: "{0} {1}".format(x.name, x.surname)})
325 |
326 | # Act
327 | result1 = mapper.map(from_class)
328 |
329 | # Assert
330 | self.assertTrue(isinstance(result1, ToTestClass), "Type must be ToTestClass")
331 | self.assertEqual(result1.name, "{0} {1}".format(from_class.name, from_class.surname),
332 | "Name mapping must be equal")
333 | self.assertEqual(result1.date, from_class.date, "Date mapping must be equal")
334 | self.assertNotIn("surname", result1.__dict__, "To class must not contain surname")
335 |
336 | def test_mapping_creation_with_custom_dir(self):
337 | """ Test mapping to objects with custom __dir__ behaviour """
338 |
339 | # Arrange
340 | _propNames = ['name', 'date']
341 |
342 | class ToCustomDirClass(object):
343 | def __dir__(self):
344 | props = list(self.__dict__.keys())
345 | props.extend(_propNames)
346 | return props
347 |
348 | def __init__(self):
349 | self.props = {k: None for k in _propNames}
350 |
351 | def __getattribute__(self, name):
352 | if name in _propNames:
353 | return self.props[name]
354 | else:
355 | return object.__getattribute__(self, name)
356 |
357 | def __setattr__(self, name, value):
358 | if name in _propNames:
359 | self.props[name] = value
360 | else:
361 | return object.__setattr__(self, name, value)
362 |
363 | # Arrange
364 | from_class = FromTestClass()
365 | mapper = ObjectMapper()
366 | mapper.create_map(FromTestClass, ToCustomDirClass)
367 |
368 | # Act
369 | result = mapper.map(FromTestClass())
370 |
371 | # Assert
372 | self.assertTrue(isinstance(result, ToCustomDirClass), "Target types must be same")
373 | self.assertEqual(result.name, from_class.name, "Name mapping must be equal")
374 | self.assertEqual(result.date, from_class.date, "Date mapping must be equal")
375 | self.assertNotIn("surname", dir(result), "To class must not contain surname")
376 |
377 | def test_mapping_excluded_field(self):
378 | """Test mapping with excluded fields"""
379 | # Arrange
380 | from_class = FromTestClass()
381 | mapper = ObjectMapper()
382 | mapper.create_map(FromTestClass, ToTestClass)
383 |
384 | #Act
385 | result = mapper.map(FromTestClass(), excluded=['date'])
386 |
387 | # Assert
388 | print(result)
389 | self.assertTrue(isinstance(result, ToTestClass), "Type must be ToTestClass")
390 | self.assertEqual(result.name, from_class.name, "Name mapping must be equal")
391 | self.assertEqual(result.date, '', "Date mapping must be equal")
392 | self.assertNotIn("surname", result.__dict__, "To class must not contain surname")
393 |
394 | def test_mapping_included_field(self):
395 | """Test mapping with included fields"""
396 | #Arrange
397 | from_class = FromTestClass()
398 | mapper = ObjectMapper()
399 | mapper.create_map(FromTestClass, ToTestClass)
400 |
401 | #Act
402 | result = mapper.map(FromTestClass(), excluded=['name'], included=['name', '_actor_name'])
403 |
404 | #Assert
405 | print(result)
406 | self.assertTrue(isinstance(result, ToTestClass), "Type must be ToTestClass")
407 | self.assertEqual(result.name, '', "Name must not be copied despite of inclusion, as exclusion take precedence")
408 | self.assertEqual(result.date, from_class.date, "Date mapping must be equal")
409 | self.assertEqual(result._actor_name, from_class._actor_name, "Private is copied if explicitly included")
410 | self.assertNotIn("surname", result.__dict__, "To class must not contain surname")
411 |
412 | def test_mapping_included_field_by_mapping(self):
413 | """Test mapping with included fields by mapping"""
414 | #Arrange
415 | from_class = FromTestClass()
416 | mapper = ObjectMapper()
417 | mapper.create_map(FromTestClass, ToTestClass, mapping={'_actor_name': lambda o: "{0} acted by {1}".format(o.name, o._actor_name)})
418 |
419 | #Act
420 | result = mapper.map(FromTestClass())
421 |
422 | #Assert
423 | print(result)
424 | self.assertTrue(isinstance(result, ToTestClass), "Type must be ToTestClass")
425 | self.assertEqual(result.name, from_class.name, "Name mapping must be equal")
426 | self.assertEqual(result.date, from_class.date, "Date mapping must be equal")
427 | self.assertEqual(result._actor_name, "{0} acted by {1}".format(from_class.name, from_class._actor_name), "Private is copied if explicitly mapped")
428 | self.assertNotIn("surname", result.__dict__, "To class must not contain surname")
429 |
430 | def test_mapping_creation_complex_without_mappings_correct(self):
431 | """ Test mapping creation for complex class without mappings """
432 |
433 | # Arrange
434 | from_class = FromTestComplexClass()
435 | mapper = ObjectMapper()
436 | mapper.create_map(FromTestComplexClass, ToTestComplexClass)
437 | mapper.create_map(FromTestComplexChildClass, ToTestComplexChildClass)
438 |
439 | # Act
440 | result = mapper.map(from_class)
441 |
442 | # Assert
443 | self.assertTrue(isinstance(result, ToTestComplexClass), "Target types must be same")
444 | self.assertTrue(isinstance(result.student, ToTestComplexChildClass), "Target types must be same")
445 | self.assertEqual(result.name, from_class.name, "Name mapping must be equal")
446 | self.assertEqual(result.date, from_class.date, "Date mapping must be equal")
447 | self.assertEqual(result.student.full_name, from_class.student.full_name, "StudentName mapping must be equal")
448 | self.assertEqual(len(result.knows), len(from_class.knows), "number of entries must be the same for Knows")
449 | self.assertTrue(all(isinstance(k, ToTestComplexChildClass) for k in result.knows), "Children target types must be same")
450 | self.assertEqual(result.knows[0].full_name, from_class.knows[0].full_name, "StudentName(0) mapping must be equal")
451 | self.assertEqual(result.knows[1].full_name, from_class.knows[1].full_name, "StudentName(1) mapping must be equal")
--------------------------------------------------------------------------------