Skip to content

Commit 13fd740

Browse files
authored
Merge pull request #383 from mpsonntag/cardSecA
Adding Section properties cardinality LGTM
2 parents eccae1f + fa2dd57 commit 13fd740

File tree

7 files changed

+379
-9
lines changed

7 files changed

+379
-9
lines changed

odml/format.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ class Section(Format):
156156
'repository': 0,
157157
'section': 0,
158158
'include': 0,
159-
'property': 0
159+
'property': 0,
160+
'prop_cardinality': 0
160161
}
161162
_map = {
162163
'section': 'sections',

odml/section.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .property import BaseProperty
1919
# it MUST however not be used to create any Property objects
2020
from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
21+
from .util import format_cardinality
2122

2223

2324
@allow_inherit_docstring
@@ -40,8 +41,12 @@ class BaseSection(base.Sectionable):
4041
:param link: Specifies a soft link, i.e. a path within the document.
4142
:param include: Specifies an arbitrary URL. Can only be used if *link* is not set.
4243
:param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
43-
an id will be generated and assigned. An id has to be unique
44-
within an odML Document.
44+
an id will be generated and assigned. An id has to be unique
45+
within an odML Document.
46+
:param prop_cardinality: Property cardinality defines how many Properties are allowed for this
47+
Section. By default unlimited Properties can be set.
48+
A required number of Properties can be set by assigning a tuple of the
49+
format "(min, max)".
4550
"""
4651

4752
type = None
@@ -54,7 +59,8 @@ class BaseSection(base.Sectionable):
5459

5560
def __init__(self, name=None, type="n.s.", parent=None,
5661
definition=None, reference=None,
57-
repository=None, link=None, include=None, oid=None):
62+
repository=None, link=None, include=None, oid=None,
63+
prop_cardinality=None):
5864

5965
# Sets _sections Smartlist and _repository to None, so run first.
6066
super(BaseSection, self).__init__()
@@ -80,11 +86,16 @@ def __init__(self, name=None, type="n.s.", parent=None,
8086
self._repository = repository
8187
self._link = link
8288
self._include = include
89+
self._prop_cardinality = None
8390

8491
# this may fire a change event, so have the section setup then
8592
self.type = type
8693
self.parent = parent
8794

95+
# This might lead to a validation warning, since properties are set
96+
# at a later point in time.
97+
self.prop_cardinality = prop_cardinality
98+
8899
for err in validation.Validation(self).errors:
89100
if err.is_error:
90101
use_name = err.obj.name if err.obj.id != err.obj.name else None
@@ -349,6 +360,59 @@ def get_repository(self):
349360
def repository(self, url):
350361
base.Sectionable.repository.fset(self, url)
351362

363+
@property
364+
def prop_cardinality(self):
365+
"""
366+
The Property cardinality of a Section. It defines how many Properties
367+
are minimally required and how many Properties should be maximally
368+
stored. Use the 'set_properties_cardinality' method to set.
369+
"""
370+
return self._prop_cardinality
371+
372+
@prop_cardinality.setter
373+
def prop_cardinality(self, new_value):
374+
"""
375+
Sets the Properties cardinality of a Section.
376+
377+
The following cardinality cases are supported:
378+
(n, n) - default, no restriction
379+
(d, n) - minimally d entries, no maximum
380+
(n, d) - maximally d entries, no minimum
381+
(d, d) - minimally d entries, maximally d entries
382+
383+
Only positive integers are supported. 'None' is used to denote
384+
no restrictions on a maximum or minimum.
385+
386+
:param new_value: Can be either 'None', a positive integer, which will set
387+
the maximum or an integer 2-tuple of the format '(min, max)'.
388+
"""
389+
self._prop_cardinality = format_cardinality(new_value)
390+
391+
# Validate and inform user if the current cardinality is violated
392+
self._properties_cardinality_validation()
393+
394+
def set_properties_cardinality(self, min_val=None, max_val=None):
395+
"""
396+
Sets the Properties cardinality of a Section.
397+
398+
:param min_val: Required minimal number of values elements. None denotes
399+
no restrictions on values elements minimum. Default is None.
400+
:param max_val: Allowed maximal number of values elements. None denotes
401+
no restrictions on values elements maximum. Default is None.
402+
"""
403+
self.prop_cardinality = (min_val, max_val)
404+
405+
def _properties_cardinality_validation(self):
406+
"""
407+
Runs a validation to check whether the properties cardinality
408+
is respected and prints a warning message otherwise.
409+
"""
410+
valid = validation.Validation(self)
411+
# Make sure to display only warnings of the current section
412+
res = [curr for curr in valid.errors if self.id == curr.obj.id]
413+
for err in res:
414+
print("%s: %s" % (err.rank.capitalize(), err.msg))
415+
352416
@inherit_docstring
353417
def get_terminology_equivalent(self):
354418
repo = self.get_repository()

odml/tools/dict_parser.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ def get_sections(self, section_list):
114114
section_dict[attr] = sections
115115
else:
116116
tag = getattr(section, attr)
117-
118-
if tag:
117+
# Tuples have to be serialized as lists to avoid
118+
# nasty python code annotations when writing to yaml.
119+
if tag and isinstance(tag, tuple):
120+
section_dict[i] = list(tag)
121+
elif tag:
119122
# Always use the arguments key attribute name when saving
120123
section_dict[i] = tag
121124

@@ -143,6 +146,8 @@ def get_properties(props_list):
143146

144147
if hasattr(prop, attr):
145148
tag = getattr(prop, attr)
149+
# Tuples have to be serialized as lists to avoid
150+
# nasty python code annotations when writing to yaml.
146151
if isinstance(tag, tuple):
147152
prop_dict[attr] = list(tag)
148153
elif (tag == []) or tag: # Even if 'values' is empty, allow '[]'
@@ -266,8 +271,14 @@ def parse_sections(self, section_list):
266271
elif attr == 'sections':
267272
children_secs = self.parse_sections(section['sections'])
268273
elif attr:
274+
# Tuples had to be serialized as lists to support the yaml format.
275+
# Now convert cardinality lists back to tuples.
276+
content = section[attr]
277+
if attr.endswith("_cardinality"):
278+
content = parse_cardinality(content)
279+
269280
# Make sure to always use the correct odml format attribute name
270-
sec_attrs[odmlfmt.Section.map(attr)] = section[attr]
281+
sec_attrs[odmlfmt.Section.map(attr)] = content
271282

272283
sec = odmlfmt.Section.create(**sec_attrs)
273284
for prop in sec_props:
@@ -297,6 +308,8 @@ def parse_properties(self, props_list):
297308
attr = self.is_valid_attribute(i, odmlfmt.Property)
298309
if attr:
299310
content = _property[attr]
311+
# Tuples had to be serialized as lists to support the yaml format.
312+
# Now convert cardinality lists back to tuples.
300313
if attr.endswith("_cardinality"):
301314
content = parse_cardinality(content)
302315

odml/validation.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,35 @@ def property_values_string_check(prop):
455455
Validation.register_handler('property', property_values_string_check)
456456

457457

458+
def section_properties_cardinality(obj):
459+
"""
460+
Checks Section properties against any set property cardinality.
461+
462+
:param obj: odml.Section
463+
:return: Yields a ValidationError warning, if a set cardinality is not met.
464+
"""
465+
if obj.prop_cardinality and isinstance(obj.prop_cardinality, tuple):
466+
467+
val_min = obj.prop_cardinality[0]
468+
val_max = obj.prop_cardinality[1]
469+
470+
val_len = len(obj.properties) if obj.properties else 0
471+
472+
invalid_cause = ""
473+
if val_min and val_len < val_min:
474+
invalid_cause = "minimum %s" % val_min
475+
elif val_max and (obj.properties and len(obj.properties) > val_max):
476+
invalid_cause = "maximum %s" % val_max
477+
478+
if invalid_cause:
479+
msg = "Section properties cardinality violated"
480+
msg += " (%s values, %s found)" % (invalid_cause, val_len)
481+
yield ValidationError(obj, msg, LABEL_WARNING)
482+
483+
484+
Validation.register_handler("section", section_properties_cardinality)
485+
486+
458487
def property_values_cardinality(prop):
459488
"""
460489
Checks Property values against any set value cardinality.

test/test_section.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,147 @@ def test_export_leaf(self):
978978
self.assertEqual(len(ex3['first'].sections), 1)
979979
self.assertEqual(len(ex3['first']['third']), 0)
980980

981+
def _test_cardinality_re_assignment(self, obj, obj_attribute):
982+
"""
983+
Tests the basic set of both Section properties and sub-sections cardinality.
984+
985+
:param obj: odml Section
986+
:param obj_attribute: string with the cardinality attribute that is supposed to be tested.
987+
Should be either 'prop_cardinality' or 'sec_cardinality'.
988+
"""
989+
oat = obj_attribute
990+
991+
# Test Section prop/sec cardinality reset
992+
for non_val in [None, "", [], (), {}]:
993+
setattr(obj, oat, non_val)
994+
self.assertIsNone(getattr(obj, oat))
995+
setattr(obj, oat, 1)
996+
997+
# Test Section prop/sec cardinality single int max assignment
998+
setattr(obj, oat, 10)
999+
self.assertEqual(getattr(obj, oat), (None, 10))
1000+
1001+
# Test Section prop/sec cardinality tuple max assignment
1002+
setattr(obj, oat, (None, 5))
1003+
self.assertEqual(getattr(obj, oat), (None, 5))
1004+
1005+
# Test Section prop/sec cardinality tuple min assignment
1006+
setattr(obj, oat, (5, None))
1007+
self.assertEqual(getattr(obj, oat), (5, None))
1008+
1009+
# Test Section prop/sec cardinality min/max assignment
1010+
setattr(obj, oat, (1, 5))
1011+
self.assertEqual(getattr(obj, oat), (1, 5))
1012+
1013+
# -- Test Section prop/sec cardinality assignment failures
1014+
with self.assertRaises(ValueError):
1015+
setattr(obj, oat, "a")
1016+
1017+
with self.assertRaises(ValueError):
1018+
setattr(obj, oat, -1)
1019+
1020+
with self.assertRaises(ValueError):
1021+
setattr(obj, oat, (1, "b"))
1022+
1023+
with self.assertRaises(ValueError):
1024+
setattr(obj, oat, (1, 2, 3))
1025+
1026+
with self.assertRaises(ValueError):
1027+
setattr(obj, oat, [1, 2, 3])
1028+
1029+
with self.assertRaises(ValueError):
1030+
setattr(obj, oat, {1: 2, 3: 4})
1031+
1032+
with self.assertRaises(ValueError):
1033+
setattr(obj, oat, (-1, 1))
1034+
1035+
with self.assertRaises(ValueError):
1036+
setattr(obj, oat, (1, -5))
1037+
1038+
with self.assertRaises(ValueError):
1039+
setattr(obj, oat, (5, 1))
1040+
1041+
def test_properties_cardinality(self):
1042+
"""
1043+
Tests the basic assignment rules for Section Properties cardinality
1044+
on init and re-assignment but does not test properties assignment or
1045+
the actual cardinality validation.
1046+
"""
1047+
doc = Document()
1048+
1049+
# -- Test set cardinality on Section init
1050+
# Test empty init
1051+
sec_prop_card_none = Section(name="sec_prop_card_none", type="test", parent=doc)
1052+
self.assertIsNone(sec_prop_card_none.prop_cardinality)
1053+
1054+
# Test single int max init
1055+
sec_card_max = Section(name="prop_cardinality_max", prop_cardinality=10, parent=doc)
1056+
self.assertEqual(sec_card_max.prop_cardinality, (None, 10))
1057+
1058+
# Test tuple init
1059+
sec_card_min = Section(name="prop_cardinality_min", prop_cardinality=(2, None), parent=doc)
1060+
self.assertEqual(sec_card_min.prop_cardinality, (2, None))
1061+
1062+
# -- Test Section properties cardinality re-assignment
1063+
sec = Section(name="prop", prop_cardinality=(None, 10), parent=doc)
1064+
self.assertEqual(sec.prop_cardinality, (None, 10))
1065+
1066+
# Use general method to reduce redundancy
1067+
self._test_cardinality_re_assignment(sec, 'prop_cardinality')
1068+
1069+
def _test_set_cardinality_method(self, obj, obj_attribute, set_cardinality_method):
1070+
"""
1071+
Tests the basic set convenience method of both Section properties and
1072+
sub-sections cardinality.
1073+
1074+
:param obj: odml Section
1075+
:param obj_attribute: string with the cardinality attribute that is supposed to be tested.
1076+
Should be either 'prop_cardinality' or 'sec_cardinality'.
1077+
:param set_cardinality_method: The convenience method used to set the cardinality.
1078+
"""
1079+
oba = obj_attribute
1080+
1081+
# Test Section prop/sec cardinality min assignment
1082+
set_cardinality_method(1)
1083+
self.assertEqual(getattr(obj, oba), (1, None))
1084+
1085+
# Test Section prop/sec cardinality keyword min assignment
1086+
set_cardinality_method(min_val=2)
1087+
self.assertEqual(getattr(obj, oba), (2, None))
1088+
1089+
# Test Section prop/sec cardinality max assignment
1090+
set_cardinality_method(None, 1)
1091+
self.assertEqual(getattr(obj, oba), (None, 1))
1092+
1093+
# Test Section prop/sec cardinality keyword max assignment
1094+
set_cardinality_method(max_val=2)
1095+
self.assertEqual(getattr(obj, oba), (None, 2))
1096+
1097+
# Test Section prop/sec cardinality min max assignment
1098+
set_cardinality_method(1, 2)
1099+
self.assertEqual(getattr(obj, oba), (1, 2))
1100+
1101+
# Test Section prop/sec cardinality keyword min max assignment
1102+
set_cardinality_method(min_val=2, max_val=5)
1103+
self.assertEqual(getattr(obj, oba), (2, 5))
1104+
1105+
# Test Section prop/sec cardinality empty reset
1106+
set_cardinality_method()
1107+
self.assertIsNone(getattr(obj, oba))
1108+
1109+
# Test Section prop/sec cardinality keyword empty reset
1110+
set_cardinality_method(1)
1111+
self.assertIsNotNone(getattr(obj, oba))
1112+
set_cardinality_method(min_val=None, max_val=None)
1113+
self.assertIsNone(getattr(obj, oba))
1114+
1115+
def test_set_properties_cardinality(self):
1116+
doc = Document()
1117+
sec = Section(name="sec", type="test", parent=doc)
1118+
1119+
# Use general method to reduce redundancy
1120+
self._test_set_cardinality_method(sec, 'prop_cardinality', sec.set_properties_cardinality)
1121+
9811122
def test_link(self):
9821123
pass
9831124

0 commit comments

Comments
 (0)