-
Notifications
You must be signed in to change notification settings - Fork 290
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
Handle recursively serializing a dataclasses as a dictionary. #547
Changes from 7 commits
c5d8d63
5652d7e
215be7e
506ae99
2fd95d0
6f3c3e7
71f57ea
a70aed3
2d53e30
f85ee9f
f59f346
b3c62d3
d160959
2834dbe
7ebd740
2eaa982
82744ef
81d27a0
19f1728
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,8 @@ | |
import re | ||
import time | ||
import uuid | ||
from dataclasses import asdict | ||
from typing import Dict, List, Union | ||
from dataclasses import Field, asdict, is_dataclass | ||
from typing import Any, Dict, List, Union, get_args, get_origin | ||
|
||
from ocpp.exceptions import NotImplementedError, NotSupportedError, OCPPError | ||
from ocpp.messages import Call, MessageType, unpack, validate_payload | ||
|
@@ -69,6 +69,85 @@ def snake_to_camel_case(data): | |
return data | ||
|
||
|
||
def _is_dataclass_instance(input: Any) -> bool: | ||
"""Verify if given `input` is a dataclass.""" | ||
return is_dataclass(input) and not isinstance(input, type) | ||
|
||
|
||
def _is_optional_field(field: Field) -> bool: | ||
"""Verify if given `field` allows `None` as value. | ||
|
||
The fields `schema` and `host` on the following class would return `False`. | ||
While the fields `post` and `query` return `True`. | ||
|
||
@dataclass | ||
class URL: | ||
schema: str, | ||
host: str, | ||
post: Optional[str], | ||
query: Union[None, str] | ||
|
||
""" | ||
return get_origin(field.type) is Union and type(None) in get_args(field.type) | ||
|
||
|
||
def serialize_as_dict(dataclass, remove_empty_optional_fields: bool = True): | ||
"""Serialize the given `dataclass` as a `dict` recursively. | ||
|
||
|
||
@dataclass | ||
class StatusInfoType: | ||
reason_code: str | ||
additional_info: Optional[str] = None | ||
|
||
with_additional_info = StatusInfoType(reason="Unknown", | ||
additional_info="More details") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you put this on a single line, to make the code sample valid Python? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done - satisfied with b3c62d3 |
||
|
||
assert serialize_as_dict(with_additional_info) == { | ||
'reason': 'Unknown', | ||
'additional_info': 'More details', | ||
} | ||
|
||
without_additional_info = StatusInfoType(reason="Unknown") | ||
|
||
assert serialize_as_dict(with_additional_info) == { | ||
'reason': 'Unknown', | ||
'additional_info': None, | ||
} | ||
|
||
assert serialize_as_dict(with_additional_info, | ||
remove_empty_optional_fields) == { | ||
'reason': 'Unknown', | ||
} | ||
|
||
|
||
""" | ||
serialized = asdict(dataclass) | ||
|
||
for field in dataclass.__dataclass_fields__.values(): | ||
# Remove field from serialized output if the field is optional and | ||
# `None`. | ||
if ( | ||
remove_empty_optional_fields | ||
and _is_optional_field(field) | ||
and serialized[field.name] is None | ||
): | ||
del serialized[field.name] | ||
continue | ||
|
||
value = getattr(dataclass, field.name) | ||
if _is_dataclass_instance(value): | ||
serialized[field.name] = serialize_as_dict(value) | ||
continue | ||
|
||
if isinstance(value, list): | ||
for item in value: | ||
if _is_dataclass_instance(item): | ||
serialized[field.name] = [serialize_as_dict(item)] | ||
|
||
return snake_to_camel_case(serialized) | ||
|
||
|
||
def remove_nones(data: Union[List, Dict]) -> Union[List, Dict]: | ||
if isinstance(data, dict): | ||
return {k: remove_nones(v) for k, v in data.items() if v is not None} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ classifiers = [ | |
[tool.poetry.dependencies] | ||
python = "^3.7" | ||
jsonschema = "^4.4.0" | ||
typing-extensions = "^4.0.0" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we drop support for Python 3.7, this dependency is not required right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah - Corrected with commit f59f346 |
||
|
||
[tool.poetry.dev-dependencies] | ||
# Starting from Python 3.8, asynctest is replaced with a unittest.mock.AsyncMock in standard library. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the solution can be simplified.
The root of issue #255 is that
dataclasses.asdict()
only serializes the top level dataclass as a dictionary. Any inner dataclasses are not serializes as a dict.The body of this function fixes that: recursively serializing a dataclasses as a dictionary.
But this function does more:
None
.I think we can remove the latter to features of this function.
And, in lines 318 and 378, replace the call to
dataclass.asdict()
withserialize_as_dict()
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asdict() corrected in commit d160959
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the more with commit 2834dbe