Skip to content

Commit 2e67d8e

Browse files
authored
Merge pull request #313 from reagento/develop
Develop
2 parents df4f341 + 2e51e3a commit 2e67d8e

27 files changed

+123
-73
lines changed

.github/workflows/coverage_external_pr.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
# DO NOT run actions/checkout here, for security reasons
1919
# For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
2020
- name: Post comment
21-
uses: py-cov-action/python-coverage-comment-action@b16205b76b824c17afe95a014fb22e58b4f239cb
21+
uses: py-cov-action/python-coverage-comment-action@44f4df022ec3c3cbb61e56e0b550a490bde8aa73
2222
with:
2323
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2424
GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }}

.github/workflows/lint_and_test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121

122122
- name: Coverage comment
123123
id: coverage_comment
124-
uses: py-cov-action/python-coverage-comment-action@b16205b76b824c17afe95a014fb22e58b4f239cb
124+
uses: py-cov-action/python-coverage-comment-action@44f4df022ec3c3cbb61e56e0b550a490bde8aa73
125125
with:
126126
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
127127
MERGE_COVERAGE_FILES: true

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ An extremely flexible and configurable data model conversion library.
2525

2626
Install
2727
```bash
28-
pip install adaptix==3.0.0b6
28+
pip install adaptix==3.0.0b7
2929
```
3030

3131
Use for model loading and dumping.

docs/changelog/changelog_body.rst

+23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
----------------------------------------------------
22

33

4+
.. _v3.0.0b7:
5+
6+
`3.0.0b7 <https://github.com/reagento/adaptix/tree/v3.0.0b7>`__ -- 2024-06-10
7+
=============================================================================
8+
9+
.. _v3.0.0b7-Deprecations:
10+
11+
Deprecations
12+
------------
13+
14+
- ``NoSuitableProvider`` exception was renamed to ``ProviderNotFoundError``. `#245 <https://github.com/reagento/adaptix/issues/245>`__
15+
16+
.. _v3.0.0b7-Bug Fixes:
17+
18+
Bug Fixes
19+
---------
20+
21+
- Allow redefining coercer inside ``Optional`` using an inner type if source and destination types are same. `#279 <https://github.com/reagento/adaptix/issues/279>`__
22+
- Fix ``ForwardRef`` evaluation inside bound of ``TypeVar`` for ``Python 3.12.4``. `#312 <https://github.com/reagento/adaptix/issues/312>`__
23+
24+
----------------------------------------------------
25+
26+
427
.. _v3.0.0b6:
528

629
`3.0.0b6 <https://github.com/reagento/adaptix/tree/v3.0.0b6>`__ -- 2024-05-23

docs/common/installation.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Just use pip to install the library
22

33
.. code-block:: text
44
5-
pip install adaptix==3.0.0b6
5+
pip install adaptix==3.0.0b7
66
77
88
Integrations with 3-rd party libraries are turned on automatically,

docs/examples/loading-and-dumping/extended_usage/fields_filtering_only.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22

3-
from adaptix import NoSuitableProvider, Retort, name_mapping
3+
from adaptix import ProviderNotFoundError, Retort, name_mapping
44

55

66
@dataclass
@@ -33,5 +33,5 @@ class User:
3333

3434
try:
3535
retort.get_loader(User)
36-
except NoSuitableProvider:
36+
except ProviderNotFoundError:
3737
pass

docs/examples/loading-and-dumping/extended_usage/fields_filtering_skip.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22

3-
from adaptix import NoSuitableProvider, Retort, name_mapping
3+
from adaptix import ProviderNotFoundError, Retort, name_mapping
44

55

66
@dataclass
@@ -33,5 +33,5 @@ class User:
3333

3434
try:
3535
retort.get_loader(User)
36-
except NoSuitableProvider:
36+
except ProviderNotFoundError:
3737
pass

docs/overview.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Installation
1616

1717
.. code-block:: text
1818
19-
pip install adaptix==3.0.0b6
19+
pip install adaptix==3.0.0b7
2020
2121
2222
Example

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
44

55
[project]
66
name = 'adaptix'
7-
version = '3.0.0b6'
7+
version = '3.0.0b7'
88
description = 'An extremely flexible and configurable data model conversion library'
99
readme = 'README.md'
1010
requires-python = '>=3.8'

src/adaptix/__init__.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from ._internal.morphing.name_layout.base import ExtraIn, ExtraOut
3131
from ._internal.name_style import NameStyle
3232
from ._internal.provider.facade.provider import bound
33-
from ._internal.utils import Omittable, Omitted
33+
from ._internal.utils import Omittable, Omitted, create_deprecated_alias_getter
3434
from .provider import (
3535
AggregateCannotProvide,
3636
CannotProvide,
@@ -42,7 +42,7 @@
4242
Request,
4343
create_loc_stack_checker,
4444
)
45-
from .retort import NoSuitableProvider
45+
from .retort import ProviderNotFoundError
4646

4747
__all__ = (
4848
"Dumper",
@@ -89,8 +89,15 @@
8989
"create_loc_stack_checker",
9090
"retort",
9191
"Provider",
92-
"NoSuitableProvider",
92+
"ProviderNotFoundError",
9393
"Request",
9494
"load",
9595
"dump",
9696
)
97+
98+
__getattr__ = create_deprecated_alias_getter(
99+
__name__,
100+
{
101+
"NoSuitableProvider": "ProviderNotFoundError",
102+
},
103+
)

src/adaptix/_internal/conversion/coercer_provider.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,25 @@ def _provide_coercer_norm_types(
133133
not_none_dst = self._get_not_none(norm_dst)
134134
not_none_request = replace(
135135
request,
136-
src=request.src.replace_last_type(not_none_src),
137-
dst=request.dst.replace_last_type(not_none_dst),
136+
src=request.src.append_with(
137+
GenericParamLoc(
138+
type=not_none_src.source,
139+
generic_pos=0,
140+
),
141+
),
142+
dst=request.dst.append_with(
143+
GenericParamLoc(
144+
type=not_none_dst.source,
145+
generic_pos=0,
146+
),
147+
),
138148
)
139149
not_none_coercer = mediator.mandatory_provide(
140150
not_none_request,
141151
lambda x: "Cannot create coercer for optionals. Coercer for wrapped value cannot be created",
142152
)
153+
if not_none_coercer == as_is_stub_with_ctx:
154+
return as_is_stub_with_ctx
143155

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

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

157169

158170
class TypeHintTagsUnwrappingProvider(CoercerProvider):

src/adaptix/_internal/conversion/facade/retort.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ class FilledConversionRetort(OperatingRetort):
4040
ModelCoercerProvider(),
4141
IterableCoercerProvider(),
4242
DictCoercerProvider(),
43+
OptionalCoercerProvider(),
44+
TypeHintTagsUnwrappingProvider(),
4345

4446
SameTypeCoercerProvider(),
4547
DstAnyCoercerProvider(),
46-
SubclassCoercerProvider(),
4748
UnionSubcaseCoercerProvider(),
48-
OptionalCoercerProvider(),
49-
TypeHintTagsUnwrappingProvider(),
49+
SubclassCoercerProvider(),
5050

5151
forbid_unlinked_optional(P.ANY),
5252
]

src/adaptix/_internal/morphing/generic_provider.py

+7-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from ..provider.static_provider import StaticProvider, static_provision_action
2626
from ..special_cases_optimization import as_is_stub
2727
from ..type_tools import BaseNormType, NormTypeAlias, is_new_type, is_subclass_soft, strip_tags
28-
from ..type_tools.norm_utils import strip_annotated
2928
from .load_error import BadVariantLoadError, LoadError, TypeLoadError, UnionLoadError
3029
from .provider_template import DumperProvider, LoaderProvider
3130
from .request_cls import DumperRequest, LoaderRequest
@@ -182,18 +181,16 @@ def _provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
182181
norm = try_normalize_type(get_type_from_request(request))
183182
strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
184183

185-
cleaned_args = [strip_annotated(arg) for arg in norm.args]
186-
187-
enum_cases = [arg for arg in cleaned_args if isinstance(arg, Enum)]
184+
enum_cases = [arg for arg in norm.args if isinstance(arg, Enum)]
188185
enum_loaders = list(self._fetch_enum_loaders(mediator, request, self._get_enum_types(enum_cases)))
189-
allowed_values_repr = self._get_allowed_values_repr(cleaned_args, mediator, request.loc_stack)
186+
allowed_values_repr = self._get_allowed_values_repr(norm.args, mediator, request.loc_stack)
190187

191188
if strict_coercion and any(
192189
isinstance(arg, bool) or _is_exact_zero_or_one(arg)
193-
for arg in cleaned_args
190+
for arg in norm.args
194191
):
195192
allowed_values_with_types = self._get_allowed_values_collection(
196-
[(type(el), el) for el in cleaned_args],
193+
[(type(el), el) for el in norm.args],
197194
)
198195

199196
# since True == 1 and False == 0
@@ -206,7 +203,7 @@ def literal_loader_sc(data):
206203
literal_loader_sc, enum_loaders, allowed_values_with_types,
207204
)
208205

209-
allowed_values = self._get_allowed_values_collection(cleaned_args)
206+
allowed_values = self._get_allowed_values_collection(norm.args)
210207

211208
def literal_loader(data):
212209
if data in allowed_values:
@@ -217,8 +214,7 @@ def literal_loader(data):
217214

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

223219
if not enum_cases:
224220
return as_is_stub
@@ -457,8 +453,7 @@ def _get_dumper_for_literal(
457453
except StopIteration:
458454
return None
459455

460-
literal_cases = [strip_annotated(arg) for arg in literal_type.args]
461-
return self._produce_dumper_for_literal(dumper_type_dispatcher, literal_dumper, literal_cases)
456+
return self._produce_dumper_for_literal(dumper_type_dispatcher, literal_dumper, literal_type.args)
462457

463458
def _get_single_optional_dumper(self, dumper: Dumper) -> Dumper:
464459
def optional_dumper(data):

src/adaptix/_internal/retort/operating_retort.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def process_request_result(self, request: Request[T], result: T) -> None:
4545

4646

4747
@with_module("adaptix")
48-
class NoSuitableProvider(Exception):
48+
class ProviderNotFoundError(Exception):
4949
def __init__(self, message: str):
5050
self.message = message
5151

@@ -105,7 +105,7 @@ def _facade_provide(self, request: Request[T], *, error_message: str) -> T:
105105
return self._provide_from_recipe(request)
106106
except CannotProvide as e:
107107
cause = self._get_exception_cause(e)
108-
exception = NoSuitableProvider(error_message)
108+
exception = ProviderNotFoundError(error_message)
109109
if cause is not None:
110110
add_note(exception, "Note: The attached exception above contains verbose description of the problem")
111111
raise exception from cause

src/adaptix/_internal/type_tools/basic_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def get_type_vars_of_parametrized(tp: TypeHint) -> VarTuple[TypeVar]:
139139

140140
if HAS_PY_39:
141141
def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
142-
return forward_ref._evaluate(namespace, None, frozenset())
142+
return forward_ref._evaluate(namespace, None, recursive_guard=frozenset())
143143
else:
144144
def eval_forward_ref(namespace: Dict[str, Any], forward_ref: ForwardRef):
145145
return forward_ref._evaluate(namespace, None) # type: ignore[call-arg]

src/adaptix/_internal/type_tools/norm_utils.py

-6
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ def strip_tags(norm: BaseNormType) -> BaseNormType:
2626
N = TypeVar("N", bound=BaseNormType)
2727

2828

29-
def strip_annotated(value: N) -> N:
30-
if HAS_ANNOTATED and isinstance(value, BaseNormType) and value.origin == typing.Annotated:
31-
return strip_annotated(value)
32-
return value
33-
34-
3529
def is_class_var(norm: BaseNormType) -> bool:
3630
if norm.origin == ClassVar:
3731
return True

src/adaptix/retort.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from adaptix._internal.retort.base_retort import BaseRetort
2-
from adaptix._internal.retort.operating_retort import NoSuitableProvider, OperatingRetort
2+
from adaptix._internal.retort.operating_retort import OperatingRetort, ProviderNotFoundError
3+
from adaptix._internal.utils import create_deprecated_alias_getter
34

45
__all__ = (
56
"BaseRetort",
6-
"NoSuitableProvider",
7+
"ProviderNotFoundError",
78
"OperatingRetort",
89
)
10+
11+
__getattr__ = create_deprecated_alias_getter(
12+
__name__,
13+
{
14+
"NoSuitableProvider": "ProviderNotFoundError",
15+
},
16+
)

tests/integration/conversion/test_coercer.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from datetime import datetime, timezone
23
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Union
34

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

141142

143+
SOME_DATETIME_NAIVE = datetime(year=1048, month=3, day=4, tzinfo=None) # noqa: DTZ001
144+
SOME_DATETIME_UTC = SOME_DATETIME_NAIVE.replace(tzinfo=timezone.utc)
145+
146+
142147
@pytest.mark.parametrize(
143148
["src_tp", "dst_tp", "src_value", "dst_value"],
144149
[
145150
pytest.param(Optional[int], Optional[int], 10, 10),
146151
pytest.param(Optional[int], Optional[int], None, None),
147152
pytest.param(Optional[str], Optional[str], "abc", "abc"),
153+
pytest.param(Optional[datetime], Optional[datetime], SOME_DATETIME_NAIVE, SOME_DATETIME_UTC),
148154
pytest.param(Optional[str], Optional[str], None, None),
149155
pytest.param(Optional[bool], Optional[int], True, True),
150156
pytest.param(Optional[str], Optional[int], "123", 123),
@@ -170,7 +176,12 @@ class DestModel(*model_spec.bases):
170176
field1: int
171177
field2: dst_tp
172178

173-
@impl_converter(recipe=[coercer(str, int, func=int)])
179+
@impl_converter(
180+
recipe=[
181+
coercer(str, int, func=int),
182+
coercer(datetime, datetime, lambda x: x.replace(tzinfo=timezone.utc)),
183+
],
184+
)
174185
def convert(a: SourceModel) -> DestModel:
175186
...
176187

tests/integration/conversion/test_link_function.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from tests_helpers import raises_exc, with_cause, with_notes
22

3-
from adaptix import AggregateCannotProvide, CannotProvide, NoSuitableProvider
3+
from adaptix import AggregateCannotProvide, CannotProvide, ProviderNotFoundError
44
from adaptix._internal.conversion.facade.func import get_converter
55
from adaptix._internal.conversion.facade.provider import coercer
66
from adaptix.conversion import impl_converter, link_function
@@ -116,7 +116,7 @@ def my_function(model, p1: str, *, f1: str):
116116
raises_exc(
117117
with_cause(
118118
with_notes(
119-
NoSuitableProvider(
119+
ProviderNotFoundError(
120120
f"Cannot produce converter for"
121121
f" <Signature (src: {SourceModel.__module__}.{SourceModel.__qualname__}, /)"
122122
f" -> {DestModel.__module__}.{DestModel.__qualname__}>",
@@ -194,7 +194,7 @@ def convert(src: SourceModel, p1: int) -> DestModel:
194194
raises_exc(
195195
with_cause(
196196
with_notes(
197-
NoSuitableProvider(
197+
ProviderNotFoundError(
198198
f"Cannot produce converter for"
199199
f" <Signature (src: {SourceModel.__module__}.{SourceModel.__qualname__}, p1: int)"
200200
f" -> {DestModel.__module__}.{DestModel.__qualname__}>",

0 commit comments

Comments
 (0)