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 defaultdict support #222

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 0 deletions src/adaptix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
as_is_loader,
bound,
constructor,
default_dict,
dumper,
enum_by_exact_value,
enum_by_name,
Expand Down Expand Up @@ -59,6 +60,7 @@
'enum_by_name',
'enum_by_value',
'name_mapping',
'default_dict',
'AdornedRetort',
'FilledRetort',
'Retort',
Expand Down
38 changes: 37 additions & 1 deletion src/adaptix/_internal/morphing/dict_provider.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
zhPavel marked this conversation as resolved.
Show resolved Hide resolved

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))
zhPavel marked this conversation as resolved.
Show resolved Hide resolved

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)))
)
7 changes: 6 additions & 1 deletion src/adaptix/_internal/morphing/facade/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
zhPavel marked this conversation as resolved.
Show resolved Hide resolved
zhPavel marked this conversation as resolved.
Show resolved Hide resolved
zhPavel marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion src/adaptix/_internal/morphing/facade/retort.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -135,6 +135,7 @@ class FilledRetort(OperatingRetort, ABC):
ConstantLengthTupleProvider(),
IterableProvider(),
DictProvider(),
DefaultDictProvider(),
RegexPatternProvider(),
SelfTypeProvider(),
LiteralStringProvider(),
Expand Down
40 changes: 37 additions & 3 deletions tests/unit/morphing/test_dict_provider.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,8 +26,10 @@ def retort():
return TestRetort(
recipe=[
DictProvider(),
DefaultDictProvider(),
STR_LOADER_PROVIDER,
dumper(str, string_dumper),
IterableProvider()
]
)

Expand Down Expand Up @@ -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]])
zhPavel marked this conversation as resolved.
Show resolved Hide resolved

assert loader_({'a': ['b', 'c']}) == defaultdict(list, {'a': ['b', 'c']})
zhPavel marked this conversation as resolved.
Show resolved Hide resolved


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'}
Loading