Skip to content

Commit 1c4b203

Browse files
JohnPrestonkddejong
authored andcommitted
Implemented nested dict re-cast (aws-cloudformation#174)
1 parent 3e21600 commit 1c4b203

File tree

3 files changed

+78
-2
lines changed

3 files changed

+78
-2
lines changed

src/cloudformation_cli_python_lib/recast.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ def recast_object(
1919
return
2020
for k, v in json_data.items():
2121
if isinstance(v, dict):
22-
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
23-
recast_object(child_cls, v, classes)
22+
_recast_nested_dict(cls, json_data, k, v, classes)
2423
elif isinstance(v, list):
2524
json_data[k] = _recast_lists(cls, k, v, classes)
2625
elif isinstance(v, set):
@@ -34,6 +33,37 @@ def recast_object(
3433
raise InvalidRequest(f"Unsupported type: {type(v)} for {k}")
3534

3635

36+
def _recast_nested_dict(
37+
cls: Any,
38+
json_data: Mapping[str, Any],
39+
k: str,
40+
v: Dict[str, Any],
41+
classes: Dict[str, Any],
42+
) -> None:
43+
"""
44+
Attempts to recursively cast the dict elements.
45+
On KeyError, we know that the "parent" property ,
46+
does not have a specific class to be casted to.
47+
Therefore we figure out what the "nested object" class might be,
48+
recursively cast those,
49+
to finally assign to the "parent" dict the right class
50+
51+
:param Any cls:
52+
:param Mapping json_data:
53+
:param str k:
54+
:param Any v:
55+
:param dict classes:
56+
"""
57+
try:
58+
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
59+
recast_object(child_cls, v, classes)
60+
except KeyError:
61+
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
62+
for _child, _child_definition in v.items():
63+
recast_object(child_cls, _child_definition, classes)
64+
json_data[k][_child] = _child_definition
65+
66+
3767
def _recast_lists(cls: Any, k: str, v: List[Any], classes: Dict[str, Any]) -> List[Any]:
3868
# Leave as is if type is Any
3969
if cls is typing.Any:

tests/lib/recast_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ def test_recast_complex_object():
4747
[{"NestedListInt": "true", "NestedListList": ["1", "2", "3"]}],
4848
[{"NestedListInt": "false", "NestedListList": ["11", "12", "13"]}],
4949
],
50+
"NestedObject": {
51+
"first_object": {"AttributeA": "AWS_CFN", "AttributeB": "FTW"},
52+
"second_object": {"AttributeA": "AllHailPython"},
53+
"third_object": {
54+
"ListAttribute": ["2.1", "42.0"],
55+
"AttributeA": "WeAlsoHaveLists",
56+
},
57+
"fourth-0bject": {"BoolAttribute": "false", "AttributeB": "ThatIsNotTrue"},
58+
},
5059
}
5160
expected = {
5261
"ListSetInt": [{1, 2, 3}],
@@ -79,6 +88,15 @@ def test_recast_complex_object():
7988
[{"NestedListInt": True, "NestedListList": [1.0, 2.0, 3.0]}],
8089
[{"NestedListInt": False, "NestedListList": [11.0, 12.0, 13.0]}],
8190
],
91+
"NestedObject": {
92+
"first_object": {"AttributeA": "AWS_CFN", "AttributeB": "FTW"},
93+
"second_object": {"AttributeA": "AllHailPython"},
94+
"third_object": {
95+
"ListAttribute": [2.1, 42.0],
96+
"AttributeA": "WeAlsoHaveLists",
97+
},
98+
"fourth-0bject": {"BoolAttribute": False, "AttributeB": "ThatIsNotTrue"},
99+
},
82100
}
83101
model = ComplexResourceModel._deserialize(payload)
84102
assert payload == expected

tests/lib/sample_model.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class ResourceModel(BaseModel):
5555
HttpsUrl: Optional[str]
5656
Namespace: Optional[str]
5757
Id: Optional[int]
58+
NestedObject: Optional[MutableMapping[str, "_NestedObjectDefinition"]]
5859

5960
@classmethod
6061
def _deserialize(
@@ -83,6 +84,7 @@ def _deserialize(
8384
HttpsUrl=json_data.get("HttpsUrl"),
8485
Namespace=json_data.get("Namespace"),
8586
Id=json_data.get("Id"),
87+
NestedObject=json_data.get("NestedObject"),
8688
)
8789

8890

@@ -109,6 +111,32 @@ def _deserialize(
109111
_NestedList = NestedList
110112

111113

114+
@dataclass
115+
class NestedObjectDefinition(BaseModel):
116+
AttributeA: Optional[str]
117+
AttributeB: Optional[str]
118+
BoolAttribute: Optional[bool]
119+
ListAttribute: Optional[Sequence[float]]
120+
121+
@classmethod
122+
def _deserialize(
123+
cls: Type["_NestedObjectDefinition"],
124+
json_data: Optional[Mapping[str, Any]],
125+
) -> Optional["_NestedObjectDefinition"]:
126+
if not json_data:
127+
return None
128+
return cls(
129+
AttributeA=json_data.get("AttributeA"),
130+
AttributeB=json_data.get("AttributeB"),
131+
BoolAttribute=json_data.get("BoolAttribute"),
132+
ListAttribute=json_data.get("ListAttribute"),
133+
)
134+
135+
136+
# work around possible type aliasing issues when variable has same name as a model
137+
_NestedObjectDefinition = NestedObjectDefinition
138+
139+
112140
@dataclass
113141
class AList(BaseModel):
114142
DeeperBool: Optional[bool]

0 commit comments

Comments
 (0)