4141VersionType = str
4242ValueTypeIEC61360 = str
4343
44+ MAX_RECURSION_DEPTH = 32 * 2 # see https://github.com/admin-shell-io/aas-specs-metamodel/issues/333
45+
4446
4547@unique
4648class KeyTypes (Enum ):
@@ -453,25 +455,31 @@ def from_referable(referable: "Referable") -> "Key":
453455 """
454456 # Get the `type` by finding the first class from the base classes list (via inspect.getmro), that is contained
455457 # in KEY_ELEMENTS_CLASSES
456- from . import KEY_TYPES_CLASSES , SubmodelElementList
457- try :
458- key_type = next (iter (KEY_TYPES_CLASSES [t ]
459- for t in inspect .getmro (type (referable ))
460- if t in KEY_TYPES_CLASSES ))
461- except StopIteration :
462- key_type = KeyTypes .PROPERTY
458+ key_type = Key ._get_key_type_for_referable (referable )
459+ key_value = Key ._get_key_value_for_referable (referable )
460+ return Key (key_type , key_value )
463461
462+ @staticmethod
463+ def _get_key_type_for_referable (referable : "Referable" ) -> KeyTypes :
464+ from . import KEY_TYPES_CLASSES , resolve_referable_class_in_key_types
465+ ref_type = resolve_referable_class_in_key_types (referable )
466+ key_type = KEY_TYPES_CLASSES [ref_type ]
467+ return key_type
468+
469+ @staticmethod
470+ def _get_key_value_for_referable (referable : "Referable" ) -> str :
471+ from . import SubmodelElementList
464472 if isinstance (referable , Identifiable ):
465- return Key ( key_type , referable .id )
473+ return referable .id
466474 elif isinstance (referable .parent , SubmodelElementList ):
467475 try :
468- return Key ( key_type , str (referable .parent .value .index (referable ) )) # type: ignore
476+ return str (referable .parent .value .index (referable )) # type: ignore
469477 except ValueError as e :
470478 raise ValueError (f"Object { referable !r} is not contained within its parent { referable .parent !r} " ) from e
471479 else :
472480 if referable .id_short is None :
473- raise ValueError (f"Can't create Key for { referable !r} without an id_short!" )
474- return Key ( key_type , referable .id_short )
481+ raise ValueError (f"Can't create Key value for { referable !r} without an id_short!" )
482+ return referable .id_short
475483
476484
477485_NSO = TypeVar ('_NSO' , bound = Union ["Referable" , "Qualifier" , "HasSemantics" , "Extension" ])
@@ -614,26 +622,75 @@ def __init__(self):
614622 self .parent : Optional [UniqueIdShortNamespace ] = None
615623
616624 def __repr__ (self ) -> str :
617- reversed_path = []
625+ root = self .get_identifiable_root ()
626+ try :
627+ id_short_path = self .get_id_short_path ()
628+ except (ValueError , AttributeError ):
629+ id_short_path = self .id_short if self .id_short is not None else ""
630+ item_cls_name = self .__class__ .__name__
631+
632+ if root is None :
633+ item_path = f"[{ id_short_path } ]" if id_short_path else ""
634+ else :
635+ item_path = f"[{ root .id } / { id_short_path } ]" if id_short_path else f"[{ root .id } ]"
636+
637+ return f"{ item_cls_name } { item_path } "
638+
639+ def get_identifiable_root (self ) -> Optional ["Identifiable" ]:
640+ """
641+ Get the root :class:`~.Identifiable` of this referable, if it exists.
642+
643+ :return: The root :class:`~.Identifiable` or None if no such root exists
644+ """
618645 item = self # type: Any
619- if item .id_short is not None :
620- from .submodel import SubmodelElementList
621- while item is not None :
622- if isinstance (item , Identifiable ):
623- reversed_path .append (item .id )
624- break
625- elif isinstance (item , Referable ):
626- if isinstance (item .parent , SubmodelElementList ):
627- reversed_path .append (f"{ item .parent .id_short } [{ item .parent .value .index (item )} ]" )
628- item = item .parent
629- else :
630- reversed_path .append (item .id_short )
631- item = item .parent
632- else :
633- raise AttributeError ('Referable must have an identifiable as root object and only parents that are '
634- 'referable' )
646+ while item is not None :
647+ if isinstance (item , Identifiable ):
648+ return item
649+ elif isinstance (item , Referable ):
650+ item = item .parent
651+ else :
652+ raise AttributeError ('Referable must have an identifiable as root object and only parents that are '
653+ 'referable' )
654+ return None
635655
636- return self .__class__ .__name__ + ("[{}]" .format (" / " .join (reversed (reversed_path ))) if reversed_path else "" )
656+ def get_id_short_path (self ) -> str :
657+ """
658+ Get the id_short path of this referable, i.e. the id_short of this referable and all its parents.
659+
660+ :return: The id_short path as a string, e.g. "MySECollection.MySEList[2]MySubProperty1"
661+ """
662+ path_list = self .get_id_short_path_as_a_list ()
663+ return self .build_id_short_path (path_list )
664+
665+ def get_id_short_path_as_a_list (self ) -> List [str ]:
666+ """
667+ Get the id_short path of this referable as a list of id_shorts and indexes.
668+
669+ :return: The id_short path as a list, e.g. '["MySECollection", "MySEList", "2", "MySubProperty1"]'
670+ :raises ValueError: If this referable has no id_short or
671+ if its parent is not a :class:`~basyx.aas.model.submodel.SubmodelElementList`
672+ :raises AttributeError: If the parent chain is broken, i.e. if a parent is neither a :class:`~.Referable` nor an
673+ :class:`~.Identifiable`
674+ """
675+ from .submodel import SubmodelElementList
676+ if self .id_short is None and not isinstance (self .parent , SubmodelElementList ):
677+ raise ValueError (f"Can't create id_short_path for { self .__class__ .__name__ } without an id_short or "
678+ f"if its parent is a SubmodelElementList!" )
679+
680+ item = self # type: Any
681+ path : List [str ] = []
682+ while item is not None :
683+ if not isinstance (item , Referable ):
684+ raise AttributeError ('Referable must have an identifiable as root object and only parents that are '
685+ 'referable' )
686+ if isinstance (item , Identifiable ):
687+ break
688+ elif isinstance (item .parent , SubmodelElementList ):
689+ path .insert (0 , str (item .parent .value .index (item )))
690+ else :
691+ path .insert (0 , item .id_short )
692+ item = item .parent
693+ return path
637694
638695 def _get_id_short (self ) -> Optional [NameType ]:
639696 return self ._id_short
@@ -653,6 +710,49 @@ def _set_category(self, category: Optional[NameType]):
653710 def _get_category (self ) -> Optional [NameType ]:
654711 return self ._category
655712
713+ @classmethod
714+ def parse_id_short_path (cls , id_short_path : str ) -> List [str ]:
715+ """
716+ Parse an id_short_path string into a list of id_shorts and indexes.
717+
718+ :param id_short_path: The id_short_path string, e.g. "MySECollection.MySEList[2]MySubProperty1"
719+ :return: The id_short path as a list, e.g. '["MySECollection", "MySEList", "2", "MySubProperty1"]'
720+ """
721+ id_shorts_and_indexes = []
722+ for part in id_short_path .split ("." ):
723+ id_short = part [0 :part .find ('[' )] if '[' in part else part
724+ id_shorts_and_indexes .append (id_short )
725+
726+ indexes_part = part .removeprefix (id_short )
727+ if indexes_part :
728+ if not re .fullmatch (r'(?:\[\d+\])+' , indexes_part ):
729+ raise ValueError (f"Invalid index format in id_short_path: '{ id_short_path } ', part: '{ part } '" )
730+ indexes = indexes_part .strip ("[]" ).split ("][" )
731+ id_shorts_and_indexes .extend (indexes )
732+ cls .validate_id_short_path (id_shorts_and_indexes )
733+ return id_shorts_and_indexes
734+
735+ @classmethod
736+ def build_id_short_path (cls , id_short_path : Iterable [str ]) -> str :
737+ """
738+ Build an id_short_path string from a list of id_shorts and indexes.
739+ """
740+ if isinstance (id_short_path , str ):
741+ raise ValueError ("id_short_path must be an Iterable of strings, not a single string" )
742+ path_list_with_dots_and_brackets = [f"[{ part } ]" if part .isdigit () else f".{ part } " for part in id_short_path ]
743+ id_short_path = "" .join (path_list_with_dots_and_brackets ).removeprefix ("." )
744+ return id_short_path
745+
746+ @classmethod
747+ def validate_id_short_path (cls , id_short_path : Union [str , NameType , Iterable [NameType ]]):
748+ if isinstance (id_short_path , str ):
749+ id_short_path = cls .parse_id_short_path (id_short_path )
750+ for id_short in id_short_path :
751+ if id_short .isdigit ():
752+ # This is an index, skip validation
753+ continue
754+ cls .validate_id_short (id_short )
755+
656756 @classmethod
657757 def validate_id_short (cls , id_short : NameType ) -> None :
658758 """
@@ -1001,22 +1101,24 @@ def from_referable(referable: Referable) -> "ModelReference":
10011101 object's ancestors
10021102 """
10031103 # Get the first class from the base classes list (via inspect.getmro), that is contained in KEY_ELEMENTS_CLASSES
1004- from . import KEY_TYPES_CLASSES
1104+ from . import resolve_referable_class_in_key_types
10051105 try :
1006- ref_type = next ( iter ( t for t in inspect . getmro ( type ( referable )) if t in KEY_TYPES_CLASSES ) )
1106+ ref_type = resolve_referable_class_in_key_types ( referable )
10071107 except StopIteration :
10081108 ref_type = Referable
10091109
10101110 ref : Referable = referable
10111111 keys : List [Key ] = []
10121112 while True :
1013- keys .append ( Key .from_referable (ref ))
1113+ keys .insert ( 0 , Key .from_referable (ref ))
10141114 if isinstance (ref , Identifiable ):
1015- keys .reverse ()
10161115 return ModelReference (tuple (keys ), ref_type )
10171116 if ref .parent is None or not isinstance (ref .parent , Referable ):
1018- raise ValueError ("The given Referable object is not embedded within an Identifiable object" )
1117+ raise ValueError (f "The given Referable object is not embedded within an Identifiable object: { ref } " )
10191118 ref = ref .parent
1119+ if len (keys ) > MAX_RECURSION_DEPTH :
1120+ raise ValueError (f"The given Referable object is embedded in >64 layers of Referables "
1121+ f"or there is a loop in the parent chain { ref } " )
10201122
10211123
10221124@_string_constraints .constrain_content_type ("content_type" )
@@ -1624,12 +1726,12 @@ def __init__(self) -> None:
16241726 super ().__init__ ()
16251727 self .namespace_element_sets : List [NamespaceSet ] = []
16261728
1627- def get_referable (self , id_short : Union [NameType , Iterable [NameType ]]) -> Referable :
1729+ def get_referable (self , id_short_path : Union [str , NameType , Iterable [NameType ]]) -> Referable :
16281730 """
16291731 Find a :class:`~.Referable` in this Namespace by its id_short or by its id_short path.
16301732 The id_short path may contain :class:`~basyx.aas.model.submodel.SubmodelElementList` indices.
16311733
1632- :param id_short : id_short or id_short path as any :class:`Iterable`
1734+ :param id_short_path : id_short or id_short path as a str or any :class:`Iterable`
16331735 :returns: :class:`~.Referable`
16341736 :raises TypeError: If one of the intermediate objects on the path is not a
16351737 :class:`~.UniqueIdShortNamespace`
@@ -1638,10 +1740,10 @@ def get_referable(self, id_short: Union[NameType, Iterable[NameType]]) -> Refera
16381740 :raises KeyError: If no such :class:`~.Referable` can be found
16391741 """
16401742 from .submodel import SubmodelElementList
1641- if isinstance (id_short , NameType ):
1642- id_short = [ id_short ]
1743+ if isinstance (id_short_path , ( str , NameType ) ):
1744+ id_short_path = Referable . parse_id_short_path ( id_short_path )
16431745 item : Union [UniqueIdShortNamespace , Referable ] = self
1644- for id_ in id_short :
1746+ for id_ in id_short_path :
16451747 # This is redundant on first iteration, but it's a negligible overhead.
16461748 # Also, ModelReference.resolve() relies on this check.
16471749 if not isinstance (item , UniqueIdShortNamespace ):
0 commit comments