Source code for obsplus.utils.validate

"""
Core modules for validate.
"""
import inspect
from collections import defaultdict
from contextlib import contextmanager, suppress
from functools import singledispatch
from itertools import product
from typing import Optional, Dict

import pandas as pd
from obspy import Stream, Trace, Catalog, Inventory
from obspy.core.inventory.util import BaseNode
from obspy.core.util import AttribDict

from obsplus.exceptions import ValidationNameError
from obsplus.utils.misc import yield_obj_parent_attr, iterate

# The validator state is of the form:
# {"namespace": {cls: {id1: validator1, id2: validator2, ...], ...}, ...}
# where id is the id of the validator (eg returned by id(validator))
_VALIDATOR_STATE = dict(validators=defaultdict(lambda: defaultdict(dict)))


# Get a listing of all classes associated with obspy's


def _create_decompose_func():
    """Create a single dispatch function for decomposing objects."""

    @singledispatch
    def _decompose(obj):
        """
        Decompose an object into its constitutive parts.

        This function should return a dict of the form:
            {cls: [instance1, instance2, instance3, ...]}
        """
        return {type(obj): [obj]}

    return _decompose


_VALIDATOR_STATE["decomposer"] = _create_decompose_func()


@contextmanager
def _temp_validate_namespace():
    """
    A contextmanager for setting temporary validator state for testing.
    """
    old_validators = _VALIDATOR_STATE["validators"]
    old_decomposer = _VALIDATOR_STATE["decomposer"]
    _VALIDATOR_STATE["validators"] = defaultdict(lambda: defaultdict(dict))
    _VALIDATOR_STATE["decomposer"] = _create_decompose_func()
    yield
    _VALIDATOR_STATE["validators"] = old_validators
    _VALIDATOR_STATE["decomposer"] = old_decomposer


[docs] def decomposer(cls): """ Register a function as a decomposer for a given class. The decomposer simply splits the object into its constitutive parts that may need to be tested. It should return a dict of the following form: {cls: [instance1, instance2, ...], ... } Parameters ---------- cls The class the registered decomposer is to act on. Can be a tuple of classes. Returns ------- """ def _wrap(func): _decomposer = _VALIDATOR_STATE["decomposer"] for cls_ in iterate(cls): _decomposer.register(cls_)(func) return func return _wrap
[docs] def validator(namespace: str, cls: type): """ Register a callable to a given namespace to operate on type cls. Parameters ---------- namespace cls Returns ------- """ def _wrap(func): # register function and return it state = _VALIDATOR_STATE["validators"] # get the name of the function for hashing; if not found use id name = getattr(func, "__name__", id(func)) state[namespace][cls][name] = func return func return _wrap
def _get_validators(namespace): """Get validators for a specified namespace if sound else raise.""" validator_namespaces = _VALIDATOR_STATE["validators"] if namespace not in validator_namespaces: msg = f"{namespace} has no registered validators" raise ValidationNameError(msg) return validator_namespaces[namespace] def _get_decomposer(): """Get the current decomposer.""" return _VALIDATOR_STATE["decomposer"] def _get_validate_obj_intersection(validators, obj_tree): """get the intersection between validators and obj_tree.""" validators_to_run = defaultdict(dict) for cls1, cls2 in product(obj_tree, validators): if issubclass(cls1, cls2): validators_to_run[cls1].update(validators[cls2]) return validators_to_run def _make_validator_report(validator, obj, kwargs): """run a validator against an object and make a report.""" val_name = getattr(validator, "__name__", validator) out = {"validator": val_name, "object": obj, "message": ""} try: _run_validator(validator, obj, kwargs) except AssertionError as e: # some objects raise when trying to represent them as str, protect # against this. obj_rep = "" with suppress(TypeError, ValueError): obj_rep = str(obj) msg = f"validator {val_name} failed object: {obj_rep} raising:\n {e}" out["passed"] = False out["message"] = msg else: out["passed"] = True return out def _run_validator(validator, obj, kwargs): """Run the validator.""" if kwargs: validator(obj, **kwargs) else: validator(obj)
[docs] def validate( obj, namespace: str, report: bool = False, **kwargs ) -> Optional[pd.DataFrame]: """ Validate an object using validators in specified namespace. Parameters ---------- obj Any function whose type has registered validators. namespace The validation namespace specified by validators. report If True, return a dataframe which passed/failed validators and objects and suppress all failures. Notes ----- Parameters can be passed individual validators using kwargs. """ # get validators and decompose object to testable parts. validators_raw = _get_validators(namespace) obj_tree = decompose(obj) # get validators to run on obj_tree validators = _get_validate_obj_intersection(validators_raw, obj_tree) # iterate over each validator run it. reports = [] for cls, validate_func_dict in validators.items(): funcs = validate_func_dict.values() for validator, obj in product(funcs, obj_tree.get(cls, [])): # get the main arg and any addition requested kwargs arg, *wanted_args = list(inspect.signature(validator).parameters) vkwargs = {x: kwargs[x] for x in set(wanted_args) & set(kwargs)} # make report and shallow assertion errors if report requested if report: reports.append(_make_validator_report(validator, obj, vkwargs)) else: # else just run the validator _run_validator(validator, obj, vkwargs) return pd.DataFrame(reports)
[docs] def decompose(obj) -> Dict[type, object]: """ Decompose an object into a dict of {class: [instance1, instance2, ...]}. Parameters ---------- obj The object to decompose. Returns ------- A dict of class definitions (keys) and lists of instances (values). """ return _get_decomposer()(obj)
# default decomposer for obspy stuff @decomposer((Catalog, AttribDict, BaseNode, Inventory, Stream, Trace)) def _decompose_generic(events): """Decompose an event or a catalog.""" out = defaultdict(list) for obj, parent, attr in yield_obj_parent_attr(events): out[type(obj)].append(obj) return out