From 84a72e401a35bfe2e56906374cf444e1a1d85396 Mon Sep 17 00:00:00 2001 From: Andrew Srg Date: Sun, 21 Jan 2024 17:55:41 +0200 Subject: [PATCH 1/4] add defaultdict support --- src/adaptix/__init__.py | 2 + .../_internal/morphing/dict_provider.py | 38 +++++++++++++++++- .../_internal/morphing/facade/provider.py | 7 +++- .../_internal/morphing/facade/retort.py | 3 +- tests/unit/morphing/test_dict_provider.py | 40 +++++++++++++++++-- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/adaptix/__init__.py b/src/adaptix/__init__.py index 5fa3feda..475c7821 100644 --- a/src/adaptix/__init__.py +++ b/src/adaptix/__init__.py @@ -8,6 +8,7 @@ as_is_loader, bound, constructor, + default_dict, dumper, enum_by_exact_value, enum_by_name, @@ -59,6 +60,7 @@ 'enum_by_name', 'enum_by_value', 'name_mapping', + 'default_dict', 'AdornedRetort', 'FilledRetort', 'Retort', diff --git a/src/adaptix/_internal/morphing/dict_provider.py b/src/adaptix/_internal/morphing/dict_provider.py index 1a986d23..edc94a24 100644 --- a/src/adaptix/_internal/morphing/dict_provider.py +++ b/src/adaptix/_internal/morphing/dict_provider.py @@ -1,5 +1,7 @@ import collections.abc -from typing import Dict, Mapping, Tuple +from collections import defaultdict +from dataclasses import replace +from typing import DefaultDict, Dict, Mapping, Optional, Tuple, Type from ..common import Dumper, Loader from ..compat import CompatExceptionGroup @@ -269,3 +271,37 @@ def dict_dumper_dt_all(data: Mapping): return result return dict_dumper_dt_all + + +@for_predicate(DefaultDict) +class DefaultDictProvider(LoaderProvider, DumperProvider): + _DICT_PROVIDER = DictProvider() + + def __init__(self, default_factory: Optional[Type] = None): + self.default_factory = default_factory + + def _extract_key_value(self, request: LocatedRequest) -> Tuple[BaseNormType, BaseNormType]: + norm = try_normalize_type(get_type_from_request(request)) + return norm.args + + def _provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader: + key, value = self._extract_key_value(request) + dict_type_hint = Dict[key.source, value.source] # type: ignore + dict_loader = self._DICT_PROVIDER.apply_provider( + mediator, + replace(request, loc_stack=request.loc_stack.add_to_last_map(TypeHintLoc(dict_type_hint))) + ) + + def defaultdict_loader(data): + return defaultdict(self.default_factory, dict_loader(data)) + + return defaultdict_loader + + def _provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper: + key, value = self._extract_key_value(request) + dict_type_hint = Dict[key.source, value.source] # type: ignore + + return self._DICT_PROVIDER.apply_provider( + mediator, + replace(request, loc_stack=request.loc_stack.add_to_last_map(TypeHintLoc(dict_type_hint))) + ) diff --git a/src/adaptix/_internal/morphing/facade/provider.py b/src/adaptix/_internal/morphing/facade/provider.py index 7bb29b3b..be82e2fb 100644 --- a/src/adaptix/_internal/morphing/facade/provider.py +++ b/src/adaptix/_internal/morphing/facade/provider.py @@ -2,7 +2,7 @@ from enum import Enum, EnumMeta from types import MappingProxyType -from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence, TypeVar, Union +from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence, Type, TypeVar, Union from ...common import Catchable, Dumper, Loader, TypeHint, VarTuple from ...model_tools.definitions import Default, DescriptorAccessor, NoDefault, OutputField @@ -23,6 +23,7 @@ from ...provider.shape_provider import PropertyExtender from ...special_cases_optimization import as_is_stub from ...utils import Omittable, Omitted +from ..dict_provider import DefaultDictProvider from ..enum_provider import EnumExactValueProvider, EnumNameProvider, EnumValueProvider from ..load_error import LoadError, ValidationError from ..model.loader_provider import InlinedShapeModelLoaderProvider @@ -368,3 +369,7 @@ def validating_loader(data): raise exception_factory(data) return loader(pred, validating_loader, chain) + + +def default_dict(default_factory: Optional[Type] = None) -> Provider: + return DefaultDictProvider(default_factory) diff --git a/src/adaptix/_internal/morphing/facade/retort.py b/src/adaptix/_internal/morphing/facade/retort.py index 7ddd6d30..b13d00b1 100644 --- a/src/adaptix/_internal/morphing/facade/retort.py +++ b/src/adaptix/_internal/morphing/facade/retort.py @@ -42,7 +42,7 @@ SelfTypeProvider, ) from ..constant_length_tuple_provider import ConstantLengthTupleProvider -from ..dict_provider import DictProvider +from ..dict_provider import DefaultDictProvider, DictProvider from ..enum_provider import EnumExactValueProvider from ..generic_provider import ( LiteralProvider, @@ -135,6 +135,7 @@ class FilledRetort(OperatingRetort, ABC): ConstantLengthTupleProvider(), IterableProvider(), DictProvider(), + DefaultDictProvider(), RegexPatternProvider(), SelfTypeProvider(), LiteralStringProvider(), diff --git a/tests/unit/morphing/test_dict_provider.py b/tests/unit/morphing/test_dict_provider.py index 8670feba..a2729eae 100644 --- a/tests/unit/morphing/test_dict_provider.py +++ b/tests/unit/morphing/test_dict_provider.py @@ -1,13 +1,15 @@ import collections.abc -from typing import Dict +from collections import defaultdict +from typing import DefaultDict, Dict import pytest from tests_helpers import TestRetort, raises_exc, with_trail -from adaptix import DebugTrail, dumper, loader +from adaptix import DebugTrail, default_dict, dumper, loader from adaptix._internal.compat import CompatExceptionGroup from adaptix._internal.morphing.concrete_provider import STR_LOADER_PROVIDER -from adaptix._internal.morphing.dict_provider import DictProvider +from adaptix._internal.morphing.dict_provider import DefaultDictProvider, DictProvider +from adaptix._internal.morphing.iterable_provider import IterableProvider from adaptix._internal.morphing.load_error import AggregateLoadError from adaptix._internal.struct_trail import ItemKey from adaptix.load_error import TypeLoadError @@ -24,8 +26,10 @@ def retort(): return TestRetort( recipe=[ DictProvider(), + DefaultDictProvider(), STR_LOADER_PROVIDER, dumper(str, string_dumper), + IterableProvider() ] ) @@ -190,3 +194,33 @@ def test_dumping(retort, debug_trail): ), lambda: dumper_({'a': 'b', 0: 'd'}), ) + + +def test_defaultdict_loading(retort, strict_coercion, debug_trail): + loader_ = retort.replace( + strict_coercion=strict_coercion, + debug_trail=debug_trail, + ).get_loader( + DefaultDict[str, str], + ) + + assert loader_({'a': 'b', 'c': 'd'}) == defaultdict(None, {'a': 'b', 'c': 'd'}) + + +def test_defaultdict_loader(retort, strict_coercion, debug_trail): + loader_ = retort.replace( + strict_coercion=strict_coercion, + debug_trail=debug_trail, + ).extend(recipe=[default_dict(default_factory=list)]).get_loader(DefaultDict[str, list[str]]) + + assert loader_({'a': ['b', 'c']}) == defaultdict(list, {'a': ['b', 'c']}) + + +def test_defaultdict_dumping(retort, debug_trail): + dumper_ = retort.replace( + debug_trail=debug_trail, + ).get_dumper( + DefaultDict[str, str], + ) + + assert dumper_(defaultdict(None, {'a': 'b', 'c': 'd'})) == {'a': 'b', 'c': 'd'} From 635b1fcb94a16efdc7c086c18c2a38a231243145 Mon Sep 17 00:00:00 2001 From: Andrew Srg Date: Sun, 21 Jan 2024 21:55:49 +0200 Subject: [PATCH 2/4] refactor defaultdict provider add pred argument to defaultdict provider high-level factory, refactor annotations, formatting code --- src/adaptix/_internal/morphing/dict_provider.py | 7 ++++--- src/adaptix/_internal/morphing/facade/provider.py | 6 +++--- tests/unit/morphing/test_dict_provider.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/adaptix/_internal/morphing/dict_provider.py b/src/adaptix/_internal/morphing/dict_provider.py index edc94a24..17305e97 100644 --- a/src/adaptix/_internal/morphing/dict_provider.py +++ b/src/adaptix/_internal/morphing/dict_provider.py @@ -1,7 +1,7 @@ import collections.abc from collections import defaultdict from dataclasses import replace -from typing import DefaultDict, Dict, Mapping, Optional, Tuple, Type +from typing import Callable, DefaultDict, Dict, Mapping, Optional, Tuple from ..common import Dumper, Loader from ..compat import CompatExceptionGroup @@ -277,7 +277,7 @@ def dict_dumper_dt_all(data: Mapping): class DefaultDictProvider(LoaderProvider, DumperProvider): _DICT_PROVIDER = DictProvider() - def __init__(self, default_factory: Optional[Type] = None): + def __init__(self, default_factory: Optional[Callable] = None): self.default_factory = default_factory def _extract_key_value(self, request: LocatedRequest) -> Tuple[BaseNormType, BaseNormType]: @@ -291,9 +291,10 @@ def _provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader: mediator, replace(request, loc_stack=request.loc_stack.add_to_last_map(TypeHintLoc(dict_type_hint))) ) + default_factory = self.default_factory def defaultdict_loader(data): - return defaultdict(self.default_factory, dict_loader(data)) + return defaultdict(default_factory, dict_loader(data)) return defaultdict_loader diff --git a/src/adaptix/_internal/morphing/facade/provider.py b/src/adaptix/_internal/morphing/facade/provider.py index be82e2fb..623708ae 100644 --- a/src/adaptix/_internal/morphing/facade/provider.py +++ b/src/adaptix/_internal/morphing/facade/provider.py @@ -2,7 +2,7 @@ from enum import Enum, EnumMeta from types import MappingProxyType -from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence, Type, TypeVar, Union +from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence, TypeVar, Union from ...common import Catchable, Dumper, Loader, TypeHint, VarTuple from ...model_tools.definitions import Default, DescriptorAccessor, NoDefault, OutputField @@ -371,5 +371,5 @@ def validating_loader(data): return loader(pred, validating_loader, chain) -def default_dict(default_factory: Optional[Type] = None) -> Provider: - return DefaultDictProvider(default_factory) +def default_dict(pred: Pred, default_factory: Callable) -> Provider: + return bound(pred, DefaultDictProvider(default_factory)) diff --git a/tests/unit/morphing/test_dict_provider.py b/tests/unit/morphing/test_dict_provider.py index a2729eae..7ed1de8a 100644 --- a/tests/unit/morphing/test_dict_provider.py +++ b/tests/unit/morphing/test_dict_provider.py @@ -1,6 +1,6 @@ import collections.abc from collections import defaultdict -from typing import DefaultDict, Dict +from typing import DefaultDict, Dict, List import pytest from tests_helpers import TestRetort, raises_exc, with_trail @@ -208,12 +208,18 @@ def test_defaultdict_loading(retort, strict_coercion, debug_trail): def test_defaultdict_loader(retort, strict_coercion, debug_trail): + df = list loader_ = retort.replace( strict_coercion=strict_coercion, debug_trail=debug_trail, - ).extend(recipe=[default_dict(default_factory=list)]).get_loader(DefaultDict[str, list[str]]) + ).extend( + recipe=[ + default_dict(defaultdict, default_factory=df), + ] + ).get_loader(DefaultDict[str, List[str]]) assert loader_({'a': ['b', 'c']}) == defaultdict(list, {'a': ['b', 'c']}) + assert loader_({'a': ['b', 'c']}).default_factory == df def test_defaultdict_dumping(retort, debug_trail): From 3e01a0268159fb26e3e9f1890fdb896a7eee9d47 Mon Sep 17 00:00:00 2001 From: Andrew Srg Date: Tue, 23 Jan 2024 11:31:25 +0200 Subject: [PATCH 3/4] add defaultdict support documentation --- docs/changelog/fragments/216.feature.rst | 1 + docs/loading-and-dumping/specific-types-behavior.rst | 6 ++++++ src/adaptix/_internal/morphing/facade/provider.py | 6 ++++++ 3 files changed, 13 insertions(+) create mode 100644 docs/changelog/fragments/216.feature.rst diff --git a/docs/changelog/fragments/216.feature.rst b/docs/changelog/fragments/216.feature.rst new file mode 100644 index 00000000..3881aff8 --- /dev/null +++ b/docs/changelog/fragments/216.feature.rst @@ -0,0 +1 @@ + Added defaultdict support diff --git a/docs/loading-and-dumping/specific-types-behavior.rst b/docs/loading-and-dumping/specific-types-behavior.rst index 61598791..aa9317b3 100644 --- a/docs/loading-and-dumping/specific-types-behavior.rst +++ b/docs/loading-and-dumping/specific-types-behavior.rst @@ -216,6 +216,11 @@ Dict and Mapping Loader accepts any other ``Mapping`` and makes ``dict`` instances. Dumper also constructs dict with converted keys and values. +DefaultDict +''''''''''''''''''''' +Loader makes instances of ``defaultdict`` with the ``default_factory`` parameter set to ``None``. +To customize this behavior, there are factory :func:`.default_dict` that have :paramref:`.default_dict.default_factory` parameter that can be overridden. + Models '''''''''' @@ -249,3 +254,4 @@ Known limitations: - ``__init__`` introspection or using :func:`.constructor` - Fields of unpacked typed dict (``**kwargs: Unpack[YourTypedDict]``) cannot collide with parameters of function + diff --git a/src/adaptix/_internal/morphing/facade/provider.py b/src/adaptix/_internal/morphing/facade/provider.py index 623708ae..259d5e8c 100644 --- a/src/adaptix/_internal/morphing/facade/provider.py +++ b/src/adaptix/_internal/morphing/facade/provider.py @@ -372,4 +372,10 @@ def validating_loader(data): def default_dict(pred: Pred, default_factory: Callable) -> Provider: + """DefaultDictProvider factory with overriden default_factory parameter + + :param pred: Predicate specifying where the provider should be used. + See :ref:`predicate-system` for details. + :param default_factory: param that will be passed to DefaultDictProvider + """ return bound(pred, DefaultDictProvider(default_factory)) From 62e0a91f9bdcb07e7a116324aea9ddbd6cce8b4a Mon Sep 17 00:00:00 2001 From: Andrew Srg Date: Tue, 23 Jan 2024 20:41:43 +0200 Subject: [PATCH 4/4] refactor defaultdict support documentation --- src/adaptix/_internal/morphing/facade/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adaptix/_internal/morphing/facade/provider.py b/src/adaptix/_internal/morphing/facade/provider.py index 259d5e8c..ffb18bbe 100644 --- a/src/adaptix/_internal/morphing/facade/provider.py +++ b/src/adaptix/_internal/morphing/facade/provider.py @@ -372,10 +372,10 @@ def validating_loader(data): def default_dict(pred: Pred, default_factory: Callable) -> Provider: - """DefaultDictProvider factory with overriden default_factory parameter + """DefaultDict provider with overriden default_factory parameter :param pred: Predicate specifying where the provider should be used. See :ref:`predicate-system` for details. - :param default_factory: param that will be passed to DefaultDictProvider + :param default_factory: default_factory parameter of the defaultdict instance to be created by the loader """ return bound(pred, DefaultDictProvider(default_factory))