Skip to content

Commit dd29249

Browse files
authored
Merge pull request #384 from mpsonntag/cardSecB
Adding Section sections cardinality
2 parents 13fd740 + bbecc62 commit dd29249

File tree

6 files changed

+294
-39
lines changed

6 files changed

+294
-39
lines changed

odml/format.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ class Section(Format):
157157
'section': 0,
158158
'include': 0,
159159
'property': 0,
160+
'sec_cardinality': 0,
160161
'prop_cardinality': 0
161162
}
162163
_map = {

odml/section.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class BaseSection(base.Sectionable):
4343
:param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
4444
an id will be generated and assigned. An id has to be unique
4545
within an odML Document.
46+
:param sec_cardinality: Section cardinality defines how many Sub-Sections are allowed for this
47+
Section. By default unlimited Sections can be set.
48+
A required number of Sections can be set by assigning a tuple of the
49+
format "(min, max)"
4650
:param prop_cardinality: Property cardinality defines how many Properties are allowed for this
4751
Section. By default unlimited Properties can be set.
4852
A required number of Properties can be set by assigning a tuple of the
@@ -60,7 +64,7 @@ class BaseSection(base.Sectionable):
6064
def __init__(self, name=None, type="n.s.", parent=None,
6165
definition=None, reference=None,
6266
repository=None, link=None, include=None, oid=None,
63-
prop_cardinality=None):
67+
sec_cardinality=None, prop_cardinality=None):
6468

6569
# Sets _sections Smartlist and _repository to None, so run first.
6670
super(BaseSection, self).__init__()
@@ -86,6 +90,7 @@ def __init__(self, name=None, type="n.s.", parent=None,
8690
self._repository = repository
8791
self._link = link
8892
self._include = include
93+
self._sec_cardinality = None
8994
self._prop_cardinality = None
9095

9196
# this may fire a change event, so have the section setup then
@@ -94,6 +99,7 @@ def __init__(self, name=None, type="n.s.", parent=None,
9499

95100
# This might lead to a validation warning, since properties are set
96101
# at a later point in time.
102+
self.sec_cardinality = sec_cardinality
97103
self.prop_cardinality = prop_cardinality
98104

99105
for err in validation.Validation(self).errors:
@@ -360,6 +366,59 @@ def get_repository(self):
360366
def repository(self, url):
361367
base.Sectionable.repository.fset(self, url)
362368

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

odml/validation.py

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ def __repr__(self):
5353
class Validation(object):
5454
"""
5555
Validation provides a set of default validations that can used to validate
56-
an odml.Document. Custom validations can be added via the 'register_handler' method.
56+
odml objects. Custom validations can be added via the 'register_handler' method.
5757
58-
:param doc: odml.Document that the validation will be applied to.
58+
:param obj: odml object the validation will be applied to.
5959
"""
6060

6161
_handlers = {}
@@ -77,19 +77,18 @@ def register_handler(klass, handler):
7777
"""
7878
Validation._handlers.setdefault(klass, set()).add(handler)
7979

80-
def __init__(self, obj):
81-
self.doc = obj # may also be a section
80+
def __init__(self, obj, validate=True, reset=False):
81+
self.obj = obj # may also be a section
8282
self.errors = []
8383

84-
self.validate(obj)
85-
86-
if obj.format().name == "property":
84+
# If initialized with reset=True, reset all handlers and
85+
# do not run any validation yet to allow custom Validation objects.
86+
if reset:
87+
self._handlers = {}
8788
return
8889

89-
for sec in obj.itersections(recursive=True):
90-
self.validate(sec)
91-
for prop in sec.properties:
92-
self.validate(prop)
90+
if validate:
91+
self.run_validation()
9392

9493
def validate(self, obj):
9594
"""
@@ -109,6 +108,38 @@ def error(self, validation_error):
109108
"""
110109
self.errors.append(validation_error)
111110

111+
def run_validation(self):
112+
"""
113+
Runs a clean new validation on the registered Validation object.
114+
"""
115+
self.errors = []
116+
117+
self.validate(self.obj)
118+
119+
if self.obj.format().name == "property":
120+
return
121+
122+
for sec in self.obj.itersections(recursive=True):
123+
self.validate(sec)
124+
for prop in sec.properties:
125+
self.validate(prop)
126+
127+
def register_custom_handler(self, klass, handler):
128+
"""
129+
Adds a validation handler for an odml class. The handler is called in the
130+
validation process for each corresponding object.
131+
The *handler* is assumed to be a generator function yielding
132+
all ValidationErrors it finds.
133+
134+
Section handlers are only called for sections and not for the document node.
135+
If both are required, the handler needs to be registered twice.
136+
137+
:param klass: string corresponding to an odml class. Valid strings are
138+
'odML', 'section' and 'property'.
139+
:param handler: validation function applied to the odml class.
140+
"""
141+
self._handlers.setdefault(klass, set()).add(handler)
142+
112143
def __getitem__(self, obj):
113144
"""
114145
Return a list of the errors for a certain object.
@@ -455,59 +486,89 @@ def property_values_string_check(prop):
455486
Validation.register_handler('property', property_values_string_check)
456487

457488

458-
def section_properties_cardinality(obj):
489+
def _cardinality_validation(obj, cardinality, card_target_attr, validation_rank):
459490
"""
460-
Checks Section properties against any set property cardinality.
491+
Helper function that validates the cardinality of an odml object attribute.
492+
Valid object-attribute combinations are Section-sections, Section-properties and
493+
Property-values.
461494
462-
:param obj: odml.Section
463-
:return: Yields a ValidationError warning, if a set cardinality is not met.
495+
:param obj: an odml.Section or an odml.Property
496+
:param cardinality: 2-int tuple containing the cardinality value
497+
:param card_target_attr: string containing the name of the attribute the cardinality is
498+
applied against. Supported values are:
499+
'sections', 'properties' or 'values'
500+
:param validation_rank: Rank of the yielded ValidationError.
501+
502+
:return: Returns a ValidationError, if a set cardinality is not met or None.
464503
"""
465-
if obj.prop_cardinality and isinstance(obj.prop_cardinality, tuple):
504+
err = None
505+
if cardinality and isinstance(cardinality, tuple):
466506

467-
val_min = obj.prop_cardinality[0]
468-
val_max = obj.prop_cardinality[1]
507+
val_min = cardinality[0]
508+
val_max = cardinality[1]
469509

470-
val_len = len(obj.properties) if obj.properties else 0
510+
card_target = getattr(obj, card_target_attr)
511+
val_len = len(card_target) if card_target else 0
471512

472513
invalid_cause = ""
473514
if val_min and val_len < val_min:
474515
invalid_cause = "minimum %s" % val_min
475-
elif val_max and (obj.properties and len(obj.properties) > val_max):
516+
elif val_max and val_len > val_max:
476517
invalid_cause = "maximum %s" % val_max
477518

478519
if invalid_cause:
479-
msg = "Section properties cardinality violated"
520+
obj_name = obj.format().name.capitalize()
521+
msg = "%s %s cardinality violated" % (obj_name, card_target_attr)
480522
msg += " (%s values, %s found)" % (invalid_cause, val_len)
481-
yield ValidationError(obj, msg, LABEL_WARNING)
523+
524+
err = ValidationError(obj, msg, validation_rank)
525+
526+
return err
527+
528+
529+
def section_properties_cardinality(obj):
530+
"""
531+
Checks Section properties against any set property cardinality.
532+
533+
:param obj: odml.Section
534+
535+
:return: Yields a ValidationError warning, if a set cardinality is not met.
536+
"""
537+
err = _cardinality_validation(obj, obj.prop_cardinality, 'properties', LABEL_WARNING)
538+
if err:
539+
yield err
482540

483541

484542
Validation.register_handler("section", section_properties_cardinality)
485543

486544

487-
def property_values_cardinality(prop):
545+
def section_sections_cardinality(obj):
488546
"""
489-
Checks Property values against any set value cardinality.
547+
Checks Section sub-sections against any set sub-section cardinality.
548+
549+
:param obj: odml.Section
490550
491-
:param prop: odml.Property
492551
:return: Yields a ValidationError warning, if a set cardinality is not met.
493552
"""
494-
if prop.val_cardinality and isinstance(prop.val_cardinality, tuple):
553+
err = _cardinality_validation(obj, obj.sec_cardinality, 'sections', LABEL_WARNING)
554+
if err:
555+
yield err
495556

496-
val_min = prop.val_cardinality[0]
497-
val_max = prop.val_cardinality[1]
498557

499-
val_len = len(prop.values) if prop.values else 0
558+
Validation.register_handler("section", section_sections_cardinality)
500559

501-
invalid_cause = ""
502-
if val_min and val_len < val_min:
503-
invalid_cause = "minimum %s" % val_min
504-
elif val_max and (prop.values and len(prop.values) > val_max):
505-
invalid_cause = "maximum %s" % val_max
506560

507-
if invalid_cause:
508-
msg = "Property values cardinality violated"
509-
msg += " (%s values, %s found)" % (invalid_cause, val_len)
510-
yield ValidationError(prop, msg, LABEL_WARNING)
561+
def property_values_cardinality(obj):
562+
"""
563+
Checks Property values against any set value cardinality.
564+
565+
:param obj: odml.Property
566+
567+
:return: Yields a ValidationError warning, if a set cardinality is not met.
568+
"""
569+
err = _cardinality_validation(obj, obj.val_cardinality, 'values', LABEL_WARNING)
570+
if err:
571+
yield err
511572

512573

513574
Validation.register_handler("property", property_values_cardinality)

test/test_section.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,34 @@ def test_properties_cardinality(self):
10661066
# Use general method to reduce redundancy
10671067
self._test_cardinality_re_assignment(sec, 'prop_cardinality')
10681068

1069+
def test_sections_cardinality(self):
1070+
"""
1071+
Tests the basic assignment rules for Section sections cardinality
1072+
on init and re-assignment but does not test sections assignment or
1073+
the actual cardinality validation.
1074+
"""
1075+
doc = Document()
1076+
1077+
# -- Test set cardinality on Section init
1078+
# Test empty init
1079+
sec_card_none = Section(name="sec_cardinality_none", type="test", parent=doc)
1080+
self.assertIsNone(sec_card_none.sec_cardinality)
1081+
1082+
# Test single int max init
1083+
sec_card_max = Section(name="sec_cardinality_max", sec_cardinality=10, parent=doc)
1084+
self.assertEqual(sec_card_max.sec_cardinality, (None, 10))
1085+
1086+
# Test tuple init
1087+
sec_card_min = Section(name="sec_cardinality_min", sec_cardinality=(2, None), parent=doc)
1088+
self.assertEqual(sec_card_min.sec_cardinality, (2, None))
1089+
1090+
# -- Test Section properties cardinality re-assignment
1091+
sec = Section(name="sec", sec_cardinality=(None, 10), parent=doc)
1092+
self.assertEqual(sec.sec_cardinality, (None, 10))
1093+
1094+
# Use general method to reduce redundancy
1095+
self._test_cardinality_re_assignment(sec, 'sec_cardinality')
1096+
10691097
def _test_set_cardinality_method(self, obj, obj_attribute, set_cardinality_method):
10701098
"""
10711099
Tests the basic set convenience method of both Section properties and
@@ -1119,6 +1147,13 @@ def test_set_properties_cardinality(self):
11191147
# Use general method to reduce redundancy
11201148
self._test_set_cardinality_method(sec, 'prop_cardinality', sec.set_properties_cardinality)
11211149

1150+
def test_set_sections_cardinality(self):
1151+
doc = Document()
1152+
sec = Section(name="sec", type="test", parent=doc)
1153+
1154+
# Use general method to reduce redundancy
1155+
self._test_set_cardinality_method(sec, 'sec_cardinality', sec.set_sections_cardinality)
1156+
11221157
def test_link(self):
11231158
pass
11241159

test/test_section_integration.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,48 @@ def test_prop_cardinality(self):
257257
self._test_cardinality_load("prop_cardinality", yaml_doc, card_dict,
258258
sec_empty, sec_max, sec_min, sec_full)
259259

260+
def test_sec_cardinality(self):
261+
"""
262+
Test saving and loading of Section sections cardinality variants to
263+
and from all supported file formats.
264+
"""
265+
doc = odml.Document()
266+
267+
sec_empty = "card_empty"
268+
sec_max = "card_max"
269+
sec_min = "card_min"
270+
sec_full = "card_full"
271+
card_dict = {
272+
sec_empty: None,
273+
sec_max: (None, 10),
274+
sec_min: (2, None),
275+
sec_full: (1, 5)
276+
}
277+
278+
_ = odml.Section(name=sec_empty, type="test", parent=doc)
279+
_ = odml.Section(name=sec_max, sec_cardinality=card_dict[sec_max], type="test", parent=doc)
280+
_ = odml.Section(name=sec_min, sec_cardinality=card_dict[sec_min], type="test", parent=doc)
281+
_ = odml.Section(name=sec_full, sec_cardinality=card_dict[sec_full],
282+
type="test", parent=doc)
283+
284+
# Test saving to and loading from an XML file
285+
odml.save(doc, self.xml_file)
286+
xml_doc = odml.load(self.xml_file)
287+
self._test_cardinality_load("sec_cardinality", xml_doc, card_dict,
288+
sec_empty, sec_max, sec_min, sec_full)
289+
290+
# Test saving to and loading from a JSON file
291+
odml.save(doc, self.json_file, "JSON")
292+
json_doc = odml.load(self.json_file, "JSON")
293+
self._test_cardinality_load("sec_cardinality", json_doc, card_dict,
294+
sec_empty, sec_max, sec_min, sec_full)
295+
296+
# Test saving to and loading from a YAML file
297+
odml.save(doc, self.yaml_file, "YAML")
298+
yaml_doc = odml.load(self.yaml_file, "YAML")
299+
self._test_cardinality_load("sec_cardinality", yaml_doc, card_dict,
300+
sec_empty, sec_max, sec_min, sec_full)
301+
260302
def test_link(self):
261303
pass
262304

0 commit comments

Comments
 (0)