Skip to content

Commit

Permalink
rename ConverterRetort to ConversionRetort
Browse files Browse the repository at this point in the history
add high-level methods to AdornedRetort
add convert high-level function and method
  • Loading branch information
zhPavel committed Feb 12, 2024
1 parent 9099508 commit 9ebaf33
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 62 deletions.
8 changes: 8 additions & 0 deletions docs/conversion/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ Also, it works for nested models.

.. literalinclude:: /examples/conversion/tutorial/nested.py

Furthermore, there is :func:`.conversion.convert` that can directly convert one model to another,
but it is quite limited and can not configured, so it won't be considered onwards.

.. dropdown:: Usage of :func:`.conversion.convert`

.. literalinclude:: /examples/conversion/tutorial/convert_function.py


Downcasting
=============

Expand Down
37 changes: 37 additions & 0 deletions docs/examples/conversion/tutorial/convert_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass

from adaptix.conversion import convert


@dataclass
class Person:
name: str


@dataclass
class Book:
title: str
price: int
author: Person


@dataclass
class PersonDTO:
name: str


@dataclass
class BookDTO:
title: str
price: int
author: PersonDTO


assert (
convert(
Book(title="Fahrenheit 451", price=100, author=Person("Ray Bradbury")),
BookDTO,
)
==
BookDTO(title="Fahrenheit 451", price=100, author=PersonDTO("Ray Bradbury"))
)
68 changes: 19 additions & 49 deletions src/adaptix/_internal/conversion/facade/func.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import inspect
from functools import partial
from inspect import Parameter, Signature
from typing import Any, Callable, Iterable, Optional, Type, TypeVar, overload

from ...common import TypeHint
from ...provider.essential import Provider
from .checker import ensure_function_is_stub
from .retort import AdornedConverterRetort, ConverterRetort
from .retort import ConversionRetort

_global_retort = ConverterRetort()
_global_retort = ConversionRetort()

SrcT = TypeVar('SrcT')
DstT = TypeVar('DstT')
CallableT = TypeVar('CallableT', bound=Callable)


def convert(src_obj: Any, dst: Type[DstT], *, recipe: Iterable[Provider] = ()) -> DstT:
"""Function transforming a source object to destination.
:param src_obj: A type of converter input data.
:param dst: A type of converter output data.
:param recipe: An extra recipe adding to retort.
:return: Instance of destination
"""
return _global_retort.convert(src_obj, dst, recipe=recipe)


@overload
def get_converter(
src: Type[SrcT],
dst: Type[DstT],
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
) -> Callable[[SrcT], DstT]:
Expand All @@ -32,40 +38,22 @@ def get_converter(
src: TypeHint,
dst: TypeHint,
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
) -> Callable[[Any], Any]:
...


def get_converter(
src: TypeHint,
dst: TypeHint,
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
name: Optional[str] = None,
):
def get_converter(src: TypeHint, dst: TypeHint, *, recipe: Iterable[Provider] = (), name: Optional[str] = None):
"""Factory producing basic converter.
:param src: A type of converter input data.
:param dst: A type of converter output data.
:param retort: A retort used to produce converter.
:param recipe: An extra recipe adding to retort.
:param name: Name of generated function, if value is None, name will be derived.
:return: Desired converter function
"""
if recipe:
retort = retort.extend(recipe=recipe)
return retort.produce_converter(
signature=Signature(
parameters=[Parameter('src', kind=Parameter.POSITIONAL_ONLY, annotation=src)],
return_annotation=dst,
),
stub_function=None,
function_name=name,
)
return _global_retort.get_converter(src, dst, recipe=recipe, name=name)


@overload
Expand All @@ -74,35 +62,17 @@ def impl_converter(func_stub: CallableT, /) -> CallableT:


@overload
def impl_converter(
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
) -> Callable[[CallableT], CallableT]:
def impl_converter(*, recipe: Iterable[Provider] = ()) -> Callable[[CallableT], CallableT]:
...


def impl_converter(
stub_function: Optional[Callable] = None,
*,
retort: AdornedConverterRetort = _global_retort,
recipe: Iterable[Provider] = (),
):
def impl_converter(stub_function: Optional[Callable] = None, *, recipe: Iterable[Provider] = ()):
"""Decorator producing converter with signature of stub function.
:param stub_function: A function that signature is used to generate converter.
:param retort: A retort used to produce converter.
:param recipe: An extra recipe adding to retort.
:return: Desired converter function
"""
if stub_function is None:
return partial(impl_converter, retort=retort, recipe=recipe)

if recipe:
retort = retort.extend(recipe=recipe)
ensure_function_is_stub(stub_function)
return retort.produce_converter(
signature=inspect.signature(stub_function),
stub_function=stub_function,
function_name=None,
)
return _global_retort.impl_converter(recipe=recipe)
return _global_retort.impl_converter(stub_function)
129 changes: 122 additions & 7 deletions src/adaptix/_internal/conversion/facade/retort.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
from inspect import Signature
from typing import Any, Callable, Iterable, Optional, TypeVar
# pylint: disable=protected-access, not-callable
import inspect
from functools import partial
from inspect import Parameter, Signature
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Type, TypeVar, overload

from adaptix import TypeHint

from ...common import Converter
from ...provider.essential import Provider
from ...provider.loc_stack_filtering import P
from ...provider.shape_provider import BUILTIN_SHAPE_PROVIDER
from ...retort.operating_retort import OperatingRetort
from ...type_tools import is_generic_class
from ..coercer_provider import DstAnyCoercerProvider, SameTypeCoercerProvider, SubclassCoercerProvider
from ..converter_provider import BuiltinConverterProvider
from ..linking_provider import SameNameLinkingProvider
from ..request_cls import ConverterRequest
from .checker import ensure_function_is_stub
from .provider import forbid_unlinked_optional


class FilledConverterRetort(OperatingRetort):
class FilledConversionRetort(OperatingRetort):
recipe = [
BUILTIN_SHAPE_PROVIDER,

Expand All @@ -28,10 +36,17 @@ class FilledConverterRetort(OperatingRetort):
]


AR = TypeVar('AR', bound='AdornedConverterRetort')
AR = TypeVar('AR', bound='AdornedConversionRetort')
SrcT = TypeVar('SrcT')
DstT = TypeVar('DstT')
CallableT = TypeVar('CallableT', bound=Callable)


class AdornedConversionRetort(OperatingRetort):
def _calculate_derived(self):
super()._calculate_derived()
self._simple_converter_cache: Dict[Tuple[TypeHint, TypeHint, Optional[str]], Converter] = {}

class AdornedConverterRetort(OperatingRetort):
def extend(self: AR, *, recipe: Iterable[Provider]) -> AR:
# pylint: disable=protected-access
with self._clone() as clone:
Expand All @@ -41,7 +56,7 @@ def extend(self: AR, *, recipe: Iterable[Provider]) -> AR:

return clone

def produce_converter(
def _produce_converter(
self,
signature: Signature,
stub_function: Optional[Callable],
Expand All @@ -56,6 +71,106 @@ def produce_converter(
error_message=f'Cannot produce converter for {signature!r}',
)

def _make_simple_converter(self, src: TypeHint, dst: TypeHint, name: Optional[str]) -> Converter:
return self._produce_converter(
signature=Signature(
parameters=[Parameter('src', kind=Parameter.POSITIONAL_ONLY, annotation=src)],
return_annotation=dst,
),
stub_function=None,
function_name=name,
)

@overload
def get_converter(
self,
src: Type[SrcT],
dst: Type[DstT],
*,
recipe: Iterable[Provider] = (),
) -> Callable[[SrcT], DstT]:
...

@overload
def get_converter(
self,
src: TypeHint,
dst: TypeHint,
*,
name: Optional[str] = None,
recipe: Iterable[Provider] = (),
) -> Callable[[Any], Any]:
...

def get_converter(
self,
src: TypeHint,
dst: TypeHint,
*,
name: Optional[str] = None,
recipe: Iterable[Provider] = (),
):
"""Method producing basic converter.
:param src: A type of converter input data.
:param dst: A type of converter output data.
:param recipe: An extra recipe adding to retort.
:param name: Name of generated function, if value is None, name will be derived.
:return: Desired converter function
"""
retort = self.extend(recipe=recipe) if recipe else self

try:
return retort._simple_converter_cache[(src, dst, name)]
except KeyError:
pass
converter = retort._make_simple_converter(src, dst, name)
retort._simple_converter_cache[(src, dst, name)] = converter
return converter

@overload
def impl_converter(self, func_stub: CallableT, /) -> CallableT:
...

@overload
def impl_converter(self, *, recipe: Iterable[Provider] = ()) -> Callable[[CallableT], CallableT]:
...

def impl_converter(self, stub_function: Optional[Callable] = None, *, recipe: Iterable[Provider] = ()):
"""Decorator producing converter with signature of stub function.
:param stub_function: A function that signature is used to generate converter.
:param recipe: An extra recipe adding to retort.
:return: Desired converter function
"""
if stub_function is None:
return partial(self.impl_converter, recipe=recipe)

ensure_function_is_stub(stub_function)
retort = self.extend(recipe=recipe) if recipe else self
return retort._produce_converter(
signature=inspect.signature(stub_function),
stub_function=stub_function,
function_name=None,
)

def convert(self, src_obj: Any, dst: Type[DstT], *, recipe: Iterable[Provider] = ()) -> DstT:
"""Method transforming a source object to destination.
:param src_obj: A type of converter input data.
:param dst: A type of converter output data.
:param recipe: An extra recipe adding to retort.
:return: Instance of destination
"""
src = type(src_obj)
if is_generic_class(src):
raise ValueError(
f'Can not infer the actual type of generic class instance ({src!r}),'
' you have to use `get_converter` explicitly passing the type of object'
)

return self.get_converter(src, dst, recipe=recipe)(src_obj)


class ConverterRetort(FilledConverterRetort, AdornedConverterRetort):
class ConversionRetort(FilledConversionRetort, AdornedConversionRetort):
pass
11 changes: 6 additions & 5 deletions src/adaptix/conversion/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from adaptix._internal.conversion.facade.func import get_converter, impl_converter
from adaptix._internal.conversion.facade.func import convert, get_converter, impl_converter
from adaptix._internal.conversion.facade.provider import (
allow_unlinked_optional,
coercer,
forbid_unlinked_optional,
link,
)
from adaptix._internal.conversion.facade.retort import AdornedConverterRetort, ConverterRetort, FilledConverterRetort
from adaptix._internal.conversion.facade.retort import AdornedConversionRetort, ConversionRetort, FilledConversionRetort

__all__ = (
'convert',
'get_converter',
'impl_converter',
'link',
'coercer',
'allow_unlinked_optional',
'forbid_unlinked_optional',
'AdornedConverterRetort',
'FilledConverterRetort',
'ConverterRetort',
'AdornedConversionRetort',
'FilledConversionRetort',
'ConversionRetort',
)
Loading

0 comments on commit 9ebaf33

Please sign in to comment.