Skip to content

Commit

Permalink
fix(model): Disallow creating objects through DeepProxyAccessor
Browse files Browse the repository at this point in the history
By inheriting from DirectProxyAccessor, DeepProxyAccessor also
unintentionally inherited the behaviors that allowed creating objects.
This recently led to confusion as to what's possible and what isn't.

Part of the fix to #446.
  • Loading branch information
Wuestengecko committed Aug 12, 2024
1 parent c927488 commit 46dece4
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 26 deletions.
86 changes: 81 additions & 5 deletions capellambse/model/_descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
3 changes: 3 additions & 0 deletions tests/test_metamodel_cs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")

Expand All @@ -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")

Expand Down
33 changes: 12 additions & 21 deletions tests/test_model_layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -337,27 +340,24 @@ 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")
assert len(state_machine.regions) == 1

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
assert len(region.modes) == 0
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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 46dece4

Please sign in to comment.