Skip to content
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
34 changes: 32 additions & 2 deletions src/cloudformation_cli_python_lib/recast.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ def recast_object(
return
for k, v in json_data.items():
if isinstance(v, dict):
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
recast_object(child_cls, v, classes)
_recast_nested_dict(cls, json_data, k, v, classes)
elif isinstance(v, list):
json_data[k] = _recast_lists(cls, k, v, classes)
elif isinstance(v, set):
Expand All @@ -34,6 +33,37 @@ def recast_object(
raise InvalidRequest(f"Unsupported type: {type(v)} for {k}")


def _recast_nested_dict(
cls: Any,
json_data: Mapping[str, Any],
k: str,
v: Dict[str, Any],
classes: Dict[str, Any],
) -> None:
"""
Attempts to recursively cast the dict elements.
On KeyError, we know that the "parent" property ,
does not have a specific class to be casted to.
Therefore we figure out what the "nested object" class might be,
recursively cast those,
to finally assign to the "parent" dict the right class

:param Any cls:
:param Mapping json_data:
:param str k:
:param Any v:
:param dict classes:
"""
try:
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
recast_object(child_cls, v, classes)
except KeyError:
child_cls = _field_to_type(cls.__dataclass_fields__[k].type, k, classes)
for _child, _child_definition in v.items():
recast_object(child_cls, _child_definition, classes)
json_data[k][_child] = _child_definition


def _recast_lists(cls: Any, k: str, v: List[Any], classes: Dict[str, Any]) -> List[Any]:
# Leave as is if type is Any
if cls is typing.Any:
Expand Down
18 changes: 18 additions & 0 deletions tests/lib/recast_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ def test_recast_complex_object():
[{"NestedListInt": "true", "NestedListList": ["1", "2", "3"]}],
[{"NestedListInt": "false", "NestedListList": ["11", "12", "13"]}],
],
"NestedObject": {
"first_object": {"AttributeA": "AWS_CFN", "AttributeB": "FTW"},
"second_object": {"AttributeA": "AllHailPython"},
"third_object": {
"ListAttribute": ["2.1", "42.0"],
"AttributeA": "WeAlsoHaveLists",
},
"fourth-0bject": {"BoolAttribute": "false", "AttributeB": "ThatIsNotTrue"},
},
}
expected = {
"ListSetInt": [{1, 2, 3}],
Expand Down Expand Up @@ -79,6 +88,15 @@ def test_recast_complex_object():
[{"NestedListInt": True, "NestedListList": [1.0, 2.0, 3.0]}],
[{"NestedListInt": False, "NestedListList": [11.0, 12.0, 13.0]}],
],
"NestedObject": {
"first_object": {"AttributeA": "AWS_CFN", "AttributeB": "FTW"},
"second_object": {"AttributeA": "AllHailPython"},
"third_object": {
"ListAttribute": [2.1, 42.0],
"AttributeA": "WeAlsoHaveLists",
},
"fourth-0bject": {"BoolAttribute": False, "AttributeB": "ThatIsNotTrue"},
},
}
model = ComplexResourceModel._deserialize(payload)
assert payload == expected
Expand Down
28 changes: 28 additions & 0 deletions tests/lib/sample_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class ResourceModel(BaseModel):
HttpsUrl: Optional[str]
Namespace: Optional[str]
Id: Optional[int]
NestedObject: Optional[MutableMapping[str, "_NestedObjectDefinition"]]

@classmethod
def _deserialize(
Expand Down Expand Up @@ -83,6 +84,7 @@ def _deserialize(
HttpsUrl=json_data.get("HttpsUrl"),
Namespace=json_data.get("Namespace"),
Id=json_data.get("Id"),
NestedObject=json_data.get("NestedObject"),
)


Expand All @@ -109,6 +111,32 @@ def _deserialize(
_NestedList = NestedList


@dataclass
class NestedObjectDefinition(BaseModel):
AttributeA: Optional[str]
AttributeB: Optional[str]
BoolAttribute: Optional[bool]
ListAttribute: Optional[Sequence[float]]

@classmethod
def _deserialize(
cls: Type["_NestedObjectDefinition"],
json_data: Optional[Mapping[str, Any]],
) -> Optional["_NestedObjectDefinition"]:
if not json_data:
return None
return cls(
AttributeA=json_data.get("AttributeA"),
AttributeB=json_data.get("AttributeB"),
BoolAttribute=json_data.get("BoolAttribute"),
ListAttribute=json_data.get("ListAttribute"),
)


# work around possible type aliasing issues when variable has same name as a model
_NestedObjectDefinition = NestedObjectDefinition


@dataclass
class AList(BaseModel):
DeeperBool: Optional[bool]
Expand Down