Skip to content

Commit

Permalink
Added better description for using mapper with Pydancit and TortoiseO…
Browse files Browse the repository at this point in the history
…RM, added better type casting, simplified test cases
  • Loading branch information
anikolaienko committed Jul 25, 2022
1 parent 765937f commit 04cd8c7
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 44 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
1.0.4 - 2022/07/25
* Added better description for usage with Pydantic and TortoiseORM
* Improved type support

1.0.3 - 2022/07/24
* Fixed issue with dictionary collection: https://github.com/anikolaienko/py-automapper/issues/4

Expand Down
76 changes: 73 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# py-automapper

**Version**
1.0.3
1.0.4

**Author**
anikolaienko
Expand Down Expand Up @@ -33,6 +33,9 @@ Table of Contents:
- [Different field names](#different-field-names)
- [Overwrite field value in mapping](#overwrite-field-value-in-mapping)
- [Extensions](#extensions)
- [Pydantic/FastAPI Support](#pydanticfastapi-support)
- [TortoiseORM Support](#tortoiseorm-support)
- [Create your own extension (Advanced)](#create-your-own-extension-advanced)

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


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

## Pydantic/FastAPI Support
Out of the box Pydantic models support:
```python
from pydantic import BaseModel
from typing import List
from automapper import mapper

class UserInfo(BaseModel):
id: int
full_name: str
public_name: str
hobbies: List[str]

class PublicUserInfo(BaseModel):
id: int
public_name: str
hobbies: List[str]

obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"]
)

result = default_mapper.to(PublicUserInfo).map(obj)
# same behaviour with preregistered mapping

print(vars(result))
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}
```

## TortoiseORM Support
Out of the box TortoiseORM models support:
```python
from tortoise import Model, fields
from automapper import mapper

class UserInfo(Model):
id = fields.IntField(pk=True)
full_name = fields.TextField()
public_name = fields.TextField()
hobbies = fields.JSONField()

class PublicUserInfo(Model):
id = fields.IntField(pk=True)
public_name = fields.TextField()
hobbies = fields.JSONField()

obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"],
using_db=True
)

result = default_mapper.to(PublicUserInfo).map(obj)
# same behaviour with preregistered mapping

# filtering out protected fields that start with underscore "_..."
print({key: value for key, value in vars(result) if not key.startswith("_")})
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}
```

## Create your own extension (Advanced)
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.

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.
**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.

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.

Expand Down
2 changes: 1 addition & 1 deletion automapper/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def _map_common(

_visited_stack.remove(obj_id)

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

def to(self, target_cls: Type[T]) -> MappingWrapper[T]:
"""Specify target class to map source object to"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "py-automapper"
version = "1.0.3"
version = "1.0.4"
description = "Library for automatically mapping one object to another"
authors = ["Andrii Nikolaienko <[email protected]>"]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_automapper_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self, full_name: str, profession: str):

def test_map__field_with_same_name():
user_info = UserInfo("John Malkovich", 35, "engineer")
public_user_info: PublicUserInfo = mapper.to(PublicUserInfo).map(
public_user_info = mapper.to(PublicUserInfo).map(
user_info, fields_mapping={"full_name": user_info.name}
)

Expand Down Expand Up @@ -58,7 +58,7 @@ def test_map__field_with_different_name_register():
def test_map__override_field_value():
try:
user_info = UserInfo("John Malkovich", 35, "engineer")
public_user_info: PublicUserInfo = mapper.to(PublicUserInfo).map(
public_user_info = mapper.to(PublicUserInfo).map(
user_info, fields_mapping={"name": "John Cusack"}
)

Expand Down
55 changes: 34 additions & 21 deletions tests/test_for_pydantic_extention.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,53 @@
from dataclasses import dataclass
from typing import List
from unittest import TestCase

import pytest
from pydantic import BaseModel

from automapper import mapper as global_mapper, Mapper, MappingError
from automapper import mapper as default_mapper, Mapper, MappingError


@dataclass
class SourceClass:
class UserInfo(BaseModel):
id: int
name: str
full_name: str
public_name: str
hobbies: List[str]


class TargetModel(BaseModel):
class PublicUserInfo(BaseModel):
id: int
name: str
public_name: str
hobbies: List[str]


class PydanticExtensionTest(TestCase):
"""These scenario are known for ORM systems.
e.g. Model classes in Tortoise ORM
"""
"""These scenario are known for FastAPI Framework models and Pydantic models in general."""

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

def test_map__fails_for_tortoise_mapping(self):
obj = SourceClass(15, "This is a test text")
def test_map__fails_for_pydantic_mapping(self):
obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"],
)
with pytest.raises(MappingError):
self.mapper.to(TargetModel).map(obj)

def test_map__global_mapper_works_with_provided_tortoise_extension(self):
obj = SourceClass(17, "Test obj name")

result = global_mapper.to(TargetModel).map(obj)

assert result.id == 17
assert result.name == "Test obj name"
self.mapper.to(PublicUserInfo).map(obj)

def test_map__global_mapper_works_with_provided_pydantic_extension(self):
obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"],
)

result = default_mapper.to(PublicUserInfo).map(obj)

assert result.id == 2
assert result.public_name == "dannyd"
assert set(result.hobbies) == set(["acting", "comedy", "swimming"])
with pytest.raises(AttributeError):
getattr(result, "full_name")
47 changes: 31 additions & 16 deletions tests/test_for_tortoise_extention.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
from dataclasses import dataclass
from unittest import TestCase

import pytest
from tortoise import Model, fields

from automapper import mapper as global_mapper, Mapper, MappingError
from automapper import mapper as default_mapper, Mapper, MappingError


@dataclass
class SourceClass:
id: int
name: str
class UserInfo(Model):
id = fields.IntField(pk=True)
full_name = fields.TextField()
public_name = fields.TextField()
hobbies = fields.JSONField()


class TargetModel(Model):
class PublicUserInfo(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
public_name = fields.TextField()
hobbies = fields.JSONField()


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

def test_map__fails_for_tortoise_mapping(self):
obj = SourceClass(15, "This is a test text")
obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"],
)
with pytest.raises(MappingError):
self.mapper.to(TargetModel).map(obj)
self.mapper.to(PublicUserInfo).map(obj)

def test_map__global_mapper_works_with_provided_tortoise_extension(self):
obj = SourceClass(17, "Test obj name")

result = global_mapper.to(TargetModel).map(obj)

assert result.id == 17
assert result.name == "Test obj name"
obj = UserInfo(
id=2,
full_name="Danny DeVito",
public_name="dannyd",
hobbies=["acting", "comedy", "swimming"],
using_db=True,
)

result = default_mapper.to(PublicUserInfo).map(obj)

assert result.id == 2
assert result.public_name == "dannyd"
assert set(result.hobbies) == set(["acting", "comedy", "swimming"])
with pytest.raises(AttributeError):
getattr(result, "full_name")

0 comments on commit 04cd8c7

Please sign in to comment.