Skip to content

Commit

Permalink
model.base: add id_short path resolution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jkhsjdhjs authored and s-heppner committed Mar 14, 2024
1 parent d32349c commit d77ceab
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 36 deletions.
72 changes: 42 additions & 30 deletions basyx/aas/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down
34 changes: 28 additions & 6 deletions test/model/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit d77ceab

Please sign in to comment.