diff --git a/capellambse/__init__.py b/capellambse/__init__.py index 172f48a94..9fadf1525 100644 --- a/capellambse/__init__.py +++ b/capellambse/__init__.py @@ -19,7 +19,7 @@ from .cli_helpers import * from .filehandler import * from .model import MelodyModel -from .model.common import ModelObject +from .model.common import ModelObject, new_object _has_loaded_extensions = False diff --git a/capellambse/extensions/reqif/_capellareq.py b/capellambse/extensions/reqif/_capellareq.py index a54cb18b6..272e83d3d 100644 --- a/capellambse/extensions/reqif/_capellareq.py +++ b/capellambse/extensions/reqif/_capellareq.py @@ -213,8 +213,11 @@ def insert( self, elmlist: c.ElementListCouplingMixin, index: int, - value: c.ModelObject, + value: c.ModelObject | c.new_object, ) -> None: + if isinstance(value, c.new_object): + raise NotImplementedError("Cannot insert new objects yet") + if isinstance(value, CapellaOutgoingRelation): parent = value.target._element else: diff --git a/capellambse/model/__init__.py b/capellambse/model/__init__.py index d3c832b7d..c847ba6a3 100644 --- a/capellambse/model/__init__.py +++ b/capellambse/model/__init__.py @@ -45,6 +45,7 @@ GenericElement, ModelObject, NonUniqueMemberError, + new_object, ) LOGGER = logging.getLogger(__name__) diff --git a/capellambse/model/common/__init__.py b/capellambse/model/common/__init__.py index b06e54fab..7af9172c8 100644 --- a/capellambse/model/common/__init__.py +++ b/capellambse/model/common/__init__.py @@ -145,6 +145,7 @@ def register_xtype_handler(cls: type[T]) -> type[T]: from .accessors import * +from .accessors import _NewObject as new_object from .element import * from .properties import * diff --git a/capellambse/model/common/accessors.py b/capellambse/model/common/accessors.py index 7a3d6b1f4..dc5cba4d0 100644 --- a/capellambse/model/common/accessors.py +++ b/capellambse/model/common/accessors.py @@ -72,6 +72,26 @@ def __str__(self) -> str: ) +class _NewObject: + """A marker class that indicates that a new object should be created. + + This object can be assigned to an attribute of a model object, and + will be replaced with a new object of the correct type by the + attribute's accessor. + + In the future, this class will be replaced with a function that + simply returns a new object of the correct type. + """ + + def __init__(self, /, *type_hint: str, **kw: t.Any) -> None: + self._type_hint = type_hint + self._kw = kw + + def __repr__(self) -> str: + kw = ", ".join(f"{k}={v!r}" for k, v in self._kw.items()) + return f"" + + class Accessor(t.Generic[T], metaclass=abc.ABCMeta): """Super class for all Accessor types.""" @@ -198,6 +218,13 @@ def __init__( else: self.aslist = None + def __set__( + self, + obj: element.ModelObject, + value: T | _NewObject | cabc.Iterable[T | _NewObject], + ) -> None: + raise TypeError("Cannot set this type of attribute") + def create( self, elmlist: ElementListCouplingMixin, @@ -237,7 +264,7 @@ def insert( self, elmlist: ElementListCouplingMixin, index: int, - value: element.ModelObject, + value: element.ModelObject | _NewObject, ) -> None: """Insert the ``value`` object into the model. @@ -255,6 +282,29 @@ def delete( """Delete the ``obj`` from the model.""" raise NotImplementedError("Objects in this list cannot be deleted") + def _create( + self, + parent: element.ModelObject, + xmltag: str | None, + /, + *type_hints: str | None, + **kw: t.Any, + ) -> T: + if type_hints: + elmclass, kw["xtype"] = self._match_xtype(*type_hints) + else: + elmclass, kw["xtype"] = self._guess_xtype() + assert elmclass is not None + + want_id: str | None = None + if "uuid" in kw: + want_id = kw.pop("uuid") + + pelem = parent._element + with parent._model._loader.new_uuid(pelem, want=want_id) as obj_id: + obj = elmclass(parent._model, pelem, xmltag, uuid=obj_id, **kw) + return obj + def _make_list(self, parent_obj, elements): assert hasattr(self, "class_") assert hasattr(self, "list_extra_args") @@ -325,6 +375,15 @@ def match_xt(xtp: S, itr: cabc.Iterable[S]) -> S: objtype = match_xt(objtype, candidate_classes) return candidate_classes[objtype], objtype + def _guess_xtype(self) -> tuple[type[T], str]: + try: + super_guess = super()._guess_xtype # type: ignore[misc] + except AttributeError: + pass + else: + return super_guess() + raise TypeError(f"{self._qualname} requires a type hint") + def purge_references( self, obj: element.ModelObject, target: element.ModelObject ) -> contextlib.AbstractContextManager[None]: @@ -580,7 +639,9 @@ def __get__(self, obj, objtype=None): return rv def __set__( - self, obj: element.ModelObject, value: str | T | cabc.Iterable[str | T] + self, + obj: element.ModelObject, + value: str | T | _NewObject | cabc.Iterable[str | T | _NewObject], ) -> None: if getattr(obj, "_constructed", True): sys.audit("capellambse.setattr", obj, self.__name__, value) @@ -599,9 +660,15 @@ def __set__( else: if isinstance(value, cabc.Iterable) and not isinstance(value, str): raise TypeError("Cannot set non-list attribute to an iterable") - raise NotImplementedError( - "Moving model objects is not supported yet" - ) + if not isinstance(value, _NewObject): + raise NotImplementedError( + "Moving model objects is not supported yet" + ) + if self.__get__(obj) is not None: + raise NotImplementedError( + "Replacing model objects is not supported yet" + ) + self._create(obj, None, *value._type_hint, **value._kw) def __delete__(self, obj: element.ModelObject) -> None: if self.rootelem: @@ -684,26 +751,20 @@ def create( if self.rootelem: raise TypeError("Cannot create objects here") - if type_hints: - elmclass, kw["xtype"] = self._match_xtype(*type_hints) - else: - elmclass, kw["xtype"] = self._guess_xtype() - assert elmclass is not None - - parent = elmlist._parent._element - want_id: str | None = None - if "uuid" in kw: - want_id = kw.pop("uuid") - with elmlist._model._loader.new_uuid(parent, want=want_id) as obj_id: - obj = elmclass(elmlist._model, parent, uuid=obj_id, **kw) - return obj + return self._create(elmlist._parent, None, *type_hints, **kw) def insert( self, elmlist: ElementListCouplingMixin, index: int, - value: element.ModelObject, + value: element.ModelObject | _NewObject, ) -> None: + if isinstance(value, _NewObject): + raise NotImplementedError( + "Creating new objects in lists with new_object() is not" + " supported yet" + ) + if value._model is not elmlist._model: raise ValueError("Cannot move elements between models") try: @@ -909,12 +970,14 @@ def insert( self, elmlist: ElementListCouplingMixin, index: int, - value: element.ModelObject, + value: element.ModelObject | _NewObject, ) -> None: if self.aslist is None: raise TypeError("Cannot insert: This is not a list (bug?)") if self.tag is None: raise NotImplementedError("Cannot insert: XML tag not set") + if isinstance(value, _NewObject): + raise NotImplementedError("Cannot insert new objects yet") self.__create_link( elmlist._parent, @@ -1011,7 +1074,9 @@ def __get__(self, obj, objtype=None): return rv def __set__( - self, obj: element.ModelObject, values: T | cabc.Iterable[T] + self, + obj: element.ModelObject, + values: T | _NewObject | cabc.Iterable[T | _NewObject], ) -> None: if getattr(obj, "_constructed", True): sys.audit("capellambse.setattr", obj, self.__name__, values) @@ -1023,8 +1088,11 @@ def __set__( f"{self._qualname} requires a single item, not an iterable" ) + if any(isinstance(i, _NewObject) for i in values): + raise NotImplementedError("Cannot create new objects here") + assert isinstance(values, cabc.Iterable) - self.__set_links(obj, values) + self.__set_links(obj, values) # type: ignore[arg-type] def __delete__(self, obj: element.ModelObject) -> None: if getattr(obj, "_constructed", True): @@ -1036,9 +1104,11 @@ def insert( self, elmlist: ElementListCouplingMixin, index: int, - value: element.ModelObject, + value: element.ModelObject | _NewObject, ) -> None: assert self.aslist is not None + if isinstance(value, _NewObject): + raise NotImplementedError("Cannot insert new objects yet") if value._model is not elmlist._parent._model: raise ValueError("Cannot insert elements from different models") objs = [*elmlist[:index], value, *elmlist[index:]] @@ -1551,7 +1621,9 @@ def __get__( return rv def __set__( - self, obj: element.ModelObject, value: cabc.Iterable[T] + self, + obj: element.ModelObject, + value: T | _NewObject | cabc.Iterable[T | _NewObject], ) -> None: if obj._constructed: sys.audit("capellambse.setattr", obj, self.__name__, value) @@ -1562,6 +1634,10 @@ def __set__( value = list(value) else: raise TypeError(f"Expected list, got {type(value).__name__}") + + if any(isinstance(i, _NewObject) for i in value): + raise NotImplementedError("Cannot create new objects here") + if not all(isinstance(i, self.class_) for i in value): raise TypeError( f"Expected {self.class_.__name__}, got {type(value).__name__}" @@ -1592,8 +1668,10 @@ def insert( self, elmlist: ElementListCouplingMixin, index: int, - value: element.ModelObject, + value: element.ModelObject | _NewObject, ) -> None: + if isinstance(value, _NewObject): + raise NotImplementedError("Cannot insert new objects yet") if not isinstance(value, self.class_): raise TypeError( f"Expected {self.class_.__name__}, got {type(value).__name__}" @@ -1620,9 +1698,12 @@ def purge_references( yield -class RoleTagAccessor(PhysicalAccessor): +class RoleTagAccessor(WritableAccessor, PhysicalAccessor): __slots__ = ("role_tag",) + aslist: type[ElementListCouplingMixin] | None + class_: type[element.GenericElement] + def __init__( self, role_tag: str, @@ -1650,6 +1731,81 @@ def __get__(self, obj, objtype=None): sys.audit("capellambse.getattr", obj, self.__name__, rv) return rv + def __set__( + self, + obj: element.ModelObject, + value: ( + str + | element.GenericElement + | _NewObject + | cabc.Iterable[str | element.GenericElement | _NewObject] + ), + ) -> None: + if getattr(obj, "_constructed", True): + sys.audit("capellambse.setattr", obj, self.__name__, value) + + if self.aslist: + raise NotImplementedError( + "Setting lists of model objects is not supported yet" + ) + else: + if isinstance(value, cabc.Iterable) and not isinstance(value, str): + raise TypeError("Cannot set non-list attribute to an iterable") + if not isinstance(value, _NewObject): + raise NotImplementedError( + "Moving model objects is not supported yet" + ) + if self.__get__(obj) is not None: + raise NotImplementedError( + "Replacing model objects is not supported yet" + ) + self._create(obj, self.role_tag, *value._type_hint, **value._kw) + + def create( + self, + elmlist: ElementListCouplingMixin, + /, + *type_hints: str | None, + **kw: t.Any, + ) -> element.GenericElement: + return self._create(elmlist._parent, self.role_tag, *type_hints, **kw) + + def insert( + self, + elmlist: ElementListCouplingMixin, + index: int, + value: element.ModelObject | _NewObject, + ) -> None: + if isinstance(value, _NewObject): + raise NotImplementedError("Cannot insert new objects yet") + if value._model is not elmlist._model: + raise ValueError("Cannot move elements between models") + try: + indexof = elmlist._parent._element.index + if index > 0: + parent_index = indexof(elmlist._elements[index - 1]) + 1 + elif index < -1: + parent_index = indexof(elmlist._elements[index + 1]) - 1 + else: + parent_index = index + except ValueError: + parent_index = len(elmlist._parent._element) + elmlist._parent._element.insert(parent_index, value._element) + elmlist._model._loader.idcache_index(value._element) + + def delete( + self, + elmlist: ElementListCouplingMixin, + obj: element.ModelObject, + ) -> None: + raise NotImplementedError("NYI") + + @contextlib.contextmanager + def purge_references( + self, obj: element.ModelObject, target: element.ModelObject + ) -> cabc.Iterator[None]: + yield + def no_list( desc: Accessor, diff --git a/capellambse/model/common/element.py b/capellambse/model/common/element.py index 9a415074d..7f0fa34bb 100644 --- a/capellambse/model/common/element.py +++ b/capellambse/model/common/element.py @@ -111,6 +111,8 @@ def __init__( self, model: capellambse.MelodyModel, parent: etree._Element, + xmltag: str | None, + /, **kw: t.Any, ) -> None: """Create a new model object. @@ -214,6 +216,7 @@ def __init__( self, model: capellambse.MelodyModel, parent: etree._Element, + xmltag: str | None = None, /, **kw: t.Any, ) -> None: @@ -228,13 +231,15 @@ def __init__( raise TypeError(f"Missing required keyword arguments: {mattrs}") super().__init__() - if self._xmltag is None: + if xmltag is None: + xmltag = self._xmltag + if xmltag is None: raise TypeError( f"Cannot instantiate {type(self).__name__} directly" ) self._constructed = False self._model = model - self._element: etree._Element = etree.Element(self._xmltag) + self._element: etree._Element = etree.Element(xmltag) parent.append(self._element) try: for key, val in kw.items(): diff --git a/capellambse/model/crosslayer/information/__init__.py b/capellambse/model/crosslayer/information/__init__.py index 3bb5918e5..416e7156d 100644 --- a/capellambse/model/crosslayer/information/__init__.py +++ b/capellambse/model/crosslayer/information/__init__.py @@ -172,12 +172,28 @@ class Collection(c.GenericElement): class DataPkg(c.GenericElement): """A data package that can hold classes.""" + _xmltag = "ownedDataPkgs" + + owned_associations = c.DirectProxyAccessor( + Association, aslist=c.ElementList + ) classes = c.DirectProxyAccessor(Class, aslist=c.ElementList) unions = c.DirectProxyAccessor(Union, aslist=c.ElementList) collections = c.DirectProxyAccessor(Collection, aslist=c.ElementList) enumerations = c.DirectProxyAccessor( datatype.Enumeration, aslist=c.ElementList ) + datatypes = c.DirectProxyAccessor( + c.GenericElement, + ( + datatype.BooleanType, + datatype.Enumeration, + datatype.StringType, + datatype.NumericType, + datatype.PhysicalQuantity, + ), + aslist=c.MixedElementList, + ) complex_values = c.DirectProxyAccessor( datavalue.ComplexValue, aslist=c.ElementList ) @@ -239,7 +255,7 @@ class ExchangeItem(c.GenericElement): c.set_accessor( Association, "members", - c.DirectProxyAccessor(Property, aslist=c.ElementList), + c.RoleTagAccessor("ownedMembers", aslist=c.ElementList), ) c.set_accessor( Association, diff --git a/capellambse/model/crosslayer/information/datatype.py b/capellambse/model/crosslayer/information/datatype.py index 54d47c408..6a1ed6b54 100644 --- a/capellambse/model/crosslayer/information/datatype.py +++ b/capellambse/model/crosslayer/information/datatype.py @@ -4,14 +4,43 @@ from __future__ import annotations from ... import common as c +from ... import modeltypes from . import datavalue +class DataType(c.GenericElement): + _xmltag = "ownedDataTypes" + + is_discrete = c.BooleanAttributeProperty( + "discrete", + __doc__=( + "Specifies whether or not this data type characterizes a discrete" + " value (versus continuous value)" + ), + ) + min_inclusive = c.BooleanAttributeProperty("minInclusive") + max_inclusive = c.BooleanAttributeProperty("maxInclusive") + pattern = c.AttributeProperty( + "pattern", + __doc__=( + "Textual specification of a constraint associated to this data" + " type" + ), + ) + visibility = c.EnumAttributeProperty( + "visibility", modeltypes.VisibilityKind, default="UNSET" + ) + + @c.xtype_handler(None) -class Enumeration(c.GenericElement): - """An Enumeration.""" +class BooleanType(DataType): + literals = c.DirectProxyAccessor(datavalue.LiteralBooleanValue) + default = c.RoleTagAccessor("ownedDefaultValue") - _xmltag = "ownedDataTypes" + +@c.xtype_handler(None) +class Enumeration(DataType): + """An Enumeration.""" owned_literals = c.DirectProxyAccessor( datavalue.EnumerationLiteral, aslist=c.ElementList @@ -28,3 +57,27 @@ def literals(self) -> c.ElementList[datavalue.EnumerationLiteral]: if isinstance(self.super, Enumeration) else self.owned_literals ) + + +@c.xtype_handler(None) +class StringType(DataType): + default_value = c.RoleTagAccessor("ownedDefaultValue") + null_value = c.RoleTagAccessor("ownedNullValue") + min_length = c.RoleTagAccessor("ownedMinLength") + max_length = c.RoleTagAccessor("ownedMaxLength") + + +@c.xtype_handler(None) +class NumericType(DataType): + kind = c.EnumAttributeProperty( + "kind", modeltypes.NumericTypeKind, default="INTEGER" + ) + default_value = c.RoleTagAccessor("ownedDefaultValue") + null_value = c.RoleTagAccessor("ownedNullValue") + min_value = c.RoleTagAccessor("ownedMinValue") + max_value = c.RoleTagAccessor("ownedMaxValue") + + +@c.xtype_handler(None) +class PhysicalQuantity(NumericType): + unit = c.RoleTagAccessor("ownedUnit") diff --git a/capellambse/model/crosslayer/information/datavalue.py b/capellambse/model/crosslayer/information/datavalue.py index 61eaf1f8e..ef23ee80d 100644 --- a/capellambse/model/crosslayer/information/datavalue.py +++ b/capellambse/model/crosslayer/information/datavalue.py @@ -5,6 +5,14 @@ from ... import common as c +class LiteralBooleanValue(c.GenericElement): + """A Literal Boolean Value.""" + + _xmltag = "ownedLiterals" + + value = c.BooleanAttributeProperty("value") + + class LiteralValue(c.GenericElement): is_abstract = c.BooleanAttributeProperty( "abstract", __doc__="Indicates if property is abstract" @@ -56,6 +64,7 @@ class EnumerationLiteral(c.GenericElement): _xmltag = "ownedLiterals" name = c.AttributeProperty("name", returntype=str) + value = c.RoleTagAccessor("domainValue") owner: c.Accessor diff --git a/capellambse/model/modeltypes.py b/capellambse/model/modeltypes.py index 2798a8c5f..2ce938ca4 100644 --- a/capellambse/model/modeltypes.py +++ b/capellambse/model/modeltypes.py @@ -243,3 +243,10 @@ class FunctionalChainKind(_StringyEnumMixin, _enum.Enum): SIMPLE = _enum.auto() COMPOSITE = _enum.auto() FRAGMENT = _enum.auto() + + +class NumericTypeKind(_StringyEnumMixin, _enum.Enum): + """Specifies the kind of this numeric data type.""" + + INTEGER = _enum.auto() + FLOAT = _enum.auto()