Skip to content

Commit

Permalink
Merge pull request #222 from AndrewSergienko/feature/defaultdict_support
Browse files Browse the repository at this point in the history
Add defaultdict support
  • Loading branch information
zhPavel authored Jan 23, 2024
2 parents 3422988 + 62e0a91 commit 8002be8
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/changelog/fragments/216.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added defaultdict support
6 changes: 6 additions & 0 deletions docs/loading-and-dumping/specific-types-behavior.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
''''''''''

Expand Down Expand Up @@ -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

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
39 changes: 38 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 Callable, DefaultDict, Dict, Mapping, Optional, Tuple

from ..common import Dumper, Loader
from ..compat import CompatExceptionGroup
Expand Down Expand Up @@ -269,3 +271,38 @@ 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[Callable] = 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)))
)
default_factory = self.default_factory

def defaultdict_loader(data):
return defaultdict(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)))
)
11 changes: 11 additions & 0 deletions src/adaptix/_internal/morphing/facade/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,13 @@ def validating_loader(data):
raise exception_factory(data)

return loader(pred, validating_loader, chain)


def default_dict(pred: Pred, default_factory: Callable) -> Provider:
"""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: default_factory parameter of the defaultdict instance to be created by the loader
"""
return bound(pred, DefaultDictProvider(default_factory))
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
46 changes: 43 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, List

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,39 @@ 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):
df = list
loader_ = retort.replace(
strict_coercion=strict_coercion,
debug_trail=debug_trail,
).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):
dumper_ = retort.replace(
debug_trail=debug_trail,
).get_dumper(
DefaultDict[str, str],
)

assert dumper_(defaultdict(None, {'a': 'b', 'c': 'd'})) == {'a': 'b', 'c': 'd'}

0 comments on commit 8002be8

Please sign in to comment.