Skip to content

Commit eccae1f

Browse files
authored
Merge pull request #382 from mpsonntag/cardUpdates
General cardinality feature updates LGTM
2 parents 7dde867 + 6aa02e4 commit eccae1f

File tree

4 files changed

+165
-45
lines changed

4 files changed

+165
-45
lines changed

odml/property.py

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from . import validation
1010
from . import format as frmt
1111
from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
12+
from .util import format_cardinality
1213

1314

1415
def odml_tuple_import(t_count, new_value):
@@ -413,9 +414,7 @@ def values(self, new_value):
413414
self._values = [dtypes.get(v, self.dtype) for v in new_value]
414415

415416
# Validate and inform user if the current values cardinality is violated
416-
valid = validation.Validation(self)
417-
for err in valid.errors:
418-
print("%s: %s" % (err.rank.capitalize(), err.msg))
417+
self._values_cardinality_validation()
419418

420419
@property
421420
def value_origin(self):
@@ -528,7 +527,7 @@ def val_cardinality(self):
528527
"""
529528
The value cardinality of a Property. It defines how many values
530529
are minimally required and how many values should be maximally
531-
stored. Use 'values_set_cardinality' to set.
530+
stored. Use the 'set_values_cardinality' method to set.
532531
"""
533532
return self._val_cardinality
534533

@@ -549,50 +548,22 @@ def val_cardinality(self, new_value):
549548
:param new_value: Can be either 'None', a positive integer, which will set
550549
the maximum or an integer 2-tuple of the format '(min, max)'.
551550
"""
552-
invalid_input = False
553-
exc_msg = "Can only assign positive single int or int-tuples of the format '(min, max)'"
554-
555-
# Empty values reset the cardinality to None.
556-
if not new_value or new_value == (None, None):
557-
self._val_cardinality = None
558-
559-
# Providing a single integer sets the maximum value in a tuple.
560-
elif isinstance(new_value, int) and new_value > 0:
561-
self._val_cardinality = (None, new_value)
562-
563-
# Only integer 2-tuples of the format '(min, max)' are supported to set the cardinality
564-
elif isinstance(new_value, tuple) and len(new_value) == 2:
565-
v_min = new_value[0]
566-
v_max = new_value[1]
567-
568-
min_int = isinstance(v_min, int) and v_min >= 0
569-
max_int = isinstance(v_max, int) and v_max >= 0
570-
571-
if max_int and min_int and v_max > v_min:
572-
self._val_cardinality = (v_min, v_max)
573-
574-
elif max_int and not v_min:
575-
self._val_cardinality = (None, v_max)
576-
577-
elif min_int and not v_max:
578-
self._val_cardinality = (v_min, None)
551+
self._val_cardinality = format_cardinality(new_value)
579552

580-
else:
581-
invalid_input = True
553+
# Validate and inform user if the current values cardinality is violated
554+
self._values_cardinality_validation()
582555

583-
# Use helpful exception message in the following case:
584-
if max_int and min_int and v_max < v_min:
585-
exc_msg = "Minimum larger than maximum (min=%s, max=%s)" % (v_min, v_max)
586-
else:
587-
invalid_input = True
556+
def _values_cardinality_validation(self):
557+
"""
558+
Runs a validation to check whether the values cardinality
559+
is respected and prints a warning message otherwise.
560+
"""
561+
valid = validation.Validation(self)
588562

589-
if not invalid_input:
590-
# Validate and inform user if the current values cardinality is violated
591-
valid = validation.Validation(self)
592-
for err in valid.errors:
593-
print("%s: %s" % (err.rank.capitalize(), err.msg))
594-
else:
595-
raise ValueError(exc_msg)
563+
# Make sure to display only warnings of the current property
564+
res = [curr for curr in valid.errors if self.id == curr.obj.id]
565+
for err in res:
566+
print("%s: %s" % (err.rank.capitalize(), err.msg))
596567

597568
def set_values_cardinality(self, min_val=None, max_val=None):
598569
"""

odml/util.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8
2+
"""
3+
Module containing general utility functions.
4+
"""
5+
6+
7+
def format_cardinality(in_val):
8+
"""
9+
Checks an input value and formats it towards a custom tuple format
10+
used in odml Section, Property and Values cardinality.
11+
12+
The following cases are supported:
13+
(n, n) - default, no restriction
14+
(d, n) - minimally d entries, no maximum
15+
(n, d) - maximally d entries, no minimum
16+
(d, d) - minimally d entries, maximally d entries
17+
18+
Only positive integers are supported. 'None' is used to denote
19+
no restrictions on a maximum or minimum.
20+
21+
:param in_val: Can either be 'None', a positive integer, which will set
22+
the maximum or an integer 2-tuple of the format '(min, max)'.
23+
24+
:returns: None or the value as tuple. A ValueError is raised, if the
25+
provided value was not in an acceptable format.
26+
"""
27+
exc_msg = "Can only assign positive single int or int-tuples of the format '(min, max)'"
28+
29+
# Empty values reset the cardinality to None.
30+
if not in_val:
31+
return None
32+
33+
# Catch tuple edge cases (0, 0); (None, None); (0, None); (None, 0)
34+
if isinstance(in_val, (tuple, list)) and len(in_val) == 2 and not in_val[0] and not in_val[1]:
35+
return None
36+
37+
# Providing a single integer sets the maximum value in a tuple.
38+
if isinstance(in_val, int) and in_val > 0:
39+
return None, in_val
40+
41+
# Integer 2-tuples of the format '(min, max)' are supported to set the cardinality.
42+
# Also support lists with a length of 2 without advertising it.
43+
if isinstance(in_val, (tuple, list)) and len(in_val) == 2:
44+
v_min = in_val[0]
45+
v_max = in_val[1]
46+
47+
min_int = isinstance(v_min, int) and v_min >= 0
48+
max_int = isinstance(v_max, int) and v_max >= 0
49+
50+
if max_int and min_int and v_max >= v_min:
51+
return v_min, v_max
52+
53+
if max_int and not v_min:
54+
return None, v_max
55+
56+
if min_int and not v_max:
57+
return v_min, None
58+
59+
# Use helpful exception message in the following case:
60+
if max_int and min_int and v_max < v_min:
61+
exc_msg = "Minimum larger than maximum (min=%s, max=%s)" % (v_min, v_max)
62+
63+
raise ValueError(exc_msg)

test/test_util.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
This file tests odml util functions.
3+
"""
4+
5+
import unittest
6+
7+
from odml.util import format_cardinality
8+
9+
10+
class TestUtil(unittest.TestCase):
11+
12+
def test_format_cardinality(self):
13+
# Test empty set
14+
self.assertIsNone(format_cardinality(None))
15+
self.assertIsNone(format_cardinality([]))
16+
self.assertIsNone(format_cardinality({}))
17+
self.assertIsNone(format_cardinality(""))
18+
self.assertIsNone(format_cardinality(()))
19+
20+
# Test empty tuple edge cases
21+
self.assertIsNone(format_cardinality((None, None)))
22+
self.assertIsNone(format_cardinality((0, 0)))
23+
self.assertIsNone(format_cardinality((None, 0)))
24+
self.assertIsNone(format_cardinality((0, None)))
25+
26+
# Test single int max set
27+
self.assertEqual(format_cardinality(10), (None, 10))
28+
29+
# Test tuple set
30+
set_val = (2, None)
31+
self.assertEqual(format_cardinality(set_val), set_val)
32+
set_val = (None, 2)
33+
self.assertEqual(format_cardinality(set_val), set_val)
34+
set_val = (2, 3)
35+
self.assertEqual(format_cardinality(set_val), set_val)
36+
37+
# Test list simple list set
38+
set_val = [2, None]
39+
self.assertEqual(format_cardinality(set_val), tuple(set_val))
40+
set_val = [None, 2]
41+
self.assertEqual(format_cardinality(set_val), tuple(set_val))
42+
set_val = [2, 3]
43+
self.assertEqual(format_cardinality(set_val), tuple(set_val))
44+
45+
# Test exact value tuple set
46+
set_val = (5, 5)
47+
self.assertEqual(format_cardinality(set_val), set_val)
48+
49+
# Test set failures
50+
with self.assertRaises(ValueError):
51+
format_cardinality("a")
52+
53+
with self.assertRaises(ValueError):
54+
format_cardinality([1])
55+
56+
with self.assertRaises(ValueError):
57+
format_cardinality([1, 2, 3])
58+
59+
with self.assertRaises(ValueError):
60+
format_cardinality({1: 2, 3: 4})
61+
62+
with self.assertRaises(ValueError):
63+
format_cardinality(-1)
64+
65+
with self.assertRaises(ValueError):
66+
format_cardinality((1, "b"))
67+
68+
with self.assertRaises(ValueError):
69+
format_cardinality((1, 2, 3))
70+
71+
with self.assertRaises(ValueError):
72+
format_cardinality((-1, 1))
73+
74+
with self.assertRaises(ValueError):
75+
format_cardinality((1, -5))
76+
77+
with self.assertRaises(ValueError) as exc:
78+
format_cardinality((5, 1))
79+
self.assertIn("Minimum larger than maximum ", str(exc))

test/test_validation_integration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def tearDown(self):
2828
sys.stdout = self.stdout_orig
2929
self.capture.close()
3030

31+
def _clear_output(self):
32+
self.capture.seek(0)
33+
self.capture.truncate()
34+
3135
def _get_captured_output(self):
3236
out = [txt.strip() for txt in self.capture.getvalue().split('\n') if txt]
3337

@@ -42,6 +46,9 @@ def test_property_values_cardinality(self):
4246
doc = odml.Document()
4347
sec = odml.Section(name="sec", type="sec_type", parent=doc)
4448

49+
# Making sure only the required warnings are tested
50+
self._clear_output()
51+
4552
# -- Test cardinality validation warnings on Property init
4653
# Test warning when setting invalid minimum
4754
_ = odml.Property(name="prop_card_min", values=[1], val_cardinality=(2, None), parent=sec)

0 commit comments

Comments
 (0)