From 75f60919d0d4ac578e4b888e24c4bbb3e2f94c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 2 Oct 2023 19:26:53 +0200 Subject: [PATCH 1/3] make `id_shorts` optional Since `SubmodelElementLists` require that their children elements don't have `id_shorts`, they have to be made optional for all elements, but required for `NamespaceSets`. --- .../aas/adapter/json/json_deserialization.py | 32 +++++----- basyx/aas/adapter/xml/xml_deserialization.py | 42 +++++-------- basyx/aas/adapter/xml/xml_serialization.py | 3 +- basyx/aas/model/base.py | 62 ++++++++++--------- basyx/aas/model/submodel.py | 34 +++++----- test/model/test_base.py | 12 +++- 6 files changed, 93 insertions(+), 92 deletions(-) diff --git a/basyx/aas/adapter/json/json_deserialization.py b/basyx/aas/adapter/json/json_deserialization.py index 968371718..0eb7866c6 100644 --- a/basyx/aas/adapter/json/json_deserialization.py +++ b/basyx/aas/adapter/json/json_deserialization.py @@ -233,6 +233,8 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None :param dct: The object's dict representation from JSON """ if isinstance(obj, model.Referable): + if 'idShort' in dct: + obj.id_short = _get_ts(dct, 'idShort', str) if 'category' in dct: obj.category = _get_ts(dct, 'category', str) if 'displayName' in dct: @@ -242,8 +244,6 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None obj.description = cls._construct_lang_string_set(_get_ts(dct, 'description', list), model.MultiLanguageTextType) if isinstance(obj, model.Identifiable): - if 'idShort' in dct: - obj.id_short = _get_ts(dct, 'idShort', str) if 'administration' in dct: obj.administration = cls._construct_administrative_information(_get_ts(dct, 'administration', dict)) if isinstance(obj, model.HasSemantics): @@ -536,7 +536,7 @@ def _construct_entity(cls, dct: Dict[str, object], object_class=model.Entity) -> if 'specificAssetIds' in dct: specific_asset_id = cls._construct_specific_asset_id(_get_ts(dct, 'specificAssetIds', dict)) - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, entity_type=ENTITY_TYPES_INVERSE[_get_ts(dct, "entityType", str)], global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) @@ -586,7 +586,7 @@ def _construct_submodel(cls, dct: Dict[str, object], object_class=model.Submodel @classmethod def _construct_capability(cls, dct: Dict[str, object], object_class=model.Capability) -> model.Capability: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) return ret @@ -595,7 +595,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod -> model.BasicEventElement: # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, observed=cls._construct_model_reference(_get_ts(dct, 'observed', dict), model.Referable), # type: ignore direction=DIRECTION_INVERSE[_get_ts(dct, "direction", str)], @@ -615,7 +615,7 @@ def _construct_basic_event_element(cls, dct: Dict[str, object], object_class=mod @classmethod def _construct_operation(cls, dct: Dict[str, object], object_class=model.Operation) -> model.Operation: - ret = object_class(_get_ts(dct, "idShort", str)) + ret = object_class(None) cls._amend_abstract_attributes(ret, dct) # Deserialize variables (they are not Referable, thus we don't @@ -640,7 +640,7 @@ def _construct_relationship_element( cls, dct: Dict[str, object], object_class=model.RelationshipElement) -> model.RelationshipElement: # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, first=cls._construct_reference(_get_ts(dct, 'first', dict)), second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) @@ -653,7 +653,7 @@ def _construct_annotated_relationship_element( # TODO: remove the following type: ignore comments when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 ret = object_class( - id_short=_get_ts(dct, "idShort", str), + id_short=None, first=cls._construct_reference(_get_ts(dct, 'first', dict)), second=cls._construct_reference(_get_ts(dct, 'second', dict))) cls._amend_abstract_attributes(ret, dct) @@ -667,7 +667,7 @@ def _construct_annotated_relationship_element( def _construct_submodel_element_collection(cls, dct: Dict[str, object], object_class=model.SubmodelElementCollection)\ -> model.SubmodelElementCollection: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) if not cls.stripped and 'value' in dct: for element in _get_ts(dct, "value", list): @@ -688,7 +688,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m if 'semanticIdListElement' in dct else None value_type_list_element = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueTypeListElement', str)]\ if 'valueTypeListElement' in dct else None - ret = object_class(id_short=_get_ts(dct, 'idShort', str), + ret = object_class(id_short=None, type_value_list_element=type_value_list_element, order_relevant=order_relevant, semantic_id_list_element=semantic_id_list_element, @@ -702,7 +702,7 @@ def _construct_submodel_element_list(cls, dct: Dict[str, object], object_class=m @classmethod def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> model.Blob: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) if 'value' in dct: @@ -711,7 +711,7 @@ def _construct_blob(cls, dct: Dict[str, object], object_class=model.Blob) -> mod @classmethod def _construct_file(cls, dct: Dict[str, object], object_class=model.File) -> model.File: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value=None, content_type=_get_ts(dct, "contentType", str)) cls._amend_abstract_attributes(ret, dct) @@ -730,7 +730,7 @@ def _construct_resource(cls, dct: Dict[str, object], object_class=model.Resource @classmethod def _construct_multi_language_property( cls, dct: Dict[str, object], object_class=model.MultiLanguageProperty) -> model.MultiLanguageProperty: - ret = object_class(id_short=_get_ts(dct, "idShort", str)) + ret = object_class(id_short=None) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: ret.value = cls._construct_lang_string_set(_get_ts(dct, 'value', list), model.MultiLanguageTextType) @@ -740,7 +740,7 @@ def _construct_multi_language_property( @classmethod def _construct_property(cls, dct: Dict[str, object], object_class=model.Property) -> model.Property: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: @@ -751,7 +751,7 @@ def _construct_property(cls, dct: Dict[str, object], object_class=model.Property @classmethod def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> model.Range: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value_type=model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueType', str)],) cls._amend_abstract_attributes(ret, dct) if 'min' in dct and dct['min'] is not None: @@ -763,7 +763,7 @@ def _construct_range(cls, dct: Dict[str, object], object_class=model.Range) -> m @classmethod def _construct_reference_element( cls, dct: Dict[str, object], object_class=model.ReferenceElement) -> model.ReferenceElement: - ret = object_class(id_short=_get_ts(dct, "idShort", str), + ret = object_class(id_short=None, value=None) cls._amend_abstract_attributes(ret, dct) if 'value' in dct and dct['value'] is not None: diff --git a/basyx/aas/adapter/xml/xml_deserialization.py b/basyx/aas/adapter/xml/xml_deserialization.py index ba676d662..27696e851 100644 --- a/basyx/aas/adapter/xml/xml_deserialization.py +++ b/basyx/aas/adapter/xml/xml_deserialization.py @@ -436,6 +436,9 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None :return: None """ if isinstance(obj, model.Referable): + id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) + if id_short is not None: + obj.id_short = id_short category = _get_text_or_none(element.find(NS_AAS + "category")) display_name = _failsafe_construct(element.find(NS_AAS + "displayName"), cls.construct_multi_language_name_type, cls.failsafe) @@ -448,9 +451,6 @@ def _amend_abstract_attributes(cls, obj: object, element: etree.Element) -> None if description is not None: obj.description = description if isinstance(obj, model.Identifiable): - id_short = _get_text_or_none(element.find(NS_AAS + "idShort")) - if id_short is not None: - obj.id_short = id_short administration = _failsafe_construct(element.find(NS_AAS + "administration"), cls.construct_administrative_information, cls.failsafe) if administration: @@ -491,7 +491,7 @@ def _construct_relationship_element_internal(cls, element: etree.Element, object to reduce duplicate code """ relationship_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_construct_mandatory(element, NS_AAS + "first", cls.construct_reference), _child_construct_mandatory(element, NS_AAS + "second", cls.construct_reference) ) @@ -752,7 +752,7 @@ def construct_annotated_relationship_element(cls, element: etree.Element, def construct_basic_event_element(cls, element: etree.Element, object_class=model.BasicEventElement, **_kwargs: Any) -> model.BasicEventElement: basic_event_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_construct_mandatory(element, NS_AAS + "observed", cls._construct_referable_reference), _child_text_mandatory_mapped(element, NS_AAS + "direction", DIRECTION_INVERSE), _child_text_mandatory_mapped(element, NS_AAS + "state", STATE_OF_EVENT_INVERSE) @@ -779,7 +779,7 @@ def construct_basic_event_element(cls, element: etree.Element, object_class=mode @classmethod def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwargs: Any) -> model.Blob: blob = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -791,9 +791,7 @@ def construct_blob(cls, element: etree.Element, object_class=model.Blob, **_kwar @classmethod def construct_capability(cls, element: etree.Element, object_class=model.Capability, **_kwargs: Any) \ -> model.Capability: - capability = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + capability = object_class(None) cls._amend_abstract_attributes(capability, element) return capability @@ -803,7 +801,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ specific_asset_id = _failsafe_construct(element.find(NS_AAS + "specificAssetId"), cls.construct_specific_asset_id, cls.failsafe) entity = object_class( - id_short=_child_text_mandatory(element, NS_AAS + "idShort"), + id_short=None, entity_type=_child_text_mandatory_mapped(element, NS_AAS + "entityType", ENTITY_TYPES_INVERSE), global_asset_id=global_asset_id, specific_asset_id=specific_asset_id) @@ -820,7 +818,7 @@ def construct_entity(cls, element: etree.Element, object_class=model.Entity, **_ @classmethod def construct_file(cls, element: etree.Element, object_class=model.File, **_kwargs: Any) -> model.File: file = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, _child_text_mandatory(element, NS_AAS + "contentType") ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -843,9 +841,7 @@ def construct_resource(cls, element: etree.Element, object_class=model.Resource, @classmethod def construct_multi_language_property(cls, element: etree.Element, object_class=model.MultiLanguageProperty, **_kwargs: Any) -> model.MultiLanguageProperty: - multi_language_property = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + multi_language_property = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_multi_language_text_type, cls.failsafe) if value is not None: @@ -859,9 +855,7 @@ def construct_multi_language_property(cls, element: etree.Element, object_class= @classmethod def construct_operation(cls, element: etree.Element, object_class=model.Operation, **_kwargs: Any) \ -> model.Operation: - operation = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + operation = object_class(None) input_variables = element.find(NS_AAS + "inputVariables") if input_variables is not None: for input_variable in _child_construct_multiple(input_variables, NS_AAS + "operationVariable", @@ -883,7 +877,7 @@ def construct_operation(cls, element: etree.Element, object_class=model.Operatio @classmethod def construct_property(cls, element: etree.Element, object_class=model.Property, **_kwargs: Any) -> model.Property: property_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) value = _get_text_or_none(element.find(NS_AAS + "value")) @@ -898,7 +892,7 @@ def construct_property(cls, element: etree.Element, object_class=model.Property, @classmethod def construct_range(cls, element: etree.Element, object_class=model.Range, **_kwargs: Any) -> model.Range: range_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, value_type=_child_text_mandatory_mapped(element, NS_AAS + "valueType", model.datatypes.XSD_TYPE_CLASSES) ) max_ = _get_text_or_none(element.find(NS_AAS + "max")) @@ -913,9 +907,7 @@ def construct_range(cls, element: etree.Element, object_class=model.Range, **_kw @classmethod def construct_reference_element(cls, element: etree.Element, object_class=model.ReferenceElement, **_kwargs: Any) \ -> model.ReferenceElement: - reference_element = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + reference_element = object_class(None) value = _failsafe_construct(element.find(NS_AAS + "value"), cls.construct_reference, cls.failsafe) if value is not None: reference_element.value = value @@ -930,9 +922,7 @@ def construct_relationship_element(cls, element: etree.Element, object_class=mod @classmethod def construct_submodel_element_collection(cls, element: etree.Element, object_class=model.SubmodelElementCollection, **_kwargs: Any) -> model.SubmodelElementCollection: - collection = object_class( - _child_text_mandatory(element, NS_AAS + "idShort") - ) + collection = object_class(None) if not cls.stripped: value = element.find(NS_AAS + "value") if value is not None: @@ -952,7 +942,7 @@ def construct_submodel_element_list(cls, element: etree.Element, object_class=mo f"{model.SubmodelElement}, got {type_value_list_element}!") order_relevant = element.find(NS_AAS + "orderRelevant") list_ = object_class( - _child_text_mandatory(element, NS_AAS + "idShort"), + None, type_value_list_element, semantic_id_list_element=_failsafe_construct(element.find(NS_AAS + "semanticIdListElement"), cls.construct_reference, cls.failsafe), diff --git a/basyx/aas/adapter/xml/xml_serialization.py b/basyx/aas/adapter/xml/xml_serialization.py index 9196137aa..cabec85bb 100644 --- a/basyx/aas/adapter/xml/xml_serialization.py +++ b/basyx/aas/adapter/xml/xml_serialization.py @@ -92,7 +92,8 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element: if isinstance(obj, model.Referable): if obj.category: elm.append(_generate_element(name=NS_AAS + "category", text=obj.category)) - elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) + if obj.id_short: + elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short)) if obj.display_name: elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName")) if obj.description: diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index 2d4bf2ce7..857a1ff5a 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -459,6 +459,8 @@ def from_referable(referable: "Referable") -> "Key": except ValueError as e: raise ValueError(f"Object {referable!r} is not contained within its parent {referable.parent!r}") from e else: + if referable.id_short is None: + raise ValueError(f"Can't create Key for {referable!r} without an id_short!") return Key(key_type, referable.id_short) @@ -598,7 +600,7 @@ class Referable(HasExtension, metaclass=abc.ABCMeta): @abc.abstractmethod def __init__(self): super().__init__() - self._id_short: NameType = "NotSet" + self._id_short: Optional[NameType] = None self.display_name: Optional[MultiLanguageNameType] = dict() self._category: Optional[NameType] = None self.description: Optional[MultiLanguageTextType] = dict() @@ -610,25 +612,26 @@ def __init__(self): def __repr__(self) -> str: reversed_path = [] item = self # type: Any - from .submodel import SubmodelElementList - while item is not None: - if isinstance(item, Identifiable): - reversed_path.append(item.id) - break - elif isinstance(item, Referable): - if isinstance(item.parent, SubmodelElementList): - reversed_path.append(f"{item.parent.id_short}[{item.parent.value.index(item)}]") + if item.id_short is not None: + from .submodel import SubmodelElementList + while item is not None: + if isinstance(item, Identifiable): + reversed_path.append(item.id) + break + elif isinstance(item, Referable): + if isinstance(item.parent, SubmodelElementList): + reversed_path.append(f"{item.parent.id_short}[{item.parent.value.index(item)}]") + item = item.parent + else: + reversed_path.append(item.id_short) item = item.parent else: - reversed_path.append(item.id_short) - item = item.parent - else: - raise AttributeError('Referable must have an identifiable as root object and only parents that are ' - 'referable') + raise AttributeError('Referable must have an identifiable as root object and only parents that are ' + 'referable') - return "{}[{}]".format(self.__class__.__name__, " / ".join(reversed(reversed_path))) + return self.__class__.__name__ + ("[{}]".format(" / ".join(reversed(reversed_path))) if reversed_path else "") - def _get_id_short(self): + def _get_id_short(self) -> Optional[NameType]: return self._id_short def _set_category(self, category: Optional[NameType]): @@ -648,7 +651,7 @@ def _get_category(self) -> Optional[NameType]: category = property(_get_category, _set_category) - def _set_id_short(self, id_short: NameType): + def _set_id_short(self, id_short: Optional[NameType]): """ Check the input string @@ -663,18 +666,19 @@ def _set_id_short(self, id_short: NameType): if id_short == self.id_short: return - _string_constraints.check_name_type(id_short) - test_id_short: NameType = str(id_short) - if not re.fullmatch("[a-zA-Z0-9_]*", test_id_short): - raise AASConstraintViolation( - 2, - "The id_short must contain only letters, digits and underscore" - ) - if not test_id_short[0].isalpha(): - raise AASConstraintViolation( - 2, - "The id_short must start with a letter" - ) + if id_short is not None: + _string_constraints.check_name_type(id_short) + test_id_short: NameType = str(id_short) + if not re.fullmatch("[a-zA-Z0-9_]*", test_id_short): + raise AASConstraintViolation( + 2, + "The id_short must contain only letters, digits and underscore" + ) + if not test_id_short[0].isalpha(): + raise AASConstraintViolation( + 2, + "The id_short must start with a letter" + ) if self.parent is not None: for set_ in self.parent.namespace_element_sets: diff --git a/basyx/aas/model/submodel.py b/basyx/aas/model/submodel.py index 104eff995..d14e87178 100644 --- a/basyx/aas/model/submodel.py +++ b/basyx/aas/model/submodel.py @@ -52,7 +52,7 @@ class SubmodelElement(base.Referable, base.Qualifiable, base.HasSemantics, """ @abc.abstractmethod def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -192,7 +192,7 @@ class DataElement(SubmodelElement, metaclass=abc.ABCMeta): """ @abc.abstractmethod def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -255,7 +255,7 @@ class Property(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], value_type: base.DataTypeDefXsd, value: Optional[base.ValueDataType] = None, value_id: Optional[base.Reference] = None, @@ -325,7 +325,7 @@ class MultiLanguageProperty(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], value: Optional[base.MultiLanguageTextType] = None, value_id: Optional[base.Reference] = None, display_name: Optional[base.MultiLanguageNameType] = None, @@ -382,7 +382,7 @@ class Range(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], value_type: base.DataTypeDefXsd, min: Optional[base.ValueDataType] = None, max: Optional[base.ValueDataType] = None, @@ -464,7 +464,7 @@ class Blob(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], content_type: base.ContentType, value: Optional[base.BlobType] = None, display_name: Optional[base.MultiLanguageNameType] = None, @@ -518,7 +518,7 @@ class File(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], content_type: base.ContentType, value: Optional[base.PathType] = None, display_name: Optional[base.MultiLanguageNameType] = None, @@ -571,7 +571,7 @@ class ReferenceElement(DataElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], value: Optional[base.Reference] = None, display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, @@ -619,7 +619,7 @@ class SubmodelElementCollection(SubmodelElement, base.UniqueIdShortNamespace): :ivar embedded_data_specifications: List of Embedded data specification. """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], value: Iterable[SubmodelElement] = (), display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, @@ -686,7 +686,7 @@ class SubmodelElementList(SubmodelElement, base.UniqueIdShortNamespace, Generic[ :ivar embedded_data_specifications: List of Embedded data specification. """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], type_value_list_element: Type[_SE], value: Iterable[_SE] = (), semantic_id_list_element: Optional[base.Reference] = None, @@ -826,7 +826,7 @@ class RelationshipElement(SubmodelElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], first: base.Reference, second: base.Reference, display_name: Optional[base.MultiLanguageNameType] = None, @@ -883,7 +883,7 @@ class AnnotatedRelationshipElement(RelationshipElement, base.UniqueIdShortNamesp """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], first: base.Reference, second: base.Reference, display_name: Optional[base.MultiLanguageNameType] = None, @@ -951,7 +951,7 @@ class Operation(SubmodelElement): :ivar embedded_data_specifications: List of Embedded data specification. """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], input_variable: Optional[List[OperationVariable]] = None, output_variable: Optional[List[OperationVariable]] = None, in_output_variable: Optional[List[OperationVariable]] = None, @@ -1004,7 +1004,7 @@ class Capability(SubmodelElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -1061,7 +1061,7 @@ class Entity(SubmodelElement, base.UniqueIdShortNamespace): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], entity_type: base.EntityType, statement: Iterable[SubmodelElement] = (), global_asset_id: Optional[base.Identifier] = None, @@ -1134,7 +1134,7 @@ class EventElement(SubmodelElement, metaclass=abc.ABCMeta): """ @abc.abstractmethod def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], display_name: Optional[base.MultiLanguageNameType] = None, category: Optional[base.NameType] = None, description: Optional[base.MultiLanguageTextType] = None, @@ -1198,7 +1198,7 @@ class BasicEventElement(EventElement): """ def __init__(self, - id_short: base.NameType, + id_short: Optional[base.NameType], observed: base.ModelReference[Union["aas.AssetAdministrationShell", Submodel, SubmodelElement]], direction: base.Direction, state: base.StateOfEvent, diff --git a/test/model/test_base.py b/test/model/test_base.py index 7e186db09..0a1fe1b5f 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -35,12 +35,17 @@ def test_equality(self): def test_from_referable(self): mlp1 = model.MultiLanguageProperty("mlp1") mlp2 = model.MultiLanguageProperty("mlp2") - model.SubmodelElementList("list", model.MultiLanguageProperty, [mlp1, mlp2]) + se_list = model.SubmodelElementList("list", model.MultiLanguageProperty, [mlp1, mlp2]) self.assertEqual(model.Key(model.KeyTypes.MULTI_LANGUAGE_PROPERTY, "0"), model.Key.from_referable(mlp1)) self.assertEqual(model.Key(model.KeyTypes.MULTI_LANGUAGE_PROPERTY, "1"), model.Key.from_referable(mlp2)) - mlp1.parent = mlp2.parent = None + del se_list.value[0] + mlp1.id_short = None + self.assertEqual(model.Key(model.KeyTypes.MULTI_LANGUAGE_PROPERTY, "0"), model.Key.from_referable(mlp2)) + with self.assertRaises(ValueError) as cm: + model.Key.from_referable(mlp1) + self.assertEqual("Can't create Key for MultiLanguageProperty without an id_short!", str(cm.exception)) + mlp1.id_short = "mlp1" self.assertEqual(model.Key(model.KeyTypes.MULTI_LANGUAGE_PROPERTY, "mlp1"), model.Key.from_referable(mlp1)) - self.assertEqual(model.Key(model.KeyTypes.MULTI_LANGUAGE_PROPERTY, "mlp2"), model.Key.from_referable(mlp2)) class ExampleReferable(model.Referable): @@ -146,6 +151,7 @@ def __init__(self, value: model.Referable): ref = ExampleReferable() test_object = DummyClass(ref) + ref.id_short = "NotNone" ref.parent = test_object with self.assertRaises(AttributeError) as cm: ref.__repr__() From 9ef348d5b0e81775869bc3bf1fdcd60d1e38d76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 2 Oct 2023 18:50:01 +0200 Subject: [PATCH 2/3] model.base: implement AASd-022 correctly Type is changed from `KeyError` to `AASConstraintViolation` and the tests are adjusted accordingly. Furthermore, the NamespaceSet.add() identifier uniqueness check is moved after the simple parent check for performance reasons, solely because the parent check is a lot faster. --- basyx/aas/model/base.py | 26 +++++++------ test/model/test_base.py | 85 +++++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index 857a1ff5a..1c840df6b 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -683,8 +683,8 @@ def _set_id_short(self, id_short: Optional[NameType]): if self.parent is not None: for set_ in self.parent.namespace_element_sets: if set_.contains_id("id_short", id_short): - raise KeyError("Object with id_short '{}' is already present in the parent Namespace" - .format(id_short)) + raise AASConstraintViolation(22, "Object with id_short '{}' is already present in the parent " + "Namespace".format(id_short)) set_add_list: List[NamespaceSet] = [] for set_ in self.parent.namespace_element_sets: @@ -1813,7 +1813,7 @@ def __init__(self, parent: Union[UniqueIdShortNamespace, UniqueSemanticIdNamespa :param item_add_hook: A function that is called for each item that is added to this NamespaceSet, even when it is initialized. The first parameter is the item that is added while the second is an iterator over all currently contained items. Useful for constraint checking. - :raises KeyError: When `items` contains multiple objects with same unique attribute + :raises AASConstraintViolation: When `items` contains multiple objects with same unique attribute """ self.parent = parent parent.namespace_element_sets.append(self) @@ -1862,17 +1862,19 @@ def __iter__(self) -> Iterator[_NSO]: return iter(next(iter(self._backend.values()))[0].values()) def add(self, value: _NSO): - for set_ in self.parent.namespace_element_sets: - for attr_name, (backend, case_sensitive) in set_._backend.items(): - if hasattr(value, attr_name): - if self._get_attribute(value, attr_name, case_sensitive) in backend: - raise KeyError("Object with attribute (name='{}', value='{}') is already present in {}" - .format(attr_name, str(getattr(value, attr_name)), - "this set of objects" - if set_ is self else "another set in the same namespace")) if value.parent is not None and value.parent is not self.parent: raise ValueError("Object has already a parent, but it must not be part of two namespaces.") # TODO remove from current parent instead (allow moving)? + for set_ in self.parent.namespace_element_sets: + for attr_name, (backend, case_sensitive) in set_._backend.items(): + if hasattr(value, attr_name): + attr_value = self._get_attribute(value, attr_name, case_sensitive) + if attr_value in backend: + raise AASConstraintViolation(22, "Object with attribute (name='{}', value='{}') is already " + "present in {}" + .format(attr_name, str(getattr(value, attr_name)), + "this set of objects" + if set_ is self else "another set in the same namespace")) if self._item_add_hook is not None: self._item_add_hook(value, self.__iter__()) value.parent = self.parent @@ -2004,7 +2006,7 @@ def __init__(self, parent: Union[UniqueIdShortNamespace, UniqueSemanticIdNamespa :param item_add_hook: A function that is called for each item that is added to this NamespaceSet, even when it is initialized. The first parameter is the item that is added while the second is an iterator over all currently contained items. Useful for constraint checking. - :raises KeyError: When `items` contains multiple objects with same id_short + :raises AASConstraintViolation: When `items` contains multiple objects with same id_short """ self._order: List[_NSO] = [] super().__init__(parent, attribute_names, items, item_add_hook) diff --git a/test/model/test_base.py b/test/model/test_base.py index 0a1fe1b5f..d41e712ef 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -370,26 +370,27 @@ def setUp(self): def test_NamespaceSet(self) -> None: self.namespace.set1.add(self.prop1) self.assertEqual(1, len(self.namespace.set1)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set1.add(self.prop2) self.assertEqual( - '"Object with attribute (name=\'semantic_id\', value=\'ExternalReference(key=(Key(' - 'type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))\') is already present in this set of objects"', + "Object with attribute (name='semantic_id', value='ExternalReference(key=(Key(" + "type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))') is already present in this set of objects " + "(Constraint AASd-022)", str(cm.exception)) self.namespace.set2.add(self.prop5) self.namespace.set2.add(self.prop6) self.assertEqual(2, len(self.namespace.set2)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set2.add(self.prop1) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop1\') is already present in another ' - 'set in the same namespace"', + self.assertEqual("Object with attribute (name='id_short', value='Prop1') is already present in another " + "set in the same namespace (Constraint AASd-022)", str(cm.exception)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set2.add(self.prop4) self.assertEqual( - '"Object with attribute (name=\'semantic_id\', value=\'' - 'ExternalReference(key=(Key(type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))\')' - ' is already present in another set in the same namespace"', + "Object with attribute (name='semantic_id', value='" + "ExternalReference(key=(Key(type=GLOBAL_REFERENCE, value=http://acplt.org/Test1),))')" + " is already present in another set in the same namespace (Constraint AASd-022)", str(cm.exception)) self.assertIs(self.prop1, self.namespace.set1.get("id_short", "Prop1")) @@ -399,22 +400,22 @@ def test_NamespaceSet(self) -> None: self.assertIs(self.prop5, self.namespace.set2.get("id_short", "Prop3")) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set1.add(self.prop1alt) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop1\') is already present in this set of' - ' objects"', + self.assertEqual("Object with attribute (name='id_short', value='Prop1') is already present in this set of" + " objects (Constraint AASd-022)", str(cm.exception)) self.namespace.set1.add(self.prop3) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set1.add(self.prop7) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop2\') is already present in this set ' - 'of objects"', + self.assertEqual("Object with attribute (name='id_short', value='Prop2') is already present in this set " + "of objects (Constraint AASd-022)", str(cm.exception)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set1.add(self.prop8) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'ProP2\') is already present in this set ' - 'of objects"', + self.assertEqual("Object with attribute (name='id_short', value='ProP2') is already present in this set " + "of objects (Constraint AASd-022)", str(cm.exception)) namespace2 = self._namespace_class() @@ -453,10 +454,10 @@ def test_NamespaceSet(self) -> None: self.assertEqual(1, len(self.namespace3.set1)) self.namespace3.set1.add(self.qualifier2) self.assertEqual(2, len(self.namespace3.set1)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace3.set1.add(self.qualifier1alt) - self.assertEqual('"Object with attribute (name=\'type\', value=\'type1\') is already present in this set ' - 'of objects"', + self.assertEqual("Object with attribute (name='type', value='type1') is already present in this set " + "of objects (Constraint AASd-022)", str(cm.exception)) def test_namespaceset_item_add_hook(self) -> None: @@ -501,28 +502,28 @@ def dummy_hook(new, existing): self.assertIn(prop, existing_items) def test_Namespace(self) -> None: - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: namespace_test = ExampleNamespaceReferable([self.prop1, self.prop2, self.prop1alt]) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop1\') is already present in this set ' - 'of objects"', + self.assertEqual("Object with attribute (name='id_short', value='Prop1') is already present in this set " + "of objects (Constraint AASd-022)", str(cm.exception)) self.assertIsNone(self.prop1.parent) namespace = self._namespace_class([self.prop1, self.prop2]) self.assertIs(self.prop2, namespace.get_referable("Prop2")) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(KeyError) as cm2: namespace.get_referable("Prop3") self.assertEqual("'Referable with id_short Prop3 not found in this namespace'", - str(cm.exception)) + str(cm2.exception)) namespace.remove_referable("Prop2") - with self.assertRaises(KeyError) as cm2: + with self.assertRaises(KeyError) as cm3: namespace.get_referable("Prop2") - self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm2.exception)) + self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm3.exception)) - with self.assertRaises(KeyError) as cm3: + with self.assertRaises(KeyError) as cm4: namespace.remove_referable("Prop2") - self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm3.exception)) + self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm4.exception)) def test_renaming(self) -> None: self.namespace.set2.add(self.prop1) @@ -539,9 +540,9 @@ def test_renaming(self) -> None: self.assertEqual("'Referable with id_short Prop1 not found in this namespace'", str(cm.exception)) self.assertIs(self.prop2, self.namespace.get_referable("Prop2")) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm2: self.prop1.id_short = "Prop2" - self.assertIn("already present", str(cm.exception)) + self.assertIn("already present", str(cm2.exception)) self.namespace.set3.add(self.extension1) self.namespace.set3.add(self.extension2) @@ -615,20 +616,20 @@ def test_OrderedNamespace(self) -> None: self.assertEqual(1, len(self.namespace.set2)) self.namespace.set2.insert(0, self.prop2) self.assertEqual(2, len(self.namespace.set2)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set1.insert(0, self.prop1alt) - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop1\') is already present in another ' - 'set in the same namespace"', + self.assertEqual('Object with attribute (name=\'id_short\', value=\'Prop1\') is already present in another ' + 'set in the same namespace (Constraint AASd-022)', str(cm.exception)) self.assertEqual((self.prop2, self.prop1), tuple(self.namespace.set2)) self.assertEqual(self.prop1, self.namespace.set2[1]) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(model.AASConstraintViolation) as cm: self.namespace.set2[1] = self.prop2 - self.assertEqual('"Object with attribute (name=\'id_short\', value=\'Prop2\') is already present in this ' - 'set of objects"', + self.assertEqual('Object with attribute (name=\'id_short\', value=\'Prop2\') is already present in this ' + 'set of objects (Constraint AASd-022)', str(cm.exception)) - prop3 = model.Property("Prop3", model.datatypes.Int) + prop3 = model.Property("Prop3", model.datatypes.Int, semantic_id=self.propSemanticID3) self.assertEqual(2, len(self.namespace.set2)) self.namespace.set2[1] = prop3 self.assertEqual(2, len(self.namespace.set2)) @@ -647,10 +648,10 @@ def test_OrderedNamespace(self) -> None: self.assertIs(self.prop1, namespace2.set2.get("id_short", "Prop1")) namespace2.set2.remove(("id_short", "Prop1")) self.assertEqual(1, len(namespace2.set2)) - with self.assertRaises(KeyError) as cm: + with self.assertRaises(KeyError) as cm2: namespace2.get_referable("Prop1") self.assertEqual("'Referable with id_short Prop1 not found in this namespace'", - str(cm.exception)) + str(cm2.exception)) class ExternalReferenceTest(unittest.TestCase): From ee4d6acb5c8acf1583955f9807ccb528426fcd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Mon, 2 Oct 2023 19:29:08 +0200 Subject: [PATCH 3/3] model.base: implement constraint `AASd-117` --- basyx/aas/model/base.py | 12 ++++++++++-- test/model/test_base.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index 1c840df6b..db2d35327 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -681,6 +681,9 @@ def _set_id_short(self, id_short: Optional[NameType]): ) if self.parent is not None: + if id_short is None: + raise AASConstraintViolation(117, f"id_short of {self!r} cannot be unset, since it is already " + f"contained in {self.parent!r}") for set_ in self.parent.namespace_element_sets: if set_.contains_id("id_short", id_short): raise AASConstraintViolation(22, "Object with id_short '{}' is already present in the parent " @@ -1813,7 +1816,8 @@ def __init__(self, parent: Union[UniqueIdShortNamespace, UniqueSemanticIdNamespa :param item_add_hook: A function that is called for each item that is added to this NamespaceSet, even when it is initialized. The first parameter is the item that is added while the second is an iterator over all currently contained items. Useful for constraint checking. - :raises AASConstraintViolation: When `items` contains multiple objects with same unique attribute + :raises AASConstraintViolation: When `items` contains multiple objects with same unique attribute or when an + item doesn't has an identifying attribute """ self.parent = parent parent.namespace_element_sets.append(self) @@ -1869,6 +1873,9 @@ def add(self, value: _NSO): for attr_name, (backend, case_sensitive) in set_._backend.items(): if hasattr(value, attr_name): attr_value = self._get_attribute(value, attr_name, case_sensitive) + if attr_value is None: + raise AASConstraintViolation(117, f"{value!r} has attribute {attr_name}=None, which is not " + f"allowed within a {self.parent.__class__.__name__}!") if attr_value in backend: raise AASConstraintViolation(22, "Object with attribute (name='{}', value='{}') is already " "present in {}" @@ -2006,7 +2013,8 @@ def __init__(self, parent: Union[UniqueIdShortNamespace, UniqueSemanticIdNamespa :param item_add_hook: A function that is called for each item that is added to this NamespaceSet, even when it is initialized. The first parameter is the item that is added while the second is an iterator over all currently contained items. Useful for constraint checking. - :raises AASConstraintViolation: When `items` contains multiple objects with same id_short + :raises AASConstraintViolation: When `items` contains multiple objects with same unique attribute or when an + item doesn't has an identifying attribute """ self._order: List[_NSO] = [] super().__init__(parent, attribute_names, items, item_add_hook) diff --git a/test/model/test_base.py b/test/model/test_base.py index d41e712ef..65b8e02f6 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -358,12 +358,12 @@ def setUp(self): self.prop6 = model.Property("Prop4", model.datatypes.Int, semantic_id=self.propSemanticID2) self.prop7 = model.Property("Prop2", model.datatypes.Int, semantic_id=self.propSemanticID3) self.prop8 = model.Property("ProP2", model.datatypes.Int, semantic_id=self.propSemanticID3) - self.prop1alt = model.Property("Prop1", model.datatypes.Int) - self.qualifier1 = model.Qualifier("type1", model.datatypes.Int, 1) - self.qualifier2 = model.Qualifier("type2", model.datatypes.Int, 1) - self.qualifier1alt = model.Qualifier("type1", model.datatypes.Int, 1) - self.extension1 = model.Extension("Ext1", model.datatypes.Int, 1) - self.extension2 = model.Extension("Ext2", model.datatypes.Int, 1) + self.prop1alt = model.Property("Prop1", model.datatypes.Int, semantic_id=self.propSemanticID) + self.qualifier1 = model.Qualifier("type1", model.datatypes.Int, 1, semantic_id=self.propSemanticID) + self.qualifier2 = model.Qualifier("type2", model.datatypes.Int, 1, semantic_id=self.propSemanticID2) + self.qualifier1alt = model.Qualifier("type1", model.datatypes.Int, 1, semantic_id=self.propSemanticID) + self.extension1 = model.Extension("Ext1", model.datatypes.Int, 1, semantic_id=self.propSemanticID) + self.extension2 = model.Extension("Ext2", model.datatypes.Int, 1, semantic_id=self.propSemanticID2) self.namespace = self._namespace_class() self.namespace3 = self._namespace_class_qualifier() @@ -565,13 +565,13 @@ def test_Namespaceset_update_from(self) -> None: # Prop2 is getting deleted since it does not exist in namespace2.set1 # Prop3 is getting added, since it does not exist in namespace1.set1 yet namespace1 = self._namespace_class() - prop1 = model.Property("Prop1", model.datatypes.Int, 1) - prop2 = model.Property("Prop2", model.datatypes.Int, 0) + prop1 = model.Property("Prop1", model.datatypes.Int, 1, semantic_id=self.propSemanticID) + prop2 = model.Property("Prop2", model.datatypes.Int, 0, semantic_id=self.propSemanticID2) namespace1.set2.add(prop1) namespace1.set2.add(prop2) namespace2 = self._namespace_class() - namespace2.set2.add(model.Property("Prop1", model.datatypes.Int, 0)) - namespace2.set2.add(model.Property("Prop3", model.datatypes.Int, 2)) + namespace2.set2.add(model.Property("Prop1", model.datatypes.Int, 0, semantic_id=self.propSemanticID)) + namespace2.set2.add(model.Property("Prop3", model.datatypes.Int, 2, semantic_id=self.propSemanticID2)) namespace1.set2.update_nss_from(namespace2.set2) # Check that Prop1 got updated correctly self.assertIs(namespace1.get_referable("Prop1"), prop1) @@ -596,6 +596,21 @@ def test_qualifiable_id_short_namespace(self) -> None: self.assertIs(submodel_element_collection.get_referable("Prop1"), prop1) self.assertIs(submodel_element_collection.get_qualifier_by_type("Qualifier1"), qualifier1) + def test_aasd_117(self) -> None: + property = model.Property(None, model.datatypes.Int, semantic_id=self.propSemanticID) + se_collection = model.SubmodelElementCollection("foo") + with self.assertRaises(model.AASConstraintViolation) as cm: + se_collection.add_referable(property) + self.assertEqual("Property has attribute id_short=None, which is not allowed within a " + "SubmodelElementCollection! (Constraint AASd-117)", str(cm.exception)) + property.id_short = "property" + se_collection.add_referable(property) + with self.assertRaises(model.AASConstraintViolation) as cm: + property.id_short = None + self.assertEqual("id_short of Property[foo / property] cannot be unset, since it is already contained in " + "SubmodelElementCollection[foo] (Constraint AASd-117)", str(cm.exception)) + property.id_short = "bar" + class ExampleOrderedNamespace(model.UniqueIdShortNamespace, model.UniqueSemanticIdNamespace): def __init__(self, values=()):