Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coercer parameter to link function (#256) #257

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/changelog/fragments/252.feature.rst
Original file line number Diff line number Diff line change
@@ -1 +1 @@
No coercer error is now contains type information
No coercer error now contains type information
1 change: 1 addition & 0 deletions docs/changelog/fragments/256.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:func:`.conversion.link` accepts `coercer` parameter
11 changes: 8 additions & 3 deletions docs/conversion/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ but it is quite limited and can not configured, so it won't be considered onward

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


Downcasting
Upcasting
=============

All source model additional fields not found in the destination model are simply ignored.

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

Upcasting
Downcasting
=============

Sometimes you need to add extra data to the source model. For this, you can use a special decorator.
Expand Down Expand Up @@ -116,6 +115,12 @@ the third parameter is the function that casts source data to the destination ty

Usually, only field types are used as predicates here.

Also you can set coercer for specific linking via :paramref:`.conversion.link.coercer` parameter.

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

This coercer will have higher priority than defined via :func:`.conversion.coercer` function.


Putting together
===================
Expand Down
32 changes: 32 additions & 0 deletions docs/examples/conversion/tutorial/type_coercion_via_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from dataclasses import dataclass
from decimal import Decimal

from adaptix import P
from adaptix.conversion import get_converter, link


@dataclass
class Book:
name: str
price: int # same as BookDTO.cost
author: str


@dataclass
class BookDTO:
name: str
cost: Decimal # same as Book.price
author: str


convert_book_to_dto = get_converter(
src=Book,
dst=BookDTO,
recipe=[link(P[Book].price, P[BookDTO].cost, coercer=lambda x: Decimal(x) / 100)],
)

assert (
convert_book_to_dto(Book(name="Fahrenheit 451", price=100, author="Ray Bradbury"))
==
BookDTO(name="Fahrenheit 451", cost=Decimal("1"), author="Ray Bradbury")
)
28 changes: 11 additions & 17 deletions src/adaptix/_internal/conversion/converter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ..code_tools.compiler import BasicClosureCompiler, ClosureCompiler
from ..code_tools.name_sanitizer import BuiltinNameSanitizer, NameSanitizer
from ..common import Converter, TypeHint
from ..common import Coercer, Converter, TypeHint
from ..conversion.broaching.code_generator import BroachingCodeGenerator, BroachingPlan, BuiltinBroachingCodeGenerator
from ..conversion.broaching.definitions import (
AccessorElement,
Expand Down Expand Up @@ -253,18 +253,7 @@ def _linking_source_to_plan(self, linking_src: LinkingSource) -> BroachingPlan:
cast(BroachingPlan, ParameterElement(linking_src.head.id)),
)

def _get_coercer_sub_plan(
self,
mediator: Mediator,
linking_src: LinkingSource,
linking_dst: LinkingDest,
) -> BroachingPlan:
coercer = mediator.provide(
CoercerRequest(
src=linking_src,
dst=linking_dst,
),
)
def _get_coercer_sub_plan(self, coercer: Coercer, linking_src: LinkingSource) -> BroachingPlan:
return FunctionElement(
func=coercer,
args=(
Expand All @@ -282,12 +271,16 @@ def _generate_field_to_sub_plan(
owner_linking_dst: LinkingDest,
) -> Mapping[InputField, BroachingPlan]:
def generate_sub_plan(input_field: InputField, linking: LinkingResult):
if linking.coercer is not None:
return self._get_coercer_sub_plan(linking.coercer, linking.source)

linking_dst = owner_linking_dst.append_with(input_field)
try:
return self._get_coercer_sub_plan(
mediator=mediator,
linking_src=linking.source,
linking_dst=linking_dst,
coercer = mediator.provide(
CoercerRequest(
src=linking.source,
dst=linking_dst,
),
)
except CannotProvide as e:
result = self._get_nested_models_sub_plan(
Expand All @@ -299,6 +292,7 @@ def generate_sub_plan(input_field: InputField, linking: LinkingResult):
if result is not None:
return result
raise e
return self._get_coercer_sub_plan(coercer, linking.source)

coercers = mandatory_apply_by_iterable(
generate_sub_plan,
Expand Down
8 changes: 7 additions & 1 deletion src/adaptix/_internal/conversion/facade/provider.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# pylint: disable=redefined-outer-name
from typing import Optional

from ...common import Coercer
from ...provider.essential import Provider
from ...provider.facade.provider import bound_by_any
Expand All @@ -7,16 +10,19 @@
from ..policy_provider import UnlinkedOptionalPolicyProvider


def link(src: Pred, dst: Pred) -> Provider:
def link(src: Pred, dst: Pred, *, coercer: Optional[Coercer] = None) -> Provider:
"""Basic provider to define custom linking between fields.

:param src: Predicate specifying source point of linking. See :ref:`predicate-system` for details.
:param dst: Predicate specifying destination point of linking. See :ref:`predicate-system` for details.
:param coercer: Function transforming source value to target.
It has higher priority than generic coercers defined by :func:`.coercer`.
:return: Desired provider
"""
return MatchingLinkingProvider(
src_lsc=create_loc_stack_checker(src),
dst_lsc=create_loc_stack_checker(dst),
coercer=coercer,
)


Expand Down
8 changes: 5 additions & 3 deletions src/adaptix/_internal/conversion/linking_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC, abstractmethod
from typing import Iterable
from typing import Iterable, Optional

from ..common import Coercer
from ..provider.essential import CannotProvide, Mediator
from ..provider.loc_stack_filtering import LocStackChecker
from ..provider.static_provider import StaticProvider, static_provision_action
Expand Down Expand Up @@ -35,15 +36,16 @@ def _provide_linking(self, mediator: Mediator, request: LinkingRequest) -> Linki


class MatchingLinkingProvider(LinkingProvider):
def __init__(self, src_lsc: LocStackChecker, dst_lsc: LocStackChecker):
def __init__(self, src_lsc: LocStackChecker, dst_lsc: LocStackChecker, coercer: Optional[Coercer]):
self._src_lsc = src_lsc
self._dst_lsc = dst_lsc
self._coercer = coercer

def _provide_linking(self, mediator: Mediator, request: LinkingRequest) -> LinkingResult:
if not self._dst_lsc.check_loc_stack(mediator, request.destination.to_loc_stack()):
raise CannotProvide

for source in iterate_source_candidates(request.sources):
if self._src_lsc.check_loc_stack(mediator, source.to_loc_stack()):
return LinkingResult(source=source)
return LinkingResult(source=source, coercer=self._coercer)
raise CannotProvide
1 change: 1 addition & 0 deletions src/adaptix/_internal/conversion/request_cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def to_loc_stack(self) -> LocStack:
@dataclass(frozen=True)
class LinkingResult:
source: LinkingSource
coercer: Optional[Coercer] = None
is_default: bool = False


Expand Down
4 changes: 2 additions & 2 deletions tests/integration/conversion/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def convert(a: SourceModel, field2: Any) -> DestModel:
assert convert(SourceModel(field1=1, field2=2), field2=3) == DestModel(field1=1, field2=3)


def test_downcast(src_model_spec, dst_model_spec, factory_way):
def test_upcast(src_model_spec, dst_model_spec, factory_way):
@src_model_spec.decorator
class SourceModel(*src_model_spec.bases):
field1: Any
Expand All @@ -104,7 +104,7 @@ def convert(a: SourceModel) -> DestModel:
assert convert(SourceModel(field1=1, field2=2, field3=3)) == DestModel(field1=1, field2=2)


def test_upcast(src_model_spec, dst_model_spec):
def test_downcast(src_model_spec, dst_model_spec):
@src_model_spec.decorator
class SourceModel(*src_model_spec.bases):
field1: Any
Expand Down
51 changes: 49 additions & 2 deletions tests/integration/conversion/test_linking.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any

from adaptix.conversion import get_converter, impl_converter, link
from adaptix.conversion import coercer, get_converter, impl_converter, link

from .local_helpers import FactoryWay

Expand Down Expand Up @@ -47,7 +47,7 @@ def convert(a: SourceModel) -> DestModel:
assert convert(SourceModel(field1=1, field2=2)) == DestModel(field1=2, field2=1)


def test_upcast(src_model_spec, dst_model_spec):
def test_downcast(src_model_spec, dst_model_spec):
@src_model_spec.decorator
class SourceModel(*src_model_spec.bases):
field1: Any
Expand Down Expand Up @@ -146,3 +146,50 @@ def convert(a: SourceModel) -> DestModel:
field2=2,
nested=DestModelNested(field1_dst=3),
)


def test_coercer(src_model_spec, dst_model_spec, factory_way):
@src_model_spec.decorator
class SourceModel(*src_model_spec.bases):
field1: Any
field2_src: int

@dst_model_spec.decorator
class DestModel(*dst_model_spec.bases):
field1: Any
field2_dst: str

if factory_way == FactoryWay.IMPL_CONVERTER:
@impl_converter(recipe=[link("field2_src", "field2_dst", coercer=str)])
def convert(a: SourceModel) -> DestModel:
...
else:
convert = get_converter(SourceModel, DestModel, recipe=[link("field2_src", "field2_dst", coercer=str)])

assert convert(SourceModel(field1=1, field2_src=2)) == DestModel(field1=1, field2_dst="2")


def test_coercer_priority(src_model_spec, dst_model_spec, factory_way):
@src_model_spec.decorator
class SourceModel(*src_model_spec.bases):
field1: Any
field2_src: int

@dst_model_spec.decorator
class DestModel(*dst_model_spec.bases):
field1: Any
field2_dst: str

recipe = [
coercer(int, str, func=str),
link("field2_src", "field2_dst", coercer=lambda x: str(x + 1)),
]
if factory_way == FactoryWay.IMPL_CONVERTER:
@impl_converter(recipe=recipe)
def convert(a: SourceModel) -> DestModel:
...
else:
convert = get_converter(SourceModel, DestModel, recipe=recipe)

assert convert(SourceModel(field1=1, field2_src=2)) == DestModel(field1=1, field2_dst="3")

Loading