Source code for glotaran.model.item

"""The model item decorator."""
from __future__ import annotations

import copy
from textwrap import indent
from typing import TYPE_CHECKING
from typing import Callable
from typing import List
from typing import Type

from glotaran.model.property import ModelProperty
from glotaran.model.util import wrap_func_as_method
from glotaran.utils.ipython import MarkdownStr

if TYPE_CHECKING:
    from typing import Any

    from glotaran.model.model import Model
    from glotaran.parameter import ParameterGroup

    Validator = Callable[
        [Type[object], Type[Model]],
        List[str],
    ]

    ValidatorParameter = Callable[
        [Type[object], Type[Model], Type[ParameterGroup]],
        List[str],
    ]


[docs]def model_item( properties: None | dict[str, dict[str, Any]] = None, has_type: bool = False, has_label: bool = True, ) -> Callable: """The `@model_item` decorator adds the given properties to the class. Further it adds classmethods for deserialization, validation and printing. By default, a `label` property is added. The `properties` dictionary contains the name of the properties as keys. The values must be either a `type` or dictionary with the following values: * type: a `type` (required) * doc: a string for documentation (optional) * default: a default value (optional) * allow_none: if `True`, the property can be set to None (optional) Classes with the `model_item` decorator intended to be used in glotaran models. Parameters ---------- properties : A dictionary of property names and options. has_type : If true, a type property will added. Used for model attributes, which can have more then one type. has_label : If false no label property will be added. """ if properties is None: properties = {} def decorator(cls): setattr(cls, "_glotaran_has_label", has_label) setattr(cls, "_glotaran_model_item", True) # store for later sanity checking if not hasattr(cls, "_glotaran_properties"): setattr(cls, "_glotaran_properties", []) if has_label: doc = f"The label of {cls.__name__} item." prop = ModelProperty(cls, "label", str, doc, None, False) setattr(cls, "label", prop) getattr(cls, "_glotaran_properties").append("label") if has_type: doc = f"The type string of {cls.__name__}." prop = ModelProperty(cls, "type", str, doc, None, True) setattr(cls, "type", prop) getattr(cls, "_glotaran_properties").append("type") else: setattr( cls, "_glotaran_properties", list(getattr(cls, "_glotaran_properties")), ) for name, options in properties.items(): if not isinstance(options, dict): options = {"type": options} prop = ModelProperty( cls, name, options.get("type"), options.get("doc", f"{name}"), options.get("default", None), options.get("allow_none", False), ) setattr(cls, name, prop) if name not in getattr(cls, "_glotaran_properties"): getattr(cls, "_glotaran_properties").append(name) validators = _get_validators(cls) setattr(cls, "_glotaran_validators", validators) init = _init_factory(cls) setattr(cls, "__init__", init) from_dict = _from_dict_factory(cls) setattr(cls, "from_dict", from_dict) validate = _validation_factory(cls) setattr(cls, "validate", validate) as_dict = _as_dict_factory(cls) setattr(cls, "as_dict", as_dict) get_state = _get_state_factory(cls) setattr(cls, "__getstate__", get_state) set_state = _set_state_factory(cls) setattr(cls, "__setstate__", set_state) fill = _fill_factory(cls) setattr(cls, "fill", fill) markdown = _markdown_factory(cls) setattr(cls, "markdown", markdown) return cls return decorator
[docs]def model_item_typed( *, types: dict[str, Any], has_label: bool = True, default_type: str = None, ): """The model_item_typed decorator adds attributes to the class to enable the glotaran model parser to infer the correct class for an item when there are multiple variants. Parameters ---------- types : A dictionary of types and options. has_label: If `False` no label property will be added. """ def decorator(cls): setattr(cls, "_glotaran_model_item", True) setattr(cls, "_glotaran_model_item_typed", True) setattr(cls, "_glotaran_model_item_types", types) setattr(cls, "_glotaran_model_item_default_type", default_type) get_default_type = _get_default_type_factory(cls) setattr(cls, "get_default_type", get_default_type) add_type = _add_type_factory(cls) setattr(cls, "add_type", add_type) setattr(cls, "_glotaran_has_label", has_label) return cls return decorator
[docs]def model_item_validator(need_parameter: bool): """The model_item_validator marks a method of a model item as validation function""" def decorator(method: Validator | ValidatorParameter): setattr(method, "_glotaran_validator", need_parameter) return method return decorator
def _get_validators(cls): return { method: getattr(getattr(cls, method), "_glotaran_validator") for method in dir(cls) if hasattr(getattr(cls, method), "_glotaran_validator") } def _get_default_type_factory(cls): @classmethod @wrap_func_as_method(cls) def get_default_type(cls) -> str: return getattr(cls, "_glotaran_model_item_default_type") return get_default_type def _add_type_factory(cls): @classmethod @wrap_func_as_method(cls) def add_type(cls, type_name: str, attribute_type: type): getattr(cls, "_glotaran_model_item_types")[type_name] = attribute_type return add_type def _init_factory(cls): @classmethod @wrap_func_as_method(cls) def __init__(self): for attr in self._glotaran_properties: setattr(self, f"_{attr}", None) return __init__ def _from_dict_factory(cls): @classmethod @wrap_func_as_method(cls) def from_dict(ncls, values: dict) -> cls: f"""Creates an instance of {cls.__name__} from a dictionary of values. Intended only for internal use. Parameters ---------- values : A list of values. """ item = ncls() for name in ncls._glotaran_properties: if name in values: value = values[name] prop = getattr(item.__class__, name) if prop.glotaran_property_type == float: value = float(value) elif prop.glotaran_property_type == int: value = int(value) setattr(item, name, value) elif not getattr(ncls, name).glotaran_allow_none and getattr(item, name) is None: raise ValueError(f"Missing Property '{name}' For Item '{ncls.__name__}'") return item return from_dict def _validation_factory(cls): @wrap_func_as_method(cls) def validate(self, model: Model, parameters: ParameterGroup | None = None) -> list[str]: f"""Creates a list of parameters needed by this instance of {cls.__name__} not present in a set of parameters. Parameters ---------- model : The model to validate. parameter : The parameter to validate. missing : A list the missing will be appended to. """ problems = [] for name in self._glotaran_properties: prop = getattr(self.__class__, name) value = getattr(self, name) problems += prop.glotaran_validate(value, model, parameters) for validator, need_parameter in self._glotaran_validators.items(): if need_parameter: if parameters is not None: problems += getattr(self, validator)(model, parameters) else: problems += getattr(self, validator)(model) return problems return validate def _as_dict_factory(cls): @wrap_func_as_method(cls) def as_dict(self) -> dict: return { name: getattr(self.__class__, name).glotaran_replace_parameter_with_labels( getattr(self, name) ) for name in self._glotaran_properties if name != "label" and getattr(self, name) is not None } return as_dict def _fill_factory(cls): @wrap_func_as_method(cls) def fill(self, model: Model, parameters: ParameterGroup) -> cls: f"""Returns a copy of the {cls.__name__} instance with all members which are Parameters are replaced by the value of the corresponding parameter in the parameter group. Parameters ---------- model : A glotaran model. parameter : ParameterGroup The parameter group to fill from. """ item = copy.deepcopy(self) for name in self._glotaran_properties: prop = getattr(self.__class__, name) value = getattr(self, name) value = prop.glotaran_fill(value, model, parameters) setattr(item, name, value) return item return fill def _get_state_factory(cls): @wrap_func_as_method(cls) def get_state(self) -> cls: return tuple(getattr(self, name) for name in self._glotaran_properties) return get_state def _set_state_factory(cls): @wrap_func_as_method(cls) def set_state(self, state) -> cls: for i, name in enumerate(self._glotaran_properties): setattr(self, name, state[i]) return set_state def _markdown_factory(cls): @wrap_func_as_method(cls, name="markdown") def mprint_item( self, all_parameters: ParameterGroup = None, initial_parameters: ParameterGroup = None ) -> MarkdownStr: f"""Returns a string with the {cls.__name__} formatted in markdown.""" md = "\n" if self._glotaran_has_label: md = f"**{self.label}**" if hasattr(self, "type"): md += f" ({self.type})" md += ":\n" elif hasattr(self, "type"): md = f"**{self.type}**:\n" for name in self._glotaran_properties: prop = getattr(self.__class__, name) value = getattr(self, name) if value is None: continue property_md = indent( f"* *{name.replace('_', ' ').title()}*: " f"{prop.glotaran_value_as_markdown(value,all_parameters, initial_parameters)}\n", " ", ) md += property_md return MarkdownStr(md) return mprint_item