From 7814502976e6ed6f5164156546faaa6cf20d4aaa Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 27 Sep 2023 09:24:53 +0300 Subject: [PATCH 1/8] Remove unnecessary check with canWrite --- Magritte/MAJsonWriter_visitors.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index 7e96a100..b4b5f81a 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -28,10 +28,7 @@ def write_json_string(self, model) -> str: return json.dumps(self.write_json(model)) def visit(self, description: MADescription): - if description.accessor.canRead(self._model): - super().visit(description) - else: - raise ValueError("MAValueJsonWriter failed to read value from the model using the description provided.") + super().visit(description) def visitElementDescription(self, description: MADescription): self._json = description.accessor.read(self._model) From 731b4ef80c275bacc680a4412244fe2ceb9adfc6 Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 27 Sep 2023 17:06:09 +0300 Subject: [PATCH 2/8] Check for undefined value before encoding --- Magritte/MAJsonWriter_visitors.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index b4b5f81a..559f4076 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -28,13 +28,15 @@ def write_json_string(self, model) -> str: return json.dumps(self.write_json(model)) def visit(self, description: MADescription): - super().visit(description) + if self._model != description.undefinedValue: + super().visit(description) def visitElementDescription(self, description: MADescription): self._json = description.accessor.read(self._model) def visitDateAndTimeDescription(self, description: MADescription): - self._json = description.accessor.read(self._model).isoformat() + value = description.accessor.read(self._model) + self._json = value.isoformat() if value else None def visitMagnitudeDescription(self, description: MADescription): # !TODO Override exact visit methods like visitDateAndTimeDescription for each type of magnitude when they are defined. @@ -63,6 +65,10 @@ def __init__(self, description: MAContainer): self._model = None self._json = None + def visit(self, description: MADescription): + if self._model != description.undefinedValue: + super().visit(description) + def write_json(self, model) -> Dict[str, Any]: self._model = model self._json = {} # Reset the json dict. From b82bab5cf7193ce21f97ffd8af75008edf29dcf8 Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 4 Oct 2023 19:16:02 +0300 Subject: [PATCH 3/8] MAJsonWriter visitors refactored Reference encoding doesn't work as expected yet... --- Magritte/MAJsonWriter_Test.py | 136 +++++++++++++++-------- Magritte/MAJsonWriter_visitors.py | 175 +++++++++++++++++++----------- 2 files changed, 199 insertions(+), 112 deletions(-) diff --git a/Magritte/MAJsonWriter_Test.py b/Magritte/MAJsonWriter_Test.py index db9bdd9b..e004e9f4 100644 --- a/Magritte/MAJsonWriter_Test.py +++ b/Magritte/MAJsonWriter_Test.py @@ -33,35 +33,38 @@ def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime class MAJsonWriter_Test(TestCase): def setUp(self): + # ==================== Encoders for testing. ==================== + self.value_encoder = MAValueJsonWriter() + self.object_encoder = MAObjectJsonWriter() + # ==================== Scalar values testing. ==================== self.int_value = 123 self.int_desc = MAIntDescription(name='TestInt', label='Test Int', default=0, accessor=MAIdentityAccessor()) - self.int_encoder = MAValueJsonWriter(self.int_desc) self.str_value = 'abc' - self.str_desc = MAStringDescription(name='TestString', label='Test String', default='', - accessor=MAIdentityAccessor()) - self.str_encoder = MAValueJsonWriter(self.str_desc) - + self.str_desc = MAStringDescription( + name='TestString', label='Test String', default='', + accessor=MAIdentityAccessor() + ) self.float_value = 1.23 - self.float_desc = MAFloatDescription(name='TestFloat', label='Test Float', default=0.0, - accessor=MAIdentityAccessor()) - self.float_encoder = MAValueJsonWriter(self.float_desc) + self.float_desc = MAFloatDescription( + name='TestFloat', label='Test Float', default=0.0, + accessor=MAIdentityAccessor() + ) self.time_now = datetime.now() self.date_value = self.time_now self.date_desc = MADateAndTimeDescription( name='TestDate', label='Test Date', default=self.time_now, accessor=MAIdentityAccessor() - ) - self.date_encoder = MAValueJsonWriter(self.date_desc) + ) + # ==================== Scalar relation value testing. ==================== self.scalar_rel_value = self.int_value self.scalar_rel_desc = MARelationDescription( name='TestScalarRel', label='Test Scalar Relation', accessor=MAIdentityAccessor() - ) + ) # Cannot set reference in constructor because of current MARelationDescription implementation. self.scalar_rel_desc.reference = self.int_desc - self.scalar_rel_encoder = MAValueJsonWriter(self.scalar_rel_desc) # ==================== Object encoding testing. ==================== self.object1 = TestObject1('object1', 123, 1.23, self.time_now) @@ -72,9 +75,8 @@ def setUp(self): MAIntDescription(label='Int Value', default=0, accessor='int_value'), MAFloatDescription(label='Float Value', default=0.0, accessor='float_value'), MADateAndTimeDescription(label='Date Value', default=self.time_now, accessor='date_value'), - ] - ) - self.object_encoder = MAObjectJsonWriter(self.object_desc) + ] + ) # ==================== Object reference value testing. ==================== self.object_rel_desc = MARelationDescription( @@ -82,7 +84,6 @@ def setUp(self): ) # Cannot set reference in constructor because of current MARelationDescription implementation. self.object_rel_desc.reference = self.object_desc - self.object_rel_encoder = MAValueJsonWriter(self.object_rel_desc) # ==================== Compound object with reference testing. ==================== self.compound_object = TestObject2('object2', 234, 2.34, self.time_now, self.object1) @@ -96,61 +97,102 @@ def setUp(self): ]) # Cannot set reference in constructor because of current MARelationDescription implementation. self.compound_object_desc.children[4].reference = self.object_desc - self.compound_object_encoder = MAObjectJsonWriter(self.compound_object_desc) - def test_int_encoder(self): - self.assertEqual(self.int_encoder.write_json(self.int_value), 123) - json_string = self.int_encoder.write_json_string(self.int_value) + def test_int_encoding(self): + self.assertEqual(self.value_encoder.write_json(self.int_value, self.int_desc), 123) + json_string = self.value_encoder.write_json_string(self.int_value, self.int_desc) obj = json.loads(json_string) self.assertEqual(obj, 123) - def test_str_encoder(self): - self.assertEqual(self.str_encoder.write_json(self.str_value), 'abc') - json_string = self.str_encoder.write_json_string(self.str_value) + def test_str_encoding(self): + self.assertEqual(self.value_encoder.write_json(self.str_value, self.str_desc), 'abc') + json_string = self.value_encoder.write_json_string(self.str_value, self.str_desc) obj = json.loads(json_string) self.assertEqual(obj, 'abc') - def test_float_encoder(self): - self.assertEqual(self.float_encoder.write_json(self.float_value), 1.23) - json_string = self.float_encoder.write_json_string(self.float_value) + def test_float_encoding(self): + self.assertEqual(self.value_encoder.write_json(self.float_value, self.float_desc), 1.23) + json_string = self.value_encoder.write_json_string(self.float_value, self.float_desc) obj = json.loads(json_string) self.assertEqual(obj, 1.23) - def test_date_encoder(self): - self.assertEqual(self.date_encoder.write_json(self.date_value), self.time_now.isoformat()) - json_string = self.date_encoder.write_json_string(self.date_value) + def test_date_encoding(self): + self.assertEqual(self.value_encoder.write_json(self.date_value, self.date_desc), self.time_now.isoformat()) + json_string = self.value_encoder.write_json_string(self.date_value, self.date_desc) obj = json.loads(json_string) self.assertEqual(obj, self.time_now.isoformat()) - def test_scalar_encoder(self): - self.assertEqual(self.scalar_rel_encoder.write_json(self.scalar_rel_value), self.int_value) - json_string = self.scalar_rel_encoder.write_json_string(self.scalar_rel_value) + def test_error_relation_description(self): + with self.assertRaises(TypeError): + self.value_encoder.write_json(self.scalar_rel_value, self.scalar_rel_desc) + with self.assertRaises(TypeError): + self.value_encoder.write_json_string(self.scalar_rel_value, self.scalar_rel_desc) + + # !TODO add test_error_container_description + + def test_error_object_value(self): + with self.assertRaises(TypeError): + self.value_encoder.write_json(self.object1, self.object_desc) + with self.assertRaises(TypeError): + self.value_encoder.write_json_string(self.object1, self.object_desc) + + ''' + Think whether we need this test. + def test_scalar_rel_encoding(self): + self.assertEqual(self.object_encoder.write_json(self.scalar_rel_value, self.scalar_rel_desc), self.int_value) + json_string = self.object_encoder.write_json_string(self.scalar_rel_value, self.scalar_rel_desc) obj = json.loads(json_string) self.assertEqual(obj, self.int_value) + ''' - def test_object_encoder(self): - self.assertEqual(self.object_encoder.write_json(self.object1), - {'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat()}) - json_string = self.object_encoder.write_json_string(self.object1) + def test_object_encoding(self): + self.assertEqual( + self.object_encoder.write_json(self.object1, self.object_desc), + {'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat()} + ) + json_string = self.object_encoder.write_json_string(self.object1, self.object_desc) obj = json.loads(json_string) - self.assertEqual(obj, {"name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}"}) + self.assertEqual( + obj, + {"name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}"} + ) + ''' + Think whether we need this test. def test_object_rel_encoder(self): - self.assertEqual(self.object_rel_encoder.write_json(self.object1), - {'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat()}) + self.assertEqual( + self.object_rel_encoder.write_json(self.object1), + {'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat()}) json_string = self.object_rel_encoder.write_json_string(self.object1) obj = json.loads(json_string) - self.assertEqual(obj, {"name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}"}) + self.assertEqual( + obj, + {"name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}"} + ) + ''' def test_compound_object_encoder(self): - self.assertEqual(self.compound_object_encoder.write_json(self.compound_object), - {'name': 'object2', 'int_value': 234, 'float_value': 2.34, 'date_value': self.time_now.isoformat(), - 'ref_object': {'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat()}}) - json_string = self.compound_object_encoder.write_json_string(self.compound_object) + self.assertEqual( + self.object_encoder.write_json(self.compound_object, self.compound_object_desc), + { + 'name': 'object2', 'int_value': 234, 'float_value': 2.34, 'date_value': self.time_now.isoformat(), + 'ref_object': { + 'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat() + } + } + ) + json_string = self.object_encoder.write_json_string(self.compound_object, self.compound_object_desc) obj = json.loads(json_string) - self.assertEqual(obj, {"name": "object2", "int_value": 234, "float_value": 2.34, "date_value": f"{self.time_now.isoformat()}", - "ref_object": {"name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}"}}) - + self.assertEqual( + obj, + { + "name": "object2", "int_value": 234, "float_value": 2.34, "date_value": f"{self.time_now.isoformat()}", + "ref_object": { + "name": "object1", "int_value": 123, "float_value": 1.23, + "date_value": f"{self.time_now.isoformat()}" + } + } + ) # if __name__ == "__main__": # diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index 559f4076..c75d8b12 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -1,110 +1,155 @@ import json -from typing import Dict, Any +from typing import Dict, Any, Union, List -from MAVisitor_class import MAVisitor +from MABooleanDescription_class import MABooleanDescription +from MAStringDescription_class import MAStringDescription +from MAMagnitudeDescription_class import MAMagnitudeDescription from MAContainer_class import MAContainer from MADescription_class import MADescription from MAReferenceDescription_class import MAReferenceDescription +from MAVisitor_class import MAVisitor + class MAValueJsonWriter(MAVisitor): """Encodes the value described by the descriptions into JSON.""" - def __init__(self, description: MADescription): - if description.isContainer(): - raise TypeError( - "MAValueJsonWriter cannot encode using container description. Only scalar values are allowed. Use MAObjectJsonWriter instead." - ) - self._description = description + def __init__(self): self._model = None self._json = None - def write_json(self, model) -> Any: + @staticmethod + def _test_jsonable(src: Any) -> Any: + try: + json.dumps(src) + except (ValueError, TypeError): + # just re-raise the exception + raise + else: + return src + + def write_json(self, model: Any, description: MADescription) -> Any: self._model = model - self._json = self._description.undefinedValue - self.visit(self._description) + self._json = self._test_jsonable(description.undefinedValue) + self.visit(description) return self._json - def write_json_string(self, model) -> str: - return json.dumps(self.write_json(model)) + def write_json_string(self, model: Any, description: MADescription) -> str: + return json.dumps(self.write_json(model, description)) def visit(self, description: MADescription): if self._model != description.undefinedValue: super().visit(description) def visitElementDescription(self, description: MADescription): - self._json = description.accessor.read(self._model) + self._json = self._test_jsonable(description.accessor.read(self._model)) def visitDateAndTimeDescription(self, description: MADescription): value = description.accessor.read(self._model) - self._json = value.isoformat() if value else None - - def visitMagnitudeDescription(self, description: MADescription): - # !TODO Override exact visit methods like visitDateAndTimeDescription for each type of magnitude when they are defined. - self._json = description.accessor.read(self._model) + self._json = self._test_jsonable(value.isoformat() if value else None) def visitReferenceDescription(self, description: MAReferenceDescription): - if description.reference.isContainer(): - # referenced value is an object - nested_encoder = MAObjectJsonWriter(description.reference) - else: - # referenced value is a scalar value - nested_encoder = MAValueJsonWriter(description.reference) - self._json = nested_encoder.write_json(description.accessor.read(self._model)) + raise TypeError( + "MAValueJsonWriter cannot encode using reference description." + " Only scalar values are allowed. Use MAObjectJsonWriter instead." + ) + def visitContainer(self, description: MAContainer): + raise TypeError( + "MAValueJsonWriter cannot encode using container description." + " Only scalar values are allowed. Use MAObjectJsonWriter instead." + ) class MAObjectJsonWriter(MAVisitor): """Encodes the object described by the descriptions into JSON.""" - def __init__(self, description: MAContainer): - if not description.isContainer(): - raise TypeError( - "MAObjectJsonWriter cannot encode using scalar description. Only container values are allowed. Use MAValueJsonWriter instead." - ) - self._description = description + def __init__(self): self._model = None self._json = None + self._value_encoder = MAValueJsonWriter() - def visit(self, description: MADescription): - if self._model != description.undefinedValue: - super().visit(description) + def _validate_name(self, description: MADescription) -> None: + name = description.name + if name is None: + raise ValueError( + f"MAObjectJsonWriter requires names for all the descriptions to construct valid Json. " + f"Found None value for {description.label}." + ) + if not isinstance(name, str): + raise ValueError( + f"MAObjectJsonWriter requires names for all the descriptions to be str to construct valid Json. " + f"Found: {type(name)}." + ) + if name in self._json: + raise ValueError( + f"MAObjectJsonWriter requires distinct names for all the descriptions to construct valid Json. " + f"Found duplicate: {name}." + ) - def write_json(self, model) -> Dict[str, Any]: + def write_json(self, model: Any, description: MADescription) -> Union[Dict, List, Any, None]: self._model = model - self._json = {} # Reset the json dict. - for elementDescription in self._description: - self.visit(elementDescription) + self._json = None + self.visit(description) return self._json - def write_json_string(self, model) -> str: - return json.dumps(self.write_json(model)) + def write_json_string(self, model: Any, description: MADescription) -> str: + return json.dumps(self.write_json(model, description)) - def visitElementDescription(self, description: MADescription): - if not description.visible: return + def _deeper(self, model: Any, description: MADescription) -> Union[Dict, List, Any, None]: + if model is None: + return None + # if isinstance(model, (int, float, str, bool, datetime)): + # Better to check the type of the description, not the model, since the model can be an object but accessor can + # return a scalar value. + if isinstance(description, (MABooleanDescription, MAMagnitudeDescription, MAStringDescription)): + return self._value_encoder.write_json(model, description) - value_encoder = MAValueJsonWriter(description) - name = description.name - if name is None: - raise ValueError(f"MAObjectJsonWriter requires names for all the descriptions to construct valid Json. Found None value for {description.label}") - if not isinstance(name, str): - raise ValueError(f"MAObjectJsonWriter requires names for all the descriptions to be str to construct valid Json. Found: {type(name)}") - if name in self._json: - raise ValueError(f"MAObjectJsonWriter requires distinct names for all the descriptions to construct valid Json. Found duplicate: {name}.") - self._json[name] = value_encoder.write_json(self._model) - + prev_json = self._json + prev_model = self._model - def visitToManyRelationDescription(self, description): - if not description.visible: return + res = self.write_json(model, description) - name = description.name - if name is None: - raise ValueError(f"MAObjectJsonWriter requires names for all the descriptions to construct valid Json. Found None value for {description.label}") - if not isinstance(name, str): - raise ValueError(f"MAObjectJsonWriter requires names for all the descriptions to be str to construct valid Json. Found: {type(name)}") - if name in self._json: - raise ValueError(f"MAObjectJsonWriter requires distinct names for all the descriptions to construct valid Json. Found duplicate: {name}.") + self._json = prev_json + self._model = prev_model + + def visit(self, description: MADescription): + if self._model == description.undefinedValue: + return + if not description.visible: + return + super().visit(description) + + def visitElementDescription(self, description: MADescription): + self._validate_name(description) + self._json[description.name] = self._value_encoder.write_json(self._model, description) + + def visitContainer(self, description: MADescription): + if not self._json: + self._json = {} + self.visitAll(description) + else: + raise Exception("Shouldn't reach visitContainer with nonempty self.json") - object_encoder = MAObjectJsonWriter(description.reference) - collection = description.accessor.read(self._model) # TODO: replace on model.readUsing or description.read (both are not implemented yet) - self._json[name] = [object_encoder.write_json(entry) for entry in collection] + def visitReferenceDescription(self, description): + self._validate_name(description) + model = description.accessor.read(self._model) + if model is None: + self._json[description.name] = None + else: + self._json[description.name] = self._deeper(model, description.reference) + + def visitMultipleOptionDescription(self, description): # MAMultipleOptionDescription is not implemented yet + self._validate_name(description) + selected_options = description.accessor.read(self._model) + if selected_options is None: + self._json[description.name] = None + else: + self._json[description.name] = {self._deeper(entry, description.reference) for entry in selected_options} + + def visitToManyRelationDescription(self, description): + self._validate_name(description) + collection = description.accessor.read(self._model) + self._json[description.name] = [ + self._deeper(entry, description.reference) for entry in collection + ] From 21e1c5d1bcfb9101edb09237f016fd51771134db Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 4 Oct 2023 19:22:32 +0300 Subject: [PATCH 4/8] MAJsonWriter visitors: need more Original tests seem to work fine. Need more tests to cover all the cases. --- Magritte/MAJsonWriter_visitors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index c75d8b12..5a555af8 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -112,6 +112,8 @@ def _deeper(self, model: Any, description: MADescription) -> Union[Dict, List, A self._json = prev_json self._model = prev_model + return res + def visit(self, description: MADescription): if self._model == description.undefinedValue: return From b6daaa20f5618f05b9eb96a45f94b6934eb66ddd Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 4 Oct 2023 22:00:14 +0300 Subject: [PATCH 5/8] Some more tests added --- Magritte/MAJsonWriter_Test.py | 109 ++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 24 deletions(-) diff --git a/Magritte/MAJsonWriter_Test.py b/Magritte/MAJsonWriter_Test.py index e004e9f4..fa2cc74b 100644 --- a/Magritte/MAJsonWriter_Test.py +++ b/Magritte/MAJsonWriter_Test.py @@ -10,6 +10,8 @@ from accessors.MAIdentityAccessor_class import MAIdentityAccessor from MAIntDescription_class import MAIntDescription from MARelationDescription_class import MARelationDescription +from MAToOneRelationDescription_class import MAToOneRelationDescription +from MAToManyRelationDescription_class import MAToManyRelationDescription from MAStringDescription_class import MAStringDescription from MAJsonWriter_visitors import MAValueJsonWriter, MAObjectJsonWriter @@ -31,6 +33,15 @@ def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime self.ref_object = ref_object +class TestObject3: + def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime, ref_objects: list[TestObject1]): + self.name = name + self.int_value = int_val + self.date_value = date_val + self.float_value = float_val + self.ref_objects = ref_objects + + class MAJsonWriter_Test(TestCase): def setUp(self): # ==================== Encoders for testing. ==================== @@ -60,14 +71,13 @@ def setUp(self): # ==================== Scalar relation value testing. ==================== self.scalar_rel_value = self.int_value - self.scalar_rel_desc = MARelationDescription( - name='TestScalarRel', label='Test Scalar Relation', accessor=MAIdentityAccessor() + self.scalar_rel_desc = MAToOneRelationDescription( + name='TestScalarRel', label='Test Scalar Relation', accessor=MAIdentityAccessor(), reference=self.int_desc ) - # Cannot set reference in constructor because of current MARelationDescription implementation. - self.scalar_rel_desc.reference = self.int_desc # ==================== Object encoding testing. ==================== self.object1 = TestObject1('object1', 123, 1.23, self.time_now) + self.object2 = TestObject1('object2', 234, 2.34, self.time_now) self.object_desc = MAContainer() self.object_desc.setChildren( [ @@ -79,24 +89,43 @@ def setUp(self): ) # ==================== Object reference value testing. ==================== - self.object_rel_desc = MARelationDescription( + self.object_rel_desc = MAToOneRelationDescription( name='TestObjectRel', label='Test Object Relation', accessor=MAIdentityAccessor() - ) + ) # Cannot set reference in constructor because of current MARelationDescription implementation. self.object_rel_desc.reference = self.object_desc - # ==================== Compound object with reference testing. ==================== - self.compound_object = TestObject2('object2', 234, 2.34, self.time_now, self.object1) + # ==================== Compound object with to-one relation testing. ==================== + self.compound_object = TestObject2('object3', 345, 3.45, self.time_now, self.object1) self.compound_object_desc = MAContainer() - self.compound_object_desc.setChildren([ - MAStringDescription(label='Name', default='', accessor='name'), - MAIntDescription(label='Int Value', default=0, accessor='int_value'), - MAFloatDescription(label='Float Value', default=0.0, accessor='float_value'), - MADateAndTimeDescription(label='Date Value', default=self.time_now, accessor='date_value'), - MARelationDescription(label='Referenced Object', accessor='ref_object'), - ]) - # Cannot set reference in constructor because of current MARelationDescription implementation. - self.compound_object_desc.children[4].reference = self.object_desc + self.compound_object_desc.setChildren( + [ + MAStringDescription(label='Name', default='', accessor='name'), + MAIntDescription(label='Int Value', default=0, accessor='int_value'), + MAFloatDescription(label='Float Value', default=0.0, accessor='float_value'), + MADateAndTimeDescription(label='Date Value', default=self.time_now, accessor='date_value'), + MAToOneRelationDescription( + label='Referenced Object', accessor='ref_object', reference=self.object_desc + ), + ] + ) + + # ==================== Compound object with to-many relation testing. ==================== + self.compound_object2 = TestObject3( + 'object4', 456, 4.56, self.time_now, [self.object1, self.object2] + ) + self.compound_object2_desc = MAContainer() + self.compound_object2_desc.setChildren( + [ + MAStringDescription(label='Name', default='', accessor='name'), + MAIntDescription(label='Int Value', default=0, accessor='int_value'), + MAFloatDescription(label='Float Value', default=0.0, accessor='float_value'), + MADateAndTimeDescription(label='Date Value', default=self.time_now, accessor='date_value'), + MAToManyRelationDescription( + label='Referenced Objects', accessor='ref_objects', reference=self.object_desc + ), + ] + ) def test_int_encoding(self): self.assertEqual(self.value_encoder.write_json(self.int_value, self.int_desc), 123) @@ -128,22 +157,18 @@ def test_error_relation_description(self): with self.assertRaises(TypeError): self.value_encoder.write_json_string(self.scalar_rel_value, self.scalar_rel_desc) - # !TODO add test_error_container_description - def test_error_object_value(self): with self.assertRaises(TypeError): self.value_encoder.write_json(self.object1, self.object_desc) with self.assertRaises(TypeError): self.value_encoder.write_json_string(self.object1, self.object_desc) - ''' - Think whether we need this test. + # Think whether we need this test. def test_scalar_rel_encoding(self): self.assertEqual(self.object_encoder.write_json(self.scalar_rel_value, self.scalar_rel_desc), self.int_value) json_string = self.object_encoder.write_json_string(self.scalar_rel_value, self.scalar_rel_desc) obj = json.loads(json_string) self.assertEqual(obj, self.int_value) - ''' def test_object_encoding(self): self.assertEqual( @@ -175,7 +200,7 @@ def test_compound_object_encoder(self): self.assertEqual( self.object_encoder.write_json(self.compound_object, self.compound_object_desc), { - 'name': 'object2', 'int_value': 234, 'float_value': 2.34, 'date_value': self.time_now.isoformat(), + 'name': 'object3', 'int_value': 345, 'float_value': 3.45, 'date_value': self.time_now.isoformat(), 'ref_object': { 'name': 'object1', 'int_value': 123, 'float_value': 1.23, 'date_value': self.time_now.isoformat() } @@ -186,7 +211,7 @@ def test_compound_object_encoder(self): self.assertEqual( obj, { - "name": "object2", "int_value": 234, "float_value": 2.34, "date_value": f"{self.time_now.isoformat()}", + "name": "object3", "int_value": 345, "float_value": 3.45, "date_value": f"{self.time_now.isoformat()}", "ref_object": { "name": "object1", "int_value": 123, "float_value": 1.23, "date_value": f"{self.time_now.isoformat()}" @@ -194,6 +219,42 @@ def test_compound_object_encoder(self): } ) + def test_compound_object2_encoder(self): + self.assertEqual( + self.object_encoder.write_json(self.compound_object2, self.compound_object2_desc), + { + 'name': 'object4', 'int_value': 456, 'float_value': 4.56, 'date_value': self.time_now.isoformat(), + 'ref_objects': [ + { + 'name': 'object1', 'int_value': 123, 'float_value': 1.23, + 'date_value': self.time_now.isoformat() + }, + { + 'name': 'object2', 'int_value': 234, 'float_value': 2.34, + 'date_value': self.time_now.isoformat() + } + ] + } + ) + json_string = self.object_encoder.write_json_string(self.compound_object2, self.compound_object2_desc) + obj = json.loads(json_string) + self.assertEqual( + obj, + { + "name": "object4", "int_value": 456, "float_value": 4.56, "date_value": f"{self.time_now.isoformat()}", + "ref_objects": [ + { + "name": "object1", "int_value": 123, "float_value": 1.23, + "date_value": f"{self.time_now.isoformat()}" + }, + { + "name": "object2", "int_value": 234, "float_value": 2.34, + "date_value": f"{self.time_now.isoformat()}" + } + ] + } + ) + # if __name__ == "__main__": # # # ==================== Scalar values testing. ==================== From e7d2a4f78e9e937411ef1356b704b5543b72663e Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 5 Oct 2023 13:50:22 +0300 Subject: [PATCH 6/8] Minor encoder tuning for edge cases + some more tests --- Magritte/MAJsonWriter_Test.py | 148 +++++++++++------------------- Magritte/MAJsonWriter_visitors.py | 22 +++-- 2 files changed, 70 insertions(+), 100 deletions(-) diff --git a/Magritte/MAJsonWriter_Test.py b/Magritte/MAJsonWriter_Test.py index fa2cc74b..57604b9a 100644 --- a/Magritte/MAJsonWriter_Test.py +++ b/Magritte/MAJsonWriter_Test.py @@ -3,13 +3,12 @@ from unittest import TestCase import json -from accessors.MAAttrAccessor_class import MAAttrAccessor +from MAOptionDescription_class import MAOptionDescription from MAContainer_class import MAContainer from MADateAndTimeDescription_class import MADateAndTimeDescription from MAFloatDescription_class import MAFloatDescription from accessors.MAIdentityAccessor_class import MAIdentityAccessor from MAIntDescription_class import MAIntDescription -from MARelationDescription_class import MARelationDescription from MAToOneRelationDescription_class import MAToOneRelationDescription from MAToManyRelationDescription_class import MAToManyRelationDescription from MAStringDescription_class import MAStringDescription @@ -42,6 +41,17 @@ def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime self.ref_objects = ref_objects +class TestGlossary: + def __init__(self, name: str): + self.name = name + + +class TestObject4: + def __init__(self, name: str, selection: TestGlossary): + self.name = name + self.selection = selection + + class MAJsonWriter_Test(TestCase): def setUp(self): # ==================== Encoders for testing. ==================== @@ -88,12 +98,14 @@ def setUp(self): ] ) + ''' # ==================== Object reference value testing. ==================== self.object_rel_desc = MAToOneRelationDescription( name='TestObjectRel', label='Test Object Relation', accessor=MAIdentityAccessor() ) # Cannot set reference in constructor because of current MARelationDescription implementation. self.object_rel_desc.reference = self.object_desc + ''' # ==================== Compound object with to-one relation testing. ==================== self.compound_object = TestObject2('object3', 345, 3.45, self.time_now, self.object1) @@ -127,6 +139,28 @@ def setUp(self): ] ) + # ==================== Object with option testing. ==================== + self.glossary1 = TestGlossary('glossary1') + self.glossary2 = TestGlossary('glossary2') + self.glossary_desc = MAContainer() + self.glossary_desc.setChildren( + [ + MAStringDescription(label='Name', default='', accessor='name'), + ] + ) + + self.object4 = TestObject4('object4', self.glossary1) + self.object4_desc = MAContainer() + self.object4_desc.setChildren( + [ + MAStringDescription(label='Name', default='', accessor='name'), + MAOptionDescription( + label='Selection', accessor='selection', reference=self.glossary_desc, + options=[self.glossary1, self.glossary2] + ), + ] + ) + def test_int_encoding(self): self.assertEqual(self.value_encoder.write_json(self.int_value, self.int_desc), 123) json_string = self.value_encoder.write_json_string(self.int_value, self.int_desc) @@ -163,12 +197,14 @@ def test_error_object_value(self): with self.assertRaises(TypeError): self.value_encoder.write_json_string(self.object1, self.object_desc) + ''' # Think whether we need this test. def test_scalar_rel_encoding(self): self.assertEqual(self.object_encoder.write_json(self.scalar_rel_value, self.scalar_rel_desc), self.int_value) json_string = self.object_encoder.write_json_string(self.scalar_rel_value, self.scalar_rel_desc) obj = json.loads(json_string) self.assertEqual(obj, self.int_value) + ''' def test_object_encoding(self): self.assertEqual( @@ -183,7 +219,7 @@ def test_object_encoding(self): ) ''' - Think whether we need this test. + # Think whether we need this test. def test_object_rel_encoder(self): self.assertEqual( self.object_rel_encoder.write_json(self.object1), @@ -255,94 +291,18 @@ def test_compound_object2_encoder(self): } ) -# if __name__ == "__main__": -# -# # ==================== Scalar values testing. ==================== -# print(" ==================== Scalar values testing. ====================") -# int_value = 123 -# int_desc = MAIntDescription(name='TestInt', label='Test Int', default=0, accessor=MAIdentityAccessor()) -# int_encoder = MAValueJsonWriter(int_desc) -# print(f"jsonable value for int_value: {int_encoder.write_json(int_value)}.") -# print(f"json string for int_value: {int_encoder.write_json_string(int_value)}.") -# -# str_value = 'abc' -# str_desc = MAStringDescription(name='TestString', label='Test String', default='', accessor=MAIdentityAccessor()) -# str_encoder = MAValueJsonWriter(str_desc) -# print(f"jsonable value for str_value: {str_encoder.write_json(str_value)}.") -# print(f"json string for str_value: {str_encoder.write_json_string(str_value)}.") -# -# float_value = 1.23 -# float_desc = MAFloatDescription(name='TestFloat', label='Test Float', default=0.0, accessor=MAIdentityAccessor()) -# float_encoder = MAValueJsonWriter(float_desc) -# print(f"jsonable value for float_value: {float_encoder.write_json(float_value)}.") -# print(f"json string for float_value: {float_encoder.write_json_string(float_value)}.") -# -# date_value = datetime.now() -# date_desc = MADateAndTimeDescription( -# name='TestDate', label='Test Date', default=datetime.now(), accessor=MAIdentityAccessor() -# ) -# date_encoder = MAValueJsonWriter(date_desc) -# print(f"jsonable value for date_value: {date_encoder.write_json(date_value)}.") -# print(f"json string for date_value: {date_encoder.write_json_string(date_value)}.") -# -# scalar_rel_value = int_value -# scalar_rel_desc = MARelationDescription( -# name='TestScalarRel', label='Test Scalar Relation', accessor=MAIdentityAccessor() -# ) -# # Cannot set reference in constructor because of current MARelationDescription implementation. -# scalar_rel_desc.reference = int_desc -# scalar_rel_encoder = MAValueJsonWriter(scalar_rel_desc) -# print(f"jsonable value for scalar_rel_value: {scalar_rel_encoder.write_json(scalar_rel_value)}.") -# print(f"json string for scalar_rel_value: {scalar_rel_encoder.write_json_string(scalar_rel_value)}.") -# -# # ==================== Object encoding testing. ==================== -# print(" ==================== Object encoding testing. ====================") -# object1 = TestObject1('object1', 123, 1.23, datetime.now()) -# object_desc = MAContainer() -# object_desc.setChildren( -# [ -# MAStringDescription(name='name', label='Name', default='', accessor=MAAttrAccessor('name')), -# MAIntDescription(name='int_value', label='Int Value', default=0, accessor=MAAttrAccessor('int_value')), -# MAFloatDescription( -# name='float_value', label='Float Value', default=0.0, accessor=MAAttrAccessor('float_value'), -# ), -# MADateAndTimeDescription( -# name='date_value', label='Date Value', default=datetime.now(), accessor=MAAttrAccessor('date_value'), -# ), -# ] -# ) -# object_encoder = MAObjectJsonWriter(object_desc) -# print(f"jsonable value for object1: {object_encoder.write_json(object1)}.") -# print(f"json string for object1: {object_encoder.write_json_string(object1)}.") -# -# # ==================== Object reference value testing. ==================== -# print(" ==================== Object reference value testing. ====================") -# object_rel_desc = MARelationDescription( -# name='TestObjectRel', label='Test Object Relation', accessor=MAIdentityAccessor() -# ) -# # Cannot set reference in constructor because of current MARelationDescription implementation. -# object_rel_desc.reference = object_desc -# object_rel_encoder = MAValueJsonWriter(object_rel_desc) -# print(f"jsonable value for reference->object1: {object_rel_encoder.write_json(object1)}.") -# print(f"json string for reference->object1: {object_rel_encoder.write_json_string(object1)}.") -# -# # ==================== Compound object with reference testing. ==================== -# print(" ==================== Compound object with reference testing. ====================") -# compound_object = TestObject2('object2', 234, 2.34, datetime.now(), object1) -# compound_object_desc = MAContainer() -# compound_object_desc.setChildren([ -# MAStringDescription(name='name', label='Name', default='', accessor=MAAttrAccessor('name')), -# MAIntDescription(name='int_value', label='Int Value', default=0, accessor=MAAttrAccessor('int_value')), -# MAFloatDescription( -# name='float_value', label='Float Value', default=0.0, accessor=MAAttrAccessor('float_value'), -# ), -# MADateAndTimeDescription( -# name='date_value', label='Date Value', default=datetime.now(), accessor=MAAttrAccessor('date_value'), -# ), -# MARelationDescription(name='ref_object', label='Referenced Object', accessor=MAAttrAccessor('ref_object')), -# ]) -# # Cannot set reference in constructor because of current MARelationDescription implementation. -# compound_object_desc.children[4].reference = object_desc -# compound_object_encoder = MAObjectJsonWriter(compound_object_desc) -# print(f"jsonable value for compound_object: {compound_object_encoder.write_json(compound_object)}.") -# print(f"json string for compound_object: {compound_object_encoder.write_json_string(compound_object)}.") + def test_object_with_option_encoder(self): + self.assertEqual( + self.object_encoder.write_json(self.object4, self.object4_desc), + { + 'name': 'object4', 'selection': {'name': 'glossary1'} + } + ) + json_string = self.object_encoder.write_json_string(self.object4, self.object4_desc) + obj = json.loads(json_string) + self.assertEqual( + obj, + { + "name": "object4", "selection": {"name": "glossary1"} + } + ) diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index 5a555af8..29e2f9ef 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -80,7 +80,7 @@ def _validate_name(self, description: MADescription) -> None: f"MAObjectJsonWriter requires names for all the descriptions to be str to construct valid Json. " f"Found: {type(name)}." ) - if name in self._json: + if self._json and name in self._json: raise ValueError( f"MAObjectJsonWriter requires distinct names for all the descriptions to construct valid Json. " f"Found duplicate: {name}." @@ -123,6 +123,8 @@ def visit(self, description: MADescription): def visitElementDescription(self, description: MADescription): self._validate_name(description) + if not self._json: + self._json = {} self._json[description.name] = self._value_encoder.write_json(self._model, description) def visitContainer(self, description: MADescription): @@ -130,26 +132,34 @@ def visitContainer(self, description: MADescription): self._json = {} self.visitAll(description) else: - raise Exception("Shouldn't reach visitContainer with nonempty self.json") + raise Exception("Shouldn't reach visitContainer with nonempty self._json") def visitReferenceDescription(self, description): self._validate_name(description) - model = description.accessor.read(self._model) - if model is None: + if not self._json: + self._json = {} + ref_model = description.accessor.read(self._model) + if ref_model is None: self._json[description.name] = None else: - self._json[description.name] = self._deeper(model, description.reference) + self._json[description.name] = self._deeper(ref_model, description.reference) def visitMultipleOptionDescription(self, description): # MAMultipleOptionDescription is not implemented yet self._validate_name(description) + if not self._json: + self._json = {} selected_options = description.accessor.read(self._model) if selected_options is None: self._json[description.name] = None else: - self._json[description.name] = {self._deeper(entry, description.reference) for entry in selected_options} + self._json[description.name] = { + self._deeper(entry, description.reference) for entry in selected_options + } def visitToManyRelationDescription(self, description): self._validate_name(description) + if not self._json: + self._json = {} collection = description.accessor.read(self._model) self._json[description.name] = [ self._deeper(entry, description.reference) for entry in collection From cfdc32cf6d4aeeb5ed4f5fb589f7692bab983afd Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 5 Oct 2023 22:00:41 +0300 Subject: [PATCH 7/8] Handle empty collection for ToManyRelation --- Magritte/MAJsonWriter_visitors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Magritte/MAJsonWriter_visitors.py b/Magritte/MAJsonWriter_visitors.py index 29e2f9ef..1b9710b5 100644 --- a/Magritte/MAJsonWriter_visitors.py +++ b/Magritte/MAJsonWriter_visitors.py @@ -161,6 +161,8 @@ def visitToManyRelationDescription(self, description): if not self._json: self._json = {} collection = description.accessor.read(self._model) + if collection is None: + self._json[description.name] = None self._json[description.name] = [ self._deeper(entry, description.reference) for entry in collection ] From 0490f394881565494885e81bdc41ed4413b47423 Mon Sep 17 00:00:00 2001 From: Val Date: Fri, 13 Oct 2023 19:28:06 +0300 Subject: [PATCH 8/8] Modified type annotations to support python 3.8, which does not allow "list[]", "dict[]", ... --- Magritte/MAJsonWriter_Test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Magritte/MAJsonWriter_Test.py b/Magritte/MAJsonWriter_Test.py index 57604b9a..2b74503b 100644 --- a/Magritte/MAJsonWriter_Test.py +++ b/Magritte/MAJsonWriter_Test.py @@ -1,5 +1,6 @@ # ==================== For testing. ==================== from datetime import datetime +from typing import List from unittest import TestCase import json @@ -33,7 +34,7 @@ def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime class TestObject3: - def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime, ref_objects: list[TestObject1]): + def __init__(self, name: str, int_val: int, float_val: float, date_val: datetime, ref_objects: List[TestObject1]): self.name = name self.int_value = int_val self.date_value = date_val