Source code for linkml_runtime.utils.inference_utils

import logging
from dataclasses import field, dataclass
from enum import Enum
from typing import Union, Optional, Any, Dict, Callable
from jsonasobj2 import JsonObj, items

from linkml_runtime import SchemaView
from linkml_runtime.linkml_model import SlotDefinitionName, PermissibleValue, ClassDefinitionName
from linkml_runtime.utils.enumerations import EnumDefinitionImpl
from linkml_runtime.utils.eval_utils import eval_expr
from linkml_runtime.utils.walker_utils import traverse_object_tree
from linkml_runtime.utils.yamlutils import YAMLRoot

logger = logging.getLogger(__name__)


RESOLVE_FUNC = Callable[[str, Any], Any]

def obj_as_dict_nonrecursive(obj: YAMLRoot, resolve_function: RESOLVE_FUNC = None) -> Dict[str, Any]:
    """
    Translates an object into a dict, for the purposes of input into formatted strings

    :param obj:
    :param resolve_function:
    :return:
    """
    if resolve_function:
        return {k: resolve_function(v, k) for k, v in items(obj)}
    else:
        return {k: v for k, v in vars(obj).items()}

[docs]@dataclass class Config: """ Controls which inferences are performed - slot.string_serialization - slot.equals_expression """ use_string_serialization: bool = field(default_factory=lambda: True) parse_string_serialization: bool = field(default_factory=lambda: False) use_rules: bool = field(default_factory=lambda: False) use_expressions: bool = field(default_factory=lambda: False) resolve_function: RESOLVE_FUNC = None
[docs]class Policy(Enum): """ Policy for when inferred values differ from already set values """ STRICT = "strict" OVERRIDE = "override" KEEP = "keep"
def generate_slot_value(obj: YAMLRoot, slot_name: Union[str, SlotDefinitionName], schemaview: SchemaView, class_name: Union[str, ClassDefinitionName] = None, config: Config = Config()) -> Optional[Any]: """ Infer the value of a slot for a given object Utilizes: - string_serialization - equals_expression :param obj: object with slot whose value is be generated (not mutated) :param slot_name: slot whose value is to be filled :param schemaview: :param class_name: :param config: determines which rules to apply :return: inferred value, or None if not inference performed """ if class_name is None: class_name = type(obj).class_name mapped_slot = schemaview.slot_name_mappings()[slot_name] slot_name = mapped_slot.name slot = schemaview.induced_slot(slot_name, class_name) logger.debug(f' CONF={config}') if config.use_string_serialization: if slot.string_serialization: if isinstance(obj, JsonObj): return slot.string_serialization.format(**obj_as_dict_nonrecursive(obj, config.resolve_function)) if config.parse_string_serialization: raise NotImplementedError if config.use_expressions: if slot.equals_expression: if isinstance(obj, JsonObj): return eval_expr(slot.equals_expression, **obj_as_dict_nonrecursive(obj, config.resolve_function)) if config.use_rules: raise NotImplementedError(f'Rules not implemented for {config}') return None
[docs]def infer_slot_value(obj: YAMLRoot, slot_name: Union[str, SlotDefinitionName], schemaview: SchemaView, class_name: Union[str, ClassDefinitionName] = None, policy: Policy = Policy.STRICT, config: Config = Config()): """ Infer the value of a slot for an object :param obj: mutable object to be transformed :param slot_name: :param schemaview: :param class_name: :param policy: :param config: """ v = getattr(obj, slot_name, None) if v is not None and policy == Policy.KEEP: return v new_v = generate_slot_value(obj, slot_name, schemaview, class_name=class_name, config=config) logger.debug(f'SETTING {slot_name} = {new_v} // current={v}, {policy}') if new_v: # check if new value is different; not str check is necessary as enums may not be converted if v is not None and new_v != v and str(new_v) != str(v): if policy == Policy.STRICT: raise ValueError(f'Inconsistent value {v} != {new_v} for {slot_name} for {obj}') elif policy == Policy.OVERRIDE: setattr(obj, slot_name, new_v) obj.__post_init__() else: setattr(obj, slot_name, new_v) #print(f'CALLING POST INIT ON {obj} from {slot_name} = {new_v}') obj.__post_init__()
[docs]def infer_all_slot_values(obj: YAMLRoot, schemaview: SchemaView, class_name: Union[str, ClassDefinitionName] = None, policy: Policy = Policy.STRICT, config: Config = Config()): """ Walks object tree inferring all slot values. - if a slot has a string_serialization metaslot, apply this - if a slot has a equals_expression metaslot, apply this. See :func:`eval_expr()` :param obj: :param schemaview: :param class_name: :param policy: default is STRICT :param config: :return: """ def infer(in_obj: YAMLRoot): logger.debug(f'INFER={in_obj}') if isinstance(in_obj, YAMLRoot) and not isinstance(in_obj, EnumDefinitionImpl) and not isinstance(in_obj, PermissibleValue): for k, v in vars(in_obj).items(): #print(f' ISV={k} curr={v} policy={policy} in_obj={type(in_obj)}') infer_slot_value(in_obj, k, schemaview, class_name=class_name, policy=policy, config=config) return in_obj traverse_object_tree(obj, infer)