Skip to content

Commit 48d83b7

Browse files
authored
Merge pull request #363 from fschrader1992/master
Validation Updates
2 parents 5cdbce4 + 329588a commit 48d83b7

File tree

7 files changed

+143
-31
lines changed

7 files changed

+143
-31
lines changed

odml/property.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from . import base
88
from . import dtypes
9+
from . import validation
910
from . import format as frmt
1011
from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
1112

@@ -128,6 +129,11 @@ def __init__(self, name=None, values=None, parent=None, unit=None,
128129

129130
self.parent = parent
130131

132+
for err in validation.Validation(self).errors:
133+
if err.is_error:
134+
msg = "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg)
135+
print(msg)
136+
131137
def __len__(self):
132138
return len(self._values)
133139

odml/section.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from . import base
1313
from . import format as fmt
1414
from . import terminology
15+
from . import validation
1516
from .doc import BaseDocument
1617
# this is supposedly ok, as we only use it for an isinstance check
1718
from .property import BaseProperty
@@ -81,6 +82,11 @@ def __init__(self, name=None, type=None, parent=None,
8182
self.type = type
8283
self.parent = parent
8384

85+
for err in validation.Validation(self).errors:
86+
if err.is_error:
87+
msg = "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg)
88+
print(msg)
89+
8490
def __repr__(self):
8591
return "Section[%d|%d] {name = %s, type = %s, id = %s}" % (len(self._sections),
8692
len(self._props),

odml/validation.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Generic odML validation framework.
44
"""
55

6+
from . import dtypes
7+
68
LABEL_ERROR = 'error'
79
LABEL_WARNING = 'warning'
810

@@ -74,12 +76,16 @@ def register_handler(klass, handler):
7476
"""
7577
Validation._handlers.setdefault(klass, set()).add(handler)
7678

77-
def __init__(self, doc):
78-
self.doc = doc # may also be a section
79+
def __init__(self, obj):
80+
self.doc = obj # may also be a section
7981
self.errors = []
80-
self.validate(doc)
8182

82-
for sec in doc.itersections(recursive=True):
83+
self.validate(obj)
84+
85+
if obj.format().name == "property":
86+
return
87+
88+
for sec in obj.itersections(recursive=True):
8389
self.validate(sec)
8490
for prop in sec.properties:
8591
self.validate(prop)
@@ -116,6 +122,31 @@ def __getitem__(self, obj):
116122
# ------------------------------------------------
117123
# validation rules
118124

125+
def object_required_attributes(obj):
126+
"""
127+
Tests that no Object has undefined attributes, given in format.
128+
129+
:param obj: document, section or property.
130+
"""
131+
132+
args = obj.format().arguments
133+
for arg in args:
134+
if arg[1] == 1:
135+
if not hasattr(obj, arg[0]):
136+
msg = "Missing attribute %s for %s" % (obj.format().name.capitalize(), arg[0])
137+
yield ValidationError(obj, msg, LABEL_ERROR)
138+
continue
139+
obj_arg = getattr(obj, arg[0])
140+
if not obj_arg and not isinstance(obj_arg, bool):
141+
msg = "%s %s undefined" % (obj.format().name.capitalize(), arg[0])
142+
yield ValidationError(obj, msg, LABEL_ERROR)
143+
144+
145+
Validation.register_handler('odML', object_required_attributes)
146+
Validation.register_handler('section', object_required_attributes)
147+
Validation.register_handler('property', object_required_attributes)
148+
149+
119150
def section_type_must_be_defined(sec):
120151
"""
121152
Tests that no Section has an undefined type.
@@ -282,6 +313,9 @@ def property_terminology_check(prop):
282313
2. warn, if there are multiple values with different units or the unit does
283314
not match the one in the terminology.
284315
"""
316+
if not prop.parent:
317+
return
318+
285319
tsec = prop.parent.get_terminology_equivalent()
286320
if tsec is None:
287321
return
@@ -300,6 +334,9 @@ def property_dependency_check(prop):
300334
Produces a warning if the dependency attribute refers to a non-existent attribute
301335
or the dependency_value does not match.
302336
"""
337+
if not prop.parent:
338+
return
339+
303340
dep = prop.dependency
304341
if dep is None:
305342
return
@@ -317,3 +354,35 @@ def property_dependency_check(prop):
317354

318355

319356
Validation.register_handler('property', property_dependency_check)
357+
358+
359+
def property_values_check(prop):
360+
"""
361+
Tests that the values are of consistent dtype.
362+
If dtype is not given, infer from first item in list.
363+
364+
:param prop: property the validation is applied on.
365+
"""
366+
367+
if prop.dtype is not None and prop.dtype != "":
368+
dtype = prop.dtype
369+
elif prop.values:
370+
dtype = dtypes.infer_dtype(prop.values[0])
371+
else:
372+
return
373+
374+
for val in prop.values:
375+
if dtype.endswith("-tuple"):
376+
tuple_len = int(dtype[:-6])
377+
if len(val) != tuple_len:
378+
msg = "Tuple of length %s not consistent with dtype %s!" % (len(val), dtype)
379+
yield ValidationError(prop, msg, LABEL_WARNING)
380+
else:
381+
try:
382+
dtypes.get(val, dtype)
383+
except ValueError:
384+
msg = "Property values not of consistent dtype!"
385+
yield ValidationError(prop, msg, LABEL_WARNING)
386+
387+
388+
Validation.register_handler('property', property_values_check)

test/test_doc_integration.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,25 +104,26 @@ def test_children(self):
104104
"""
105105
This test checks the correct saving and loading of Section children of a Document.
106106
"""
107+
s_type = "type"
107108
# Lvl 1 child Sections
108-
sec_lvl_11 = odml.Section(name="sec_11", parent=self.doc)
109-
_ = odml.Section(name="sec_12", parent=self.doc)
109+
sec_lvl_11 = odml.Section(name="sec_11", type=s_type, parent=self.doc)
110+
_ = odml.Section(name="sec_12", type=s_type, parent=self.doc)
110111

111112
# Lvl 2 child Sections
112-
sec_lvl_21 = odml.Section(name="sec_21", parent=sec_lvl_11)
113-
_ = odml.Section(name="sec_22", parent=sec_lvl_11)
114-
_ = odml.Section(name="sec_23", parent=sec_lvl_11)
113+
sec_lvl_21 = odml.Section(name="sec_21", type=s_type, parent=sec_lvl_11)
114+
_ = odml.Section(name="sec_22", type=s_type, parent=sec_lvl_11)
115+
_ = odml.Section(name="sec_23", type=s_type, parent=sec_lvl_11)
115116

116117
# Lvl 2 child Properties
117118
_ = odml.Property(name="prop_21", parent=sec_lvl_11)
118119
_ = odml.Property(name="prop_22", parent=sec_lvl_11)
119120
_ = odml.Property(name="prop_23", parent=sec_lvl_11)
120121

121122
# Lvl 3 child Sections
122-
_ = odml.Section(name="sec_31", parent=sec_lvl_21)
123-
_ = odml.Section(name="sec_32", parent=sec_lvl_21)
124-
_ = odml.Section(name="sec_33", parent=sec_lvl_21)
125-
_ = odml.Section(name="sec_34", parent=sec_lvl_21)
123+
_ = odml.Section(name="sec_31", type=s_type, parent=sec_lvl_21)
124+
_ = odml.Section(name="sec_32", type=s_type, parent=sec_lvl_21)
125+
_ = odml.Section(name="sec_33", type=s_type, parent=sec_lvl_21)
126+
_ = odml.Section(name="sec_34", type=s_type, parent=sec_lvl_21)
126127

127128
# Lvl 3 child Properties
128129
_ = odml.Property(name="prop_31", parent=sec_lvl_21)

test/test_dumper.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ def setUp(self):
1616
self.captured_stdout = StringIO()
1717
sys.stdout = self.captured_stdout
1818

19+
s_type = "type"
20+
1921
self.doc = odml.Document(author='Rave', version='1.0')
20-
s1 = odml.Section(name='Cell')
22+
s1 = odml.Section(name='Cell', type=s_type)
2123
p1 = odml.Property(name='Type', values='Rechargeable')
2224
s1.append(p1)
2325

24-
s2 = odml.Section(name='Electrolyte')
26+
s2 = odml.Section(name='Electrolyte', type=s_type)
2527
p2 = odml.Property(name='Composition', values='Ni-Cd')
2628
s2.append(p2)
2729
s1.append(s2)
2830

29-
s3 = odml.Section(name='Electrode')
31+
s3 = odml.Section(name='Electrode', type=s_type)
3032
p3 = odml.Property(name='Material', values='Nickel')
3133
p4 = odml.Property(name='Models', values=['AA', 'AAA'])
3234
s3.append(p3)
@@ -41,11 +43,11 @@ def test_dump_doc(self):
4143
odml.tools.dumper.dump_doc(self.doc)
4244
output = [x.strip() for x in self.captured_stdout.getvalue().split('\n') if x]
4345
expected_output = []
44-
expected_output.append("*Cell ()")
46+
expected_output.append("*Cell (type='type')")
4547
expected_output.append(":Type (values=Rechargeable, dtype='string')")
46-
expected_output.append("*Electrolyte ()")
48+
expected_output.append("*Electrolyte (type='type')")
4749
expected_output.append(":Composition (values=Ni-Cd, dtype='string')")
48-
expected_output.append("*Electrode ()")
50+
expected_output.append("*Electrode (type='type')")
4951
expected_output.append(":Material (values=Nickel, dtype='string')")
5052
expected_output.append(":Models (values=[AA,AAA], dtype='string')")
5153
self.assertEqual(len(output), len(expected_output))

test/test_section_integration.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def save_load(self):
5151
def test_id(self):
5252
# Test correct save and load of generated id.
5353
sec_name = "empty_id"
54-
sec = odml.Section(name=sec_name, parent=self.doc)
54+
sec_type = "type"
55+
sec = odml.Section(name=sec_name, type=sec_type, parent=self.doc)
5556

5657
jdoc, xdoc, ydoc = self.save_load()
5758

@@ -62,7 +63,7 @@ def test_id(self):
6263
# Test correct save and load of assigned id.
6364
sec_name = "assigned_id"
6465
assigned_id = "79b613eb-a256-46bf-84f6-207df465b8f7"
65-
_ = odml.Section(name=sec_name, oid=assigned_id, parent=self.doc)
66+
_ = odml.Section(name=sec_name, oid=assigned_id, type=sec_type, parent=self.doc)
6667

6768
jdoc, xdoc, ydoc = self.save_load()
6869

@@ -116,31 +117,32 @@ def test_children(self):
116117
This test checks correct writing and loading of Section and Property
117118
children of a Section.
118119
"""
119-
root = odml.Section(name="root", parent=self.doc)
120+
s_type = "type"
121+
root = odml.Section(name="root", type=s_type, parent=self.doc)
120122

121123
# Lvl 1 child Sections
122-
sec_lvl_11 = odml.Section(name="sec_11", parent=root)
123-
_ = odml.Section(name="sec_12", parent=root)
124+
sec_lvl_11 = odml.Section(name="sec_11", type=s_type, parent=root)
125+
_ = odml.Section(name="sec_12", type=s_type, parent=root)
124126

125127
# Lvl 1 child Properties
126128
_ = odml.Property(name="prop_11", parent=root)
127129
_ = odml.Property(name="prop_12", parent=root)
128130

129131
# Lvl 2 child Sections
130-
sec_lvl_21 = odml.Section(name="sec_21", parent=sec_lvl_11)
131-
_ = odml.Section(name="sec_22", parent=sec_lvl_11)
132-
_ = odml.Section(name="sec_23", parent=sec_lvl_11)
132+
sec_lvl_21 = odml.Section(name="sec_21", type=s_type, parent=sec_lvl_11)
133+
_ = odml.Section(name="sec_22", type=s_type, parent=sec_lvl_11)
134+
_ = odml.Section(name="sec_23", type=s_type, parent=sec_lvl_11)
133135

134136
# Lvl 2 child Properties
135137
_ = odml.Property(name="prop_21", parent=sec_lvl_11)
136138
_ = odml.Property(name="prop_22", parent=sec_lvl_11)
137139
_ = odml.Property(name="prop_23", parent=sec_lvl_11)
138140

139141
# Lvl 3 child Sections
140-
_ = odml.Section(name="sec_31", parent=sec_lvl_21)
141-
_ = odml.Section(name="sec_32", parent=sec_lvl_21)
142-
_ = odml.Section(name="sec_33", parent=sec_lvl_21)
143-
_ = odml.Section(name="sec_34", parent=sec_lvl_21)
142+
_ = odml.Section(name="sec_31", type=s_type, parent=sec_lvl_21)
143+
_ = odml.Section(name="sec_32", type=s_type, parent=sec_lvl_21)
144+
_ = odml.Section(name="sec_33", type=s_type, parent=sec_lvl_21)
145+
_ = odml.Section(name="sec_34", type=s_type, parent=sec_lvl_21)
144146

145147
# Lvl 3 child Properties
146148
_ = odml.Property(name="prop_31", parent=sec_lvl_21)

test/test_validation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,29 @@ def test_section_unique_ids(self):
126126

127127
res = validate(doc)
128128
self.assertError(res, "Duplicate id in Section")
129+
130+
def test_standalone_section(self):
131+
"""
132+
Test if standalone section does not return errors if required attributes are correct.
133+
If type is undefined, check error message.
134+
"""
135+
136+
sec_one = odml.Section("sec1")
137+
138+
res = validate(sec_one)
139+
self.assertError(res, "Section type undefined")
140+
141+
doc = samplefile.parse("""s1[undefined]""")
142+
res = validate(doc)
143+
self.assertError(res, "Section type undefined")
144+
145+
def test_standalone_property(self):
146+
"""
147+
Test if standalone property does not return errors if required attributes are correct.
148+
"""
149+
150+
prop = odml.Property()
151+
prop.type = ""
152+
153+
for err in validate(prop).errors:
154+
assert not err.is_error

0 commit comments

Comments
 (0)