From db1a8cf05344ab6593ec80929334cb8be1277b3b Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 23 Apr 2020 17:28:12 +0200 Subject: [PATCH 01/16] [validation] Remove Section repo present val The Section repository present validation is no longer part of the default validation and should be added on demand. --- odml/validation.py | 5 ++--- test/test_validation.py | 15 --------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 2107e5ac..11c90714 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -191,6 +191,8 @@ def section_type_must_be_defined(sec): Validation.register_handler('section', section_type_must_be_defined) +# The Section repository present is no longer part of the default validation +# and should be added on demand. def section_repository_present(sec): """ 1. warn, if a section has no repository or @@ -214,9 +216,6 @@ def section_repository_present(sec): yield ValidationError(sec, msg, LABEL_WARNING) -Validation.register_handler('section', section_repository_present) - - def document_unique_ids(doc): """ Traverse an odML Document and check whether all diff --git a/test/test_validation.py b/test/test_validation.py index d5aded91..03968c0d 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -195,21 +195,6 @@ def test_section_sections_cardinality(self): self.assertTrue(found) - def test_section_in_terminology(self): - doc = samplefile.parse("""s1[T1]""") - res = Validate(doc) - self.assertError(res, "A section should have an associated repository", - filter_rep=False) - - odml.terminology.terminologies['map'] = samplefile.parse(""" - s0[t0] - - S1[T1] - """) - doc.sections[0].repository = 'map' - res = Validate(doc) - # self.assertEqual(list(self.filter_mapping_errors(res.errors)), []) - self.assertEqual(res.errors, []) - def test_uniques(self): self.assertRaises(KeyError, samplefile.parse, """ s1[t1] From e0dc454a61d39f4a9d31fa6736fa1e3b0dd7ba82 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 28 Apr 2020 20:29:30 +0200 Subject: [PATCH 02/16] [validation] Add ValidationID class --- odml/validation.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/odml/validation.py b/odml/validation.py index 11c90714..24a584ff 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -4,12 +4,49 @@ """ import re + +from enum import Enum + from . import dtypes LABEL_ERROR = 'error' LABEL_WARNING = 'warning' +class ValidationID(Enum): + """ + IDs identifying registered validation handlers. + """ + unspecified = 1 + + # Required attributes validations + object_required_attributes = 10 + section_type_must_be_defined = 11 + + # Unique id, name and type validations + section_unique_ids = 20 + property_unique_ids = 21 + section_unique_name_type = 22 + property_unique_name = 23 + + # Good form validations + object_name_readable = 30 + + # Property specific validations + property_terminology_check = 40 + property_dependency_check = 41 + property_values_check = 42 + property_values_string_check = 43 + + # Cardinality validations + section_properties_cardinality = 50 + section_sections_cardinality = 51 + property_values_cardinality = 52 + + # Optional validations + section_repository_present = 60 + + class ValidationError(object): """ Represents an error found in the validation process. From 281461e76ee91eaa519b489d26fc9b0026b59daa Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 28 Apr 2020 20:35:48 +0200 Subject: [PATCH 03/16] [validation] Use validation_id in handlers --- odml/validation.py | 94 +++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 24a584ff..9c7294ab 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -55,10 +55,11 @@ class ValidationError(object): a message and a rank which may be one of: 'error', 'warning'. """ - def __init__(self, obj, msg, rank=LABEL_ERROR): + def __init__(self, obj, msg, rank=LABEL_ERROR, validation_id=None): self.obj = obj self.msg = msg self.rank = rank + self.validation_id = validation_id @property def is_warning(self): @@ -197,17 +198,18 @@ def object_required_attributes(obj): :param obj: document, section or property. """ + validation_id = ValidationID.object_required_attributes args = obj.format().arguments for arg in args: if arg[1] == 1: msg = "Missing required attribute '%s'" % (arg[0]) if not hasattr(obj, arg[0]): - yield ValidationError(obj, msg, LABEL_ERROR) + yield ValidationError(obj, msg, LABEL_ERROR, validation_id) continue obj_arg = getattr(obj, arg[0]) if not obj_arg and not isinstance(obj_arg, bool): - yield ValidationError(obj, msg, LABEL_ERROR) + yield ValidationError(obj, msg, LABEL_ERROR, validation_id) Validation.register_handler('odML', object_required_attributes) @@ -221,8 +223,10 @@ def section_type_must_be_defined(sec): :param sec: odml.Section. """ + validation_id = ValidationID.section_type_must_be_defined + if sec.type and sec.type == "n.s.": - yield ValidationError(sec, "Section type not specified", LABEL_WARNING) + yield ValidationError(sec, "Section type not specified", LABEL_WARNING, validation_id) Validation.register_handler('section', section_type_must_be_defined) @@ -235,22 +239,24 @@ def section_repository_present(sec): 1. warn, if a section has no repository or 2. the section type is not present in the repository """ + validation_id = ValidationID.section_repository_present + repo = sec.get_repository() if repo is None: msg = "A section should have an associated repository" - yield ValidationError(sec, msg, LABEL_WARNING) + yield ValidationError(sec, msg, LABEL_WARNING, validation_id) return try: tsec = sec.get_terminology_equivalent() except Exception as exc: msg = "Could not load terminology: %s" % exc - yield ValidationError(sec, msg, LABEL_WARNING) + yield ValidationError(sec, msg, LABEL_WARNING, validation_id) return if tsec is None: msg = "Section type '%s' not found in terminology" % sec.type - yield ValidationError(sec, msg, LABEL_WARNING) + yield ValidationError(sec, msg, LABEL_WARNING, validation_id) def document_unique_ids(doc): @@ -280,6 +286,8 @@ def section_unique_ids(parent, id_map=None): :param parent: odML Document or Section :param id_map: "id":"odML object / path" dictionary """ + validation_id = ValidationID.section_unique_ids + if not id_map: id_map = {} @@ -289,7 +297,7 @@ def section_unique_ids(parent, id_map=None): if sec.id in id_map: msg = "Duplicate id in Section '%s' and %s" % (sec.get_path(), id_map[sec.id]) - yield ValidationError(sec, msg) + yield ValidationError(sec, msg, validation_id=validation_id) else: id_map[sec.id] = "Section '%s'" % sec.get_path() @@ -309,6 +317,8 @@ def property_unique_ids(section, id_map=None): :param section: odML Section :param id_map: "id":"odML object / path" dictionary """ + validation_id = ValidationID.property_unique_ids + if not id_map: id_map = {} @@ -316,7 +326,7 @@ def property_unique_ids(section, id_map=None): if prop.id in id_map: msg = "Duplicate id in Property '%s' and %s" % (prop.get_path(), id_map[prop.id]) - yield ValidationError(prop, msg) + yield ValidationError(prop, msg, validation_id=validation_id) else: id_map[prop.id] = "Property '%s'" % prop.get_path() @@ -324,24 +334,26 @@ def property_unique_ids(section, id_map=None): Validation.register_handler('odML', document_unique_ids) -def object_unique_names(obj, children, attr=lambda x: x.name, +def object_unique_names(obj, validation_id, children, attr=lambda x: x.name, msg="Object names must be unique"): """ - Tests that object names within one Section are unique. + Tests that object names within a Section are unique. :param obj: odml class instance the validation is applied on. + :param validation_id: id of the :param children: a function that returns the children to be considered. - This is to be able to use the same function for sections and properties. - :param attr: a function that returns the item that needs to be unique - :param msg: error message that will be registered upon a ValidationError. + Required when handling Sections. + :param attr: a function that returns the attribute that needs to be unique. + :param msg: error message that will be registered with a ValidationError. """ names = set(map(attr, children(obj))) if len(names) == len(children(obj)): - return # quick exit + return + names = set() for i in children(obj): if attr(i) in names: - yield ValidationError(i, msg, LABEL_ERROR) + yield ValidationError(i, msg, LABEL_ERROR, validation_id) names.add(attr(i)) @@ -353,6 +365,7 @@ def section_unique_name_type(obj): """ for i in object_unique_names( obj, + validation_id=ValidationID.section_unique_name_type, attr=lambda x: (x.name, x.type), children=lambda x: x.sections, msg="name/type combination must be unique"): @@ -365,7 +378,9 @@ def property_unique_names(obj): :param obj: odml class instance the validation is applied on. """ - for i in object_unique_names(obj, lambda x: x.properties): + for i in object_unique_names(obj, + validation_id=ValidationID.property_unique_name, + children=lambda x: x.properties): yield i @@ -380,8 +395,10 @@ def object_name_readable(obj): :param obj: odml.Section or odml.Property. """ + validation_id = ValidationID.object_name_readable + if obj.name == obj.id: - yield ValidationError(obj, 'Name should be readable', LABEL_WARNING) + yield ValidationError(obj, "Name should be readable", LABEL_WARNING, validation_id) Validation.register_handler('section', object_name_readable) @@ -394,6 +411,8 @@ def property_terminology_check(prop): 2. warn, if there are multiple values with different units or the unit does not match the one in the terminology. """ + validation_id = ValidationID.property_terminology_check + if not prop.parent: return @@ -404,7 +423,7 @@ def property_terminology_check(prop): tsec.properties[prop.name] except KeyError: msg = "Property '%s' not found in terminology" % prop.name - yield ValidationError(prop, msg, LABEL_WARNING) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) Validation.register_handler('property', property_terminology_check) @@ -415,6 +434,8 @@ def property_dependency_check(prop): Produces a warning if the dependency attribute refers to a non-existent attribute or the dependency_value does not match. """ + validation_id = ValidationID.property_dependency_check + if not prop.parent: return @@ -426,12 +447,12 @@ def property_dependency_check(prop): dep_obj = prop.parent[dep] except KeyError: msg = "Property refers to a non-existent dependency object" - yield ValidationError(prop, msg, LABEL_WARNING) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) return if prop.dependency_value not in dep_obj.values[0]: msg = "Dependency-value is not equal to value of the property's dependency" - yield ValidationError(prop, msg, LABEL_WARNING) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) Validation.register_handler('property', property_dependency_check) @@ -444,6 +465,7 @@ def property_values_check(prop): :param prop: property the validation is applied on. """ + validation_id = ValidationID.property_values_check if prop.dtype is not None and prop.dtype != "": dtype = prop.dtype @@ -457,13 +479,13 @@ def property_values_check(prop): tuple_len = int(dtype[:-6]) if len(val) != tuple_len: msg = "Tuple of length %s not consistent with dtype %s!" % (len(val), dtype) - yield ValidationError(prop, msg, LABEL_WARNING) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) else: try: dtypes.get(val, dtype) except ValueError: msg = "Property values not of consistent dtype!" - yield ValidationError(prop, msg, LABEL_WARNING) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) Validation.register_handler('property', property_values_check) @@ -477,6 +499,7 @@ def property_values_string_check(prop): :param prop: property the validation is applied on. """ + validation_id = ValidationID.property_values_string_check if prop.dtype != "string" or not prop.values: return @@ -515,14 +538,15 @@ def property_values_string_check(prop): res_dtype = "string" if res_dtype != "string": - msg = 'Dtype of property "%s" currently is "string", but might fit dtype "%s"!' % (prop.name, res_dtype) - yield ValidationError(prop, msg, LABEL_WARNING) + msg = 'Dtype of property "%s" currently is "string", but might fit dtype "%s"!' % \ + (prop.name, res_dtype) + yield ValidationError(prop, msg, LABEL_WARNING, validation_id) Validation.register_handler('property', property_values_string_check) -def _cardinality_validation(obj, cardinality, card_target_attr, validation_rank): +def _cardinality_validation(obj, cardinality, card_target_attr, validation_rank, validation_id): """ Helper function that validates the cardinality of an odml object attribute. Valid object-attribute combinations are Section-sections, Section-properties and @@ -534,6 +558,7 @@ def _cardinality_validation(obj, cardinality, card_target_attr, validation_rank) applied against. Supported values are: 'sections', 'properties' or 'values' :param validation_rank: Rank of the yielded ValidationError. + :param validation_id: string containing the id of the parent validation. :return: Returns a ValidationError, if a set cardinality is not met or None. """ @@ -557,7 +582,7 @@ def _cardinality_validation(obj, cardinality, card_target_attr, validation_rank) msg = "%s %s cardinality violated" % (obj_name, card_target_attr) msg += " (%s values, %s found)" % (invalid_cause, val_len) - err = ValidationError(obj, msg, validation_rank) + err = ValidationError(obj, msg, validation_rank, validation_id) return err @@ -570,7 +595,10 @@ def section_properties_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - err = _cardinality_validation(obj, obj.prop_cardinality, 'properties', LABEL_WARNING) + validation_id = ValidationID.section_properties_cardinality + + err = _cardinality_validation(obj, obj.prop_cardinality, 'properties', + LABEL_WARNING, validation_id) if err: yield err @@ -586,7 +614,10 @@ def section_sections_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - err = _cardinality_validation(obj, obj.sec_cardinality, 'sections', LABEL_WARNING) + validation_id = ValidationID.section_sections_cardinality + + err = _cardinality_validation(obj, obj.sec_cardinality, 'sections', + LABEL_WARNING, validation_id) if err: yield err @@ -602,7 +633,10 @@ def property_values_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - err = _cardinality_validation(obj, obj.val_cardinality, 'values', LABEL_WARNING) + validation_id = ValidationID.property_values_cardinality + + err = _cardinality_validation(obj, obj.val_cardinality, 'values', + LABEL_WARNING, validation_id) if err: yield err From ec16774bdb3b2f8ece3c020dab0aca6ed560b5d7 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 28 Apr 2020 20:45:26 +0200 Subject: [PATCH 04/16] [section/property] Filter cardinality validation --- odml/property.py | 7 ++++--- odml/section.py | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/odml/property.py b/odml/property.py index 54d267fa..056e4efb 100644 --- a/odml/property.py +++ b/odml/property.py @@ -569,11 +569,12 @@ def _values_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) + val_id = validation.ValidationID.property_values_cardinality # 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)) + for curr in valid.errors: + if curr.validation_id == val_id and self.id == curr.obj.id: + print("%s: %s" % (curr.rank.capitalize(), curr.msg)) def set_values_cardinality(self, min_val=None, max_val=None): """ diff --git a/odml/section.py b/odml/section.py index 536a56ef..b751ee3b 100644 --- a/odml/section.py +++ b/odml/section.py @@ -416,10 +416,12 @@ def _sections_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) + val_id = validation.ValidationID.section_sections_cardinality + # Make sure to display only warnings of the current section - 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)) + for curr in valid.errors: + if curr.validation_id == val_id and self.id == curr.obj.id: + print("%s: %s" % (curr.rank.capitalize(), curr.msg)) @property def prop_cardinality(self): @@ -469,10 +471,12 @@ def _properties_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) + val_id = validation.ValidationID.section_properties_cardinality + # Make sure to display only warnings of the current section - 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)) + for curr in valid.errors: + if curr.validation_id == val_id and self.id == curr.obj.id: + print("%s: %s" % (curr.rank.capitalize(), curr.msg)) @inherit_docstring def get_terminology_equivalent(self): From 30c72cfa40b10049cf6b5ee3515f959beb8bd8e1 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Tue, 28 Apr 2020 21:03:51 +0200 Subject: [PATCH 05/16] [section/property] Ensure name cannot be set None --- odml/property.py | 5 +++++ odml/section.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/odml/property.py b/odml/property.py index 056e4efb..f9fe403b 100644 --- a/odml/property.py +++ b/odml/property.py @@ -213,6 +213,11 @@ def name(self, new_name): if self.name == new_name: return + # Make sure name cannot be set to None or empty + if not new_name: + self._name = self._id + return + curr_parent = self.parent if hasattr(curr_parent, "properties") and new_name in curr_parent.properties: diff --git a/odml/section.py b/odml/section.py index b751ee3b..8937a88e 100644 --- a/odml/section.py +++ b/odml/section.py @@ -171,6 +171,11 @@ def name(self, new_value): if self.name == new_value: return + # Make sure name cannot be set to None or empty + if not new_value: + self._name = self._id + return + curr_parent = self.parent if hasattr(curr_parent, "sections") and new_value in curr_parent.sections: raise KeyError("Object with the same name already exists!") From 199a6490f49e9996e1834f7ab6de3e421a44c507 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 29 Apr 2020 14:45:48 +0200 Subject: [PATCH 06/16] [validation] Shorten ValidationError repr string --- odml/validation.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 9c7294ab..98d56d59 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -9,6 +9,11 @@ from . import dtypes +try: + unicode = unicode +except NameError: + unicode = str + LABEL_ERROR = 'error' LABEL_WARNING = 'warning' @@ -83,9 +88,15 @@ def path(self): return self.obj.get_path() def __repr__(self): - return "" % (self.rank, - self.obj, - self.msg) + # Cleanup the odml object print strings + print_str = unicode(self.obj).split()[0].split("[")[0].split(":")[0] + # Document has no name attribute and should not print id or name info + if hasattr(self.obj, "name"): + if self.obj.name and self.obj.name != self.obj.id: + print_str = "%s[%s]" % (print_str, self.obj.name) + else: + print_str = "%s[%s]" % (print_str, self.obj.id) + return "Validation%s: %s '%s'" % (self.rank.capitalize(), print_str, self.msg) class Validation(object): From 186d80c9118898bf065eb389245e4e54873a41b3 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 29 Apr 2020 16:43:36 +0200 Subject: [PATCH 07/16] [validation] Change object name missing msg --- odml/validation.py | 2 +- test/test_validation.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 98d56d59..a77e65f3 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -409,7 +409,7 @@ def object_name_readable(obj): validation_id = ValidationID.object_name_readable if obj.name == obj.id: - yield ValidationError(obj, "Name should be readable", LABEL_WARNING, validation_id) + yield ValidationError(obj, "Name not assigned", LABEL_WARNING, validation_id) Validation.register_handler('section', object_name_readable) diff --git a/test/test_validation.py b/test/test_validation.py index 03968c0d..451067d2 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -276,7 +276,7 @@ def test_section_name_readable(self): sec = odml.Section("sec", parent=doc) sec.name = sec.id res = Validate(doc) - self.assertError(res, "Name should be readable") + self.assertError(res, "Name not assigned") def test_property_name_readable(self): """ @@ -287,7 +287,7 @@ def test_property_name_readable(self): prop = odml.Property("prop", parent=sec) prop.name = prop.id res = Validate(doc) - self.assertError(res, "Name should be readable") + self.assertError(res, "Name not assigned") def test_standalone_section(self): """ From 93af689137beeb2c0ccc0c129e7de2b578d1a285 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 29 Apr 2020 17:24:48 +0200 Subject: [PATCH 08/16] [validation] Add reports method --- odml/validation.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/odml/validation.py b/odml/validation.py index a77e65f3..44b44213 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -173,6 +173,36 @@ def run_validation(self): for prop in sec.properties: self.validate(prop) + def report(self): + """ + Validates the registered object and returns a results report. + """ + self.run_validation() + + err_count = 0 + reduce = set() + sec_count = 0 + prop_count = 0 + + for i in self.errors: + if i.is_error: + err_count += 1 + + if i.obj not in reduce and 'section' in str(i.obj).lower(): + sec_count += 1 + elif i.obj not in reduce and 'property' in str(i.obj).lower(): + prop_count += 1 + + reduce.add(i.obj) + + warn_count = len(self.errors) - err_count + msg = "" + if err_count or warn_count: + msg = "Validation found %s errors and %s warnings" % (err_count, warn_count) + msg += " in %s Sections and %s Properties." % (sec_count, prop_count) + + return msg + def register_custom_handler(self, klass, handler): """ Adds a validation handler for an odml class. The handler is called in the From 9f4147176d42e80237daca57091480019208a139 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Wed, 29 Apr 2020 17:25:13 +0200 Subject: [PATCH 09/16] [odmlparser] Validation results on save and load A validation report is now printed as a warnings module UserWarnings if a saved or loaded Document contains Validation warnings or errors. On load the warnings will only be printed when the Parsers 'show_warnings' attribute is True. --- odml/tools/odmlparser.py | 65 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/odml/tools/odmlparser.py b/odml/tools/odmlparser.py index 8a25a61e..5ac97a32 100644 --- a/odml/tools/odmlparser.py +++ b/odml/tools/odmlparser.py @@ -7,6 +7,7 @@ import datetime import json import sys +import warnings from os.path import basename @@ -61,11 +62,18 @@ def write_file(self, odml_document, filename): msg = "" for err in validation.errors: if err.is_error: - msg += "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg) + # msg += "\n\t- %s %s: %s" % (err.obj, err.rank, err.msg) + msg += "\n- %s" % err if msg != "": msg = "Resolve document validation errors before saving %s" % msg raise ParserException(msg) + report = validation.report() + if report: + msg += "The saved Document contains formal issues." + msg += " Run 'odml.Validation' to resolve them.\n%s" % report + warnings.warn(msg) + with open(filename, 'w') as file: # Add XML header to support odML stylesheets. if self.parser == 'XML': @@ -153,6 +161,13 @@ def __init__(self, parser='XML', show_warnings=True): self.show_warnings = show_warnings self.warnings = [] + def _validation_warning(self): + report = Validation(self.doc).report() + if report: + msg = "The loaded Document contains formal issues." + msg += "Run 'odml.Validation' to resolve them.\n%s" % report + warnings.warn(msg) + def from_file(self, file, doc_format=None): """ Loads an odML document from a file. The ODMLReader.parser specifies the @@ -171,6 +186,11 @@ def from_file(self, file, doc_format=None): show_warnings=self.show_warnings) self.warnings = par.warnings self.doc = par.from_file(file) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'YAML': @@ -188,6 +208,11 @@ def from_file(self, file, doc_format=None): self.doc = par.to_odml(self.parsed_doc) # Provide original file name via the in memory document self.doc.origin_file_name = basename(file) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'JSON': @@ -202,13 +227,27 @@ def from_file(self, file, doc_format=None): self.doc = par.to_odml(self.parsed_doc) # Provide original file name via the in memory document self.doc.origin_file_name = basename(file) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'RDF': if not doc_format: raise ValueError("Format of the rdf file was not specified") + # Importing from an RDF graph can return multiple documents self.doc = RDFReader().from_file(file, doc_format) + + for doc in self.doc: + report = Validation(doc).report() + if report: + msg = "The loaded Document contains formal issues." + msg += "Run 'odml.Validation' to resolve them.\n%s" % report + warnings.warn(msg) + return self.doc def from_string(self, string, doc_format=None): @@ -227,6 +266,11 @@ def from_string(self, string, doc_format=None): if self.parser == 'XML': self.doc = xmlparser.XMLReader().from_string(string) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'YAML': @@ -237,6 +281,11 @@ def from_string(self, string, doc_format=None): return self.doc = DictReader().to_odml(self.parsed_doc) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'JSON': @@ -247,13 +296,27 @@ def from_string(self, string, doc_format=None): return self.doc = DictReader().to_odml(self.parsed_doc) + + # Print validation warnings after parsing + if self.show_warnings: + self._validation_warning() + return self.doc if self.parser == 'RDF': if not doc_format: raise ValueError("Format of the rdf file was not specified") + # Importing from an RDF graph can return multiple documents self.doc = RDFReader().from_string(string, doc_format) + + for doc in self.doc: + report = Validation(doc).report() + if report: + msg = "The loaded Document contains formal issues." + msg += "Run 'odml.Validation' to resolve them.\n%s" % report + warnings.warn(msg) + return self.doc From 5470f10fa2d570ef5c41f790f0f576c7deb026c0 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 30 Apr 2020 16:04:00 +0200 Subject: [PATCH 10/16] [odmlparser] Clarify run validation message --- odml/tools/odmlparser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/odml/tools/odmlparser.py b/odml/tools/odmlparser.py index 5ac97a32..cde94cc7 100644 --- a/odml/tools/odmlparser.py +++ b/odml/tools/odmlparser.py @@ -71,7 +71,7 @@ def write_file(self, odml_document, filename): report = validation.report() if report: msg += "The saved Document contains formal issues." - msg += " Run 'odml.Validation' to resolve them.\n%s" % report + msg += " Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report warnings.warn(msg) with open(filename, 'w') as file: @@ -165,7 +165,7 @@ def _validation_warning(self): report = Validation(self.doc).report() if report: msg = "The loaded Document contains formal issues." - msg += "Run 'odml.Validation' to resolve them.\n%s" % report + msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report warnings.warn(msg) def from_file(self, file, doc_format=None): @@ -245,7 +245,7 @@ def from_file(self, file, doc_format=None): report = Validation(doc).report() if report: msg = "The loaded Document contains formal issues." - msg += "Run 'odml.Validation' to resolve them.\n%s" % report + msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report warnings.warn(msg) return self.doc @@ -314,7 +314,7 @@ def from_string(self, string, doc_format=None): report = Validation(doc).report() if report: msg = "The loaded Document contains formal issues." - msg += "Run 'odml.Validation' to resolve them.\n%s" % report + msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report warnings.warn(msg) return self.doc From c013cb45f22efa96394c86a095646116d0659bd1 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 7 May 2020 21:39:13 +0200 Subject: [PATCH 11/16] [validation] ValidationID->IssueID --- odml/property.py | 2 +- odml/section.py | 4 ++-- odml/validation.py | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/odml/property.py b/odml/property.py index f9fe403b..82537bda 100644 --- a/odml/property.py +++ b/odml/property.py @@ -574,7 +574,7 @@ def _values_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) - val_id = validation.ValidationID.property_values_cardinality + val_id = validation.IssueID.property_values_cardinality # Make sure to display only warnings of the current property for curr in valid.errors: diff --git a/odml/section.py b/odml/section.py index 8937a88e..3a015978 100644 --- a/odml/section.py +++ b/odml/section.py @@ -421,7 +421,7 @@ def _sections_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) - val_id = validation.ValidationID.section_sections_cardinality + val_id = validation.IssueID.section_sections_cardinality # Make sure to display only warnings of the current section for curr in valid.errors: @@ -476,7 +476,7 @@ def _properties_cardinality_validation(self): is respected and prints a warning message otherwise. """ valid = validation.Validation(self) - val_id = validation.ValidationID.section_properties_cardinality + val_id = validation.IssueID.section_properties_cardinality # Make sure to display only warnings of the current section for curr in valid.errors: diff --git a/odml/validation.py b/odml/validation.py index 44b44213..7508f566 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -18,7 +18,7 @@ LABEL_WARNING = 'warning' -class ValidationID(Enum): +class IssueID(Enum): """ IDs identifying registered validation handlers. """ @@ -239,7 +239,7 @@ def object_required_attributes(obj): :param obj: document, section or property. """ - validation_id = ValidationID.object_required_attributes + validation_id = IssueID.object_required_attributes args = obj.format().arguments for arg in args: @@ -264,7 +264,7 @@ def section_type_must_be_defined(sec): :param sec: odml.Section. """ - validation_id = ValidationID.section_type_must_be_defined + validation_id = IssueID.section_type_must_be_defined if sec.type and sec.type == "n.s.": yield ValidationError(sec, "Section type not specified", LABEL_WARNING, validation_id) @@ -280,7 +280,7 @@ def section_repository_present(sec): 1. warn, if a section has no repository or 2. the section type is not present in the repository """ - validation_id = ValidationID.section_repository_present + validation_id = IssueID.section_repository_present repo = sec.get_repository() if repo is None: @@ -327,7 +327,7 @@ def section_unique_ids(parent, id_map=None): :param parent: odML Document or Section :param id_map: "id":"odML object / path" dictionary """ - validation_id = ValidationID.section_unique_ids + validation_id = IssueID.section_unique_ids if not id_map: id_map = {} @@ -358,7 +358,7 @@ def property_unique_ids(section, id_map=None): :param section: odML Section :param id_map: "id":"odML object / path" dictionary """ - validation_id = ValidationID.property_unique_ids + validation_id = IssueID.property_unique_ids if not id_map: id_map = {} @@ -406,7 +406,7 @@ def section_unique_name_type(obj): """ for i in object_unique_names( obj, - validation_id=ValidationID.section_unique_name_type, + validation_id=IssueID.section_unique_name_type, attr=lambda x: (x.name, x.type), children=lambda x: x.sections, msg="name/type combination must be unique"): @@ -420,7 +420,7 @@ def property_unique_names(obj): :param obj: odml class instance the validation is applied on. """ for i in object_unique_names(obj, - validation_id=ValidationID.property_unique_name, + validation_id=IssueID.property_unique_name, children=lambda x: x.properties): yield i @@ -436,7 +436,7 @@ def object_name_readable(obj): :param obj: odml.Section or odml.Property. """ - validation_id = ValidationID.object_name_readable + validation_id = IssueID.object_name_readable if obj.name == obj.id: yield ValidationError(obj, "Name not assigned", LABEL_WARNING, validation_id) @@ -452,7 +452,7 @@ def property_terminology_check(prop): 2. warn, if there are multiple values with different units or the unit does not match the one in the terminology. """ - validation_id = ValidationID.property_terminology_check + validation_id = IssueID.property_terminology_check if not prop.parent: return @@ -475,7 +475,7 @@ def property_dependency_check(prop): Produces a warning if the dependency attribute refers to a non-existent attribute or the dependency_value does not match. """ - validation_id = ValidationID.property_dependency_check + validation_id = IssueID.property_dependency_check if not prop.parent: return @@ -506,7 +506,7 @@ def property_values_check(prop): :param prop: property the validation is applied on. """ - validation_id = ValidationID.property_values_check + validation_id = IssueID.property_values_check if prop.dtype is not None and prop.dtype != "": dtype = prop.dtype @@ -540,7 +540,7 @@ def property_values_string_check(prop): :param prop: property the validation is applied on. """ - validation_id = ValidationID.property_values_string_check + validation_id = IssueID.property_values_string_check if prop.dtype != "string" or not prop.values: return @@ -636,7 +636,7 @@ def section_properties_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - validation_id = ValidationID.section_properties_cardinality + validation_id = IssueID.section_properties_cardinality err = _cardinality_validation(obj, obj.prop_cardinality, 'properties', LABEL_WARNING, validation_id) @@ -655,7 +655,7 @@ def section_sections_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - validation_id = ValidationID.section_sections_cardinality + validation_id = IssueID.section_sections_cardinality err = _cardinality_validation(obj, obj.sec_cardinality, 'sections', LABEL_WARNING, validation_id) @@ -674,7 +674,7 @@ def property_values_cardinality(obj): :return: Yields a ValidationError warning, if a set cardinality is not met. """ - validation_id = ValidationID.property_values_cardinality + validation_id = IssueID.property_values_cardinality err = _cardinality_validation(obj, obj.val_cardinality, 'values', LABEL_WARNING, validation_id) From 799868906b40b009f7f5a52cb3911cef0444b22d Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 7 May 2020 21:53:18 +0200 Subject: [PATCH 12/16] [doc] Add 'validate' method --- odml/doc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/odml/doc.py b/odml/doc.py index 29f366a0..fa8592bf 100644 --- a/odml/doc.py +++ b/odml/doc.py @@ -8,6 +8,7 @@ from . import dtypes from . import format as fmt from . import terminology +from . import validation from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring @@ -152,6 +153,14 @@ def finalize(self): if sec._include is not None: sec.include = sec._include + def validate(self): + """ + Runs a validation on itself and returns the Validation object. + + :return: odml.Validation + """ + return validation.Validation(self) + @inherit_docstring def get_terminology_equivalent(self): if self.repository is None: From 4bf7bf8ad9816bab7982e1a21015fb68c97058d1 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 7 May 2020 21:53:37 +0200 Subject: [PATCH 13/16] [odmlparser] Update validation warning message --- odml/tools/odmlparser.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/odml/tools/odmlparser.py b/odml/tools/odmlparser.py index cde94cc7..1e3f8f6a 100644 --- a/odml/tools/odmlparser.py +++ b/odml/tools/odmlparser.py @@ -70,8 +70,8 @@ def write_file(self, odml_document, filename): report = validation.report() if report: - msg += "The saved Document contains formal issues." - msg += " Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report + msg += "The saved Document contains unresolved issues." + msg += " Run the Documents 'validate' method to access them.\n%s" % report warnings.warn(msg) with open(filename, 'w') as file: @@ -164,8 +164,8 @@ def __init__(self, parser='XML', show_warnings=True): def _validation_warning(self): report = Validation(self.doc).report() if report: - msg = "The loaded Document contains formal issues." - msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report + msg = "The loaded Document contains unresolved issues." + msg += " Run the Documents 'validate' method to access them.\n%s" % report warnings.warn(msg) def from_file(self, file, doc_format=None): @@ -244,8 +244,8 @@ def from_file(self, file, doc_format=None): for doc in self.doc: report = Validation(doc).report() if report: - msg = "The loaded Document contains formal issues." - msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report + msg = "The loaded Document contains unresolved issues." + msg += " Run the Documents 'validate' method to access them.\n%s" % report warnings.warn(msg) return self.doc @@ -313,8 +313,8 @@ def from_string(self, string, doc_format=None): for doc in self.doc: report = Validation(doc).report() if report: - msg = "The loaded Document contains formal issues." - msg += "Run 'odml.validation.Validation(doc)' to resolve them.\n%s" % report + msg = "The loaded Document contains unresolved issues." + msg += " Run the Documents 'validate' method to access them.\n%s" % report warnings.warn(msg) return self.doc From ef270d3274fc1907ef346962d0784e892b100b07 Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Thu, 7 May 2020 21:54:17 +0200 Subject: [PATCH 14/16] [validation] Use 3 digit IssueID enum code --- odml/validation.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 7508f566..4d6cd78b 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -25,31 +25,31 @@ class IssueID(Enum): unspecified = 1 # Required attributes validations - object_required_attributes = 10 - section_type_must_be_defined = 11 + object_required_attributes = 110 + section_type_must_be_defined = 111 # Unique id, name and type validations - section_unique_ids = 20 - property_unique_ids = 21 - section_unique_name_type = 22 - property_unique_name = 23 + section_unique_ids = 120 + property_unique_ids = 121 + section_unique_name_type = 122 + property_unique_name = 123 # Good form validations - object_name_readable = 30 + object_name_readable = 130 # Property specific validations - property_terminology_check = 40 - property_dependency_check = 41 - property_values_check = 42 - property_values_string_check = 43 + property_terminology_check = 140 + property_dependency_check = 141 + property_values_check = 142 + property_values_string_check = 143 # Cardinality validations - section_properties_cardinality = 50 - section_sections_cardinality = 51 - property_values_cardinality = 52 + section_properties_cardinality = 150 + section_sections_cardinality = 151 + property_values_cardinality = 152 # Optional validations - section_repository_present = 60 + section_repository_present = 160 class ValidationError(object): From ff375622bc1385eb6856901b67085dcf3ce188ca Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 11 May 2020 10:12:46 +0200 Subject: [PATCH 15/16] [test/validation] Re-add sec repo present test --- test/test_validation.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/test_validation.py b/test/test_validation.py index 451067d2..a03b1725 100644 --- a/test/test_validation.py +++ b/test/test_validation.py @@ -467,3 +467,27 @@ def test_load_dtypes_yaml(self): path = os.path.join(RES_DIR, "validation_dtypes.yaml") doc = odml.load(path, "YAML") self.load_dtypes_validation(doc) + + def test_section_in_terminology(self): + """ + Test optional section in terminology validation. + """ + doc = samplefile.parse("""s1[T1]""") + + # Set up custom validation and add section_repository_present handler + res = odml.validation.Validation(doc, validate=False, reset=True) + handler = odml.validation.section_repository_present + res.register_custom_handler('section', handler) + + res.run_validation() + self.assertError(res, "A section should have an associated repository", + filter_rep=False) + + odml.terminology.terminologies['map'] = samplefile.parse(""" + s0[t0] + - S1[T1] + """) + doc.sections[0].repository = 'map' + res = Validate(doc) + # self.assertEqual(list(self.filter_mapping_errors(res.errors)), []) + self.assertEqual(res.errors, []) From 271d78d10fc19f052b70d0b58c1d02655b7bb58d Mon Sep 17 00:00:00 2001 From: "M. Sonntag" Date: Mon, 11 May 2020 12:45:14 +0200 Subject: [PATCH 16/16] [validation] Change IssueID codes --- odml/validation.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/odml/validation.py b/odml/validation.py index 4d6cd78b..800fc793 100644 --- a/odml/validation.py +++ b/odml/validation.py @@ -25,31 +25,31 @@ class IssueID(Enum): unspecified = 1 # Required attributes validations - object_required_attributes = 110 - section_type_must_be_defined = 111 + object_required_attributes = 101 + section_type_must_be_defined = 102 # Unique id, name and type validations - section_unique_ids = 120 - property_unique_ids = 121 - section_unique_name_type = 122 - property_unique_name = 123 + section_unique_ids = 200 + property_unique_ids = 201 + section_unique_name_type = 202 + property_unique_name = 203 # Good form validations - object_name_readable = 130 + object_name_readable = 300 # Property specific validations - property_terminology_check = 140 - property_dependency_check = 141 - property_values_check = 142 - property_values_string_check = 143 + property_terminology_check = 400 + property_dependency_check = 401 + property_values_check = 402 + property_values_string_check = 403 # Cardinality validations - section_properties_cardinality = 150 - section_sections_cardinality = 151 - property_values_cardinality = 152 + section_properties_cardinality = 500 + section_sections_cardinality = 501 + property_values_cardinality = 502 # Optional validations - section_repository_present = 160 + section_repository_present = 600 class ValidationError(object):