Repository: microsoft/electionguard-python

Issue:
Iterate to Release 1.1.3
Migrate to next version `1.1.3`

Comments:

Patch:
diff --git a/setup.py b/setup.py
index 185688b..9e2d308 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ with open(join(current_directory, "README.md"), encoding="utf-8") as readme_file
     LONG_DESCRIPTION = readme_file.read()
 
 NAME = "electionguard"
-VERSION = "1.1.2"
+VERSION = "1.1.3"
 LICENSE = "MIT"
 DESCRIPTION = "ElectionGuard: Support for e2e verified elections."
 LONG_DESCRIPTION_CONTENT_TYPE = "text/markdown"
diff --git a/src/electionguard/serializable.py b/src/electionguard/serializable.py
index c795bb8..ffd69ca 100644
--- a/src/electionguard/serializable.py
+++ b/src/electionguard/serializable.py
@@ -1,11 +1,13 @@
 from dataclasses import dataclass
 from datetime import datetime
 from os import path
-from typing import cast, TypeVar, Generic
+from typing import Any, cast, TypeVar, Generic
 
 from jsons import (
+    dump,
     dumps,
     NoneType,
+    load,
     loads,
     JsonsError,
     set_deserializer,
@@ -22,7 +24,7 @@ WRITE: str = "w"
 READ: str = "r"
 JSON_PARSE_ERROR = '{"error": "Object could not be parsed due to json issue"}'
 # TODO Issue #??: Jsons library incorrectly dumps class method
-FROM_JSON_FILE = '"from_json_file": {}, '
+KEYS_TO_REMOVE = ["from_json", "from_json_file", "from_json_object"]
 
 
 @dataclass
@@ -33,18 +35,19 @@ class Serializable(Generic[T]):
 
     def to_json(self, strip_privates: bool = True) -> str:
         """
-        Serialize to json
+        Serialize to json string
+        :param strip_privates: strip private variables
+        :return: the json string representation of this object
+        """
+        return write_json(self, strip_privates)
+
+    def to_json_object(self, strip_privates: bool = True) -> Any:
+        """
+        Serialize to json object
         :param strip_privates: strip private variables
         :return: the json representation of this object
         """
-        set_serializers()
-        suppress_warnings()
-        try:
-            return cast(
-                str, dumps(self, strip_privates=strip_privates, strip_nulls=True)
-            ).replace(FROM_JSON_FILE, "")
-        except JsonsError:
-            return JSON_PARSE_ERROR
+        return write_json_object(self, strip_privates)
 
     def to_json_file(
         self, file_name: str, file_path: str = "", strip_privates: bool = True
@@ -55,12 +58,32 @@ class Serializable(Generic[T]):
         :param file_path: File path
         :param strip_privates: Strip private variables
         """
-        write_json_file(self.to_json(strip_privates), file_name, file_path)
+        write_json_file(self, file_name, file_path, strip_privates)
+
+    @classmethod
+    def from_json(cls, data: str) -> T:
+        """
+        Deserialize the provided data string into the specified instance
+        :param data: JSON string
+        """
+        set_deserializers()
+        return cast(T, loads(data, cls))
+
+    @classmethod
+    def from_json_object(cls, data: object) -> T:
+        """
+        Deserialize the provided data object into the specified instance
+        :param data: JSON object
+        """
+        set_deserializers()
+        return cast(T, load(data, cls))
 
     @classmethod
     def from_json_file(cls, file_name: str, file_path: str = "") -> T:
         """
         Deserialize the provided file into the specified instance
+        :param file_name: File name
+        :param file_path: File path
         """
         json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
         with open(json_file_path, READ) as json_file:
@@ -68,22 +91,82 @@ class Serializable(Generic[T]):
             target = cls.from_json(data)
         return target
 
-    @classmethod
-    def from_json(cls, data: str) -> T:
-        """
-        Deserialize the provided data string into the specified instance
-        """
-        set_deserializers()
-        return cast(T, loads(data, cls))
-
 
-def write_json_file(json_data: str, file_name: str, file_path: str = "") -> None:
+def _remove_key(obj: Any, key_to_remove: str) -> Any:
+    """
+    Remove key from object recursively
+    :param obj: Any object
+    :param key_to_remove: key to remove
+    """
+    if isinstance(obj, dict):
+        for key in list(obj.keys()):
+            if key == key_to_remove:
+                del obj[key]
+            else:
+                _remove_key(obj[key], key_to_remove)
+    elif isinstance(obj, list):
+        for i in reversed(range(len(obj))):
+            if obj[i] == key_to_remove:
+                del obj[i]
+            else:
+                _remove_key(obj[i], key_to_remove)
+
+
+def write_json(object_to_write: object, strip_privates: bool = True) -> str:
+    """
+    Serialize to json string
+    :param object_to_write: object to write to json
+    :param strip_privates: strip private variables
+    :return: the json string representation of this object
+    """
+    set_serializers()
+    suppress_warnings()
+    try:
+        json_object = write_json_object(object_to_write, strip_privates)
+        json_string = cast(
+            str, dumps(json_object, strip_privates=strip_privates, strip_nulls=True)
+        )
+        return json_string
+    except JsonsError:
+        return JSON_PARSE_ERROR
+
+
+def write_json_object(object_to_write: object, strip_privates: bool = True) -> object:
+    """
+    Serialize to json object
+    :param object_to_write: object to write to json
+    :param strip_privates: strip private variables
+    :return: the json representation of this object
+    """
+    set_serializers()
+    suppress_warnings()
+    try:
+        json_object = dump(
+            object_to_write, strip_privates=strip_privates, strip_nulls=True
+        )
+        for key in KEYS_TO_REMOVE:
+            _remove_key(json_object, key)
+        return json_object
+    except JsonsError:
+        return JSON_PARSE_ERROR
+
+
+def write_json_file(
+    object_to_write: object,
+    file_name: str,
+    file_path: str = "",
+    strip_privates: bool = True,
+) -> None:
     """
-    Write json data string to json file
+    Serialize json data string to json file
+    :param object_to_write: object to write to json
+    :param file_name: File name
+    :param file_path: File path
+    :param strip_privates: strip private variables
     """
     json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
     with open(json_file_path, WRITE) as json_file:
-        json_file.write(json_data)
+        json_file.write(write_json(object_to_write, strip_privates))
 
 
 def set_serializers() -> None:


Test patch:
diff --git a/tests/test_serializable.py b/tests/test_serializable.py
index 98ad413..e64ab95 100644
--- a/tests/test_serializable.py
+++ b/tests/test_serializable.py
@@ -5,13 +5,61 @@ from electionguard.serializable import (
     set_deserializers,
     set_serializers,
     write_json_file,
+    write_json_object,
+    write_json,
 )
 
 
 class TestSerializable(TestCase):
+    def test_write_json(self) -> None:
+        # Arrange
+        json_data = {
+            "from_json_file": {},
+            "test": 1,
+            "nested": {"from_json_file": {}, "test": 1},
+            "array": [{"from_json_file": {}, "test": 1}],
+        }
+        expected_json_string = (
+            '{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
+        )
+
+        # Act
+        json_string = write_json(json_data)
+
+        # Assert
+        self.assertEqual(json_string, expected_json_string)
+
+    def test_write_json_object(self) -> None:
+        # Arrange
+        json_data = {
+            "from_json_file": {},
+            "test": 1,
+            "nested": {"from_json_file": {}, "test": 1},
+            "array": [{"from_json_file": {}, "test": 1}],
+        }
+        expected_json_object = {
+            "test": 1,
+            "nested": {"test": 1},
+            "array": [{"test": 1}],
+        }
+
+        # Act
+        json_object = write_json_object(json_data)
+
+        # Assert
+        self.assertEqual(json_object, expected_json_object)
+
     def test_write_json_file(self) -> None:
         # Arrange
-        json_data = '{ "test" : 1 }'
+        json_data = {
+            "from_json_file": {},
+            "test": 1,
+            "nested": {"from_json_file": {}, "test": 1},
+            "array": [{"from_json_file": {}, "test": 1}],
+        }
+        expected_json_data = (
+            '{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
+        )
         file_name = "json_write_test"
         json_file = file_name + ".json"
 
@@ -20,7 +68,7 @@ class TestSerializable(TestCase):
 
         # Assert
         with open(json_file) as reader:
-            self.assertEqual(reader.read(), json_data)
+            self.assertEqual(reader.read(), expected_json_data)
 
         # Cleanup
         remove(json_file)