Source code for Parser.AttributeTypes

from collections import OrderedDict
import re
import Parser.ConfigTypes as ConfigTypes
from Parser.ParserExceptions import ValidationError
from Parser.Serializer import serialize
import Parser.helpers as helpers
import Parser.constants as const
from typing import List, Tuple, Union
from Parser.helpers import overrides
from Parser.LinkElement import Link


[docs]class AttributeType: """ Holds basic common information and provides common interfaces for an attribute type definition. Specific keys are defined in the specialized attribute types. The following keys can be handled by every type of attribute: :Required: - |TYPE_KEY| :Optional: - |TOOLTIP_KEY| - |LABEL_KEY| - |PLACEHOLDER_KEY| - |HIDDEN_KEY| - |INHERIT_KEY| """ _comparison_type = None _needs_linking = False _typeSpecificKeys = [] def __init__(self, attribute_definition: dict, globalID: Union[Link, str]): """Inits the base class Parses the most basic commonly shared properties. Args: attribute_definition: A dict of the attribute definition with all of it's defined keys from the json config globalID: A unique link that describes this attribute. """ globalID = Link.force(globalID, Link.EMPHASIZE_ATTRIBUTE) if globalID.isGlobal(): if not globalID.hasAttribute(): raise ValueError( f'An attribute link must have a attribute name specified but "{globalID}" did not' ) else: raise ValueError( f'An attribute link must be global but "{globalID}" was not.' ) # internal helpers self._inherit_from: AttributeType = None self._attribute_definition = attribute_definition.copy() # check if any forbidden keys exist: try: self.checkForForbiddenKeys(self._typeSpecificKeys) except KeyError as e: raise KeyError( f'Error in attribute "{globalID}" of type "{self._comparison_type}" : {str(e)}' ) from e # special helpers self._is_placeholder = self.checkForKey(const.PLACEHOLDER_KEY, False) # required properties self.globalID = globalID self.id = globalID.attribute if const.TYPE_KEY in attribute_definition: self.type = attribute_definition[const.TYPE_KEY] else: raise AttributeError( f'Attribute "{self.globalID}" is missing the required "{const.TYPE_KEY}" key.' ) # optional properties self.tooltip = self.checkForKey(const.TOOLTIP_KEY, "") self.hidden = self.checkForKey(const.HIDDEN_KEY, False) self.label = self.checkForKey(const.LABEL_KEY, "") def __repr__(self): return f'AttributeType:{self._comparison_type}({self.globalID})' def __new__(cls, *args, **kwargs): """Prevent the instantiation of the base class""" if cls is AttributeType: raise TypeError("Base class of AttributeType shall never be instantiated") return super().__new__(cls) def getDefault(self): """Gets the default value for a specific type of attribute Returns: A value of the attribute of a specific type """ raise NotImplementedError( "getDefault of the base class AttributeType was called. But this method should always be overwritten by a more specific type" ) def checkValue(self, valueInput): """Checks the valueInput""" return valueInput def link( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, ): raise NotImplementedError( f'Error in attribute "{self.globalID}": link method for attributes of type "{self._comparison_type}" is not supported' ) def unlink( self, obj_config: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance ): raise NotImplementedError( f'Error in attribute "{self.globalID}": unlink method for attributes of type "{self._comparison_type}" is not supported' ) def relink( self, obj_config: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, new_value: Link ): raise NotImplementedError( f'Error in attribute "{self.globalID}": relink method for attributes of type "{self._comparison_type}" is not supported' ) def get_elements(self, obj_config: ConfigTypes.Configuration) -> List[Tuple[str, Link]]: raise NotImplementedError( f'Error in attribute "{self.globalID}": get_elements method for attributes of type "{self._comparison_type}" is not supported' ) def checkForForbiddenKeys(self, listOfAllowedKeys: List[str]): global baseKeys AllAllowedKeys = const.baseKeys + listOfAllowedKeys for key in self._attribute_definition: if not key in AllAllowedKeys: raise KeyError(f'The key "{key}" is not valid for this attribute type') def checkForKey(self, key: str, defaultValue): if key in self._attribute_definition: return self._attribute_definition[key] else: return defaultValue @property def needsLinking(self) -> bool: return self._needs_linking @property def is_placeholder(self) -> bool: return self._is_placeholder @property def is_inherited(self) -> bool: return self._inherit_from is not None @classmethod def is_type(cls, type: str) -> bool: if cls._comparison_type is None: return False else: return type == cls._comparison_type def create_inheritor(self, inherit_properties: dict, globalID: str): overwriteWith = inherit_properties.copy() del overwriteWith[const.INHERIT_KEY] newAttributeDefinition = self._attribute_definition.copy() newAttributeDefinition.update(overwriteWith) newAttribute = parseAttribute(newAttributeDefinition, globalID) newAttribute._inherit_from = self return newAttribute def _serialize_value(self, value): return value def serialize_value(self, value, context: Link = None): target = str(self.globalID) if context: if context.config == self.globalID.config: target = self.globalID.attribute data = {const.TARGET_KEY: target} if not self.is_placeholder: data[const.VALUE_KEY] = self._serialize_value(value) return data def _get_serialization_specifics(self): return OrderedDict() def serialize_attribute(self): attributeDef = OrderedDict() attributeDef[const.TYPE_KEY] = self.type if self.label: attributeDef[const.LABEL_KEY] = self.label if self.tooltip: attributeDef[const.TOOLTIP_KEY] = self.tooltip if self.hidden: attributeDef[const.HIDDEN_KEY] = self.hidden if self.is_placeholder: attributeDef[const.PLACEHOLDER_KEY] = self.is_placeholder serialization_specifics = self._get_serialization_specifics() for specific_key, specific_value in serialization_specifics.items(): attributeDef[specific_key] = specific_value if self.is_inherited: non_modified_keys = list() _, inherited_attrib_def = self._inherit_from.serialize_attribute() for property_key, attrib_property in attributeDef.items(): if property_key in inherited_attrib_def: if inherited_attrib_def[property_key] == attrib_property: non_modified_keys.append(property_key) for key in non_modified_keys: del attributeDef[key] attributeDef[const.INHERIT_KEY] = str(self._inherit_from.globalID) return self.id, attributeDef
[docs]class StringType(AttributeType): """String type attribute: Used if |TYPE_KEY| was set to |ATTRIB_STRING_TYPE| Holds all information necessary for a string attribute type definition. Will be rendered in the UI as a text input field. Supports the following additional keys: :Optional: - |VALIDATION_KEY| takes a regular expression as value and uses it to validate any input. """ _comparison_type = const.ATTRIB_TYPE_STRING _typeSpecificKeys = [const.VALIDATION_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) self.validation = self.checkForKey(const.VALIDATION_KEY, "") @overrides(AttributeType) def checkValue(self, valueInput: str): """Check the valueInput attribute against any defined validation rules (regex matching) Args: valueInput (str): Value to check Raises: ValueError: If the provided validation regex is invalid Returns: str: provided input value """ if not type(valueInput) is str: reportValidationError( f'Input value "{valueInput}" is not of type str but the attribute definition was expecting a string' ) if self.validation != "": try: regex = re.compile(self.validation) except Exception as e: raise ValueError( f'The regex "{self.validation}" is not valid: "{str(e)}"' ) from e if not regex.match(valueInput): reportValidationError( f'"{valueInput}" does not match the validation regex "{self.validation}"' ) return valueInput @overrides(AttributeType) def getDefault(self) -> str: """Return empty string Returns: str: empty string """ return str("") @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.validation is not None: specifics[const.VALIDATION_KEY] = self.validation return specifics
[docs]class BoolType(AttributeType): """Boolean type attribute: Used if |TYPE_KEY| was set to |ATTRIB_BOOL_TYPE| Can be either true or false. Will be rendered in the UI as a checkbox. """ _comparison_type = const.ATTRIB_TYPE_BOOL @overrides(AttributeType) def checkValue(self, valueInput: bool): return valueInput @overrides(AttributeType) def getDefault(self) -> bool: return False
[docs]class IntType(AttributeType): """Integer type attribute: Used if |TYPE_KEY| was set to |ATTRIB_INT_TYPE| Can store an integer value. Will be rendered in the UI as a spinner which will only accept integer values. Supports the following additional keys: :Optional: - |MIN_KEY| constraints the input value to not be lower than the provided value. - |MAX_KEY| constraints the input value to not be higher than the provided value. - |UNIT_KEY| shows a prefix in the UI spinner element. """ _comparison_type = const.ATTRIB_TYPE_INT _typeSpecificKeys = [const.MIN_KEY, const.MAX_KEY, const.UNIT_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) self.min = self.checkForKey(const.MIN_KEY, None) self.max = self.checkForKey(const.MAX_KEY, None) self.unit = self.checkForKey(const.UNIT_KEY, None) def _checkMinMax(self, value): if type(value) is int or type(value) is float: if not self.min is None: if value < self.min: reportValidationError( f"The input value ({value}) is lower than the minimum value({self.min}) for this attribute" ) if not self.max is None: if value > self.max: reportValidationError( f"The input value ({value}) is higher than the maximum value({self.max}) for this attribute" ) return value else: reportValidationError( f"The input value ({value}) must be of type int or float but got type ({type(value)}) instead" ) @overrides(AttributeType) def checkValue(self, valueInput): return int(self._checkMinMax(valueInput)) @overrides(AttributeType) def getDefault(self) -> int: return int(0) @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.min is not None: specifics[const.MIN_KEY] = self.min if self.max is not None: specifics[const.MAX_KEY] = self.max if self.unit is not None: specifics[const.UNIT_KEY] = self.unit return specifics
[docs]class FloatType(IntType): """Float type attribute: Used if |TYPE_KEY| was set to |ATTRIB_FLOAT_TYPE| Can store a float value. Will be rendered in the UI as a spinner which will accept float values. Supports the following additional keys: :Optional: - |MIN_KEY| constraints the input value to not be lower than the provided value. - |MAX_KEY| constraints the input value to not be higher than the provided value. - |UNIT_KEY| shows a prefix in the UI spinner element. """ _comparison_type = const.ATTRIB_TYPE_FLOAT @overrides(AttributeType) def getDefault(self) -> float: return float(0) @overrides(AttributeType) def checkValue(self, valueInput): return float(self._checkMinMax(valueInput))
[docs]class HexType(AttributeType): """Hex type attribute: Used if |TYPE_KEY| was set to |ATTRIB_HEX_TYPE| Can store an integer value. Will be rendered in the UI as a special kind of spinner which will show a 0x prefix and allows selecting hex values. Supports the following additional keys: :Optional: - |MIN_KEY| constraints the input value to not be lower than the provided value. - |MAX_KEY| constraints the input value to not be higher than the provided value. - |ALIGNMENT_KEY| constraints the input value to be aligned to the provided value in the form of :math:`{alignmentValue}^{inputValue}`. """ _comparison_type = const.ATTRIB_TYPE_HEX _typeSpecificKeys = [const.MIN_KEY, const.MAX_KEY, const.ALIGNMENT_KEY, const.UNIT_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) min = self.checkForKey(const.MIN_KEY, None) max = self.checkForKey(const.MAX_KEY, None) self.__ensure_hex_consistency(min) self.__ensure_hex_consistency(max) self.min = helpers.toInt(min) self.max = helpers.toInt(max) self.alignment = self.checkForKey(const.ALIGNMENT_KEY, None) self.unit = self.checkForKey(const.UNIT_KEY, None) def __ensure_hex_consistency(self, value): if isinstance(value, str): if not value.upper().startswith("0X"): raise ValueError( f'Error parsing "{self.globalID}", the 0x prefix should always be used for hex values in the config files. If you want to specify a decimal value instead please define the value as an integer.' ) @overrides(AttributeType) def checkValue(self, valueInput: Union[str, int]): if isinstance(valueInput, int): convertedInput = valueInput else: convertedInput = helpers.toInt(valueInput) if not self.min is None: if convertedInput < self.min: reportValidationError( f"The input value ({valueInput}) is lower than the minimum value({self.min}) for this attribute" ) if not self.max is None: if convertedInput > self.max: reportValidationError( f"The input value ({valueInput}) is higher than the maximum value({self.min}) for this attribute" ) if self.alignment is not None: if not helpers.check_alignment(convertedInput, self.alignment): reportValidationError( f"The input value ({valueInput}) is not a power of ({self.alignment}) but that is specified as required" ) return convertedInput @overrides(AttributeType) def getDefault(self) -> int: return int(0) @overrides(AttributeType) def _serialize_value(self, value): return helpers.toHex(value) @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.min is not None: specifics[const.MIN_KEY] = self.min if self.max is not None: specifics[const.MAX_KEY] = self.max if self.alignment is not None: specifics[const.ALIGNMENT_KEY] = self.alignment if self.unit is not None: specifics[const.UNIT_KEY] = self.unit return specifics
[docs]class ReferenceListType(AttributeType): """Reference list type attribute: Used if |TYPE_KEY| was set to |ATTRIB_REFERENCE_LIST_TYPE| The values of attribute instances created using this type will be a list of references to the entities which are defined in the config file by using a |JSON_PROPERTY_TYPE_LINK|. Will be rendered in the UI as a list builder which will allow to select multiple specific entities. Supports the following additional keys: :OPTIONAL: - |ELEMENTS_LIST_KEY| constraints the options to choose from to be the ones defined in this property. """ _comparison_type = const.ATTRIB_TYPE_REFERENCE_LIST _needs_linking = True _typeSpecificKeys = [const.ELEMENTS_LIST_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) self.__elements_def: List[Link] = self.checkForKey(const.ELEMENTS_LIST_KEY, None) if not self.__elements_def is None: elements = helpers.forceList(self.__elements_def) elementLinks = [] for i, element in enumerate(elements): try: link = Link.force(element, Link.EMPHASIZE_CONFIG) except Exception as e: raise Exception( f'Every list item of the elements property of the attribute "{self.globalID}" has to be a link but parsing of item {i} was unsuccessful: {str(e)}' ) from e elementLinks.append(link) self.__elements = elementLinks else: self.__elements = [] @overrides(AttributeType) def checkValue(self, valueInput: List[Union[str, Link, ConfigTypes.ConfigElement]]): # just check the syntax here as no info about any valid choices is avaliable and validation will be done in the link method if type(valueInput) is list: for i, value in enumerate(valueInput): if type(value) is str or type(value) is Link: if not Link.isGlobal(value): raise ValueError( f"All elements of a reference list must be global links but element {i} ({value}) was not" ) elif type(value) is ConfigTypes.ConfigElement: if not value.link.isValidElementLink(): raise ValueError( f'Reference list for attribute "{self.globalID}" contained a ConfigElement object with an invalid link. The link "{value.link.getLink()}" cannot be used to link to an element' ) else: raise TypeError( f'Values of reference list attribute types must be of type list but found type "{type(valueInput)}" instead' ) return valueInput @overrides(AttributeType) def getDefault(self) -> List[ConfigTypes.ConfigElement]: return [] @overrides(AttributeType) def link( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, ): if not type(attributeInstance.value) is list: raise TypeError( f'Values for elements of reference list attribute types must be of type list but found type "{type(attributeInstance.value)}" instead' ) linkedTargets = [] objConfig.require(self.__elements) for targetLink in attributeInstance.value: link = Link.force(targetLink) if len(self.__elements) > 0: linkFoundMatch = False for element in self.__elements: if element.config == link.config: linkFoundMatch = True break if linkFoundMatch == False: raise ValueError( f'Error for attribute definition "{self.globalID}": Provided link in the value list ({link.getLink()}) does not match any of the allowed links for this attributes reference list ({self.elements})' ) try: targetElement = link.resolveElement(objConfig) except AttributeError as e: raise AttributeError( f'Error for attribute definition "{self.globalID}" while resolving references: {str(e)}' ) from e linkedTargets.append(targetElement.getObjReference(linkedTargets)) attributeInstance.setValueDirect(linkedTargets) @overrides(AttributeType) def get_elements(self, obj_config: ConfigTypes.Configuration): elements: List[Tuple[str,Link]] = list() if(obj_config is not None and self.__elements is not None): for element_link in self.__elements: if(element_link.hasAttribute()): attrib_list = element_link.resolveAttributeList(obj_config) for attrib_inst, config_element in attrib_list: elements.append((attrib_inst.value, config_element.link)) else: subconfig = element_link.resolveSubconfig(obj_config) for config_element in subconfig.elements.values(): config_element_link = config_element.link elements.append((config_element_link.getLink(), config_element_link)) return elements @overrides(AttributeType) def _serialize_value(self, value: List[ConfigTypes.ConfigElement]): data = list() for v in value: data.append(str(v.link)) return data @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.__elements_def is not None: specifics[const.ELEMENTS_LIST_KEY] = self.__elements_def return specifics
[docs]class StringListType(AttributeType): """String list type attribute: Used if |TYPE_KEY| was set to |ATTRIB_STRING_LIST_TYPE| The values of attribute instances created using this type will be a list of strings. Will be rendered in the UI as a list builder which will allow to add or remove arbitrary strings. """ _comparison_type = const.ATTRIB_TYPE_STRING_LIST @overrides(AttributeType) def checkValue(self, valueInput: List[str]): # The only requirement here is for the value to be of type list. if type(valueInput) is list: for i, item in enumerate(valueInput): if not type(item) is str: raise ValueError( f'All elements of a string list must be strings but element {i} was not. Found type "{type(item)}" instead' ) else: raise TypeError( f'Values of string list attribute types must be of type list but found type "{type(valueInput)}" instead' ) return valueInput @overrides(AttributeType) def getDefault(self) -> List[str]: return []
[docs]class SelectionType(AttributeType): """Selection type attribute: Used if |TYPE_KEY| was set to |ATTRIB_SELECTION_TYPE| The values of attribute instances created using this type will be the value of the selected option. Will be rendered in the UI as a combo box which will allow to select one of the options defined in the |ELEMENTS_LIST_KEY| property. Supports the following additional keys: :Required: - |ELEMENTS_LIST_KEY| constraints the options to choose from to be the ones defined in this property. :type: list[str] or str :behaviour: If a list of strings, every list entry will show up as an option as is. If a string, the string will be interpreted as a link. """ _comparison_type = const.ATTRIB_TYPE_SELECTION _needs_linking = True _typeSpecificKeys = [const.ELEMENTS_LIST_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) self.__elements: Union[Link, list[str]] = self.checkForKey(const.ELEMENTS_LIST_KEY, None) if self.__elements is not None: if isinstance(self.__elements, list): self._needs_linking = False elif isinstance(self.__elements, str): self.__elements = Link.force(self.__elements, emphasize=Link.EMPHASIZE_CONFIG) self._needs_linking = True else: raise TypeError( f'Attribute "{self.globalID}" only allows string and list types for "{const.ELEMENTS_LIST_KEY}" property but found type "{type(self.__elements)}"' ) else: raise AttributeError( f'Property "{const.ELEMENTS_LIST_KEY}" is required for type "{self._comparison_type}" but was missing for attribute "{self.globalID}"' ) @property def targetedAttribute(self): return self.__elements.attribute @overrides(AttributeType) def checkValue(self, valueInput: str): if isinstance(self.__elements, list): if not valueInput in self.__elements: reportValidationError( f"The input value ({valueInput}) does not match any of the specified elements ({self.__elements})" ) return valueInput @overrides(AttributeType) def getDefault(self) -> ConfigTypes.ConfigElement: return None @overrides(AttributeType) def link( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, ): if self._needs_linking: possibleValues = [] foundMatch = False link = self.__elements try: subconfig = self.__elements.resolveSubconfig(objConfig) except AttributeError as e: raise AttributeError( f'Error for attribute definition "{self.globalID}" while resolving references: {str(e)}' ) from e for name, element in subconfig.elements.items(): element: ConfigTypes.ConfigElement = element if not link.hasAttribute(): raise ValueError( f'The link "{link}" for the allowed elements of the attribute {self.globalID} seems to be invalid. Make sure the link is in the format of "config/:attribute"' ) if element.hasAttributeInstance(link.attribute): targetValue = element.getAttribute(link.attribute) else: print( f'WARNING: Attribute definition "{self.globalID}" requested an attribute instance named "{link.element}" from the config "{link.config}" but the element "{name}" does not have an instance of that attribute. Skipping this element.' ) continue possibleValues.append(targetValue) if ( attributeInstance.value == targetValue.value or attributeInstance.value == targetValue ): attributeInstance.setValueDirect( targetValue.parent.getObjReference(attributeInstance) ) foundMatch = True break if foundMatch == False: raise NameError( f'"{attributeInstance.value}" is not a valid choice for Attribute instances of "{self.globalID}". Valid choices are: {possibleValues}' ) @overrides(AttributeType) def get_elements(self, obj_config: ConfigTypes.Configuration): elements: List[Tuple[str,Link]] = list() if(self.__elements is not None): if(isinstance(self.__elements, Link)): if(self.__elements.hasAttribute()): options = self.__elements.resolveAttributeList(obj_config) for attrib_inst, config_element in options: elements.append((attrib_inst.value, attrib_inst.link)) else: subconfig = self.__elements.resolveSubconfig(obj_config) for element in subconfig.elements.values(): element_link = element.link elements.append((element_link.getLink(), element_link)) elif(isinstance(self.__elements, list)): elements = [(e, e) for e in self.__elements] return elements @overrides(AttributeType) def _serialize_value(self, value: Union[ConfigTypes.ConfigElement, str]): if type(value) is str: return value else: attr = self.__elements.attribute if(value is None): raise ValueError(f'For attribute "{self.globalID}" no option was selected but a selection is required.') return str(value.getAttribute(attr).value) @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.__elements is not None: specifics[const.ELEMENTS_LIST_KEY] = self.__elements return specifics
[docs]class SliderType(IntType): """Slider type attribute: Used if |TYPE_KEY| was set to |ATTRIB_SLIDER_TYPE| Can store a float or int value depending on the type of the |STEP_KEY| property. For now rendering this item in the UI is not implemented yet. Supports the following additional keys: :Optional: - |MIN_KEY| constraints the input value to not be lower than the provided value. - |MAX_KEY| constraints the input value to not be higher than the provided value. - |STEP_KEY| defines the step size for the slider. """ _comparison_type = const.ATTRIB_TYPE_SLIDER _typeSpecificKeys = [const.MIN_KEY, const.MAX_KEY, const.STEP_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): super().__init__(attribute_definition, globalID) self.step = self.checkForKey(const.STEP_KEY, 1) @overrides(AttributeType) def checkValue(self, valueInput: int): super().checkValue(valueInput) if not self.step is None: if valueInput % self.step != 0: raise ValueError( f"The value of a slider attribute must be a multiple of {self.step} but {valueInput} is not." ) return valueInput @overrides(AttributeType) def getDefault(self) -> float: return float(0) @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.min is not None: specifics[const.MIN_KEY] = self.min if self.max is not None: specifics[const.MAX_KEY] = self.max if self.step is not None: specifics[const.STEP_KEY] = self.step return specifics
[docs]class ParentReferenceType(AttributeType): """Reference list type attribute: Used if |TYPE_KEY| was set to |ATTRIB_PARENT_REFERENCE_TYPE| Will create a link between the attribute and the referenced entity. The link created will go both ways. This means that in the entity referenced by this attribute will get a reference to this attribute and vice versa. Will be rendered in the UI as a placeholder text for now because relinking these elements is not yet supported. """ _comparison_type = const.ATTRIB_TYPE_PARENT_REFERENCE _needs_linking = True _typeSpecificKeys = [const.ELEMENTS_LIST_KEY] @overrides(AttributeType) def __init__(self, attribute_definition: dict, globalID: str): if ( const.HIDDEN_KEY in attribute_definition or const.PLACEHOLDER_KEY in attribute_definition ): raise KeyError( f'Attributes of type parent reference are not allowed to contain either the "{const.HIDDEN_KEY}" nor the "{const.PLACEHOLDER_KEY}" key.' ) if(const.ELEMENTS_LIST_KEY in attribute_definition): elements: Union[Link, list[Link]] = attribute_definition[ const.ELEMENTS_LIST_KEY ] if(isinstance(elements, list)): self.__elements: list[Link] = list() for item in elements: if isinstance(item, str): self.__elements.append(Link(item)) else: raise TypeError( f'At least one element in the "elements" property list is of the unsupported type "{type(item).__name__}". Only str types are allowed' ) elif(isinstance(elements, str)): self.__elements = Link(elements, Link.EMPHASIZE_CONFIG) else: raise TypeError( f'The "elements" property was of unsupported type "{type(elements).__name__}" but only str or list types are allowed' ) else: self.__elements = None super().__init__(attribute_definition, globalID) @overrides(AttributeType) def checkValue(self, valueInput: Union[Link, str]): # just check if it is a valid link syntax try: link = Link.force(valueInput) except Exception as e: reportValidationError( f'Values of type parent reference must have a link valid link. But parsing the link "{valueInput}" threw errors: {str(e)}' ) if not link.hasConfig() or not link.hasElement() or link.hasAttribute(): reportValidationError( f'Values of type parent reference must have a link which points to another config element but "{valueInput}" does not.' ) return valueInput @overrides(AttributeType) def getDefault(self) -> None: return None @overrides(AttributeType) def link( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, ): linkTarget = Link.force(attributeInstance.value, Link.EMPHASIZE_ELEMENT) selfElement = attributeInstance.link.resolveElement(objConfig) targetedElement = linkTarget.resolveElement(objConfig) targetedElement.addReferenceObject( attributeInstance.link.config, attributeInstance.link.element, selfElement ) targetedElement.getObjReference(attributeInstance.parent) attributeInstance.setValueDirect(targetedElement) @overrides(AttributeType) def unlink( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance ): linkTarget = Link.force(attributeInstance.value, Link.EMPHASIZE_ELEMENT) selfElement = attributeInstance.link.resolveElement(objConfig) targetedElement = linkTarget.resolveElement(objConfig) targetedElement.addReferenceObject( attributeInstance.link.config, attributeInstance.link.element, selfElement ) targetedElement.getObjReference(attributeInstance.parent) attributeInstance.setValueDirect(targetedElement) @overrides(AttributeType) def relink( self, objConfig: ConfigTypes.Configuration, attributeInstance: ConfigTypes.AttributeInstance, new_parent: Link ): self.unlink() attributeInstance.setValueDirect(new_parent) self.link(objConfig, attributeInstance) @overrides(AttributeType) def get_elements(self, obj_config: ConfigTypes.Configuration): elements: List[Tuple[str,Link]] = list() if(obj_config is not None): if(self.__elements is not None): if(isinstance(self.__elements, Link)): for element in self.__elements.resolveSubconfig(obj_config).elements.values(): if(self.__elements.hasAttribute()): link_text = element.getAttribute(self.__elements.attribute).value else: link_text = element.link.getLink() elements.append((link_text, element.link)) else: for subconfig in obj_config.configs.values(): for element in subconfig.elements.values(): link = element.link elements.append(link.getLink(), link) return elements @overrides(AttributeType) def _serialize_value(self, value: ConfigTypes.ConfigElement): if value is None: raise ValueError( f"Serialization of attribute {self.globalID.attribute} failed as it is of type {self._comparison_type} and is not allowed to be None." ) return str(value.link) @overrides(AttributeType) def _get_serialization_specifics(self): specifics = OrderedDict() if self.__elements is not None: specifics[const.ELEMENTS_LIST_KEY] = self.__elements return specifics
attributeTypeList = [ StringType, BoolType, IntType, FloatType, ReferenceListType, StringListType, SelectionType, SelectionType, HexType, SliderType, ParentReferenceType, ] def reportValidationError(errorMsg: str): raise ValidationError(errorMsg) def parseAttribute( attributeDefinition: dict, AttributeGlobalID: Union[Link, str] ) -> AttributeType: if not const.TYPE_KEY in attributeDefinition: raise KeyError("Type key is required for every attribute but it is missing") parseType = attributeDefinition[const.TYPE_KEY] newAttribute = None for attribType in attributeTypeList: if attribType.is_type(parseType): newAttribute = attribType(attributeDefinition, AttributeGlobalID) break if newAttribute is None: raise KeyError(f'The type "{parseType}" is not a supported type') return newAttribute