From 5428a92a4364ea846523c285ae0edf6b0d31ccb6 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 16:37:53 +0200 Subject: [PATCH 1/9] [util] Add format_cardinality func --- odml/util.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 odml/util.py diff --git a/odml/util.py b/odml/util.py new file mode 100644 index 00000000..558bfb20 --- /dev/null +++ b/odml/util.py @@ -0,0 +1,58 @@ +# -*- 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 or in_val == (None, None): + 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 + + # Only integer 2-tuples of the format '(min, max)' are supported to set the cardinality + if isinstance(in_val, tuple) 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) From 528706598f264d41ba52957a7a9802b0fb4dc07c Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 16:38:25 +0200 Subject: [PATCH 2/9] [test/util] Add format_cardinality test --- test/test_util.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/test_util.py diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 00000000..00b9021a --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,53 @@ +""" +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(())) + self.assertIsNone(format_cardinality((None, 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 set failures + with self.assertRaises(ValueError): + format_cardinality("a") + + 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)) From 019c87bd53b9800c523ad1baf6e99921251a8a26 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 16:41:18 +0200 Subject: [PATCH 3/9] [property] Use util.format_cardinality Replaced value_cardinality implementation with usage of the more generic util.format_cardinality function. --- odml/property.py | 51 +++++++----------------------------------------- 1 file changed, 7 insertions(+), 44 deletions(-) diff --git a/odml/property.py b/odml/property.py index c3820ee7..8ef563b5 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): @@ -528,7 +529,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 +550,12 @@ 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)'" + self._val_cardinality = format_cardinality(new_value) - # 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 - - # 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 - - 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) + # 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)) def set_values_cardinality(self, min_val=None, max_val=None): """ From 5f1071450e5dda83ebc69da277fed5aec015698e Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 18:35:05 +0200 Subject: [PATCH 4/9] [util] Handle cardinality empty tuple edgecase --- odml/util.py | 6 +++++- test/test_util.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/odml/util.py b/odml/util.py index 558bfb20..dd489907 100644 --- a/odml/util.py +++ b/odml/util.py @@ -27,7 +27,11 @@ def format_cardinality(in_val): 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 or in_val == (None, None): + if not in_val: + return None + + # Catch tuple edge cases (0, 0); (None, None); (0, None); (None, 0) + if isinstance(in_val, tuple) and len(in_val) > 1 and not in_val[0] and not in_val[1]: return None # Providing a single integer sets the maximum value in a tuple. diff --git a/test/test_util.py b/test/test_util.py index 00b9021a..44adc870 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -16,7 +16,12 @@ def test_format_cardinality(self): 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)) From 7a5fd79c639163dc90e06c1ce2131af369d70173 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 18:35:38 +0200 Subject: [PATCH 5/9] [util] Handle exact val cardinality edge case --- odml/util.py | 2 +- test/test_util.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/odml/util.py b/odml/util.py index dd489907..6eea44f7 100644 --- a/odml/util.py +++ b/odml/util.py @@ -46,7 +46,7 @@ def format_cardinality(in_val): 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: + if max_int and min_int and v_max >= v_min: return v_min, v_max if max_int and not v_min: diff --git a/test/test_util.py b/test/test_util.py index 44adc870..33507e6a 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -34,6 +34,10 @@ def test_format_cardinality(self): set_val = (2, 3) self.assertEqual(format_cardinality(set_val), 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") From b248516c0d50bccf41fe5a9069f0e93f70eb872f Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 14 Apr 2020 18:40:21 +0200 Subject: [PATCH 6/9] [property] Move cardinality val to own func --- odml/property.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/odml/property.py b/odml/property.py index 8ef563b5..cf3d98ac 100644 --- a/odml/property.py +++ b/odml/property.py @@ -414,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): @@ -553,6 +551,13 @@ def val_cardinality(self, new_value): self._val_cardinality = format_cardinality(new_value) # Validate and inform user if the current values cardinality is violated + self._values_cardinality_validation() + + 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) for err in valid.errors: print("%s: %s" % (err.rank.capitalize(), err.msg)) From c325e859920d67c2a5dc6f7f1188afef34183b2f Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 15 Apr 2020 09:44:25 +0200 Subject: [PATCH 7/9] [property] Filter warnings on cardinality validate When running a values cardinality validation filter the warnings to only those that are relevant for the validated object. --- odml/property.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/odml/property.py b/odml/property.py index cf3d98ac..2c3b6be5 100644 --- a/odml/property.py +++ b/odml/property.py @@ -559,7 +559,10 @@ def _values_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) - for err in valid.errors: + + # 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): From 27cb74568e490ebc7c5f4e634f6ed35110fb63fa Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 15 Apr 2020 11:38:21 +0200 Subject: [PATCH 8/9] [test/validation] Add clear output method --- test/test_validation_integration.py | 7 +++++++ 1 file changed, 7 insertions(+) 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) From 6aa02e40c29da1ea0e2265eee50aeeed6730e1d5 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 15 Apr 2020 13:28:44 +0200 Subject: [PATCH 9/9] [util] Allow list in cardinality --- odml/util.py | 7 ++++--- test/test_util.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/odml/util.py b/odml/util.py index 6eea44f7..e3dab08b 100644 --- a/odml/util.py +++ b/odml/util.py @@ -31,15 +31,16 @@ def format_cardinality(in_val): return None # Catch tuple edge cases (0, 0); (None, None); (0, None); (None, 0) - if isinstance(in_val, tuple) and len(in_val) > 1 and not in_val[0] and not in_val[1]: + 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 - # Only integer 2-tuples of the format '(min, max)' are supported to set the cardinality - if isinstance(in_val, tuple) and len(in_val) == 2: + # 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] diff --git a/test/test_util.py b/test/test_util.py index 33507e6a..9a732ce4 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -34,6 +34,14 @@ def test_format_cardinality(self): 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) @@ -42,6 +50,15 @@ def test_format_cardinality(self): 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)