From d9da141f97fb315dca9b4e6287d442797f2ae6df Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 21:38:01 +0200 Subject: [PATCH 01/24] [info] Update version number --- odml/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odml/info.json b/odml/info.json index 747c5fa9..213697c3 100644 --- a/odml/info.json +++ b/odml/info.json @@ -1,5 +1,5 @@ { - "VERSION": "1.4.5", + "VERSION": "1.4.6", "FORMAT_VERSION": "1.1", "AUTHOR": "Hagen Fritsch, Jan Grewe, Christian Kellner, Achilleas Koutsou, Michael Sonntag, Lyuba Zehl", "COPYRIGHT": "(c) 2011-2020, German Neuroinformatics Node", From 710ccbd31f4cbdec423550aa434871fa9cc1d81d Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:16:51 +0200 Subject: [PATCH 02/24] [format] Add property cardinality attribute --- odml/format.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/odml/format.py b/odml/format.py index 47825a2f..08113f88 100644 --- a/odml/format.py +++ b/odml/format.py @@ -117,7 +117,8 @@ class Property(Format): 'uncertainty': 0, 'reference': 0, 'type': 0, - 'value_origin': 0 + 'value_origin': 0, + 'val_cardinality': 0 } _map = { 'dependencyvalue': 'dependency_value', From 9822545ec293c6552286b4510e0ecbcff416a585 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:19:39 +0200 Subject: [PATCH 03/24] [property] Add cardinality to init --- odml/property.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index 6c5caf0e..919daba7 100644 --- a/odml/property.py +++ b/odml/property.py @@ -82,6 +82,10 @@ class BaseProperty(base.BaseObject): :param oid: object id, UUID string as specified in RFC 4122. If no id is provided, an id will be generated and assigned. An id has to be unique within an odML Document. + :param val_cardinality: Value cardinality defines how many values are allowed for this Property. + By default unlimited values can be set. + A required number of values can be set by assigning a tuple of the + format "(min, max)". :param value: Legacy code to the 'values' attribute. If 'values' is provided, any data provided via 'value' will be ignored. """ @@ -91,7 +95,7 @@ class BaseProperty(base.BaseObject): def __init__(self, name=None, values=None, parent=None, unit=None, uncertainty=None, reference=None, definition=None, dependency=None, dependency_value=None, dtype=None, - value_origin=None, oid=None, value=None): + value_origin=None, oid=None, val_cardinality=None, value=None): try: if oid is not None: @@ -129,6 +133,8 @@ def __init__(self, name=None, values=None, parent=None, unit=None, self.parent = parent + self._val_cardinality = val_cardinality + for err in validation.Validation(self).errors: if err.is_error: msg = "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg) From d11c7a9b136798f3cf225c7895823ca7a01833e5 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:21:31 +0200 Subject: [PATCH 04/24] [property] Add cardinality accessor methods --- odml/property.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index 919daba7..33c3e0a1 100644 --- a/odml/property.py +++ b/odml/property.py @@ -133,7 +133,8 @@ def __init__(self, name=None, values=None, parent=None, unit=None, self.parent = parent - self._val_cardinality = val_cardinality + self._val_cardinality = None + self.val_cardinality = val_cardinality for err in validation.Validation(self).errors: if err.is_error: @@ -513,6 +514,34 @@ def dependency_value(self, new_value): new_value = None self._dependency_value = new_value + @property + def val_cardinality(self): + """ + The value cardinality of a Property. It defines how many values + are minimally required and how many values should be maximally + stored. Use 'values_set_cardinality' to set. + """ + return self._val_cardinality + + @val_cardinality.setter + def val_cardinality(self, new_value): + """ + Sets the values cardinality of a Property. + + The following cardinality cases are supported: + (n, n) - default, no restriction + (d, n) - minimally d entries, no maximum + (n, d) - maximally d entries, no minimum + (d, d) - minimally d entries, maximally d entries + + Only positive integers are supported. 'None' is used to denote + no restrictions on a maximum or minimum. + + :param new_value: Can be either 'None', a positive integer, which will set the maximum + or an integer 2-tuple of the format '(min, max)'. + """ + self._val_cardinality = new_value + def remove(self, value): """ Remove a value from this property. Only the first encountered From 61f71421220cedf302be8ec69cc08520e2b4160c Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:22:05 +0200 Subject: [PATCH 05/24] [property] Properly handle cardinality setter --- odml/property.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index 33c3e0a1..824de260 100644 --- a/odml/property.py +++ b/odml/property.py @@ -540,7 +540,41 @@ def val_cardinality(self, new_value): :param new_value: Can be either 'None', a positive integer, which will set the maximum or an integer 2-tuple of the format '(min, max)'. """ - self._val_cardinality = new_value + invalid_input = False + + # Empty values reset the cardinality to None. + if not new_value or new_value == (None, None): + self._val_cardinality = None + + # Providing a single integer sets the maximum value in a tuple. + elif isinstance(new_value, int) and new_value > 0: + self._val_cardinality = (None, new_value) + + # Only integer 2-tuples of the format '(min, max)' are supported to set the cardinality + elif isinstance(new_value, tuple) and len(new_value) == 2: + v_min = new_value[0] + v_max = new_value[1] + + min_int = isinstance(v_min, int) and v_min >= 0 + max_int = isinstance(v_max, int) and v_max >= 0 + + if max_int and min_int and v_max > v_min: + self._val_cardinality = (v_min, v_max) + + elif max_int and not v_min: + self._val_cardinality = (None, v_max) + + elif min_int and not v_max: + self._val_cardinality = (v_min, None) + + else: + invalid_input = True + else: + invalid_input = True + + if invalid_input: + msg = "Can only assign single int or int-tuples of the format '(min, max)'" + raise ValueError(msg) def remove(self, value): """ From 0d69b54096db7ffb8de98d9c6c569c7d62ac08f0 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:22:33 +0200 Subject: [PATCH 06/24] [property] Add set cardinality convenience method --- odml/property.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/odml/property.py b/odml/property.py index 824de260..689f7f73 100644 --- a/odml/property.py +++ b/odml/property.py @@ -576,6 +576,17 @@ def val_cardinality(self, new_value): msg = "Can only assign single int or int-tuples of the format '(min, max)'" raise ValueError(msg) + def values_set_cardinality(self, min_val=None, max_val=None): + """ + Sets the values cardinality of a Property. + + :param min_val: Required minimal number of values elements. None denotes + no restrictions on values elements minimum. Default is None. + :param max_val: Allowed maximal number of values elements. None denotes + no restrictions on values elements maximum. Default is None. + """ + self.val_cardinality = (min_val, max_val) + def remove(self, value): """ Remove a value from this property. Only the first encountered From 06e53bd03600ee9dda6bbac06661ccf5d6ce98d4 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:23:17 +0200 Subject: [PATCH 07/24] [test/dumper] Fix stdout testing issue --- test/test_dumper.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/test/test_dumper.py b/test/test_dumper.py index 40476b6a..24db6b50 100644 --- a/test/test_dumper.py +++ b/test/test_dumper.py @@ -1,21 +1,18 @@ import unittest import sys -import odml - try: from StringIO import StringIO except ImportError: from io import StringIO +import odml + +from odml.tools.dumper import dump_doc + class TestTypes(unittest.TestCase): def setUp(self): - # Capture the output printed by the functions to STDOUT, and use it for - # testing purposes. - self.captured_stdout = StringIO() - sys.stdout = self.captured_stdout - s_type = "type" self.doc = odml.Document(author='Rave', version='1.0') @@ -38,10 +35,19 @@ def setUp(self): self.doc.append(s1) def test_dump_doc(self): + # Capture the output printed by the functions to STDOUT, and use it for + # testing purposes. It needs to be reset after the capture. + captured_stdout = StringIO() + sys.stdout = captured_stdout + # This test dumps the whole document and checks it word by word. - # If possible, maybe some better way of testing this ? - odml.tools.dumper.dump_doc(self.doc) - output = [x.strip() for x in self.captured_stdout.getvalue().split('\n') if x] + # If possible, maybe some better way of testing this? + dump_doc(self.doc) + output = [x.strip() for x in captured_stdout.getvalue().split('\n') if x] + + # Reset stdout + sys.stdout = sys.__stdout__ + expected_output = [] expected_output.append("*Cell (type='type')") expected_output.append(":Type (values=Rechargeable, dtype='string')") @@ -50,10 +56,7 @@ def test_dump_doc(self): expected_output.append("*Electrode (type='type')") expected_output.append(":Material (values=Nickel, dtype='string')") expected_output.append(":Models (values=[AA,AAA], dtype='string')") + self.assertEqual(len(output), len(expected_output)) for i in range(len(output)): self.assertEqual(output[i], expected_output[i]) - - # Discard the document output from stdout stream - self.captured_stdout.seek(0) - self.captured_stdout.truncate(0) From efaa298445e353bf7994b50bab1a12dd36f5d7f5 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:41:18 +0200 Subject: [PATCH 08/24] [tools/xmlparser] Add parse_cardinality method --- odml/tools/xmlparser.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/odml/tools/xmlparser.py b/odml/tools/xmlparser.py index 7e0bcad0..e23359af 100644 --- a/odml/tools/xmlparser.py +++ b/odml/tools/xmlparser.py @@ -40,6 +40,43 @@ """ +def parse_cardinality(val): + """ + Parses an odml specific cardinality from a string. + + If the string content is valid, returns an appropriate tuple. + Returns None if the string is empty or the content cannot be + properly parsed. + + :param val: string + :return: None or 2-tuple + """ + if not val: + return None + + # Remove parenthesis and split on comma + parsed_vals = val.strip()[1:-1].split(",") + if len(parsed_vals) == 2: + min_val = parsed_vals[0].strip() + max_val = parsed_vals[1].strip() + + min_int = min_val.isdigit() and int(min_val) >= 0 + max_int = max_val.isdigit() and int(max_val) >= 0 + + if min_int and max_int and int(max_val) > int(min_val): + return int(min_val), int(max_val) + + if min_int and max_val == "None": + return int(min_val), None + + if max_int and min_val == "None": + return None, int(max_val) + + # Todo we were not able to properly parse the current cardinality + # add an appropriate Error/Warning + return None + + def to_csv(val): """ Modifies odML values for serialization to strings and files. @@ -410,6 +447,9 @@ def parse_tag(self, root, fmt, insert_children=True): if tag == "values" and curr_text: content = from_csv(node.text) arguments[tag] = content + # Special handling of cardinality + elif tag.endswith("_cardinality") and curr_text: + arguments[tag] = parse_cardinality(node.text) else: arguments[tag] = curr_text else: From 59835f95fd6be36d63b8dce4f94454c754aa66f5 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 30 Mar 2020 23:41:29 +0200 Subject: [PATCH 09/24] [tools/dictparser] Add parse_cardinality method --- odml/tools/dict_parser.py | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/odml/tools/dict_parser.py b/odml/tools/dict_parser.py index 4b22783c..947667cd 100644 --- a/odml/tools/dict_parser.py +++ b/odml/tools/dict_parser.py @@ -8,6 +8,47 @@ from .parser_utils import InvalidVersionException, ParserException, odml_tuple_export +def parse_cardinality(vals): + """ + Parses an odml specific cardinality from an input value. + + If the input content is valid, returns an appropriate tuple. + Returns None if the input is empty or the content cannot be + properly parsed. + + :param vals: list or tuple + :return: None or 2-tuple + """ + if not vals: + return None + + if isinstance(vals, (list, tuple)) and len(vals) == 2: + min_val = vals[0] + max_val = vals[1] + + if min_val is None or str(min_val).strip() == "None": + min_val = None + + if max_val is None or str(max_val).strip() == "None": + max_val = None + + min_int = isinstance(min_val, int) and min_val >= 0 + max_int = isinstance(max_val, int) and max_val >= 0 + + if min_int and max_int and max_val > min_val: + return min_val, max_val + + if min_int and not max_val: + return min_val, None + + if max_int and not min_val: + return None, max_val + + # We were not able to properly parse the current cardinality, so add + # an appropriate Error/Warning once the reader 'ignore_errors' option has been implemented. + return None + + class DictWriter: """ A writer to parse an odml.Document to a Python dictionary object equivalent. @@ -255,8 +296,12 @@ def parse_properties(self, props_list): for i in _property: attr = self.is_valid_attribute(i, odmlfmt.Property) if attr: + content = _property[attr] + if attr.endswith("_cardinality"): + content = parse_cardinality(content) + # Make sure to always use the correct odml format attribute name - prop_attrs[odmlfmt.Property.map(attr)] = _property[attr] + prop_attrs[odmlfmt.Property.map(attr)] = content prop = odmlfmt.Property.create(**prop_attrs) odml_props.append(prop) From 001a7255d023b0ef681cc482540160bdfb6682ab Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 31 Mar 2020 12:24:55 +0200 Subject: [PATCH 10/24] [validation] Add prop value cardinality validation --- odml/validation.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/odml/validation.py b/odml/validation.py index eb2fd05d..f43bc87a 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -454,3 +454,32 @@ def property_values_string_check(prop): Validation.register_handler('property', property_values_string_check) + + +def property_values_cardinality(prop): + """ + Checks Property values against any set value cardinality. + + :param prop: odml.Property + :return: Yields a ValidationError warning, if a set cardinality is not met. + """ + if prop.val_cardinality and isinstance(prop.val_cardinality, tuple): + + val_min = prop.val_cardinality[0] + val_max = prop.val_cardinality[1] + + val_len = len(prop.values) if prop.values else 0 + + invalid_cause = "" + if val_min and val_len < val_min: + invalid_cause = "minimum %s values" % val_min + elif val_max and (prop.values and len(prop.values) > val_max): + invalid_cause = "maximum %s values" % val_max + + if invalid_cause: + msg = "Number of Property values does not satisfy value cardinality" + msg += " (%s, %s found)" % (invalid_cause, val_len) + yield ValidationError(prop, msg, LABEL_WARNING) + + +Validation.register_handler("property", property_values_cardinality) From d63dec6618226dea5cd6096983c3bddb1d7783e2 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 1 Apr 2020 14:27:58 +0200 Subject: [PATCH 11/24] [info] Library version number set to 1.5.0 --- odml/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odml/info.json b/odml/info.json index 213697c3..8f1cd9c4 100644 --- a/odml/info.json +++ b/odml/info.json @@ -1,5 +1,5 @@ { - "VERSION": "1.4.6", + "VERSION": "1.5.0", "FORMAT_VERSION": "1.1", "AUTHOR": "Hagen Fritsch, Jan Grewe, Christian Kellner, Achilleas Koutsou, Michael Sonntag, Lyuba Zehl", "COPYRIGHT": "(c) 2011-2020, German Neuroinformatics Node", From d2fda93e4db5af1b4bbdc24bb0a2bda1637b38df Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 1 Apr 2020 14:33:01 +0200 Subject: [PATCH 12/24] [property] Rename method to set_values_cardinality --- odml/property.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/odml/property.py b/odml/property.py index 689f7f73..b9c7e013 100644 --- a/odml/property.py +++ b/odml/property.py @@ -537,8 +537,8 @@ def val_cardinality(self, new_value): Only positive integers are supported. 'None' is used to denote no restrictions on a maximum or minimum. - :param new_value: Can be either 'None', a positive integer, which will set the maximum - or an integer 2-tuple of the format '(min, max)'. + :param new_value: Can be either 'None', a positive integer, which will set + the maximum or an integer 2-tuple of the format '(min, max)'. """ invalid_input = False @@ -576,7 +576,7 @@ def val_cardinality(self, new_value): msg = "Can only assign single int or int-tuples of the format '(min, max)'" raise ValueError(msg) - def values_set_cardinality(self, min_val=None, max_val=None): + def set_values_cardinality(self, min_val=None, max_val=None): """ Sets the values cardinality of a Property. From e5a5ac746847c2b94e2c2e94db7e227dad6e1c2f Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 1 Apr 2020 17:46:17 +0200 Subject: [PATCH 13/24] [validation] Shorten prop val cardinality message --- odml/validation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index f43bc87a..7f8b9386 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -472,13 +472,13 @@ def property_values_cardinality(prop): invalid_cause = "" if val_min and val_len < val_min: - invalid_cause = "minimum %s values" % val_min + invalid_cause = "minimum %s" % val_min elif val_max and (prop.values and len(prop.values) > val_max): - invalid_cause = "maximum %s values" % val_max + invalid_cause = "maximum %s" % val_max if invalid_cause: - msg = "Number of Property values does not satisfy value cardinality" - msg += " (%s, %s found)" % (invalid_cause, val_len) + msg = "Property values cardinality violated" + msg += " (%s values, %s found)" % (invalid_cause, val_len) yield ValidationError(prop, msg, LABEL_WARNING) From 8a49b3282af13a8b56d71356d00cf7bd17462a7b Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 1 Apr 2020 17:54:39 +0200 Subject: [PATCH 14/24] [property] Add validation on val_cardinality set When setting a values cardinality and the current number of values violate the cardinality a warning is printed to the user. --- odml/property.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index b9c7e013..0a1c723d 100644 --- a/odml/property.py +++ b/odml/property.py @@ -133,6 +133,8 @@ def __init__(self, name=None, values=None, parent=None, unit=None, self.parent = parent + # Cardinality should always be set after values have been added + # since it is always tested against values when it is set. self._val_cardinality = None self.val_cardinality = val_cardinality @@ -572,7 +574,12 @@ def val_cardinality(self, new_value): else: invalid_input = True - if invalid_input: + if not invalid_input: + # Validate and inform user if the current values cardinality is violated + valid = validation.Validation(self) + for err in valid.errors: + print("%s: %s" % (err.rank.capitalize(), err.msg)) + else: msg = "Can only assign single int or int-tuples of the format '(min, max)'" raise ValueError(msg) From c7a7db7611b3e63190fd35d9ae1ed9575ec96dd5 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 1 Apr 2020 18:03:42 +0200 Subject: [PATCH 15/24] [property] Add validation on values set --- odml/property.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index 0a1c723d..336a5a6c 100644 --- a/odml/property.py +++ b/odml/property.py @@ -119,6 +119,7 @@ def __init__(self, name=None, values=None, parent=None, unit=None, self._definition = definition self._dependency = dependency self._dependency_value = dependency_value + self._val_cardinality = None self._dtype = None if dtypes.valid_type(dtype): @@ -135,7 +136,6 @@ def __init__(self, name=None, values=None, parent=None, unit=None, # Cardinality should always be set after values have been added # since it is always tested against values when it is set. - self._val_cardinality = None self.val_cardinality = val_cardinality for err in validation.Validation(self).errors: @@ -410,6 +410,11 @@ def values(self, new_value): raise ValueError(msg) self._values = [dtypes.get(v, self.dtype) for v in new_value] + # Validate and inform user if the current values cardinality is violated + valid = validation.Validation(self) + for err in valid.errors: + print("%s: %s" % (err.rank.capitalize(), err.msg)) + @property def value_origin(self): """ From 9b23378aa461a74ff61e59110a90edb78f630283 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 09:58:52 +0200 Subject: [PATCH 16/24] [test/property] Add basic values cardinality test --- test/test_property.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/test_property.py b/test/test_property.py index ac9dd96d..ebef94e3 100644 --- a/test/test_property.py +++ b/test/test_property.py @@ -794,6 +794,71 @@ def test_export_leaf(self): self.assertEqual(len(ex2['first'].sections), 1) self.assertEqual(len(ex2['first']['second'].properties), 1) + def test_values_cardinality(self): + doc = Document() + sec = Section(name="sec", type="type", parent=doc) + + # -- Test set cardinality on Property init + # Test empty init + prop_card_none = Property(name="prop_cardinality_empty", parent=sec) + self.assertIsNone(prop_card_none.val_cardinality) + + # Test single int max init + prop_card_max = Property(name="prop_cardinality_max", val_cardinality=10, parent=sec) + self.assertEqual(prop_card_max.val_cardinality, (None, 10)) + + # Test tuple init + prop_card_min = Property(name="prop_cardinality_min", val_cardinality=(2, None), parent=sec) + self.assertEqual(prop_card_min.val_cardinality, (2, None)) + + # -- Test Property cardinality re-assignment + prop = Property(name="prop", val_cardinality=(None, 10), parent=sec) + self.assertEqual(prop.val_cardinality, (None, 10)) + + # Test Property cardinality reset + for non_val in [None, "", [], ()]: + prop.val_cardinality = non_val + self.assertIsNone(prop.val_cardinality) + prop.val_cardinality = 1 + + # Test Property cardinality single int max assignment + prop.val_cardinality = 10 + self.assertEqual(prop.val_cardinality, (None, 10)) + + # Test Property cardinality tuple max assignment + prop.val_cardinality = (None, 5) + self.assertEqual(prop.val_cardinality, (None, 5)) + + # Test Property cardinality tuple min assignment + prop.val_cardinality = (5, None) + self.assertEqual(prop.val_cardinality, (5, None)) + + # Test Property cardinality min/max assignment + prop.val_cardinality = (1, 5) + self.assertEqual(prop.val_cardinality, (1, 5)) + + # -- Test Property cardinality assignment failures + with self.assertRaises(ValueError): + prop.val_cardinality = "a" + + with self.assertRaises(ValueError): + prop.val_cardinality = -1 + + with self.assertRaises(ValueError): + prop.val_cardinality = (1, "b") + + with self.assertRaises(ValueError): + prop.val_cardinality = (1, 2, 3) + + with self.assertRaises(ValueError): + prop.val_cardinality = (-1, 1) + + with self.assertRaises(ValueError): + prop.val_cardinality = (1, -5) + + with self.assertRaises(ValueError): + prop.val_cardinality = (5, 1) + if __name__ == "__main__": print("TestProperty") From bff325c271d8cc245b46ccbdbef078221d140c98 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 10:13:41 +0200 Subject: [PATCH 17/24] [test/property_int] Add values cardinality test --- test/test_property_integration.py | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/test_property_integration.py b/test/test_property_integration.py index b19766e5..985b454a 100644 --- a/test/test_property_integration.py +++ b/test/test_property_integration.py @@ -131,3 +131,71 @@ def test_simple_attributes(self): self.assertEqual(yprop.definition, p_def) self.assertEqual(yprop.dependency, p_dep) self.assertEqual(yprop.dependency_value, p_dep_val) + + def test_cardinality(self): + """ + Test saving and loading of property values cardinality variants to + and from all supported file formats. + """ + doc = odml.Document() + sec = odml.Section(name="sec", type="sometype", parent=doc) + + prop_empty = "prop_cardinality_empty" + prop_max = "prop_cardinality_max" + prop_max_card = (None, 10) + prop_min = "prop_cardinality_min" + prop_min_card = (2, None) + prop_full = "prop_full" + prop_full_card = (1, 5) + + _ = odml.Property(name=prop_empty, parent=sec) + _ = odml.Property(name=prop_max, val_cardinality=prop_max_card, parent=sec) + _ = odml.Property(name=prop_min, val_cardinality=prop_min_card, parent=sec) + _ = odml.Property(name=prop_full, val_cardinality=prop_full_card, parent=sec) + + # Test saving to and loading from an XML file + odml.save(doc, self.xml_file) + xml_doc = odml.load(self.xml_file) + xml_prop = xml_doc.sections["sec"].properties[prop_empty] + self.assertIsNone(xml_prop.val_cardinality) + + xml_prop = xml_doc.sections["sec"].properties[prop_max] + self.assertEqual(xml_prop.val_cardinality, prop_max_card) + + xml_prop = xml_doc.sections["sec"].properties[prop_min] + self.assertEqual(xml_prop.val_cardinality, prop_min_card) + + xml_prop = xml_doc.sections["sec"].properties[prop_full] + self.assertEqual(xml_prop.val_cardinality, prop_full_card) + + # Test saving to and loading from a JSON file + odml.save(doc, self.json_file, "JSON") + json_doc = odml.load(self.json_file, "JSON") + + json_prop = json_doc.sections["sec"].properties[prop_empty] + self.assertIsNone(json_prop.val_cardinality) + + json_prop = json_doc.sections["sec"].properties[prop_max] + self.assertEqual(json_prop.val_cardinality, prop_max_card) + + json_prop = json_doc.sections["sec"].properties[prop_min] + self.assertEqual(json_prop.val_cardinality, prop_min_card) + + json_prop = json_doc.sections["sec"].properties[prop_full] + self.assertEqual(json_prop.val_cardinality, prop_full_card) + + # Test saving to and loading from a YAML file + odml.save(doc, self.yaml_file, "YAML") + yaml_doc = odml.load(self.yaml_file, "YAML") + + yaml_prop = yaml_doc.sections["sec"].properties[prop_empty] + self.assertIsNone(yaml_prop.val_cardinality) + + yaml_prop = yaml_doc.sections["sec"].properties[prop_max] + self.assertEqual(yaml_prop.val_cardinality, prop_max_card) + + yaml_prop = yaml_doc.sections["sec"].properties[prop_min] + self.assertEqual(yaml_prop.val_cardinality, prop_min_card) + + yaml_prop = yaml_doc.sections["sec"].properties[prop_full] + self.assertEqual(yaml_prop.val_cardinality, prop_full_card) From bb30904529c47292fc20d5f7d7a931ca4d758f9d Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 11:21:37 +0200 Subject: [PATCH 18/24] [test/validation_integration] Add test file --- test/test_validation_integration.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 test/test_validation_integration.py diff --git a/test/test_validation_integration.py b/test/test_validation_integration.py new file mode 100644 index 00000000..569150c6 --- /dev/null +++ b/test/test_validation_integration.py @@ -0,0 +1,28 @@ +""" +This file tests built-in odml validations. +""" + +import sys +import unittest + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import odml + + +class TestValidationIntegration(unittest.TestCase): + + def setUp(self): + # Redirect stdout to test messages + self.capture = StringIO() + sys.stdout = self.capture + + self.msg_base = "Property values cardinality violated" + + def tearDown(self): + # Reset stdout + sys.stdout = sys.__stdout__ + From b764372cfffa14de38af44f2c845b802a3dcc400 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 11:22:29 +0200 Subject: [PATCH 19/24] [test/validation_int] Add buffer read method --- test/test_validation_integration.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/test_validation_integration.py b/test/test_validation_integration.py index 569150c6..2da2229a 100644 --- a/test/test_validation_integration.py +++ b/test/test_validation_integration.py @@ -26,3 +26,12 @@ def tearDown(self): # Reset stdout sys.stdout = sys.__stdout__ + def _get_captured_output(self): + out = [txt.strip() for txt in self.capture.getvalue().split('\n') if txt] + + # Buffer reset + self.capture.seek(0) + self.capture.truncate() + + return out + From 89c8538ab68ff50cefb1947ab584fe61b2902aad Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 11:23:10 +0200 Subject: [PATCH 20/24] [test/validation_int] Test prop values cardinality --- test/test_validation_integration.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/test_validation_integration.py b/test/test_validation_integration.py index 2da2229a..f2d23fc9 100644 --- a/test/test_validation_integration.py +++ b/test/test_validation_integration.py @@ -35,3 +35,81 @@ def _get_captured_output(self): return out + def test_property_values_cardinality(self): + # -- Test assignment validation warnings + doc = odml.Document() + sec = odml.Section(name="sec", type="sec_type", parent=doc) + + # -- Test cardinality validation warnings on Property init + # Test warning when setting invalid minimum + _ = odml.Property(name="prop_card_min", values=[1], val_cardinality=(2, None), parent=sec) + output = self._get_captured_output() + test_msg = "%s (minimum %s values, %s found)" % (self.msg_base, 2, 1) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test warning when setting invalid maximum + _ = odml.Property(name="prop_card_max", values=[1, 2, 3], val_cardinality=2, parent=sec) + output = self._get_captured_output() + test_msg = "%s (maximum %s values, %s found)" % (self.msg_base, 2, 3) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test no warning on valid init + prop_card = odml.Property(name="prop_card", values=[1, 2], + val_cardinality=(1, 5), parent=sec) + output = self._get_captured_output() + self.assertEqual(output, []) + + # -- Test cardinality validation warnings on cardinality updates + # Test warning when setting minimally required values cardinality + prop_card.val_cardinality = (3, None) + output = self._get_captured_output() + test_msg = "%s (minimum %s values, %s found)" % (self.msg_base, 3, 2) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test warning when setting maximally required values cardinality + prop_card.values = [1, 2, 3] + prop_card.val_cardinality = 2 + output = self._get_captured_output() + test_msg = "%s (maximum %s values, %s found)" % (self.msg_base, 2, 3) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test no warning on valid cardinality + prop_card.val_cardinality = (1, 10) + output = self._get_captured_output() + self.assertEqual(output, []) + + # Test no warning when setting cardinality to None + prop_card.val_cardinality = None + output = self._get_captured_output() + self.assertEqual(output, []) + + # -- Test cardinality validation warnings on values updates + # Test warning when violating minimally required values cardinality + prop_card.val_cardinality = (3, None) + prop_card.values = [1, 2] + output = self._get_captured_output() + test_msg = "%s (minimum %s values, %s found)" % (self.msg_base, 3, 2) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test warning when violating maximally required values cardinality + prop_card.val_cardinality = (None, 2) + prop_card.values = [1, 2, 3] + output = self._get_captured_output() + test_msg = "%s (maximum %s values, %s found)" % (self.msg_base, 2, 3) + self.assertEqual(len(output), 1) + self.assertIn(test_msg, output[0]) + + # Test no warning when setting correct number of values + prop_card.values = [1, 2] + output = self._get_captured_output() + self.assertEqual(output, []) + + # Test no warning when setting values to None + prop_card.values = None + output = self._get_captured_output() + self.assertEqual(output, []) From 6a9f3f76f0cf96d98bc6c72152f1e8dc51d6288c Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 20:06:05 +0200 Subject: [PATCH 21/24] [test/validation] Add prop values cardinality test --- test/test_validation.py | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/test_validation.py b/test/test_validation.py index 1dc8a330..e31cccff 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -41,6 +41,49 @@ def assertError(self, res, err, filter_rep=True, filter_map=False): return self.assertEqual(errs, err) + def test_property_values_cardinality(self): + doc = odml.Document() + sec = odml.Section(name="sec", type="sec_type", parent=doc) + + # Test no caught warning on empty cardinality + prop = odml.Property(name="prop_empty_cardinality", values=[1, 2, 3, 4], parent=sec) + # Check that the current property is not in the list of validation warnings or errors + for err in validate(doc).errors: + self.assertNotEqual(err.obj.id, prop.id) + + # Test no warning on valid cardinality + prop = odml.Property(name="prop_valid_cardinality", values=[1, 2, 3, 4], + val_cardinality=(2, 10), parent=sec) + for err in validate(doc).errors: + self.assertNotEqual(err.obj.id, prop.id) + + # Test minimum value cardinality validation + test_val = [1, 2, 3] + test_card = 2 + + prop = odml.Property(name="prop_invalid_max_val", values=test_val, + val_cardinality=test_card, parent=sec) + + test_msg_base = "Property values cardinality violated" + test_msg = "%s (maximum %s values, %s found)" % (test_msg_base, test_card, len(prop.values)) + for err in validate(doc).errors: + if err.obj.id == prop.id: + self.assertFalse(err.is_error) + self.assertIn(test_msg, err.msg) + + # Test maximum value cardinality validation + test_val = "I am a nice text to test" + test_card = (4, None) + + prop = odml.Property(name="prop_invalid_min_val", values=test_val, + val_cardinality=test_card, parent=sec) + + test_msg = "%s (minimum %s values, %s found)" % (test_msg_base, test_card[0], len(prop.values)) + for err in validate(doc).errors: + if err.obj.id == prop.id: + self.assertFalse(err.is_error) + self.assertIn(test_msg, err.msg) + def test_section_type(self): doc = samplefile.parse("""s1[undefined]""") res = validate(doc) From ad4cc1890c14929e304e6459cb55f59aff966d47 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 21:03:23 +0200 Subject: [PATCH 22/24] [test/property] set_values_cardinality method test --- test/test_property.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/test_property.py b/test/test_property.py index ebef94e3..371c8bc8 100644 --- a/test/test_property.py +++ b/test/test_property.py @@ -859,6 +859,46 @@ def test_values_cardinality(self): with self.assertRaises(ValueError): prop.val_cardinality = (5, 1) + def test_set_values_cardinality(self): + doc = Document() + sec = Section(name="sec", type="sec_type", parent=doc) + + prop = Property(name="prop", val_cardinality=1, parent=sec) + + # Test Property values cardinality min assignment + prop.set_values_cardinality(1) + self.assertEqual(prop.val_cardinality, (1, None)) + + # Test Property values cardinality keyword min assignment + prop.set_values_cardinality(min_val=2) + self.assertEqual(prop.val_cardinality, (2, None)) + + # Test Property values cardinality max assignment + prop.set_values_cardinality(None, 1) + self.assertEqual(prop.val_cardinality, (None, 1)) + + # Test Property values cardinality keyword max assignment + prop.set_values_cardinality(max_val=2) + self.assertEqual(prop.val_cardinality, (None, 2)) + + # Test Property values cardinality min max assignment + prop.set_values_cardinality(1, 2) + self.assertEqual(prop.val_cardinality, (1, 2)) + + # Test Property values cardinality keyword min max assignment + prop.set_values_cardinality(min_val=2, max_val=5) + self.assertEqual(prop.val_cardinality, (2, 5)) + + # Test Property values cardinality empty reset + prop.set_values_cardinality() + self.assertIsNone(prop.val_cardinality) + + # Test Property values cardinality keyword empty reset + prop.set_values_cardinality(1) + self.assertIsNotNone(prop.val_cardinality) + prop.set_values_cardinality(min_val=None, max_val=None) + self.assertIsNone(prop.val_cardinality) + if __name__ == "__main__": print("TestProperty") From e04387d0050420ee5966c713348d4ca2cf3cb1c7 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sat, 4 Apr 2020 21:03:57 +0200 Subject: [PATCH 23/24] [property] Clarify values cardinality exc msgs --- odml/property.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/odml/property.py b/odml/property.py index 336a5a6c..08a69508 100644 --- a/odml/property.py +++ b/odml/property.py @@ -548,6 +548,7 @@ def val_cardinality(self, new_value): the maximum or an integer 2-tuple of the format '(min, max)'. """ invalid_input = False + exc_msg = "Can only assign positive single int or int-tuples of the format '(min, max)'" # Empty values reset the cardinality to None. if not new_value or new_value == (None, None): @@ -576,6 +577,10 @@ def val_cardinality(self, new_value): else: invalid_input = True + + # Use helpful exception message in the following case: + if max_int and min_int and v_max < v_min: + exc_msg = "Minimum larger than maximum (min=%s, max=%s)" % (v_min, v_max) else: invalid_input = True @@ -585,8 +590,7 @@ def val_cardinality(self, new_value): for err in valid.errors: print("%s: %s" % (err.rank.capitalize(), err.msg)) else: - msg = "Can only assign single int or int-tuples of the format '(min, max)'" - raise ValueError(msg) + raise ValueError(exc_msg) def set_values_cardinality(self, min_val=None, max_val=None): """ From 79e5acf0b405627fc5c7a008ed4972bed3fcbbfe Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Sun, 5 Apr 2020 10:43:47 +0200 Subject: [PATCH 24/24] [test/validation_init] Fix Win stdout redirect Windows does not support fetching the systems stdout via the 'sys.__stdout__' magic method in all cases. --- test/test_validation_integration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_validation_integration.py b/test/test_validation_integration.py index f2d23fc9..1e978be3 100644 --- a/test/test_validation_integration.py +++ b/test/test_validation_integration.py @@ -17,14 +17,16 @@ class TestValidationIntegration(unittest.TestCase): def setUp(self): # Redirect stdout to test messages + self.stdout_orig = sys.stdout self.capture = StringIO() sys.stdout = self.capture self.msg_base = "Property values cardinality violated" def tearDown(self): - # Reset stdout - sys.stdout = sys.__stdout__ + # Reset stdout; resetting using 'sys.__stdout__' fails on windows + sys.stdout = self.stdout_orig + self.capture.close() def _get_captured_output(self): out = [txt.strip() for txt in self.capture.getvalue().split('\n') if txt]