From d77ceab1569c758a37c426ffbf83bade9529e24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20M=C3=B6ller?= Date: Thu, 7 Mar 2024 23:15:20 +0100 Subject: [PATCH] model.base: add id_short path resolution Resolution of id_short paths is added via `UniqueIdShortNamespace.get_referable()`, such that it can be used on every object, that spans such a namespace. `ModelReference.resolve()` is simplified to make use of this new functionality. Furthermore, tests for this are added. --- basyx/aas/model/base.py | 72 ++++++++++++++++++++++++----------------- test/model/test_base.py | 34 +++++++++++++++---- 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/basyx/aas/model/base.py b/basyx/aas/model/base.py index 51112c9b5..ded73997a 100644 --- a/basyx/aas/model/base.py +++ b/basyx/aas/model/base.py @@ -1030,47 +1030,29 @@ def resolve(self, provider_: "provider.AbstractObjectProvider") -> _RT: :raises IndexError: If the list of keys is empty :raises TypeError: If one of the intermediate objects on the path is not a :class:`~basyx.aas.model.base.Namespace` + :raises ValueError: If a non-numeric index is given to resolve in a + :class:`~basyx.aas.model.submodel.SubmodelElementList` :raises UnexpectedTypeError: If the retrieved object is not of the expected type (or one of its subclasses). The object is stored in the ``value`` attribute of the exception :raises KeyError: If the reference could not be resolved """ - from . import SubmodelElementList - # For ModelReferences, the first key must be an AasIdentifiable. So resolve the first key via the provider. identifier: Optional[Identifier] = self.key[0].get_identifier() if identifier is None: raise AssertionError("Retrieving the identifier of the first key failed.") - resolved_keys: List[str] = [] # for more helpful error messages try: item: Referable = provider_.get_identifiable(identifier) except KeyError as e: raise KeyError("Could not resolve identifier {}".format(identifier)) from e - resolved_keys.append(str(identifier)) - # All keys following the first must not reference identifiables (AASd-125). Thus, we can just follow the path - # recursively. - for key in self.key[1:]: - if not isinstance(item, UniqueIdShortNamespace): - raise TypeError("Object retrieved at {} is not a Namespace".format(" / ".join(resolved_keys))) - is_submodel_element_list = isinstance(item, SubmodelElementList) - try: - if is_submodel_element_list: - # The key's value must be numeric, since this is checked for keys following keys of type - # SUBMODEL_ELEMENT_LIST on construction of ModelReferences. - # Additionally item is known to be a SubmodelElementList which supports __getitem__ because we're in - # the `is_submodel_element_list` branch, but mypy doesn't infer types based on isinstance checks - # stored in boolean variables. - item = item.value[int(key.value)] # type: ignore - resolved_keys[-1] += f"[{key.value}]" - else: - item = item.get_referable(key.value) - resolved_keys.append(item.id_short) - except (KeyError, IndexError) as e: - raise KeyError("Could not resolve {} {} at {}".format( - "index" if is_submodel_element_list else "id_short", key.value, " / ".join(resolved_keys)))\ - from e + # All keys following the first must not reference identifiables (AASd-125). Thus, we can just resolve the + # id_short path via get_referable(). + # This is cursed af, but at least it keeps the code DRY. get_referable() will check the type of self in the + # first iteration, so we can ignore the type here. + item = UniqueIdShortNamespace.get_referable(item, # type: ignore[arg-type] + map(lambda k: k.value, self.key[1:])) # Check type if not isinstance(item, self.type): @@ -1734,15 +1716,45 @@ def __init__(self) -> None: super().__init__() self.namespace_element_sets: List[NamespaceSet] = [] - def get_referable(self, id_short: NameType) -> Referable: + def get_referable(self, id_short: Union[NameType, Iterable[NameType]]) -> Referable: """ - Find a :class:`~.Referable` in this Namespace by its id_short + Find a :class:`~.Referable` in this Namespace by its id_short or by its id_short path. + The id_short path may contain :class:`~basyx.aas.model.submodel.SubmodelElementList` indices. - :param id_short: id_short + :param id_short: id_short or id_short path as any :class:`Iterable` :returns: :class:`~.Referable` + :raises TypeError: If one of the intermediate objects on the path is not a + :class:`~.UniqueIdShortNamespace` + :raises ValueError: If a non-numeric index is given to resolve in a + :class:`~basyx.aas.model.submodel.SubmodelElementList` :raises KeyError: If no such :class:`~.Referable` can be found """ - return super()._get_object(Referable, "id_short", id_short) # type: ignore + from .submodel import SubmodelElementList + if isinstance(id_short, NameType): + id_short = [id_short] + item: Union[UniqueIdShortNamespace, Referable] = self + for id_ in id_short: + # This is redundant on first iteration, but it's a negligible overhead. + # Also, ModelReference.resolve() relies on this check. + if not isinstance(item, UniqueIdShortNamespace): + raise TypeError(f"Cannot resolve id_short or index '{id_}', " + f"because it is not a {UniqueIdShortNamespace.__name__}!") + is_submodel_element_list = isinstance(item, SubmodelElementList) + try: + if is_submodel_element_list: + # item is known to be a SubmodelElementList which supports __getitem__ because we're in + # the `is_submodel_element_list` branch, but mypy doesn't infer types based on isinstance checks + # stored in boolean variables. + item = item.value[int(id_)] # type: ignore + else: + item = item._get_object(Referable, "id_short", id_) # type: ignore[type-abstract] + except ValueError as e: + raise ValueError(f"Cannot resolve '{id_}', because it is not a numeric index!") from e + except (KeyError, IndexError) as e: + raise KeyError("Referable with {} {} not found in this namespace".format( + "index" if is_submodel_element_list else "id_short", id_)) from e + # All UniqueIdShortNamespaces are Referables, and we only ever assign Referable to item. + return item # type: ignore[return-value] def add_referable(self, referable: Referable) -> None: """ diff --git a/test/model/test_base.py b/test/model/test_base.py index 44ac6861d..8eee42de1 100644 --- a/test/model/test_base.py +++ b/test/model/test_base.py @@ -358,6 +358,9 @@ def setUp(self): 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, semantic_id=self.propSemanticID) + self.collection1 = model.SubmodelElementCollection(None) + self.list1 = model.SubmodelElementList("List1", model.SubmodelElementCollection, + 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) @@ -584,6 +587,26 @@ def test_Namespace(self) -> None: namespace.remove_referable("Prop2") self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm4.exception)) + def test_id_short_path_resolution(self) -> None: + self.namespace.set2.add(self.list1) + self.list1.add_referable(self.collection1) + self.collection1.add_referable(self.prop1) + + with self.assertRaises(ValueError) as cm: + self.namespace.get_referable(["List1", "a"]) + self.assertEqual("Cannot resolve 'a', because it is not a numeric index!", str(cm.exception)) + + with self.assertRaises(KeyError) as cm_2: + self.namespace.get_referable(["List1", "0", "Prop2"]) + self.assertEqual("'Referable with id_short Prop2 not found in this namespace'", str(cm_2.exception)) + + with self.assertRaises(TypeError) as cm_3: + self.namespace.get_referable(["List1", "0", "Prop1", "Test"]) + self.assertEqual("Cannot resolve id_short or index 'Test', because it is not a UniqueIdShortNamespace!", + str(cm_3.exception)) + + self.namespace.get_referable(["List1", "0", "Prop1"]) + def test_renaming(self) -> None: self.namespace.set2.add(self.prop1) self.namespace.set2.add(self.prop2) @@ -887,7 +910,7 @@ def get_identifiable(self, identifier: Identifier) -> Identifiable: model.Property) with self.assertRaises(KeyError) as cm: ref1.resolve(DummyObjectProvider()) - self.assertEqual("'Could not resolve id_short lst at urn:x-test:submodel'", str(cm.exception)) + self.assertEqual("'Referable with id_short lst not found in this namespace'", str(cm.exception)) ref2 = model.ModelReference((model.Key(model.KeyTypes.SUBMODEL, "urn:x-test:submodel"), model.Key(model.KeyTypes.SUBMODEL_ELEMENT_LIST, "list"), @@ -896,7 +919,7 @@ def get_identifiable(self, identifier: Identifier) -> Identifiable: model.Property) with self.assertRaises(KeyError) as cm_2: ref2.resolve(DummyObjectProvider()) - self.assertEqual("'Could not resolve index 99 at urn:x-test:submodel / list'", str(cm_2.exception)) + self.assertEqual("'Referable with index 99 not found in this namespace'", str(cm_2.exception)) ref3 = model.ModelReference((model.Key(model.KeyTypes.SUBMODEL, "urn:x-test:submodel"), model.Key(model.KeyTypes.SUBMODEL_ELEMENT_LIST, "list"), @@ -913,8 +936,8 @@ def get_identifiable(self, identifier: Identifier) -> Identifiable: model.Property) with self.assertRaises(TypeError) as cm_3: ref4.resolve(DummyObjectProvider()) - self.assertEqual("Object retrieved at urn:x-test:submodel / list[0] / prop is not a Namespace", - str(cm_3.exception)) + self.assertEqual("Cannot resolve id_short or index 'prop', " + "because it is not a UniqueIdShortNamespace!", str(cm_3.exception)) with self.assertRaises(AttributeError) as cm_4: ref1.key[2].value = "prop1" @@ -944,8 +967,7 @@ def get_identifiable(self, identifier: Identifier) -> Identifiable: with self.assertRaises(KeyError) as cm_8: ref8.resolve(DummyObjectProvider()) - self.assertEqual("'Could not resolve id_short prop_false at urn:x-test:submodel / list[0]'", - str(cm_8.exception)) + self.assertEqual("'Referable with id_short prop_false not found in this namespace'", str(cm_8.exception)) with self.assertRaises(ValueError) as cm_9: ref9 = model.ModelReference((), model.Submodel)