Skip to content

Commit 04cd8c7

Browse files
committed
Added better description for using mapper with Pydancit and TortoiseORM, added better type casting, simplified test cases
1 parent 765937f commit 04cd8c7

File tree

7 files changed

+146
-44
lines changed

7 files changed

+146
-44
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
1.0.4 - 2022/07/25
2+
* Added better description for usage with Pydantic and TortoiseORM
3+
* Improved type support
4+
15
1.0.3 - 2022/07/24
26
* Fixed issue with dictionary collection: https://github.com/anikolaienko/py-automapper/issues/4
37

README.md

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# py-automapper
44

55
**Version**
6-
1.0.3
6+
1.0.4
77

88
**Author**
99
anikolaienko
@@ -33,6 +33,9 @@ Table of Contents:
3333
- [Different field names](#different-field-names)
3434
- [Overwrite field value in mapping](#overwrite-field-value-in-mapping)
3535
- [Extensions](#extensions)
36+
- [Pydantic/FastAPI Support](#pydanticfastapi-support)
37+
- [TortoiseORM Support](#tortoiseorm-support)
38+
- [Create your own extension (Advanced)](#create-your-own-extension-advanced)
3639

3740
# Versions
3841
Check [CHANGELOG.md](/CHANGELOG.md)
@@ -120,14 +123,81 @@ print(vars(public_user_info))
120123
# {'full_name': 'John Cusack', 'profession': 'engineer'}
121124
```
122125

126+
123127
## Extensions
124-
`py-automapper` has few predefined extensions for mapping to classes for frameworks:
128+
`py-automapper` has few predefined extensions for mapping support to classes for frameworks:
125129
* [FastAPI](https://github.com/tiangolo/fastapi) and [Pydantic](https://github.com/samuelcolvin/pydantic)
126130
* [TortoiseORM](https://github.com/tortoise/tortoise-orm)
127131

132+
## Pydantic/FastAPI Support
133+
Out of the box Pydantic models support:
134+
```python
135+
from pydantic import BaseModel
136+
from typing import List
137+
from automapper import mapper
138+
139+
class UserInfo(BaseModel):
140+
id: int
141+
full_name: str
142+
public_name: str
143+
hobbies: List[str]
144+
145+
class PublicUserInfo(BaseModel):
146+
id: int
147+
public_name: str
148+
hobbies: List[str]
149+
150+
obj = UserInfo(
151+
id=2,
152+
full_name="Danny DeVito",
153+
public_name="dannyd",
154+
hobbies=["acting", "comedy", "swimming"]
155+
)
156+
157+
result = default_mapper.to(PublicUserInfo).map(obj)
158+
# same behaviour with preregistered mapping
159+
160+
print(vars(result))
161+
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}
162+
```
163+
164+
## TortoiseORM Support
165+
Out of the box TortoiseORM models support:
166+
```python
167+
from tortoise import Model, fields
168+
from automapper import mapper
169+
170+
class UserInfo(Model):
171+
id = fields.IntField(pk=True)
172+
full_name = fields.TextField()
173+
public_name = fields.TextField()
174+
hobbies = fields.JSONField()
175+
176+
class PublicUserInfo(Model):
177+
id = fields.IntField(pk=True)
178+
public_name = fields.TextField()
179+
hobbies = fields.JSONField()
180+
181+
obj = UserInfo(
182+
id=2,
183+
full_name="Danny DeVito",
184+
public_name="dannyd",
185+
hobbies=["acting", "comedy", "swimming"],
186+
using_db=True
187+
)
188+
189+
result = default_mapper.to(PublicUserInfo).map(obj)
190+
# same behaviour with preregistered mapping
191+
192+
# filtering out protected fields that start with underscore "_..."
193+
print({key: value for key, value in vars(result) if not key.startswith("_")})
194+
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}
195+
```
196+
197+
## Create your own extension (Advanced)
128198
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.
129199

130-
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.
200+
**What does extension do?** To know what fields in Target class are available for mapping, `py-automapper` needs to know how to extract the list of fields. There is no generic way to do that for all Python objects. For this purpose `py-automapper` uses extensions.
131201

132202
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.
133203

automapper/mapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def _map_common(
301301

302302
_visited_stack.remove(obj_id)
303303

304-
return target_cls(**mapped_values) # type: ignore [call-arg]
304+
return cast(target_cls, target_cls(**mapped_values)) # type: ignore [call-arg, redundant-cast, valid-type]
305305

306306
def to(self, target_cls: Type[T]) -> MappingWrapper[T]:
307307
"""Specify target class to map source object to"""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "py-automapper"
3-
version = "1.0.3"
3+
version = "1.0.4"
44
description = "Library for automatically mapping one object to another"
55
authors = ["Andrii Nikolaienko <[email protected]>"]
66
license = "MIT"

tests/test_automapper_sample.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(self, full_name: str, profession: str):
2222

2323
def test_map__field_with_same_name():
2424
user_info = UserInfo("John Malkovich", 35, "engineer")
25-
public_user_info: PublicUserInfo = mapper.to(PublicUserInfo).map(
25+
public_user_info = mapper.to(PublicUserInfo).map(
2626
user_info, fields_mapping={"full_name": user_info.name}
2727
)
2828

@@ -58,7 +58,7 @@ def test_map__field_with_different_name_register():
5858
def test_map__override_field_value():
5959
try:
6060
user_info = UserInfo("John Malkovich", 35, "engineer")
61-
public_user_info: PublicUserInfo = mapper.to(PublicUserInfo).map(
61+
public_user_info = mapper.to(PublicUserInfo).map(
6262
user_info, fields_mapping={"name": "John Cusack"}
6363
)
6464

tests/test_for_pydantic_extention.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,53 @@
1-
from dataclasses import dataclass
1+
from typing import List
22
from unittest import TestCase
33

44
import pytest
55
from pydantic import BaseModel
66

7-
from automapper import mapper as global_mapper, Mapper, MappingError
7+
from automapper import mapper as default_mapper, Mapper, MappingError
88

99

10-
@dataclass
11-
class SourceClass:
10+
class UserInfo(BaseModel):
1211
id: int
13-
name: str
12+
full_name: str
13+
public_name: str
14+
hobbies: List[str]
1415

1516

16-
class TargetModel(BaseModel):
17+
class PublicUserInfo(BaseModel):
1718
id: int
18-
name: str
19+
public_name: str
20+
hobbies: List[str]
1921

2022

2123
class PydanticExtensionTest(TestCase):
22-
"""These scenario are known for ORM systems.
23-
e.g. Model classes in Tortoise ORM
24-
"""
24+
"""These scenario are known for FastAPI Framework models and Pydantic models in general."""
2525

2626
def setUp(self) -> None:
2727
self.mapper = Mapper()
2828

29-
def test_map__fails_for_tortoise_mapping(self):
30-
obj = SourceClass(15, "This is a test text")
29+
def test_map__fails_for_pydantic_mapping(self):
30+
obj = UserInfo(
31+
id=2,
32+
full_name="Danny DeVito",
33+
public_name="dannyd",
34+
hobbies=["acting", "comedy", "swimming"],
35+
)
3136
with pytest.raises(MappingError):
32-
self.mapper.to(TargetModel).map(obj)
33-
34-
def test_map__global_mapper_works_with_provided_tortoise_extension(self):
35-
obj = SourceClass(17, "Test obj name")
36-
37-
result = global_mapper.to(TargetModel).map(obj)
38-
39-
assert result.id == 17
40-
assert result.name == "Test obj name"
37+
self.mapper.to(PublicUserInfo).map(obj)
38+
39+
def test_map__global_mapper_works_with_provided_pydantic_extension(self):
40+
obj = UserInfo(
41+
id=2,
42+
full_name="Danny DeVito",
43+
public_name="dannyd",
44+
hobbies=["acting", "comedy", "swimming"],
45+
)
46+
47+
result = default_mapper.to(PublicUserInfo).map(obj)
48+
49+
assert result.id == 2
50+
assert result.public_name == "dannyd"
51+
assert set(result.hobbies) == set(["acting", "comedy", "swimming"])
52+
with pytest.raises(AttributeError):
53+
getattr(result, "full_name")

tests/test_for_tortoise_extention.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
from dataclasses import dataclass
21
from unittest import TestCase
32

43
import pytest
54
from tortoise import Model, fields
65

7-
from automapper import mapper as global_mapper, Mapper, MappingError
6+
from automapper import mapper as default_mapper, Mapper, MappingError
87

98

10-
@dataclass
11-
class SourceClass:
12-
id: int
13-
name: str
9+
class UserInfo(Model):
10+
id = fields.IntField(pk=True)
11+
full_name = fields.TextField()
12+
public_name = fields.TextField()
13+
hobbies = fields.JSONField()
1414

1515

16-
class TargetModel(Model):
16+
class PublicUserInfo(Model):
1717
id = fields.IntField(pk=True)
18-
name = fields.TextField()
18+
public_name = fields.TextField()
19+
hobbies = fields.JSONField()
1920

2021

2122
class TortoiseORMExtensionTest(TestCase):
@@ -27,14 +28,28 @@ def setUp(self) -> None:
2728
self.mapper = Mapper()
2829

2930
def test_map__fails_for_tortoise_mapping(self):
30-
obj = SourceClass(15, "This is a test text")
31+
obj = UserInfo(
32+
id=2,
33+
full_name="Danny DeVito",
34+
public_name="dannyd",
35+
hobbies=["acting", "comedy", "swimming"],
36+
)
3137
with pytest.raises(MappingError):
32-
self.mapper.to(TargetModel).map(obj)
38+
self.mapper.to(PublicUserInfo).map(obj)
3339

3440
def test_map__global_mapper_works_with_provided_tortoise_extension(self):
35-
obj = SourceClass(17, "Test obj name")
36-
37-
result = global_mapper.to(TargetModel).map(obj)
38-
39-
assert result.id == 17
40-
assert result.name == "Test obj name"
41+
obj = UserInfo(
42+
id=2,
43+
full_name="Danny DeVito",
44+
public_name="dannyd",
45+
hobbies=["acting", "comedy", "swimming"],
46+
using_db=True,
47+
)
48+
49+
result = default_mapper.to(PublicUserInfo).map(obj)
50+
51+
assert result.id == 2
52+
assert result.public_name == "dannyd"
53+
assert set(result.hobbies) == set(["acting", "comedy", "swimming"])
54+
with pytest.raises(AttributeError):
55+
getattr(result, "full_name")

0 commit comments

Comments
 (0)