Created
November 20, 2022 21:48
-
-
Save ahancock1/5e5e0c665c3e696f1e8085f7b38bd123 to your computer and use it in GitHub Desktop.
Simple dataclass Automapper for python 3.10
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import Protocol, TypeVar, Callable | |
from dataclasses import is_dataclass, fields | |
from dataclasses import MISSING | |
S = TypeVar("S") | |
T = TypeVar("T") | |
class IProfile(Protocol): | |
mappings: dict[tuple[type[S], type[T]], dict[str, Callable[[S], object]]] | |
def create_map(self, | |
source_type: type[S], | |
target_type: type[T], | |
**mappings: Callable[[S], object]) -> None: | |
... | |
class IMapper(Protocol): | |
def map(self, data: object, data_type: type[T]) -> T: | |
... | |
class Profile: | |
mappings: dict[tuple[type[S], type[T]], dict[str, Callable[[S], object]]] | |
def __init__(self) -> None: | |
self.mappings = {} | |
def create_map(self, | |
source_type: type[S], | |
target_type: type[T], | |
**mappings: Callable[[S], object]) -> None: | |
self.mappings[(source_type, target_type)] = dict(mappings) | |
class Mapper: | |
_mappings: dict[tuple[type[S], type[T]], dict[str, Callable[[S], object]]] | |
def __init__(self, profiles: list[IProfile]) -> None: | |
self._mappings = {} | |
for profile in profiles: | |
for key, value in profile.mappings.items(): | |
self._mappings[key] = value | |
def map(self, data: object, data_type: type[T]) -> T: | |
if not is_dataclass(data_type): | |
raise TypeError("type must be a dataclass") | |
mapping_key = (type(data), data_type,) | |
data_fields = fields(data_type) | |
data_params = {} | |
mappings = self._mappings.get(mapping_key, {}) | |
for field in data_fields: | |
field_name, field_type = field.name, field.type | |
field_value = getattr(data, field_name, None) | |
if is_dataclass(field_type): | |
field_value = self.map(field_value, field_type) | |
else: | |
if field_name in mappings: | |
field_value = mappings[field_name](field_value) | |
if not field_value and field.default is not MISSING: | |
field_value = field.default | |
data_params[field_name] = field_value | |
return data_type(**data_params) | |
from dataclasses import dataclass | |
@dataclass | |
class Middle: | |
value: str | |
@dataclass | |
class Source: | |
a: str | |
b: int | |
c: Middle | |
@dataclass | |
class Target: | |
c: Middle | |
b: int | |
d: str = "Test" | |
class TestProfile(Profile): | |
def __init__(self) -> None: | |
super().__init__() | |
self.create_map( | |
Source, Target, | |
c=lambda x: x.c, | |
b=lambda _: 1) | |
mapper = Mapper([TestProfile()]) | |
x = mapper.map(Source("a", 2, Middle("value")), Target) | |
print(x) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment