diff --git a/docs/changelog/fragments/247.bugfix.rst b/docs/changelog/fragments/247.bugfix.rst new file mode 100644 index 00000000..feaaa6fd --- /dev/null +++ b/docs/changelog/fragments/247.bugfix.rst @@ -0,0 +1 @@ +Model dumping now trying to save the original order of fields inside the dict diff --git a/src/adaptix/_internal/morphing/name_layout/crown_builder.py b/src/adaptix/_internal/morphing/name_layout/crown_builder.py index de36a2df..e33692ba 100644 --- a/src/adaptix/_internal/morphing/name_layout/crown_builder.py +++ b/src/adaptix/_internal/morphing/name_layout/crown_builder.py @@ -1,3 +1,4 @@ +import math from abc import ABC, abstractmethod from dataclasses import dataclass from itertools import groupby @@ -35,13 +36,17 @@ class PathWithLeaf(Generic[LeafCr]): class BaseCrownBuilder(ABC, Generic[LeafCr, DictCr, ListCr]): + def __init__(self, paths_to_leaves: PathsTo[LeafCr]): + self._paths_to_leaves = paths_to_leaves + self._paths_to_order = {path: i for i, path in enumerate(paths_to_leaves)} + def build_empty_crown(self, *, as_list: bool) -> Union[DictCr, ListCr]: if as_list: return self._make_list_crown(current_path=(), paths_with_leaves=[]) return self._make_dict_crown(current_path=(), paths_with_leaves=[]) - def build_crown(self, paths_to_leaves: PathsTo[LeafCr]) -> Union[DictCr, ListCr]: - paths_with_leaves = [PathWithLeaf(path, leaf) for path, leaf in paths_to_leaves.items()] + def build_crown(self) -> Union[DictCr, ListCr]: + paths_with_leaves = [PathWithLeaf(path, leaf) for path, leaf in self._paths_to_leaves.items()] paths_with_leaves.sort(key=lambda x: x.path) return cast(Union[DictCr, ListCr], self._build_crown(paths_with_leaves, 0)) @@ -67,10 +72,15 @@ def _get_dict_crown_map( current_path: KeyPath, paths_with_leaves: PathedLeaves[LeafCr], ) -> Mapping[str, Union[LeafCr, DictCr, ListCr]]: - return { - cast(str, key): self._build_crown(list(path_group), len(current_path) + 1) + dict_crown_map = { + key: self._build_crown(list(path_group), len(current_path) + 1) for key, path_group in groupby(paths_with_leaves, lambda x: x.path[len(current_path)]) } + sorted_keys = sorted( + dict_crown_map, + key=lambda key: self._paths_to_order.get((*current_path, key), math.inf), + ) + return {key: dict_crown_map[key] for key in sorted_keys} @abstractmethod def _make_dict_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeaves[LeafCr]) -> DictCr: @@ -98,8 +108,9 @@ def _make_list_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeave class InpCrownBuilder(BaseCrownBuilder[LeafInpCrown, InpDictCrown, InpListCrown]): - def __init__(self, extra_policies: PathsTo[DictExtraPolicy]): + def __init__(self, extra_policies: PathsTo[DictExtraPolicy], paths_to_leaves: PathsTo[LeafInpCrown]): self.extra_policies = extra_policies + super().__init__(paths_to_leaves) def _make_dict_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeaves[LeafInpCrown]) -> InpDictCrown: return InpDictCrown( @@ -115,8 +126,9 @@ def _make_list_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeave class OutCrownBuilder(BaseCrownBuilder[LeafOutCrown, OutDictCrown, OutListCrown]): - def __init__(self, path_to_sieves: PathsTo[Sieve]): + def __init__(self, path_to_sieves: PathsTo[Sieve], paths_to_leaves: PathsTo[LeafOutCrown]): self.path_to_sieves = path_to_sieves + super().__init__(paths_to_leaves) def _make_dict_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeaves[LeafOutCrown]) -> OutDictCrown: key_to_sieve: Dict[str, Sieve] = {} diff --git a/src/adaptix/_internal/morphing/name_layout/provider.py b/src/adaptix/_internal/morphing/name_layout/provider.py index 74d16a0c..8ae15fd7 100644 --- a/src/adaptix/_internal/morphing/name_layout/provider.py +++ b/src/adaptix/_internal/morphing/name_layout/provider.py @@ -62,7 +62,7 @@ def _create_input_crown( paths_to_leaves: PathsTo[LeafInpCrown], extra_policies: PathsTo[DictExtraPolicy], ) -> BranchInpCrown: - return InpCrownBuilder(extra_policies).build_crown(paths_to_leaves) + return InpCrownBuilder(extra_policies, paths_to_leaves).build_crown() def _create_empty_input_crown( self, @@ -72,7 +72,7 @@ def _create_empty_input_crown( *, as_list: bool, ) -> BranchInpCrown: - return InpCrownBuilder(extra_policies).build_empty_crown(as_list=as_list) + return InpCrownBuilder(extra_policies, {}).build_empty_crown(as_list=as_list) @static_provision_action def _provide_output_name_layout(self, mediator: Mediator, request: OutputNameLayoutRequest) -> OutputNameLayout: @@ -102,7 +102,7 @@ def _create_output_crown( paths_to_leaves: PathsTo[LeafOutCrown], path_to_sieve: PathsTo[Sieve], ) -> BranchOutCrown: - return OutCrownBuilder(path_to_sieve).build_crown(paths_to_leaves) + return OutCrownBuilder(path_to_sieve, paths_to_leaves).build_crown() def _create_empty_output_crown( self, @@ -112,4 +112,4 @@ def _create_empty_output_crown( *, as_list: bool, ): - return OutCrownBuilder(path_to_sieve).build_empty_crown(as_list=as_list) + return OutCrownBuilder(path_to_sieve, {}).build_empty_crown(as_list=as_list) diff --git a/tests/integration/morphing/test_dump_order.py b/tests/integration/morphing/test_dump_order.py new file mode 100644 index 00000000..ae3bbbf7 --- /dev/null +++ b/tests/integration/morphing/test_dump_order.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass + +from adaptix import Retort, name_mapping + + +def test_simple(accum): + @dataclass + class Example: + c: int + a: int + b: int + + retort = Retort(recipe=[accum]) + + dumper = retort.get_dumper(Example) + assert list(dumper(Example(c=1, a=2, b=3)).items()) == [("c", 1), ("a", 2), ("b", 3)] + + +def test_name_flatenning(accum): + @dataclass + class Example: + c: int + a: int + e: int + b: int + + retort = Retort( + recipe=[ + accum, + name_mapping(Example, map={"e": ("q", "e")}), + ], + ) + + dumper = retort.get_dumper(Example) + assert list(dumper(Example(c=1, a=2, e=3, b=4)).items()) == [("c", 1), ("a", 2), ("b", 4), ("q", {"e": 3})]