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 all commits
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
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']})
zhPavel marked this conversation as resolved.
Show resolved Hide resolved
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'}