Skip to content

Commit b9cf7f5

Browse files
committed
Merge branch 'main' of https://github.com/anikolaienko/py-automapper into main
2 parents d4406ce + a9868bc commit b9cf7f5

12 files changed

+560
-71
lines changed

.pre-commit-config.yaml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
repos:
4+
- repo: local
5+
hooks:
6+
- id: black
7+
name: black
8+
description: "Black: The uncompromising Python code formatter"
9+
entry: black
10+
language: python
11+
require_serial: true
12+
types_or: [python, pyi]
13+
- id: flake8
14+
name: flake8
15+
description: '`flake8` is a command-line utility for enforcing style consistency across Python projects.'
16+
entry: flake8
17+
language: python
18+
types: [python]
19+
require_serial: true
20+
- id: mypy
21+
name: mypy
22+
description: 'Mypy is a static type checker for Python 3'
23+
entry: mypy
24+
language: python
25+
types: [python]
26+
require_serial: true

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
1.0.0 - 2022/01/05
2+
* Finalized documentation, fixed defects
3+
14
0.1.1 - 2021/07/18
25
* No changes, set version as Alpha
36

README.md

+179-39
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,203 @@
1+
<img src="logo.png" align="left" style="width:128px; margin-right: 20px;" />
2+
13
# py-automapper
2-
Python object auto mapper
34

4-
Current mapper can be useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc).
5+
**Version**
6+
1.0.0
7+
8+
**Author**
9+
anikolaienko
10+
11+
**Copyright**
12+
anikolaienko
13+
14+
**License**
15+
The MIT License (MIT)
16+
17+
**Last updated**
18+
5 Jan 2022
19+
20+
**Package Download**
21+
https://pypi.python.org/pypi/py-automapper
22+
23+
**Build Status**
24+
TODO
25+
26+
---
27+
28+
## Versions
29+
Check [CHANGELOG.md](/CHANGELOG.md)
30+
31+
## About
32+
33+
**Python auto mapper** is useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc).
534

6-
For more information read the [documentation](https://anikolaienko.github.io/py-automapper).
35+
Inspired by: [object-mapper](https://github.com/marazt/object-mapper)
736

8-
## Usage example:
37+
The major advantage of py-automapper is its extensibility, that allows it to map practically any type, discover custom class fields and customize mapping rules. Read more in [documentation](https://anikolaienko.github.io/py-automapper).
38+
39+
## Usage
40+
Install package:
41+
```bash
42+
pip install py-automapper
43+
```
44+
45+
Simple mapping:
946
```python
1047
from automapper import mapper
1148

12-
# Add automatic mappings
49+
class SourceClass:
50+
def __init__(self, name: str, age: int, profession: str):
51+
self.name = name
52+
self.age = age
53+
self.profession = profession
54+
55+
class TargetClass:
56+
def __init__(self, name: str, age: int):
57+
self.name = name
58+
self.age = age
59+
60+
# Register mapping
1361
mapper.add(SourceClass, TargetClass)
1462

15-
# Map object of SourceClass to output object of TargetClass
16-
mapper.map(obj)
63+
source_obj = SourceClass("Andrii", 30, "software developer")
1764

18-
# Map object to AnotherTargetClass not added to mapping collection
19-
mapper.to(AnotherTargetClass).map(obj)
65+
# Map object
66+
target_obj = mapper.map(source_obj)
2067

21-
# Override specific fields or provide missing ones
22-
mapper.map(obj, field1=value1, field2=value2)
68+
# or one time mapping without registering in mapper
69+
target_obj = mapper.to(TargetClass).map(source_obj)
2370

24-
# Don't map None values to target object
25-
mapper.map(obj, skip_none_values = True)
71+
print(f"Name: {target_obj.name}; Age: {target_obj.age}; has profession: {hasattr(target_obj, 'profession')}")
72+
73+
# Output:
74+
# Name: Andrii; age: 30; has profession: False
2675
```
2776

28-
## Advanced features
77+
## Override fields
78+
If you want to override some field and/or add mapping for field not existing in SourceClass:
2979
```python
30-
from automapper import Mapper
80+
from typing import List
81+
from automapper import mapper
3182

32-
# Create your own Mapper object without any predefined extensions
33-
mapper = Mapper()
83+
class SourceClass:
84+
def __init__(self, name: str, age: int):
85+
self.name = name
86+
self.age = age
3487

35-
# Add your own extension for extracting list of fields from class
36-
# for all classes inherited from base class
37-
mapper.add_spec(
38-
BaseClass,
39-
lambda child_class: child_class.get_fields_function()
40-
)
41-
42-
# Add your own extension for extracting list of fields from class
43-
# for all classes that can be identified in verification function
44-
mapper.add_spec(
45-
lambda cls: hasattr(cls, "get_fields_function"),
46-
lambda cls: cls.get_fields_function()
47-
)
88+
class TargetClass:
89+
def __init__(self, name: str, age: int, hobbies: List[str]):
90+
self.name = name
91+
self.age = age
92+
self.hobbies = hobbies
93+
94+
mapper.add(SourceClass, TargetClass)
95+
96+
source_obj = SourceClass("Andrii", 30)
97+
hobbies = ["Diving", "Languages", "Sports"]
98+
99+
# Override `age` and provide missing field `hobbies`
100+
target_obj = mapper.map(source_obj, age=25, hobbies=hobbies)
101+
102+
print(f"Name: {target_obj.name}; Age: {target_obj.age}; hobbies: {target_obj.hobbies}")
103+
# Output:
104+
# Name: Andrii; Age: 25; hobbies: ['Diving', 'Languages', 'Sports']
105+
106+
# Modifying initial `hobbies` object will not modify `target_obj`
107+
hobbies.pop()
108+
109+
print(f"Hobbies: {hobbies}")
110+
print(f"Target hobbies: {target_obj.hobbies}")
111+
112+
# Output:
113+
# Hobbies: ['Diving', 'Languages']
114+
# Target hobbies: ['Diving', 'Languages', 'Sports']
115+
```
116+
117+
## Extensions
118+
`py-automapper` has few predefined extensions for mapping to classes for frameworks:
119+
* [FastAPI](https://github.com/tiangolo/fastapi) and [Pydantic](https://github.com/samuelcolvin/pydantic)
120+
* [TortoiseORM](https://github.com/tortoise/tortoise-orm)
121+
122+
When you first time import `mapper` from `automapper` it checks default extensions and if modules are found for these extensions, then they will be automatically loaded for default `mapper` object.
123+
124+
What does extension do? To know what fields in Target class are available for mapping `py-automapper` need to extract the list of these fields. There is no generic way to do that for all Python objects. For this purpose `py-automapper` uses extensions.
125+
126+
List of default extensions can be found in [/automapper/extensions](/automapper/extensions) folder. You can take a look how it's done for a class with `__init__` method or for Pydantic or TortoiseORM models.
127+
128+
You can create your own extension and register in `mapper`:
129+
```python
130+
from automapper import mapper
131+
132+
class TargetClass:
133+
def __init__(self, **kwargs):
134+
self.name = kwargs["name"]
135+
self.age = kwargs["age"]
136+
137+
@staticmethod
138+
def get_fields(cls):
139+
return ["name", "age"]
140+
141+
source_obj = {"name": "Andrii", "age": 30}
142+
143+
try:
144+
# Map object
145+
target_obj = mapper.to(TargetClass).map(source_obj)
146+
except Exception as e:
147+
print(f"Exception: {repr(e)}")
148+
# Output:
149+
# Exception: KeyError('name')
150+
151+
# mapper could not find list of fields from BaseClass
152+
# let's register extension for class BaseClass and all inherited ones
153+
mapper.add_spec(TargetClass, TargetClass.get_fields)
154+
target_obj = mapper.to(TargetClass).map(source_obj)
155+
156+
print(f"Name: {target_obj.name}; Age: {target_obj.age}")
48157
```
49-
For more information about extensions check out existing extensions in `automapper/extensions` folder
50158

51-
## Not yet implemented features
159+
You can also create your own clean Mapper without any extensions and define extension for very specific classes, e.g. if class accepts `kwargs` parameter in `__init__` method and you want to copy only specific fields. Next example is a bit complex but probably rarely will be needed:
52160
```python
161+
from typing import Type, TypeVar
53162

54-
# TODO: multiple from classes
55-
mapper.add(FromClassA, FromClassB, ToClassC)
163+
from automapper import Mapper
56164

57-
# TODO: add custom mappings for fields
58-
mapper.add(ClassA, ClassB, {"Afield1": "Bfield1", "Afield2": "Bfield2"})
165+
# Create your own Mapper object without any predefined extensions
166+
mapper = Mapper()
59167

60-
# TODO: Advanced: map multiple objects to output type
61-
mapper.multimap(obj1, obj2)
62-
mapper.to(TargetType).multimap(obj1, obj2)
168+
class TargetClass:
169+
def __init__(self, **kwargs):
170+
self.data = kwargs.copy()
171+
172+
@classmethod
173+
def fields(cls):
174+
return ["name", "age", "profession"]
175+
176+
source_obj = {"name": "Andrii", "age": 30, "profession": None}
177+
178+
try:
179+
target_obj = mapper.to(TargetClass).map(source_obj)
180+
except Exception as e:
181+
print(f"Exception: {repr(e)}")
182+
# Output:
183+
# Exception: MappingError("No spec function is added for base class of <class 'type'>")
184+
185+
# Instead of using base class, we define spec for all classes that have `fields` property
186+
T = TypeVar("T")
187+
188+
def class_has_fields_property(target_cls: Type[T]) -> bool:
189+
return callable(getattr(target_cls, "fields", None))
190+
191+
mapper.add_spec(class_has_fields_property, lambda t: getattr(t, "fields")())
192+
193+
target_obj = mapper.to(TargetClass).map(source_obj)
194+
print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Profession: {target_obj.data['profession']}")
195+
# Output:
196+
# Name: Andrii; Age: 30; Profession: None
197+
198+
# Skip `None` value
199+
target_obj = mapper.to(TargetClass).map(source_obj, skip_none_values=True)
200+
print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Has profession: {hasattr(target_obj, 'profession')}")
201+
# Output:
202+
# Name: Andrii; Age: 30; Has profession: False
63203
```

automapper/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# flake8: noqa: F401
22
from .mapper import Mapper
33

4-
from .exceptions import DuplicatedRegistrationError, MappingError, CircularReferenceError
4+
from .exceptions import (
5+
DuplicatedRegistrationError,
6+
MappingError,
7+
CircularReferenceError,
8+
)
59

610
from .mapper_initializer import create_mapper
711

automapper/exceptions.py

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

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

automapper/extensions/default.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ def __init_method_classifier__(target_cls: Type[T]) -> bool:
1212
return (
1313
hasattr(target_cls, "__init__")
1414
and hasattr(getattr(target_cls, "__init__"), "__annotations__")
15-
and isinstance(getattr(getattr(target_cls, "__init__"), "__annotations__"), dict)
15+
and isinstance(
16+
getattr(getattr(target_cls, "__init__"), "__annotations__"), dict
17+
)
1618
)
1719

1820

0 commit comments

Comments
 (0)