├── .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 | [![Build Status](https://travis-ci.com/marazt/object-mapper.svg?branch=master)](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") --------------------------------------------------------------------------------