#-*- coding: utf-8 """ generic odml validation framework """ import odml.format as format import odml.mapping as mapping import odml.tools.event as event import odml # event capabilities are needed for mappings odml.setMinimumImplementation('event') class ValidationError(object): """ Represents an error found in the validation process The error is bound to an odml-object (*obj*) or a list of those and contains a message and a type which may be one of: 'error', 'warning', 'info' """ def __init__(self, obj, msg, type='error'): self.obj = obj self.msg = msg self.type = type @property def is_warning(self): return self.type == 'warning' @property def is_error(self): return self.type == 'error' @property def path(self): if isinstance(self.obj, odml.value.Value): return self.obj.parent.get_path() return self.obj.get_path() def __repr__(self): return "" % (self.type, self.obj, self.msg) class Validation(object): _handlers = {} @staticmethod def register_handler(klass, handler): """ Add a validation handler for a odml-class. *type* may be one of the following: * odML * section * property * value And is called in the validation process for each corresponding object. The *handler* is assumed to be a generator function yielding all ValidationErrors it finds: handler(obj) The section handlers are only called for sections and not for the document node. If both are required, you need to register the handler twice. """ Validation._handlers.setdefault(klass, set()).add(handler) def __init__(self, doc): self.doc = doc # may also be a section self.errors = [] self.validate(doc) # TODO isn't there a 'walk' method for these things? for sec in doc.itersections(recursive=True): self.validate(sec) for prop in sec.properties: self.validate(prop) for val in prop.values: self.validate(val) def validate(self, obj): handlers = self._handlers.get(obj._format._name, []) for handler in handlers: for err in handler(obj): self.error(err) def error(self, validation_error): """ Register an error found during the validation process """ self.errors.append(validation_error) def __getitem__(self, obj): """return a list of the errors for a certain object""" errors = [] for err in self.errors: if err.obj is obj: errors.append(err) return errors # ------------------------------------------------ # validation rules def section_type_must_be_defined(sec): """test that no section has an undefined type""" if sec.type is None or sec.type == '' or sec.type == 'undefined': yield ValidationError(sec, 'Section type undefined', 'warning') Validation.register_handler('section', section_type_must_be_defined) def section_repository_should_be_present(sec): """ 1. warn, if a section has no repository or 2. the section type is not present in the repository """ repo = sec.get_repository() if repo is None: yield ValidationError(sec, 'A section should have an associated repository', 'warning') return try: tsec = sec.get_terminology_equivalent() except Exception as e: yield ValidationError(sec, 'Could not load terminology: ' + e.message, 'warning') return if tsec is None: yield ValidationError(sec, "Section type '%s' not found in terminology" % sec.type, 'warning') Validation.register_handler('section', section_repository_should_be_present) def object_unique_names(obj, children, attr=lambda x: x.name, msg="Object names must be unique"): """ test that object names within one section are unique *attr* is a function, that returns the item that needs to be unique *children* is a function, that returns the children to be considered. This is to be able to use the same function for sections and properties """ names = set(map(attr, children(obj))) if len(names) == len(children(obj)): return # quick exit names = set() for s in children(obj): if attr(s) in names: yield ValidationError(s, msg, 'error') names.add(attr(s)) def section_unique_name_type_combination(obj): for i in object_unique_names(obj, attr=lambda x: (x.name, x.type), children=lambda x: x.sections, msg="name/type combination must be unique"): yield i def property_unique_names(obj): for i in object_unique_names(obj, lambda x: x.properties): yield i Validation.register_handler('odML', section_unique_name_type_combination) Validation.register_handler('section', section_unique_name_type_combination) Validation.register_handler('section', property_unique_names) def odML_mapped_document_be_valid(doc): """ try to create a mapping of the document and if that succeeds validate the mapped document according to the validation rules """ if mapping.proxy is not None and isinstance(doc, mapping.proxy.Proxy): return # don't try to map already mapped documents # first check if any object has a mapping attribute for sec in doc.itersections(recursive=True): if sec.mapping is not None: break for prop in sec.properties: if prop.mapping is not None: break else: # no break in the loop, continue with next section continue break # found a mapping can stop searching else: return # no mapping found mdoc = doc._active_mapping if mdoc is not None: mapping.unmap_document(doc) mdoc = None # TODO: if mdoc is set there is already a mapping present. However, this # TODO may have been corrupted by user interaction, thus we should actually # TODO unmap the document, create a new one and then remap the original one try: if mdoc is None: mdoc = mapping.create_mapping(doc) except mapping.MappingError as e: yield ValidationError(doc, 'mapping: %s' % str(e), 'error') return v = Validation(mdoc) for err in v.errors: err.mobj = err.obj err.obj = mapping.get_object_from_mapped_equivalent(err.obj) err.msg = "mapping: " + err.msg yield err Validation.register_handler('odML', odML_mapped_document_be_valid) def property_values_same_unit(prop, tprop=None): units = set(map(lambda x: x.unit, prop.values)) if len(units) > 1: yield ValidationError(prop, 'Values of a property should be of the same unit', 'warning') if tprop is not None and tprop.values[0].unit != prop.values[0].unit: yield ValidationError(prop, 'Values of a property should have the same unit as their terminology equivalent', 'warning') Validation.register_handler('property', property_values_same_unit) def property_terminology_check(prop): """ executes a couple of checks: 1. warn, if there are properties that do not occur in the terminology 2. warn, if there are multiple values with different units or the unit does not match the one in the terminology """ tsec = prop.parent.get_terminology_equivalent() if tsec is None: return try: tprop = tsec.properties[prop.name] except KeyError: tprop = None yield ValidationError(prop, "Property '%s' not found in terminology" % prop.name, 'warning') for err in property_values_same_unit(prop, tprop): yield err Validation.register_handler('property', property_terminology_check) def property_dependency_check(prop): """ warn, if the dependency attribute refers to a non-existant attribute or the dependency_value does not match """ dep = prop.dependency if dep is None: return try: dep_obj = prop.parent[dep] except KeyError: yield ValidationError(prop, "Property refers to a non-existant dependency object", 'warning') return if dep_obj.value.value != prop.dependency_value: yield ValidationError(prop, "Dependency-value is not equal to value of the property's dependency", 'warning') Validation.register_handler('property', property_dependency_check) def value_empty(val): if val.value == '': yield ValidationError(val, "Values may only be empty in terminologies", 'warning') Validation.register_handler('value', value_empty)