Skip to content

Commit

Permalink
Merge pull request #313 from reagento/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
zhPavel authored Jun 10, 2024
2 parents df4f341 + 2e51e3a commit 2e67d8e
Show file tree
Hide file tree
Showing 27 changed files with 123 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/coverage_external_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
# DO NOT run actions/checkout here, for security reasons
# For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
- name: Post comment
uses: py-cov-action/python-coverage-comment-action@b16205b76b824c17afe95a014fb22e58b4f239cb
uses: py-cov-action/python-coverage-comment-action@44f4df022ec3c3cbb61e56e0b550a490bde8aa73
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}
2 changes: 1 addition & 1 deletion .github/workflows/lint_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:

- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@b16205b76b824c17afe95a014fb22e58b4f239cb
uses: py-cov-action/python-coverage-comment-action@44f4df022ec3c3cbb61e56e0b550a490bde8aa73
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MERGE_COVERAGE_FILES: true
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ An extremely flexible and configurable data model conversion library.

Install
```bash
pip install adaptix==3.0.0b6
pip install adaptix==3.0.0b7
```

Use for model loading and dumping.
Expand Down
23 changes: 23 additions & 0 deletions docs/changelog/changelog_body.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
----------------------------------------------------


.. _v3.0.0b7:

`3.0.0b7 <https://github.com/reagento/adaptix/tree/v3.0.0b7>`__ -- 2024-06-10
=============================================================================

.. _v3.0.0b7-Deprecations:

Deprecations
------------

- ``NoSuitableProvider`` exception was renamed to ``ProviderNotFoundError``. `#245 <https://github.com/reagento/adaptix/issues/245>`__

.. _v3.0.0b7-Bug Fixes:

Bug Fixes
---------

- Allow redefining coercer inside ``Optional`` using an inner type if source and destination types are same. `#279 <https://github.com/reagento/adaptix/issues/279>`__
- Fix ``ForwardRef`` evaluation inside bound of ``TypeVar`` for ``Python 3.12.4``. `#312 <https://github.com/reagento/adaptix/issues/312>`__

----------------------------------------------------


.. _v3.0.0b6:

`3.0.0b6 <https://github.com/reagento/adaptix/tree/v3.0.0b6>`__ -- 2024-05-23
Expand Down
2 changes: 1 addition & 1 deletion docs/common/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Just use pip to install the library

.. code-block:: text
pip install adaptix==3.0.0b6
pip install adaptix==3.0.0b7
Integrations with 3-rd party libraries are turned on automatically,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass

from adaptix import NoSuitableProvider, Retort, name_mapping
from adaptix import ProviderNotFoundError, Retort, name_mapping


@dataclass
Expand Down Expand Up @@ -33,5 +33,5 @@ class User:

try:
retort.get_loader(User)
except NoSuitableProvider:
except ProviderNotFoundError:
pass
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dataclasses import dataclass

from adaptix import NoSuitableProvider, Retort, name_mapping
from adaptix import ProviderNotFoundError, Retort, name_mapping


@dataclass
Expand Down Expand Up @@ -33,5 +33,5 @@ class User:

try:
retort.get_loader(User)
except NoSuitableProvider:
except ProviderNotFoundError:
pass
2 changes: 1 addition & 1 deletion docs/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Installation

.. code-block:: text
pip install adaptix==3.0.0b6
pip install adaptix==3.0.0b7
Example
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'

[project]
name = 'adaptix'
version = '3.0.0b6'
version = '3.0.0b7'
description = 'An extremely flexible and configurable data model conversion library'
readme = 'README.md'
requires-python = '>=3.8'
Expand Down
13 changes: 10 additions & 3 deletions src/adaptix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ._internal.morphing.name_layout.base import ExtraIn, ExtraOut
from ._internal.name_style import NameStyle
from ._internal.provider.facade.provider import bound
from ._internal.utils import Omittable, Omitted
from ._internal.utils import Omittable, Omitted, create_deprecated_alias_getter
from .provider import (
AggregateCannotProvide,
CannotProvide,
Expand All @@ -42,7 +42,7 @@
Request,
create_loc_stack_checker,
)
from .retort import NoSuitableProvider
from .retort import ProviderNotFoundError

__all__ = (
"Dumper",
Expand Down Expand Up @@ -89,8 +89,15 @@
"create_loc_stack_checker",
"retort",
"Provider",
"NoSuitableProvider",
"ProviderNotFoundError",
"Request",
"load",
"dump",
)

__getattr__ = create_deprecated_alias_getter(
__name__,
{
"NoSuitableProvider": "ProviderNotFoundError",
},
)
18 changes: 15 additions & 3 deletions src/adaptix/_internal/conversion/coercer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,25 @@ def _provide_coercer_norm_types(
not_none_dst = self._get_not_none(norm_dst)
not_none_request = replace(
request,
src=request.src.replace_last_type(not_none_src),
dst=request.dst.replace_last_type(not_none_dst),
src=request.src.append_with(
GenericParamLoc(
type=not_none_src.source,
generic_pos=0,
),
),
dst=request.dst.append_with(
GenericParamLoc(
type=not_none_dst.source,
generic_pos=0,
),
),
)
not_none_coercer = mediator.mandatory_provide(
not_none_request,
lambda x: "Cannot create coercer for optionals. Coercer for wrapped value cannot be created",
)
if not_none_coercer == as_is_stub_with_ctx:
return as_is_stub_with_ctx

def optional_coercer(data, ctx):
if data is None:
Expand All @@ -152,7 +164,7 @@ def _is_optional(self, norm: BaseNormType) -> bool:
return norm.origin == Union and None in [case.origin for case in norm.args]

def _get_not_none(self, norm: BaseNormType) -> BaseNormType:
return next(case.origin for case in norm.args if case.origin is not None)
return next(case for case in norm.args if case.origin is not None)


class TypeHintTagsUnwrappingProvider(CoercerProvider):
Expand Down
6 changes: 3 additions & 3 deletions src/adaptix/_internal/conversion/facade/retort.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ class FilledConversionRetort(OperatingRetort):
ModelCoercerProvider(),
IterableCoercerProvider(),
DictCoercerProvider(),
OptionalCoercerProvider(),
TypeHintTagsUnwrappingProvider(),

SameTypeCoercerProvider(),
DstAnyCoercerProvider(),
SubclassCoercerProvider(),
UnionSubcaseCoercerProvider(),
OptionalCoercerProvider(),
TypeHintTagsUnwrappingProvider(),
SubclassCoercerProvider(),

forbid_unlinked_optional(P.ANY),
]
Expand Down
19 changes: 7 additions & 12 deletions src/adaptix/_internal/morphing/generic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from ..provider.static_provider import StaticProvider, static_provision_action
from ..special_cases_optimization import as_is_stub
from ..type_tools import BaseNormType, NormTypeAlias, is_new_type, is_subclass_soft, strip_tags
from ..type_tools.norm_utils import strip_annotated
from .load_error import BadVariantLoadError, LoadError, TypeLoadError, UnionLoadError
from .provider_template import DumperProvider, LoaderProvider
from .request_cls import DumperRequest, LoaderRequest
Expand Down Expand Up @@ -182,18 +181,16 @@ def _provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
norm = try_normalize_type(get_type_from_request(request))
strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))

cleaned_args = [strip_annotated(arg) for arg in norm.args]

enum_cases = [arg for arg in cleaned_args if isinstance(arg, Enum)]
enum_cases = [arg for arg in norm.args if isinstance(arg, Enum)]
enum_loaders = list(self._fetch_enum_loaders(mediator, request, self._get_enum_types(enum_cases)))
allowed_values_repr = self._get_allowed_values_repr(cleaned_args, mediator, request.loc_stack)
allowed_values_repr = self._get_allowed_values_repr(norm.args, mediator, request.loc_stack)

if strict_coercion and any(
isinstance(arg, bool) or _is_exact_zero_or_one(arg)
for arg in cleaned_args
for arg in norm.args
):
allowed_values_with_types = self._get_allowed_values_collection(
[(type(el), el) for el in cleaned_args],
[(type(el), el) for el in norm.args],
)

# since True == 1 and False == 0
Expand All @@ -206,7 +203,7 @@ def literal_loader_sc(data):
literal_loader_sc, enum_loaders, allowed_values_with_types,
)

allowed_values = self._get_allowed_values_collection(cleaned_args)
allowed_values = self._get_allowed_values_collection(norm.args)

def literal_loader(data):
if data in allowed_values:
Expand All @@ -217,8 +214,7 @@ def literal_loader(data):

def _provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
norm = try_normalize_type(get_type_from_request(request))
cleaned_args = [strip_annotated(arg) for arg in norm.args]
enum_cases = [arg for arg in cleaned_args if isinstance(arg, Enum)]
enum_cases = [arg for arg in norm.args if isinstance(arg, Enum)]

if not enum_cases:
return as_is_stub
Expand Down Expand Up @@ -457,8 +453,7 @@ def _get_dumper_for_literal(
except StopIteration:
return None

literal_cases = [strip_annotated(arg) for arg in literal_type.args]
return self._produce_dumper_for_literal(dumper_type_dispatcher, literal_dumper, literal_cases)
return self._produce_dumper_for_literal(dumper_type_dispatcher, literal_dumper, literal_type.args)

def _get_single_optional_dumper(self, dumper: Dumper) -> Dumper:
def optional_dumper(data):
Expand Down
4 changes: 2 additions & 2 deletions src/adaptix/_internal/retort/operating_retort.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def process_request_result(self, request: Request[T], result: T) -> None:


@with_module("adaptix")
class NoSuitableProvider(Exception):
class ProviderNotFoundError(Exception):
def __init__(self, message: str):
self.message = message

Expand Down Expand Up @@ -105,7 +105,7 @@ def _facade_provide(self, request: Request[T], *, error_message: str) -> T:
return self._provide_from_recipe(request)
except CannotProvide as e:
cause = self._get_exception_cause(e)
exception = NoSuitableProvider(error_message)
exception = ProviderNotFoundError(error_message)
if cause is not None:
add_note(exception, "Note: The attached exception above contains verbose description of the problem")
raise exception from cause
Expand Down
2 changes: 1 addition & 1 deletion src/adaptix/_internal/type_tools/basic_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def get_type_vars_of_parametrized(tp: TypeHint) -> VarTuple[TypeVar]:

if HAS_PY_39:
def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
return forward_ref._evaluate(namespace, None, frozenset())
return forward_ref._evaluate(namespace, None, recursive_guard=frozenset())
else:
def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
return forward_ref._evaluate(namespace, None) # type: ignore[call-arg]
6 changes: 0 additions & 6 deletions src/adaptix/_internal/type_tools/norm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ def strip_tags(norm: BaseNormType) -> BaseNormType:
N = TypeVar("N", bound=BaseNormType)


def strip_annotated(value: N) -> N:
if HAS_ANNOTATED and isinstance(value, BaseNormType) and value.origin == typing.Annotated:
return strip_annotated(value)
return value


def is_class_var(norm: BaseNormType) -> bool:
if norm.origin == ClassVar:
return True
Expand Down
12 changes: 10 additions & 2 deletions src/adaptix/retort.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from adaptix._internal.retort.base_retort import BaseRetort
from adaptix._internal.retort.operating_retort import NoSuitableProvider, OperatingRetort
from adaptix._internal.retort.operating_retort import OperatingRetort, ProviderNotFoundError
from adaptix._internal.utils import create_deprecated_alias_getter

__all__ = (
"BaseRetort",
"NoSuitableProvider",
"ProviderNotFoundError",
"OperatingRetort",
)

__getattr__ = create_deprecated_alias_getter(
__name__,
{
"NoSuitableProvider": "ProviderNotFoundError",
},
)
13 changes: 12 additions & 1 deletion tests/integration/conversion/test_coercer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Union

import pytest
Expand Down Expand Up @@ -139,12 +140,17 @@ def convert(a: SourceModel) -> DestModel:
assert convert(SourceModel(field1=1, field2=2)) == DestModel(field1=1, field2=2)


SOME_DATETIME_NAIVE = datetime(year=1048, month=3, day=4, tzinfo=None) # noqa: DTZ001
SOME_DATETIME_UTC = SOME_DATETIME_NAIVE.replace(tzinfo=timezone.utc)


@pytest.mark.parametrize(
["src_tp", "dst_tp", "src_value", "dst_value"],
[
pytest.param(Optional[int], Optional[int], 10, 10),
pytest.param(Optional[int], Optional[int], None, None),
pytest.param(Optional[str], Optional[str], "abc", "abc"),
pytest.param(Optional[datetime], Optional[datetime], SOME_DATETIME_NAIVE, SOME_DATETIME_UTC),
pytest.param(Optional[str], Optional[str], None, None),
pytest.param(Optional[bool], Optional[int], True, True),
pytest.param(Optional[str], Optional[int], "123", 123),
Expand All @@ -170,7 +176,12 @@ class DestModel(*model_spec.bases):
field1: int
field2: dst_tp

@impl_converter(recipe=[coercer(str, int, func=int)])
@impl_converter(
recipe=[
coercer(str, int, func=int),
coercer(datetime, datetime, lambda x: x.replace(tzinfo=timezone.utc)),
],
)
def convert(a: SourceModel) -> DestModel:
...

Expand Down
6 changes: 3 additions & 3 deletions tests/integration/conversion/test_link_function.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from tests_helpers import raises_exc, with_cause, with_notes

from adaptix import AggregateCannotProvide, CannotProvide, NoSuitableProvider
from adaptix import AggregateCannotProvide, CannotProvide, ProviderNotFoundError
from adaptix._internal.conversion.facade.func import get_converter
from adaptix._internal.conversion.facade.provider import coercer
from adaptix.conversion import impl_converter, link_function
Expand Down Expand Up @@ -116,7 +116,7 @@ def my_function(model, p1: str, *, f1: str):
raises_exc(
with_cause(
with_notes(
NoSuitableProvider(
ProviderNotFoundError(
f"Cannot produce converter for"
f" <Signature (src: {SourceModel.__module__}.{SourceModel.__qualname__}, /)"
f" -> {DestModel.__module__}.{DestModel.__qualname__}>",
Expand Down Expand Up @@ -194,7 +194,7 @@ def convert(src: SourceModel, p1: int) -> DestModel:
raises_exc(
with_cause(
with_notes(
NoSuitableProvider(
ProviderNotFoundError(
f"Cannot produce converter for"
f" <Signature (src: {SourceModel.__module__}.{SourceModel.__qualname__}, p1: int)"
f" -> {DestModel.__module__}.{DestModel.__qualname__}>",
Expand Down
Loading

0 comments on commit 2e67d8e

Please sign in to comment.