diff --git a/odml/property.py b/odml/property.py index c3820ee7..2c3b6be5 100644 --- a/odml/property.py +++ b/odml/property.py @@ -9,6 +9,7 @@ from . import validation from . import format as frmt from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring +from .util import format_cardinality def odml_tuple_import(t_count, new_value): @@ -413,9 +414,7 @@ def values(self, new_value): 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)) + self._values_cardinality_validation() @property def value_origin(self): @@ -528,7 +527,7 @@ 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. + stored. Use the 'set_values_cardinality' method to set. """ return self._val_cardinality @@ -549,50 +548,22 @@ 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)'. """ - 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): - 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) + self._val_cardinality = format_cardinality(new_value) - else: - invalid_input = True + # Validate and inform user if the current values cardinality is violated + self._values_cardinality_validation() - # 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 + def _values_cardinality_validation(self): + """ + Runs a validation to check whether the values cardinality + is respected and prints a warning message otherwise. + """ + valid = validation.Validation(self) - 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: - raise ValueError(exc_msg) + # Make sure to display only warnings of the current property + res = [curr for curr in valid.errors if self.id == curr.obj.id] + for err in res: + print("%s: %s" % (err.rank.capitalize(), err.msg)) def set_values_cardinality(self, min_val=None, max_val=None): """ diff --git a/odml/util.py b/odml/util.py new file mode 100644 index 00000000..e3dab08b --- /dev/null +++ b/odml/util.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 +""" +Module containing general utility functions. +""" + + +def format_cardinality(in_val): + """ + Checks an input value and formats it towards a custom tuple format + used in odml Section, Property and Values cardinality. + + The following 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 in_val: Can either be 'None', a positive integer, which will set + the maximum or an integer 2-tuple of the format '(min, max)'. + + :returns: None or the value as tuple. A ValueError is raised, if the + provided value was not in an acceptable format. + """ + 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 in_val: + return None + + # Catch tuple edge cases (0, 0); (None, None); (0, None); (None, 0) + if isinstance(in_val, (tuple, list)) and len(in_val) == 2 and not in_val[0] and not in_val[1]: + return None + + # Providing a single integer sets the maximum value in a tuple. + if isinstance(in_val, int) and in_val > 0: + return None, in_val + + # Integer 2-tuples of the format '(min, max)' are supported to set the cardinality. + # Also support lists with a length of 2 without advertising it. + if isinstance(in_val, (tuple, list)) and len(in_val) == 2: + v_min = in_val[0] + v_max = in_val[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: + return v_min, v_max + + if max_int and not v_min: + return None, v_max + + if min_int and not v_max: + return v_min, None + + # 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) + + raise ValueError(exc_msg) diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 00000000..9a732ce4 --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,79 @@ +""" +This file tests odml util functions. +""" + +import unittest + +from odml.util import format_cardinality + + +class TestUtil(unittest.TestCase): + + def test_format_cardinality(self): + # Test empty set + self.assertIsNone(format_cardinality(None)) + self.assertIsNone(format_cardinality([])) + self.assertIsNone(format_cardinality({})) + self.assertIsNone(format_cardinality("")) + self.assertIsNone(format_cardinality(())) + + # Test empty tuple edge cases + self.assertIsNone(format_cardinality((None, None))) + self.assertIsNone(format_cardinality((0, 0))) + self.assertIsNone(format_cardinality((None, 0))) + self.assertIsNone(format_cardinality((0, None))) + + # Test single int max set + self.assertEqual(format_cardinality(10), (None, 10)) + + # Test tuple set + set_val = (2, None) + self.assertEqual(format_cardinality(set_val), set_val) + set_val = (None, 2) + self.assertEqual(format_cardinality(set_val), set_val) + set_val = (2, 3) + self.assertEqual(format_cardinality(set_val), set_val) + + # Test list simple list set + set_val = [2, None] + self.assertEqual(format_cardinality(set_val), tuple(set_val)) + set_val = [None, 2] + self.assertEqual(format_cardinality(set_val), tuple(set_val)) + set_val = [2, 3] + self.assertEqual(format_cardinality(set_val), tuple(set_val)) + + # Test exact value tuple set + set_val = (5, 5) + self.assertEqual(format_cardinality(set_val), set_val) + + # Test set failures + with self.assertRaises(ValueError): + format_cardinality("a") + + with self.assertRaises(ValueError): + format_cardinality([1]) + + with self.assertRaises(ValueError): + format_cardinality([1, 2, 3]) + + with self.assertRaises(ValueError): + format_cardinality({1: 2, 3: 4}) + + with self.assertRaises(ValueError): + format_cardinality(-1) + + with self.assertRaises(ValueError): + format_cardinality((1, "b")) + + with self.assertRaises(ValueError): + format_cardinality((1, 2, 3)) + + with self.assertRaises(ValueError): + format_cardinality((-1, 1)) + + with self.assertRaises(ValueError): + format_cardinality((1, -5)) + + with self.assertRaises(ValueError) as exc: + format_cardinality((5, 1)) + self.assertIn("Minimum larger than maximum ", str(exc)) diff --git a/test/test_validation_integration.py b/test/test_validation_integration.py index 1e978be3..4937b611 100644 --- a/test/test_validation_integration.py +++ b/test/test_validation_integration.py @@ -28,6 +28,10 @@ def tearDown(self): sys.stdout = self.stdout_orig self.capture.close() + def _clear_output(self): + self.capture.seek(0) + self.capture.truncate() + def _get_captured_output(self): out = [txt.strip() for txt in self.capture.getvalue().split('\n') if txt] @@ -42,6 +46,9 @@ def test_property_values_cardinality(self): doc = odml.Document() sec = odml.Section(name="sec", type="sec_type", parent=doc) + # Making sure only the required warnings are tested + self._clear_output() + # -- 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)