diff --git a/capellambse/model/_descriptors.py b/capellambse/model/_descriptors.py index c29b05040..e11d74c7f 100644 --- a/capellambse/model/_descriptors.py +++ b/capellambse/model/_descriptors.py @@ -814,18 +814,94 @@ def purge_references( yield -class DeepProxyAccessor(DirectProxyAccessor[T]): +class DeepProxyAccessor(PhysicalAccessor[T]): """A DirectProxyAccessor that searches recursively through the tree.""" __slots__ = () + def __init__( + self, + class_: type[T], + xtypes: str | type[T] | cabc.Iterable[str | type[T]] | None = None, + *, + aslist: type[_obj.ElementList] | None = None, + rootelem: ( + type[_obj.GenericElement] + | cabc.Sequence[type[_obj.GenericElement]] + | None + ) = None, + ) -> None: + """Create a DeepProxyAccessor. + + Parameters + ---------- + class_ + The proxy class. + xtypes + The ``xsi:type`` (or types) to search for. If None, defaults + to the type of the passed ``class_``. + aslist + A subclass of :class:`~capellambse.model.ElementList` to use + for returning a list of all matched objects. If not + specified, defaults to the base ElementList. + rootelem + Limit the search scope to objects of this type, nested below + the current object. When passing a sequence, defines a path + of object types to follow. + """ + if aslist is None: + aslist = _obj.ElementList + super().__init__( + class_, + xtypes, + aslist=aslist, + ) + if rootelem is None: + self.rootelem: cabc.Sequence[str] = () + elif isinstance(rootelem, type) and issubclass( + rootelem, _obj.GenericElement + ): + self.rootelem = (_xtype.build_xtype(rootelem),) + elif not isinstance(rootelem, str): # type: ignore[unreachable] + self.rootelem = tuple(_xtype.build_xtype(i) for i in rootelem) + else: + raise TypeError( + "Invalid 'rootelem', expected a type or list of types: " + + repr(rootelem) + ) + + @t.overload + def __get__(self, obj: None, objtype: type[t.Any]) -> te.Self: ... + @t.overload + def __get__( + self, obj: _obj.ModelObject, objtype: type[t.Any] | None = ... + ) -> _obj.ElementList[T]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T]: + del objtype + if obj is None: # pragma: no cover + return self + + elems = [e for e in self._getsubelems(obj) if e.get("id") is not None] + assert self.aslist is not None + return self.aslist(obj._model, elems) + def _getsubelems( self, obj: _obj.ModelObject ) -> cabc.Iterator[etree._Element]: - return itertools.chain.from_iterable( - obj._model._loader.iterdescendants_xt(i, *iter(self.xtypes)) - for i in self._findroots(obj) - ) + ldr = obj._model._loader + roots = [obj._element] + for xtype in self.rootelem: + roots = list( + itertools.chain.from_iterable( + ldr.iterchildren_xt(i, xtype) for i in roots + ) + ) + for root in roots: + yield from ldr.iterdescendants_xt(root, *self.xtypes) class LinkAccessor(WritableAccessor[T], PhysicalAccessor[T]): diff --git a/tests/test_metamodel_cs.py b/tests/test_metamodel_cs.py index ef513c7f6..9245acd83 100644 --- a/tests/test_metamodel_cs.py +++ b/tests/test_metamodel_cs.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from capellambse import MelodyModel +from capellambse.metamodel import cs def test_PhysicalPath_has_ordered_list_of_involved_items(model: MelodyModel): @@ -51,6 +52,7 @@ def test_PhysicalLink_has_exchanges(model: MelodyModel): def test_PhysicalLink_setting_ends(model: MelodyModel): link = model.pa.all_physical_links.by_name("Eth Cable 2") + assert isinstance(link, cs.PhysicalLink) source_pp = model.by_uuid("76d9c301-c0ad-4615-9f02-b804b018decf") target_pp = model.by_uuid("53c9fe29-18e2-4642-906e-b7507bf0ff39") @@ -64,6 +66,7 @@ def test_PhysicalLink_setting_ends(model: MelodyModel): def test_PhysicalLink_setting_source_and_target(model: MelodyModel): link = model.pa.all_physical_links.by_name("Eth Cable 2") + assert isinstance(link, cs.PhysicalLink) source_pp = model.by_uuid("76d9c301-c0ad-4615-9f02-b804b018decf") target_pp = model.by_uuid("53c9fe29-18e2-4642-906e-b7507bf0ff39") diff --git a/tests/test_model_layers.py b/tests/test_model_layers.py index 439456399..20e3c4ddb 100644 --- a/tests/test_model_layers.py +++ b/tests/test_model_layers.py @@ -137,12 +137,13 @@ def test_ElementList_dictlike_setitem(model: m.MelodyModel): def test_MixedElementList_filter_by_type(model: m.MelodyModel): - process = model.oa.all_processes.by_uuid( - "d588e41f-ec4d-4fa9-ad6d-056868c66274" - ) + process = model.by_uuid("d588e41f-ec4d-4fa9-ad6d-056868c66274") + assert isinstance(process, mm.oa.OperationalProcess) + involvements = process.involved acts = involvements.by_type("OperationalActivity") fexs = involvements.by_type("FunctionalExchange") + assert len(involvements) == 7 assert len(acts) == 4 assert len(fexs) == 3 @@ -169,6 +170,7 @@ def test_GenericElement_attrs(model: m.MelodyModel, key: str, value: str): def test_GenericElement_has_diagrams(model: m.MelodyModel): elm = model.oa.all_capabilities.by_name("Eat food") + assert isinstance(elm, mm.oa.OperationalCapability) assert hasattr(elm, "diagrams") assert len(elm.diagrams) == 0 @@ -186,6 +188,7 @@ def test_GenericElement_has_progress_status(model: m.MelodyModel): def test_Capabilities_have_constraints(model: m.MelodyModel): elm = model.oa.all_capabilities.by_name("Eat food") + assert isinstance(elm, mm.oa.OperationalCapability) assert hasattr(elm, "constraints") assert len(elm.constraints) == 3 @@ -337,6 +340,7 @@ def test_stm_accessible_from_component_pkg(self, model: m.MelodyModel): def test_stm_has_regions(self, model: m.MelodyModel): entity = model.oa.all_entities.by_name("Functional Human Being") + assert isinstance(entity, mm.oa.Entity) state_machine = entity.state_machines[0] assert hasattr(state_machine, "regions") @@ -344,6 +348,7 @@ def test_stm_has_regions(self, model: m.MelodyModel): def test_stm_region(self, model: m.MelodyModel): entity = model.oa.all_entities.by_name("Functional Human Being") + assert isinstance(entity, mm.oa.Entity) region = entity.state_machines[0].regions[0] assert len(region.states) == 12 @@ -351,13 +356,8 @@ def test_stm_region(self, model: m.MelodyModel): assert len(region.transitions) == 14 def test_stm_state_mode_regions_well_defined(self, model: m.MelodyModel): - mode = ( - model.oa.all_entities.by_name("Environment") - .entities.by_name("Weather") - .state_machines[0] - .regions[0] - .modes.by_name("Day") - ) + mode = model.by_uuid("91dc2eec-c878-4fdb-91d8-8f4a4527424e") + assert isinstance(mode, mm.capellacommon.Mode) assert hasattr(mode, "regions") assert len(mode.regions) == 1 @@ -366,12 +366,8 @@ def test_stm_state_mode_regions_well_defined(self, model: m.MelodyModel): def test_stm_transition_attributes_well_defined( self, model: m.MelodyModel ): - transition = ( - model.oa.all_entities.by_name("Functional Human Being") - .state_machines[0] - .regions[0] - .transitions.by_uuid("a78cf778-0476-4e08-a3a3-c115dca55dd1") - ) + transition = model.by_uuid("a78cf778-0476-4e08-a3a3-c115dca55dd1") + assert isinstance(transition, mm.capellacommon.StateTransition) assert hasattr(transition, "source") assert transition.source is not None @@ -423,11 +419,6 @@ def test_stm_region_has_access_to_diagrams(self, model: m.MelodyModel): assert sleep_region.diagrams assert sleep_region.diagrams[0].name == "[MSM] Keep the sleep schedule" - def test_stm_state_has_functions(self, model: m.MelodyModel): - state = model.by_uuid("957c5799-1d4a-4ac0-b5de-33a65bf1519c") - assert len(state.functions) == 4 # leaf functions only - assert "teach Care of Magical Creatures" in state.functions.by_name - def test_exchange_items_of_a_function_port(model: m.MelodyModel): port = model.by_uuid("db64f0c9-ea1c-4962-b043-1774547c36f7")