Source code for SDF.data_model.parameter

import ast
from typing import Optional, Union, Iterable, Any, Sequence
from xml.etree.ElementTree import Element

import numpy as np

from SDF.data_model._helper_functions import pop_element_attribute, element_is_empty, pop_all_child_elements
from SDF.data_model.abstract import XMLWritable
from SDF.data_model.element_set import ElementSet
from SDF.data_model.name import Name


[docs]class ParameterType(XMLWritable):
[docs] @staticmethod def from_xml_element(element: Element) -> "ParameterType": if element.tag != "par": raise ValueError(f"Expected a <par> tag, got '{element.tag}'") if "value" in element.attrib: return Parameter.from_xml_element(element) return ParameterSet.from_xml_element(element)
[docs]class Parameter(ParameterType): """Represents a single-valued SDF <par> element""" def __init__(self, name: Union[str, Name], value: Any, unit: Optional[str] = None): if isinstance(name, Name): self.__name = name else: self.__name = Name(name) self.value = _parameter_value_to_string(value).strip() if unit is None: self.unit = None elif isinstance(unit, str): self.unit = unit.strip() else: raise ValueError("Unit must be a string or None") @property def name(self) -> str: return self.__name.name @property def parsed_value(self): """Try to parse `self.value` as a Python literal (e.g. int, float, list, tuple, str, bytes)""" try: # direct literal, e.g. "[1, 2, 3]" -> [1, 2, 3] return ast.literal_eval(self.value) except (SyntaxError, ValueError): pass if r"\x" in self.value: try: # literal containing raw bytes return ast.literal_eval(f"b'{self.value}'") except (SyntaxError, ValueError): pass # return original string value return self.value def __repr__(self): return f"{self.__class__.__name__}({self.name!r}, {self.value!r}, {self.unit!r})"
[docs] def to_xml_element(self) -> Element: attributes = dict(name=self.name, value=self.value) if self.unit is not None: attributes["unit"] = self.unit return Element("par", attributes)
[docs] @classmethod def from_xml_element(cls, element: Element) -> "Parameter": if element.tag != "par": raise ValueError(f"Expected a <par> element, got {element.tag}") name = pop_element_attribute(element, "name") value = pop_element_attribute(element, "value") unit = pop_element_attribute(element, "unit") if "unit" in element.attrib else None ret = cls(name, value, unit) if not element_is_empty(element): raise ValueError("Element is not empty") return ret
def __eq__(self, other): if isinstance(other, Parameter): return self.name == other.name and self.value == other.value and self.unit == other.unit return False
[docs]class AnonymousParameterSet(ElementSet[ParameterType]): def __init__(self, items: Optional[Iterable[ParameterType]] = None): super().__init__(key_func=lambda item: item.name, check_func=lambda item: isinstance(item, (Parameter, ParameterSet)), items=items)
[docs] def add_from_structure(self, structure: Sequence, parse_names_from_dicts: bool = False): self.update(*_parse_parameters(structure, parse_names_from_dicts=parse_names_from_dicts))
def __setitem__(self, key, value): # first try to parse as single parameter try: self.add(_parse_to_single_parameter((key, value))) except (ValueError, TypeError) as ex1: # on failure: try to parse as parameter set try: self.add(ParameterSet(name=key, items=_parse_parameters(value))) except (ValueError, TypeError): # if that fails as well: raise first exception since its message is less specific and more helpful raise ex1
[docs]class ParameterSet(AnonymousParameterSet, ParameterType): def __init__(self, name: Union[str, Name], items: Optional[Iterable[ParameterType]] = None): if isinstance(name, Name): self.__name = name else: self.__name = Name(name) super().__init__(items=items) @property def name(self) -> str: return self.__name.name
[docs] @classmethod def from_xml_element(cls, element: Element) -> "ParameterSet": name = pop_element_attribute(element, "name") ret = cls(name, map(ParameterType.from_xml_element, pop_all_child_elements(element))) if not element_is_empty(element): raise ValueError("Element is not empty") return ret
[docs] def to_xml_element(self) -> Element: element = Element("par", dict(name=self.name)) for par in self: element.append(par.to_xml_element()) return element
[docs] def copy(self) -> "ParameterSet": cpy = self.__class__(name=self.name) cpy.update(self) return cpy
def __repr__(self): return f"{self.__class__.__name__}({self.name!r}, {[item for item in self]!r})" def __eq__(self, other): if isinstance(other, ParameterSet): return self.name == other.name and super().__eq__(other) return False
def _parse_to_single_parameter(structure: Sequence) -> Parameter: """ Check if a sequence can be converted to a single parameter of the structure (name, value) or (name, value, unit) """ if isinstance(structure, str): # prevent "ab" -> Parameter(name="a", value="b") raise TypeError("Cannot convert a string to Parameter") if isinstance(structure, dict): # could use something like `{"name": "abc", "value": 123}`, but that's too fragile raise TypeError("Cannot convert a dict to a single Parameter") if len(structure) == 2: (name, value), unit = structure, None elif len(structure) == 3: name, value, unit = structure else: raise ValueError(f"Expected a structure of length 2 or 3, got length {len(structure)}") if isinstance(value, dict): raise TypeError(f"Value cannot be a dict") if isinstance(value, list): raise TypeError("Value cannot be a list") if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], str): value, unit = value return _parse_to_single_parameter((name, value, unit)) return Parameter(name, value, unit) def _parse_parameters(structure: Sequence, parse_names_from_dicts: bool = False) -> Iterable[ParameterType]: """ Parse a structure (like lists, tuples, dicts and combinations of those) into a collection of parameters while retaining the original nested structure. :param structure: :param parse_names_from_dicts: if true, dict entries with a key 'name' will be parsed into a ParameterSet with that name, containing the other items of the dict """ if isinstance(structure, str): # prevent "ab" -> Parameter(name="a", value="b") raise ValueError("Cannot parse a string to parameters") parameters = AnonymousParameterSet() # dict with items (name, value[, unit]) or (name, [substructure...]) # dicts have to be handled first, since their __len__ and __iter__ functions do not expose the keys if isinstance(structure, dict): if parse_names_from_dicts: if "name" in structure.keys() and isinstance(structure["name"], (bytes, str)): name = structure.pop("name") if isinstance(name, bytes): name = str(name, "ascii") parameters.add(ParameterSet(name, items=_parse_parameters(structure))) return parameters for item in sorted(structure.items()): try: parameters.add(_parse_to_single_parameter(item)) except (ValueError, TypeError): name, items = item parameters.add(ParameterSet(name, items=_parse_parameters(items))) return parameters # (name, value), (name, value, unit), (name, (value, unit)) try: parameters.add(_parse_to_single_parameter(structure)) return parameters except (ValueError, TypeError): pass # (name, [substructure...]) if len(structure) == 2 and isinstance(structure[0], str): name, items = structure parameters.add(ParameterSet(name, items=_parse_parameters(items))) return parameters # Sequence of substructures for entry in structure: parameters.update(_parse_parameters(entry)) return parameters def _parameter_value_to_string(value: Any) -> str: """ Convert value to a string representation (hopefully) suitable for usage as xml attribute. Obviously, we cannot handle all possible input types. """ # bytes to string if isinstance(value, bytes): value = repr(value) # "b'value'" return value[2:-1] # strip leading b' and trailing ' # ensure strings to be printable elif isinstance(value, str): value = repr(value) # "'value'" value = value[1:-1] # strip leading and trailing ' value = value.replace("\\\\", "\\") # str.__repr__ duplicates backslashes return value # format numpy array output elif isinstance(value, np.ndarray): # returns a (possibly nested) list # lists have a one-line and easily parsable string representation, while np.ndarray has not # can be parsed with `np.array(ast.literal_eval(...))` value = repr(value.tolist()) return value else: return str(value).strip()