diff --git a/odml/doc.py b/odml/doc.py index 279a9b2c..4a33ccea 100644 --- a/odml/doc.py +++ b/odml/doc.py @@ -141,3 +141,21 @@ def get_terminology_equivalent(self): return None term = terminology.load(self.repository) return term +def pprint(self, indent=2, max_depth=1, max_length=80, current_depth=0): + """ + Pretty print method to visualize Document-Section trees. + + :param indent: number of leading spaces for every child Section or Property. + :param max_depth: maximum number of hierarchical levels printed from the + starting Section. + :param max_length: maximum number of characters printed in one line. + :param current_depth: number of hierarchical levels printed from the + starting Section. + """ + doc_str = "[{} [{}] {}, sections: {}, repository: {}]".format(self.author, self.version, + self.date, len(self._sections), self.repository) + print(doc_str) + + for s in self._sections: + s.pprint(current_depth=current_depth+1, max_depth=max_depth, + indent=indent, max_length=max_length) diff --git a/odml/dtypes.py b/odml/dtypes.py index c6e27c3d..69cae553 100644 --- a/odml/dtypes.py +++ b/odml/dtypes.py @@ -62,6 +62,8 @@ def default_values(dtype): _dtype_map = {'str': 'string', 'bool': 'boolean'} +special_dtypes = ["url", "person", "text"] + def infer_dtype(value): dtype = (type(value)).__name__ diff --git a/odml/property.py b/odml/property.py index 5da97893..a120ab38 100644 --- a/odml/property.py +++ b/odml/property.py @@ -82,7 +82,7 @@ def __init__(self, name=None, values=None, parent=None, unit=None, self._values = [] self.values = values - if not values and (value or isinstance(value, bool)): + if not values and (value or isinstance(value, bool) or isinstance(value, int)): self.values = value self.parent = parent @@ -588,8 +588,10 @@ def extend(self, obj, strict=True): new_value = self._convert_value_input(obj) if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype: - raise ValueError("odml.Property.extend: " - "passed value data type does not match dtype!") + + if not (dtypes.infer_dtype(new_value[0]) == "string" and self.dtype in dtypes.special_dtypes): + raise ValueError("odml.Property.extend: passed value data type found (\"%s\") " + "does not match expected dtype \"%s\"!" % (dtypes.infer_dtype(new_value[0]), self._dtype)) if not self._validate_values(new_value): raise ValueError("odml.Property.extend: passed value(s) cannot be converted " @@ -618,8 +620,10 @@ def append(self, obj, strict=True): raise ValueError("odml.property.append: Use extend to add a list of values!") if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype: - raise ValueError("odml.Property.append: " - "passed value data type does not match dtype!") + + if not (dtypes.infer_dtype(new_value[0]) == "string" and self.dtype in dtypes.special_dtypes): + raise ValueError("odml.Property.append: passed value data type found (\"%s\") " + "does not match expected dtype \"%s\"!" % (dtypes.infer_dtype(new_value[0]), self._dtype)) if not self._validate_values(new_value): raise ValueError("odml.Property.append: passed value(s) cannot be converted " diff --git a/odml/tools/version_converter.py b/odml/tools/version_converter.py index 3d842e89..c7b22ab4 100644 --- a/odml/tools/version_converter.py +++ b/odml/tools/version_converter.py @@ -9,6 +9,8 @@ from ..info import FORMAT_VERSION from ..terminology import Terminologies, REPOSITORY_BASE +import uuid + try: unicode = unicode except NameError: @@ -194,6 +196,8 @@ def _convert(self, tree): if e.tag == "repository": self._handle_repository(e) + tree = self._check_add_ids(tree) + return tree def _handle_include(self, element): @@ -410,6 +414,41 @@ def _change_entity_name(tree, elem_map, name): elem_map[named_path] += 1 name.text += "-" + str(elem_map[named_path]) + def _check_add_ids(self, tree): + """ + Checks, whether elements (properties) possess an UUID + and adds one in case of absence. + :param tree: ElementTree of the doc + :return: ElementTree + """ + root = tree.getroot() + self._add_id(root) + for sec in root.iter("section"): + self._add_id(sec) + for prop in sec.iter("property"): + self._add_id(prop) + + return tree + + @staticmethod + def _add_id(element): + """ + Checks, whether an element possesses an ID. If yes, make sure it has + the right format. Otherwise a new UUID is created. + :param element: lxml element. + """ + oid = element.find("id") + new_id = ET.Element("id") + new_id.text = str(uuid.uuid4()) + if oid is not None: + try: + if oid.text is not None: + new_id.text = str(uuid.UUID(oid.text)) + except ValueError as e: + print(e) + element.remove(oid) + element.append(new_id) + def _log(self, msg): """ Adds the passed message to the conversion_log attribute and diff --git a/test/test_property.py b/test/test_property.py index 80a67d61..80f6c1cc 100644 --- a/test/test_property.py +++ b/test/test_property.py @@ -103,6 +103,18 @@ def test_value(self): with self.assertRaises(ValueError): _ = Property(name="Public-Key", value='(5689; 1254; 687)', dtype='2-tuple') + p3 = Property('myprop', value=0, dtype=DType.int) + self.assertEqual(p3.value, [0]) + self.assertEqual(p3.values, [0]) + + p4 = Property('myprop', value=0, dtype=DType.boolean) + self.assertEqual(p4.value, [False]) + self.assertEqual(p4.values, [False]) + + p5 = Property('myprop', value=0) + self.assertEqual(p5.value, [0]) + self.assertEqual(p5.values, [0]) + def test_value_append(self): # Test append w/o Property value or dtype prop = Property(name="append") @@ -183,6 +195,27 @@ def test_value_append(self): self.assertEqual(len(p5), 2) self.assertRaises(ValueError, p5.append, "[a, b, c]") + p6 = Property(name="prop", value=["A Abraham", "B Barnes", "C Clark"], dtype=DType.person) + p6.append("D Dickins") + self.assertEqual(len(p6), 4) + self.assertRaises(ValueError, p6.append, 1) + self.assertRaises(ValueError, p6.append, 1.3) + self.assertRaises(ValueError, p6.append, True) + + p7 = Property(name="prop", value=["https://en.wikipedia.org/wiki/Earth"], dtype=DType.url) + p7.append("https://en.wikipedia.org/wiki/Mars") + self.assertEqual(len(p7), 2) + self.assertRaises(ValueError, p7.append, 1) + self.assertRaises(ValueError, p7.append, 1.3) + self.assertRaises(ValueError, p7.append, True) + + p8 = Property(name="prop", value=["Earth is No. 3."], dtype=DType.text) + p8.append("Mars is No. 4.") + self.assertEqual(len(p8), 2) + self.assertRaises(ValueError, p8.append, 1) + self.assertRaises(ValueError, p8.append, 1.3) + self.assertRaises(ValueError, p8.append, True) + def test_value_extend(self): prop = Property(name="extend") @@ -260,6 +293,27 @@ def test_value_extend(self): with self.assertRaises(ValueError): prop.extend([6, "some text"]) + p1 = Property(name="prop", value=["A Abraham", "B Barnes", "C Clark"], dtype=DType.person) + p1.extend("D Dickins") + self.assertEqual(len(p1), 4) + self.assertRaises(ValueError, p1.extend, 1) + self.assertRaises(ValueError, p1.extend, 1.3) + self.assertRaises(ValueError, p1.extend, True) + + p2 = Property(name="prop", value=["https://en.wikipedia.org/wiki/Earth"], dtype=DType.url) + p2.extend("https://en.wikipedia.org/wiki/Mars") + self.assertEqual(len(p2), 2) + self.assertRaises(ValueError, p2.append, 1) + self.assertRaises(ValueError, p2.append, 1.3) + self.assertRaises(ValueError, p2.append, True) + + p3 = Property(name="prop", value=["Earth is No. 3."], dtype=DType.text) + p3.extend("Mars is No. 4.") + self.assertEqual(len(p3), 2) + self.assertRaises(ValueError, p3.append, 1) + self.assertRaises(ValueError, p3.append, 1.3) + self.assertRaises(ValueError, p3.append, True) + def test_get_set_value(self): values = [1, 2, 3, 4, 5] p = Property("property", value=values) diff --git a/test/test_version_converter.py b/test/test_version_converter.py index 29c28ab3..8b166e47 100644 --- a/test/test_version_converter.py +++ b/test/test_version_converter.py @@ -272,11 +272,13 @@ def test_convert_odml_file_section(self): conv_doc = vc._convert(vc._parse_xml()) root = conv_doc.getroot() + root_id = root.findall("id") + self.assertEqual(len(root_id), 1) sec = root.findall("section") self.assertEqual(len(sec), 2) # Test valid section tags. - self.assertEqual(len(sec[0]), 10) + self.assertEqual(len(sec[0]), 11) self.assertEqual(sec[0].find("name").text, "Section name") self.assertEqual(sec[0].find("type").text, "Section type") self.assertEqual(sec[0].find("definition").text, "Section definition") @@ -286,10 +288,11 @@ def test_convert_odml_file_section(self): self.assertEqual(sec[0].find("include").text, "Section include") self.assertEqual(len(sec[0].findall("property")), 2) self.assertEqual(len(sec[0].findall("section")), 1) + self.assertEqual(len(sec[0].findall("id")), 1) # Test valid subsection tags. subsec = sec[0].find("section") - self.assertEqual(len(subsec), 8) + self.assertEqual(len(subsec), 9) self.assertEqual(subsec.find("name").text, "SubSection name") self.assertEqual(subsec.find("type").text, "SubSection type") self.assertEqual(subsec.find("definition").text, "SubSection definition") @@ -298,10 +301,12 @@ def test_convert_odml_file_section(self): self.assertEqual(subsec.find("repository").text, local_url) self.assertEqual(subsec.find("include").text, "SubSection include") self.assertEqual(len(subsec.findall("property")), 1) + self.assertEqual(len(subsec.findall("id")), 1) # Test absence of non-Section tags - self.assertEqual(len(sec[1]), 1) + self.assertEqual(len(sec[1]), 2) self.assertEqual(len(sec[1].findall("name")), 1) + self.assertEqual(len(sec[1].findall("id")), 1) # Test presence of v1.0 repository tag and warning log entry doc = """ @@ -379,19 +384,21 @@ def test_convert_odml_file_property(self): self.assertEqual(sec[0].find("name").text, "Valid Property tags test") self.assertEqual(len(sec[0].findall("property")), 1) prop = sec[0].find("property") - self.assertEqual(len(prop), 5) + self.assertEqual(len(prop), 6) self.assertEqual(prop.find("name").text, "Property name") self.assertEqual(prop.find("type").text, "Property type") self.assertEqual(prop.find("definition").text, "Property definition") self.assertEqual(prop.find("dependency").text, "Property dependency") self.assertEqual(prop.find("dependencyvalue").text, "Property dependency value") + self.assertEqual(len(prop.findall("id")), 1) # Test non-import of Property w/o name self.assertEqual(len(sec[1].findall("property")), 1) # Test absence of non-Property tags prop = sec[1].find("property") - self.assertEqual(len(prop), 1) + self.assertEqual(len(prop), 2) self.assertEqual(len(prop.findall("name")), 1) + self.assertEqual(len(prop.findall("id")), 1) def test_convert_odml_file_value(self): """Test proper conversion of the odml.Value entity from @@ -511,6 +518,22 @@ def test_convert_odml_file_value(self): 3 int testIntListWhiteSpace + + + Single value with UUID + 1 + aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa + + + + Single value with none-UUID ID + 1 + + + + Single value with empty ID + + @@ -520,27 +543,33 @@ def test_convert_odml_file_value(self): vc = self.VC(file) conv_doc = vc._convert(vc._parse_xml()) root = conv_doc.getroot() + root_id = root.findall("id") + self.assertEqual(len(root_id), 1) sec = root.find("section") - self.assertEqual(len(sec), 14) + self.assertEqual(len(sec), 18) + self.assertEqual(len(sec.findall("id")), 1) # Test single value export prop = sec.findall("property")[0] - self.assertEqual(len(prop), 2) + self.assertEqual(len(prop), 3) self.assertEqual(prop.find("value").text, "1") + self.assertEqual(len(prop.findall("id")), 1) # Test multiple value export prop = sec.findall("property")[1] - self.assertEqual(len(prop), 2) + self.assertEqual(len(prop), 3) self.assertEqual(prop.find("value").text, "[1,2,3]") + self.assertEqual(len(prop.findall("id")), 1) # Test empty value export prop = sec.findall("property")[2] - self.assertEqual(len(prop), 1) + self.assertEqual(len(prop), 2) self.assertEqual(prop.find("name").text, "Empty value export") + self.assertEqual(len(prop.findall("id")), 1) # Test valid Value tags prop = sec.findall("property")[3] - self.assertEqual(len(prop), 7) + self.assertEqual(len(prop), 8) self.assertEqual(prop.find("value").text, "0.1") self.assertEqual(prop.find("type").text, "float") self.assertEqual(prop.find("uncertainty").text, "0.05") @@ -548,58 +577,88 @@ def test_convert_odml_file_value(self): self.assertEqual(prop.find("value_origin").text, "raw.txt") self.assertEqual(prop.find("reference").text, "Value reference") self.assertEqual(len(prop.findall("filename")), 0) + self.assertEqual(len(prop.findall("id")), 1) # Test valid multiple Value tag export prop = sec.findall("property")[4] - self.assertEqual(len(prop), 7) + self.assertEqual(len(prop), 8) self.assertEqual(prop.find("value").text, "[0.1,0.2,3]") self.assertEqual(prop.find("type").text, "float") self.assertEqual(prop.find("uncertainty").text, "0.05") self.assertEqual(prop.find("unit").text, "mV") self.assertEqual(prop.find("value_origin").text, "raw.txt") self.assertEqual(prop.find("reference").text, "Value reference") + self.assertEqual(len(prop.findall("id")), 1) # Test non-export of invalid Value tags prop = sec.findall("property")[5] - self.assertEqual(len(prop), 1) + self.assertEqual(len(prop), 2) self.assertEqual(len(prop.findall("name")), 1) + self.assertEqual(len(prop.findall("id")), 1) # Test dtype 'binary' replacement prop = sec.findall("property")[6] self.assertEqual(prop.find("name").text, "Unsupported binary value type replace") self.assertEqual(prop.find("type").text, "text") + self.assertEqual(len(prop.findall("id")), 1) prop = sec.findall("property")[7] self.assertEqual(prop.find("name").text, "Unsupported binary value dtype replace") self.assertEqual(prop.find("type").text, "text") + self.assertEqual(len(prop.findall("id")), 1) # Test single string value with commata prop = sec.findall("property")[8] self.assertEqual(prop.find("name").text, "testSingleString") self.assertEqual(prop.find("value").text, "Single, string, value, with, many, commata.") + self.assertEqual(len(prop.findall("id")), 1) # Test string list import prop = sec.findall("property")[9] self.assertEqual(prop.find("name").text, "testStringList") self.assertEqual(prop.find("value").text, "[A,B,C]") + self.assertEqual(len(prop.findall("id")), 1) # Test single string values wrapping whitespace removal prop = sec.findall("property")[10] self.assertEqual(prop.find("name").text, "testStringWhiteSpace") self.assertEqual(prop.find("value").text, "Single string value with wrapping whitespace") + self.assertEqual(len(prop.findall("id")), 1) # Test multiple string values with wrapping whitespace removal prop = sec.findall("property")[11] self.assertEqual(prop.find("name").text, "testStringListWhiteSpace") self.assertEqual(prop.find("value").text, "[Multiple Strings,with wrapping,Whitespace]") + self.assertEqual(len(prop.findall("id")), 1) # Test multiple int values with wrapping whitespaces prop = sec.findall("property")[12] self.assertEqual(prop.find("name").text, "testIntListWhiteSpace") self.assertEqual(prop.find("type").text, "int") self.assertEqual(prop.find("value").text, "[1,2,3]") + self.assertEqual(len(prop.findall("id")), 1) + + # Test single value export + prop = sec.findall("property")[13] + self.assertEqual(len(prop), 3) + self.assertEqual(prop.find("value").text, "1") + self.assertEqual(len(prop.findall("id")), 1) + self.assertEqual(prop.find("id").text, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + + # Test single value export + prop = sec.findall("property")[14] + self.assertEqual(len(prop), 2) + self.assertEqual(len(prop.findall("id")), 1) + self.assertNotEqual(prop.find("id").text, "1") + + # Test single value export + prop = sec.findall("property")[15] + self.assertEqual(len(prop), 2) + self.assertEqual(len(prop.findall("id")), 1) + self.assertNotEqual(prop.find("id").text, "1") + def test_parse_dict_document(self): # Test appending tags; not appending empty sections