Skip to content

Commit 48ed717

Browse files
authored
✨ Open up serialization to reduce clients from depending on jsons (#156)
* ✨Open serialization methods to ensure consumers don't need jsons Internal serialization was not fully exposed. This prevented for instance to get objects that would normally be returned in an API. This ensures API's will not have to implement jsons. Added additional logic to handle keys that were getting placed within the deserialization. * 🆙 Raise version to 1.1.3
1 parent a8de188 commit 48ed717

3 files changed

Lines changed: 157 additions & 26 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
LONG_DESCRIPTION = readme_file.read()
1515

1616
NAME = "electionguard"
17-
VERSION = "1.1.2"
17+
VERSION = "1.1.3"
1818
LICENSE = "MIT"
1919
DESCRIPTION = "ElectionGuard: Support for e2e verified elections."
2020
LONG_DESCRIPTION_CONTENT_TYPE = "text/markdown"

src/electionguard/serializable.py

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from dataclasses import dataclass
22
from datetime import datetime
33
from os import path
4-
from typing import cast, TypeVar, Generic
4+
from typing import Any, cast, TypeVar, Generic
55

66
from jsons import (
7+
dump,
78
dumps,
89
NoneType,
10+
load,
911
loads,
1012
JsonsError,
1113
set_deserializer,
@@ -22,7 +24,7 @@
2224
READ: str = "r"
2325
JSON_PARSE_ERROR = '{"error": "Object could not be parsed due to json issue"}'
2426
# TODO Issue #??: Jsons library incorrectly dumps class method
25-
FROM_JSON_FILE = '"from_json_file": {}, '
27+
KEYS_TO_REMOVE = ["from_json", "from_json_file", "from_json_object"]
2628

2729

2830
@dataclass
@@ -33,18 +35,19 @@ class Serializable(Generic[T]):
3335

3436
def to_json(self, strip_privates: bool = True) -> str:
3537
"""
36-
Serialize to json
38+
Serialize to json string
39+
:param strip_privates: strip private variables
40+
:return: the json string representation of this object
41+
"""
42+
return write_json(self, strip_privates)
43+
44+
def to_json_object(self, strip_privates: bool = True) -> Any:
45+
"""
46+
Serialize to json object
3747
:param strip_privates: strip private variables
3848
:return: the json representation of this object
3949
"""
40-
set_serializers()
41-
suppress_warnings()
42-
try:
43-
return cast(
44-
str, dumps(self, strip_privates=strip_privates, strip_nulls=True)
45-
).replace(FROM_JSON_FILE, "")
46-
except JsonsError:
47-
return JSON_PARSE_ERROR
50+
return write_json_object(self, strip_privates)
4851

4952
def to_json_file(
5053
self, file_name: str, file_path: str = "", strip_privates: bool = True
@@ -55,35 +58,115 @@ def to_json_file(
5558
:param file_path: File path
5659
:param strip_privates: Strip private variables
5760
"""
58-
write_json_file(self.to_json(strip_privates), file_name, file_path)
61+
write_json_file(self, file_name, file_path, strip_privates)
62+
63+
@classmethod
64+
def from_json(cls, data: str) -> T:
65+
"""
66+
Deserialize the provided data string into the specified instance
67+
:param data: JSON string
68+
"""
69+
set_deserializers()
70+
return cast(T, loads(data, cls))
71+
72+
@classmethod
73+
def from_json_object(cls, data: object) -> T:
74+
"""
75+
Deserialize the provided data object into the specified instance
76+
:param data: JSON object
77+
"""
78+
set_deserializers()
79+
return cast(T, load(data, cls))
5980

6081
@classmethod
6182
def from_json_file(cls, file_name: str, file_path: str = "") -> T:
6283
"""
6384
Deserialize the provided file into the specified instance
85+
:param file_name: File name
86+
:param file_path: File path
6487
"""
6588
json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
6689
with open(json_file_path, READ) as json_file:
6790
data = json_file.read()
6891
target = cls.from_json(data)
6992
return target
7093

71-
@classmethod
72-
def from_json(cls, data: str) -> T:
73-
"""
74-
Deserialize the provided data string into the specified instance
75-
"""
76-
set_deserializers()
77-
return cast(T, loads(data, cls))
78-
7994

80-
def write_json_file(json_data: str, file_name: str, file_path: str = "") -> None:
95+
def _remove_key(obj: Any, key_to_remove: str) -> Any:
96+
"""
97+
Remove key from object recursively
98+
:param obj: Any object
99+
:param key_to_remove: key to remove
100+
"""
101+
if isinstance(obj, dict):
102+
for key in list(obj.keys()):
103+
if key == key_to_remove:
104+
del obj[key]
105+
else:
106+
_remove_key(obj[key], key_to_remove)
107+
elif isinstance(obj, list):
108+
for i in reversed(range(len(obj))):
109+
if obj[i] == key_to_remove:
110+
del obj[i]
111+
else:
112+
_remove_key(obj[i], key_to_remove)
113+
114+
115+
def write_json(object_to_write: object, strip_privates: bool = True) -> str:
116+
"""
117+
Serialize to json string
118+
:param object_to_write: object to write to json
119+
:param strip_privates: strip private variables
120+
:return: the json string representation of this object
121+
"""
122+
set_serializers()
123+
suppress_warnings()
124+
try:
125+
json_object = write_json_object(object_to_write, strip_privates)
126+
json_string = cast(
127+
str, dumps(json_object, strip_privates=strip_privates, strip_nulls=True)
128+
)
129+
return json_string
130+
except JsonsError:
131+
return JSON_PARSE_ERROR
132+
133+
134+
def write_json_object(object_to_write: object, strip_privates: bool = True) -> object:
135+
"""
136+
Serialize to json object
137+
:param object_to_write: object to write to json
138+
:param strip_privates: strip private variables
139+
:return: the json representation of this object
140+
"""
141+
set_serializers()
142+
suppress_warnings()
143+
try:
144+
json_object = dump(
145+
object_to_write, strip_privates=strip_privates, strip_nulls=True
146+
)
147+
for key in KEYS_TO_REMOVE:
148+
_remove_key(json_object, key)
149+
return json_object
150+
except JsonsError:
151+
return JSON_PARSE_ERROR
152+
153+
154+
def write_json_file(
155+
object_to_write: object,
156+
file_name: str,
157+
file_path: str = "",
158+
strip_privates: bool = True,
159+
) -> None:
81160
"""
82-
Write json data string to json file
161+
Serialize json data string to json file
162+
:param object_to_write: object to write to json
163+
:param file_name: File name
164+
:param file_path: File path
165+
:param strip_privates: strip private variables
83166
"""
84167
json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
85168
with open(json_file_path, WRITE) as json_file:
86-
json_file.write(json_data)
169+
json_file.write(write_json(object_to_write, strip_privates))
87170

88171

89172
def set_serializers() -> None:

tests/test_serializable.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,61 @@
55
set_deserializers,
66
set_serializers,
77
write_json_file,
8+
write_json_object,
9+
write_json,
810
)
911

1012

1113
class TestSerializable(TestCase):
14+
def test_write_json(self) -> None:
15+
# Arrange
16+
json_data = {
17+
"from_json_file": {},
18+
"test": 1,
19+
"nested": {"from_json_file": {}, "test": 1},
20+
"array": [{"from_json_file": {}, "test": 1}],
21+
}
22+
expected_json_string = (
23+
'{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
24+
)
25+
26+
# Act
27+
json_string = write_json(json_data)
28+
29+
# Assert
30+
self.assertEqual(json_string, expected_json_string)
31+
32+
def test_write_json_object(self) -> None:
33+
# Arrange
34+
json_data = {
35+
"from_json_file": {},
36+
"test": 1,
37+
"nested": {"from_json_file": {}, "test": 1},
38+
"array": [{"from_json_file": {}, "test": 1}],
39+
}
40+
expected_json_object = {
41+
"test": 1,
42+
"nested": {"test": 1},
43+
"array": [{"test": 1}],
44+
}
45+
46+
# Act
47+
json_object = write_json_object(json_data)
48+
49+
# Assert
50+
self.assertEqual(json_object, expected_json_object)
51+
1252
def test_write_json_file(self) -> None:
1353
# Arrange
14-
json_data = '{ "test" : 1 }'
54+
json_data = {
55+
"from_json_file": {},
56+
"test": 1,
57+
"nested": {"from_json_file": {}, "test": 1},
58+
"array": [{"from_json_file": {}, "test": 1}],
59+
}
60+
expected_json_data = (
61+
'{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
62+
)
1563
file_name = "json_write_test"
1664
json_file = file_name + ".json"
1765

@@ -20,7 +68,7 @@ def test_write_json_file(self) -> None:
2068

2169
# Assert
2270
with open(json_file) as reader:
23-
self.assertEqual(reader.read(), json_data)
71+
self.assertEqual(reader.read(), expected_json_data)
2472

2573
# Cleanup
2674
remove(json_file)

0 commit comments

Comments
 (0)