diff --git a/capellambse/extensions/filtering.py b/capellambse/extensions/filtering.py index 8d548253b..a6fd77091 100644 --- a/capellambse/extensions/filtering.py +++ b/capellambse/extensions/filtering.py @@ -20,16 +20,20 @@ import capellambse.model as m from capellambse import _native, helpers -VIEWPOINT: t.Final = "org.polarsys.capella.filtering" -NAMESPACE: t.Final = "http://www.polarsys.org/capella/filtering/6.0.0" -SYMBOLIC_NAME: t.Final = "filtering" - _LOGGER = logging.getLogger(__name__) -m.XTYPE_ANCHORS[__name__] = SYMBOLIC_NAME +NS = m.Namespace( + "http://www.polarsys.org/capella/filtering/{VERSION}", + "filtering", + "org.polarsys.capella.filtering", + "7.0.0", +) + +VIEWPOINT: t.Final = NS.viewpoint +NAMESPACE: t.Final = NS.uri.format(VERSION="6.0.0") +SYMBOLIC_NAME: t.Final = NS.alias -@m.xtype_handler(None) class FilteringCriterion(m.ModelElement): """A single filtering criterion.""" @@ -40,17 +44,15 @@ class FilteringCriterion(m.ModelElement): ) -@m.xtype_handler(None) class FilteringCriterionPkg(m.ModelElement): """A package containing multiple filtering criteria.""" _xmltag = "ownedFilteringCriterionPkgs" criteria = m.DirectProxyAccessor(FilteringCriterion, aslist=m.ElementList) - packages: m.Accessor[FilteringCriterionPkg] + packages: m.Accessor[m.ElementList[FilteringCriterionPkg]] -@m.xtype_handler(None) class FilteringModel(m.ModelElement): """A filtering model containing criteria to filter by.""" @@ -83,7 +85,7 @@ def __get__( loader = obj._model._loader try: - xt_critset = f"{SYMBOLIC_NAME}:AssociatedFilteringCriterionSet" + xt_critset = f"{NS.alias}:AssociatedFilteringCriterionSet" critset = next(loader.iterchildren_xt(obj._element, xt_critset)) except StopIteration: elems = [] @@ -94,32 +96,26 @@ def __get__( return self._make_list(obj, elems) -@m.xtype_handler(None) class FilteringResult(m.ModelElement): """A filtering result.""" -@m.xtype_handler(None) class ComposedFilteringResult(m.ModelElement): """A composed filtering result.""" def init() -> None: - m.set_accessor( - mm.capellamodeller.SystemEngineering, - "filtering_model", - m.DirectProxyAccessor(FilteringModel), + mm.capellamodeller.SystemEngineering.filtering_model = ( + m.DirectProxyAccessor(FilteringModel) ) m.MelodyModel.filtering_model = property( # type: ignore[attr-defined] operator.attrgetter("project.model_root.filtering_model") ) - m.set_accessor( - m.ModelElement, "filtering_criteria", AssociatedCriteriaAccessor() - ) + m.ModelElement.filtering_criteria = AssociatedCriteriaAccessor() -m.set_self_references( - (FilteringCriterionPkg, "packages"), +FilteringCriterionPkg.packages = m.DirectProxyAccessor( + FilteringCriterionPkg, aslist=m.ElementList ) try: diff --git a/capellambse/extensions/pvmt/__init__.py b/capellambse/extensions/pvmt/__init__.py index efd30ebaf..76f91af82 100644 --- a/capellambse/extensions/pvmt/__init__.py +++ b/capellambse/extensions/pvmt/__init__.py @@ -31,7 +31,7 @@ def _get_pvmt_configuration(model: m.MelodyModel) -> PVMTConfiguration: def init() -> None: """Initialize the PVMT extension.""" m.MelodyModel.pvmt = property(_get_pvmt_configuration) # type: ignore[attr-defined] - m.set_accessor(m.ModelElement, "pvmt", m.AlternateAccessor(ObjectPVMT)) + m.ModelElement.pvmt = m.AlternateAccessor(ObjectPVMT) if not t.TYPE_CHECKING: diff --git a/capellambse/extensions/pvmt/_config.py b/capellambse/extensions/pvmt/_config.py index 9af9ecba0..06c156982 100644 --- a/capellambse/extensions/pvmt/_config.py +++ b/capellambse/extensions/pvmt/_config.py @@ -52,6 +52,11 @@ r"(?m)\[(?P\w+)\]\s*(?P.+?)\s*\[/(?P=key)\]" ) +NS = m.Namespace( + m.VIRTUAL_NAMESPACE_PREFIX + "pvmt", + "capellambse.virtual.pvmt", +) + class ScopeError(m.InvalidModificationError): """Raised when trying to apply a PVMT group to an out-of-scope element.""" @@ -173,13 +178,40 @@ def _to_xml(self, value: SelectorRules | str, /) -> str: return value -class ManagedGroup(m.ModelElement): +class _PVMTBase: + _model: capellambse.MelodyModel + _element: etree._Element + + uuid = m.StringPOD("id") + name = m.StringPOD("name") + parent = m.ParentAccessor() + property_values: m.Accessor[m.ElementList[mm.capellacore.PropertyValue]] + + def __init__(self, *_args, **_kw) -> None: + raise TypeError("Use 'model.pvmt' to access PVMT configuration") + + @classmethod + def from_model( + cls, model: capellambse.MelodyModel, element: etree._Element + ) -> te.Self: + self = cls.__new__(cls) + self._model = model + self._element = element + return self + + def _short_repr_(self) -> str: + return f"<{type(self).__name__} {self.name!r}>" + + +_PVMTBase.property_values = mm.modellingcore.ModelElement.property_values + + +class ManagedGroup(mm.capellacore.PropertyValueGroup): """A managed group of property values.""" _required_attrs = frozenset({"name"}) selector = PVMTDescriptionProperty("description") - description = m.Alias("selector", dirhide=True) # type: ignore[assignment] @property def fullname(self) -> str: @@ -236,7 +268,9 @@ def apply(self, obj: m.ModelObject) -> mm.capellacore.PropertyValueGroup: groupobj = obj.property_value_groups.create( name=groupname, - applied_property_value_groups=[self], + applied_property_value_groups=[ + m.wrap_xml(self._model, self._element) + ], ) for propdef in self.property_values: pv = groupobj.property_values.create( @@ -259,7 +293,7 @@ def _short_html_(self) -> markupsafe.Markup: ) -class ManagedDomain(m.ModelElement): +class ManagedDomain(mm.capellacore.PropertyValuePkg): """A "domain" in the property value management extension.""" _required_attrs = frozenset({"name"}) @@ -272,18 +306,12 @@ class ManagedDomain(m.ModelElement): mm.capellacore.EnumerationPropertyType, aslist=m.ElementList, ) - groups = m.Containment( + groups = m.Containment[mm.capellacore.PropertyValueGroup]( "ownedPropertyValueGroups", mm.capellacore.PropertyValueGroup, - aslist=m.ElementList, mapkey="name", alternate=ManagedGroup, ) - enumeration_property_types = m.Containment( - "ownedEnumerationPropertyTypes", - mm.capellacore.EnumerationPropertyType, - aslist=m.ElementList, - ) def __init__( self, @@ -300,7 +328,7 @@ def __init__( def from_model( cls, model: capellambse.MelodyModel, element: etree._Element ) -> te.Self: - self = super().from_model(model, element) + self = m.wrap_xml(model, element, cls) try: version = self.property_values.by_name("version").value except Exception: @@ -316,16 +344,12 @@ def from_model( return self -class PVMTConfiguration(m.ModelElement): +class PVMTConfiguration(mm.capellacore.PropertyValuePkg): """Provides access to the model-wide PVMT configuration.""" - def __init__(self, *_args, **_kw) -> None: - raise TypeError("Use 'model.pvmt' to access PVMT configuration") - - domains = m.Containment( + domains = m.Containment[mm.capellacore.PropertyValuePkg]( "ownedPropertyValuePkgs", mm.capellacore.PropertyValuePkg, - aslist=m.ElementList, mapkey="name", alternate=ManagedDomain, ) diff --git a/capellambse/extensions/pvmt/_objects.py b/capellambse/extensions/pvmt/_objects.py index 9837e6536..53623bc8d 100644 --- a/capellambse/extensions/pvmt/_objects.py +++ b/capellambse/extensions/pvmt/_objects.py @@ -17,6 +17,7 @@ from capellambse.metamodel import capellacore from . import _config +from ._config import NS as NS e = markupsafe.escape @@ -142,6 +143,7 @@ def __repr__(self) -> str: for prop in group.property_values: fragments.append(f"\n - {prop.name}: ") if hasattr(prop.value, "_short_repr_"): + assert prop.value is not None fragments.append(prop.value._short_repr_()) else: fragments.append(repr(prop.value)) @@ -181,6 +183,7 @@ def __html__(self) -> markupsafe.Markup: else: actual = e(actual_val) if hasattr(prop.value, "_short_html_"): + assert prop.value is not None default = prop.value._short_html_() else: default = e(prop.value) diff --git a/capellambse/extensions/reqif/__init__.py b/capellambse/extensions/reqif/__init__.py index 040490aa6..45a82be0c 100644 --- a/capellambse/extensions/reqif/__init__.py +++ b/capellambse/extensions/reqif/__init__.py @@ -11,10 +11,18 @@ from ._glue import * from ._requirements import * +from ._capellareq import NS as CapellaRequirementsNS # isort: skip +from ._requirements import NS as RequirementsNS # isort: skip + if not t.TYPE_CHECKING: from ._capellareq import __all__ as _cr_all from ._requirements import __all__ as _rq_all - __all__ = [*_cr_all, *_rq_all] + __all__ = [ + "CapellaRequirementsNS", + "RequirementsNS", + *_cr_all, + *_rq_all, + ] del _cr_all, _rq_all del t diff --git a/capellambse/extensions/reqif/_capellareq.py b/capellambse/extensions/reqif/_capellareq.py index ef119a361..7b5755471 100644 --- a/capellambse/extensions/reqif/_capellareq.py +++ b/capellambse/extensions/reqif/_capellareq.py @@ -27,10 +27,13 @@ from . import _requirements as rq from . import exporter -m.XTYPE_ANCHORS[__name__] = "CapellaRequirements" +NS = m.Namespace( + "http://www.polarsys.org/capella/requirements", + "CapellaRequirements", + "org.polarsys.capella.vp.requirements", +) -@m.xtype_handler(None) class CapellaModule(rq.ReqIFElement): """A ReqIF Module that bundles multiple Requirement folders.""" @@ -38,7 +41,7 @@ class CapellaModule(rq.ReqIFElement): folders = m.DirectProxyAccessor(rq.Folder, aslist=m.ElementList) requirements = m.DirectProxyAccessor(rq.Requirement, aslist=m.ElementList) - type = m.Association(rq.ModuleType, "moduleType") + type = m.Single(m.Association(rq.ModuleType, "moduleType")) attributes = rq.AttributeAccessor() def to_reqif( @@ -81,24 +84,21 @@ def to_reqif( ) -@m.xtype_handler(None) class CapellaIncomingRelation(rq.AbstractRequirementsRelation): """A Relation between a requirement and an object.""" _xmltag = "ownedRelations" -@m.xtype_handler(None) class CapellaOutgoingRelation(rq.AbstractRequirementsRelation): """A Relation between an object and a requirement.""" _xmltag = "ownedExtensions" - source = m.Association(rq.Requirement, "target") - target = m.Association(m.ModelElement, "source") + source = m.Single(m.Association(rq.Requirement, "target")) + target = m.Single(m.Association(m.ModelElement, "source")) -@m.xtype_handler(None) class CapellaTypesFolder(rq.ReqIFElement): _xmltag = "ownedExtensions" @@ -214,6 +214,11 @@ def insert( raise NotImplementedError("Cannot insert new objects yet") if isinstance(value, CapellaOutgoingRelation): + if not value.target: + raise RuntimeError( + "Cannot insert outgoing relation without target:" + f" {value._short_repr_()}" + ) parent = value.target._element else: assert isinstance( @@ -262,7 +267,7 @@ def __init__( source: m.ModelObject, ) -> None: del elemclass - super().__init__(model, elements, rq.AbstractRequirementsRelation) + super().__init__(model, elements) self._source = source @t.overload @@ -364,5 +369,5 @@ def _make_list(self, parent_obj, elements): ) -m.set_accessor(rq.Requirement, "relations", RequirementsRelationAccessor()) -m.set_accessor(rq.Requirement, "related", ElementRelationAccessor()) +rq.Requirement.relations = RequirementsRelationAccessor() +rq.Requirement.related = ElementRelationAccessor() diff --git a/capellambse/extensions/reqif/_glue.py b/capellambse/extensions/reqif/_glue.py index f8e06accd..a025098b0 100644 --- a/capellambse/extensions/reqif/_glue.py +++ b/capellambse/extensions/reqif/_glue.py @@ -12,58 +12,30 @@ def init() -> None: - m.set_accessor( - rq.Folder, - "folders", - m.DirectProxyAccessor(rq.Folder, aslist=m.ElementList), + rq.Folder.folders = m.DirectProxyAccessor(rq.Folder, aslist=m.ElementList) + m.ModelElement.requirements = cr.ElementRelationAccessor() + cs.ComponentArchitecture.requirement_modules = m.DirectProxyAccessor( + cr.CapellaModule, aslist=m.ElementList ) - m.set_accessor( - m.ModelElement, "requirements", cr.ElementRelationAccessor() + cs.ComponentArchitecture.all_requirements = m.DeepProxyAccessor( + rq.Requirement, aslist=m.ElementList, rootelem=cr.CapellaModule ) - m.set_accessor( - cs.ComponentArchitecture, - "requirement_modules", - m.DirectProxyAccessor(cr.CapellaModule, aslist=m.ElementList), + cs.ComponentArchitecture.requirement_types_folders = m.DirectProxyAccessor( + cr.CapellaTypesFolder, aslist=m.ElementList ) - m.set_accessor( - cs.ComponentArchitecture, - "all_requirements", - m.DeepProxyAccessor( - rq.Requirement, aslist=m.ElementList, rootelem=cr.CapellaModule - ), + cr.CapellaModule.requirement_types_folders = m.DirectProxyAccessor( + cr.CapellaTypesFolder, aslist=m.ElementList ) - m.set_accessor( - cs.ComponentArchitecture, - "requirement_types_folders", - m.DirectProxyAccessor(cr.CapellaTypesFolder, aslist=m.ElementList), + cs.ComponentArchitecture.all_requirement_types = m.DeepProxyAccessor( + rq.RequirementType, + aslist=m.ElementList, + rootelem=cr.CapellaTypesFolder, ) - m.set_accessor( - cr.CapellaModule, - "requirement_types_folders", - m.DirectProxyAccessor(cr.CapellaTypesFolder, aslist=m.ElementList), + cs.ComponentArchitecture.all_module_types = m.DeepProxyAccessor( + rq.ModuleType, aslist=m.ElementList, rootelem=cr.CapellaTypesFolder ) - m.set_accessor( - cs.ComponentArchitecture, - "all_requirement_types", - m.DeepProxyAccessor( - rq.RequirementType, - aslist=m.ElementList, - rootelem=cr.CapellaTypesFolder, - ), - ) - m.set_accessor( - cs.ComponentArchitecture, - "all_module_types", - m.DeepProxyAccessor( - rq.ModuleType, aslist=m.ElementList, rootelem=cr.CapellaTypesFolder - ), - ) - m.set_accessor( - cs.ComponentArchitecture, - "all_relation_types", - m.DeepProxyAccessor( - rq.RelationType, - aslist=m.ElementList, - rootelem=cr.CapellaTypesFolder, - ), + cs.ComponentArchitecture.all_relation_types = m.DeepProxyAccessor( + rq.RelationType, + aslist=m.ElementList, + rootelem=cr.CapellaTypesFolder, ) diff --git a/capellambse/extensions/reqif/_requirements.py b/capellambse/extensions/reqif/_requirements.py index 5c06d1f41..7efac7a43 100644 --- a/capellambse/extensions/reqif/_requirements.py +++ b/capellambse/extensions/reqif/_requirements.py @@ -38,7 +38,11 @@ if t.TYPE_CHECKING: import markupsafe -m.XTYPE_ANCHORS[__name__] = "Requirements" +NS = m.Namespace( + "http://www.polarsys.org/kitalpha/requirements", + "Requirements", + "org.polarsys.kitalpha.vp.requirements", +) class ReqIFElement(m.ModelElement): @@ -46,9 +50,7 @@ class ReqIFElement(m.ModelElement): identifier = m.StringPOD("ReqIFIdentifier") long_name = m.StringPOD("ReqIFLongName") - description: str = m.StringPOD( # type: ignore[assignment] - "ReqIFDescription" - ) + description: str = m.StringPOD("ReqIFDescription") # type: ignore[assignment] name = m.StringPOD("ReqIFName") prefix = m.StringPOD("ReqIFPrefix") type: m.Accessor = property(lambda _: None) # type: ignore[assignment] @@ -82,26 +84,24 @@ def _short_html_(self) -> markupsafe.Markup: return helpers.make_short_html(type(self).__name__, self.uuid, name) -@m.xtype_handler(None) class DataTypeDefinition(ReqIFElement): """A data type definition for requirement types.""" _xmltag = "ownedDefinitionTypes" -@m.xtype_handler(None) class AttributeDefinition(ReqIFElement): """An attribute definition for requirement types.""" _xmltag = "ownedAttributes" - data_type = m.Association(DataTypeDefinition, "definitionType") + data_type = m.Single(m.Association(DataTypeDefinition, "definitionType")) -class AbstractRequirementsAttribute(m.ModelElement): +class AbstractRequirementsAttribute(m.ModelElement, abstract=True): _xmltag = "ownedAttributes" - definition = m.Association(AttributeDefinition, "definition") + definition = m.Single(m.Association(AttributeDefinition, "definition")) value: t.Any @@ -155,14 +155,12 @@ def _match_xtype( raise ValueError(f"Invalid type hint given: {type_!r}") from None -@m.xtype_handler(None) class BooleanValueAttribute(AbstractRequirementsAttribute): """A string value attribute.""" value = m.BoolPOD("value") -@m.xtype_handler(None) class DateValueAttribute(AbstractRequirementsAttribute): """A value attribute that stores a date and time.""" @@ -174,30 +172,25 @@ def _repr_value(self) -> str: return self.value.isoformat() -@m.xtype_handler(None) class IntegerValueAttribute(AbstractRequirementsAttribute): """An integer value attribute.""" value = m.IntPOD("value") -@m.xtype_handler(None) class RealValueAttribute(AbstractRequirementsAttribute): """A floating-point number value attribute.""" value = m.FloatPOD("value") -@m.xtype_handler(None) class StringValueAttribute(AbstractRequirementsAttribute): """A string value attribute.""" value = m.StringPOD("value") -@m.xtype_handler(None) -@m.attr_equal("long_name") -class EnumValue(ReqIFElement): +class EnumValue(ReqIFElement, eq="long_name"): """An enumeration value for :class:`.EnumerationDataTypeDefinition`.""" _xmltag = "specifiedValues" @@ -206,35 +199,29 @@ def __str__(self) -> str: return self.long_name -@m.xtype_handler(None) -class EnumerationDataTypeDefinition(ReqIFElement): +class EnumerationDataTypeDefinition(DataTypeDefinition): """An enumeration data type definition for requirement types.""" - _xmltag = "ownedDefinitionTypes" - values = m.DirectProxyAccessor( EnumValue, aslist=m.ElementList, single_attr="long_name" ) -@m.xtype_handler(None) -class AttributeDefinitionEnumeration(ReqIFElement): +class AttributeDefinitionEnumeration(AttributeDefinition): """An enumeration attribute definition for requirement types.""" - _xmltag = "ownedAttributes" - - data_type = m.Association(EnumerationDataTypeDefinition, "definitionType") + data_type = m.Single( + m.Association(EnumerationDataTypeDefinition, "definitionType") + ) multi_valued = m.BoolPOD("multiValued") """Whether to allow setting multiple values on this attribute.""" -@m.xtype_handler(None) class EnumerationValueAttribute(AbstractRequirementsAttribute): """An enumeration attribute.""" - definition = m.Association( - AttributeDefinitionEnumeration, # type: ignore[arg-type] - "definition", + definition = m.Single( + m.Association(AttributeDefinitionEnumeration, "definition") ) values = m.Association(EnumValue, "values", aslist=m.ElementList) @@ -251,9 +238,8 @@ def _repr_value(self) -> str: return repr([i.long_name for i in self.values]) -@m.attr_equal("long_name") -class AbstractType(ReqIFElement): - owner = m.ParentAccessor(m.ModelElement) +class AbstractType(ReqIFElement, eq="long_name"): + owner = m.ParentAccessor() attribute_definitions = m.DirectProxyAccessor( m.ModelElement, (AttributeDefinition, AttributeDefinitionEnumeration), @@ -261,46 +247,41 @@ class AbstractType(ReqIFElement): ) -@m.xtype_handler(None) class ModuleType(AbstractType): """A requirement-module type.""" _xmltag = "ownedTypes" -@m.xtype_handler(None) class RelationType(AbstractType): """A requirement-relation type.""" _xmltag = "ownedTypes" -@m.xtype_handler(None) class RequirementType(AbstractType): """A requirement type.""" _xmltag = "ownedTypes" -@m.xtype_handler(None) class Requirement(ReqIFElement): """A ReqIF Requirement.""" _xmltag = "ownedRequirements" - owner = m.ParentAccessor(m.ModelElement) + owner = m.ParentAccessor() chapter_name = m.StringPOD("ReqIFChapterName") foreign_id = m.IntPOD("ReqIFForeignID") text = m.HTMLStringPOD("ReqIFText") attributes = AttributeAccessor() - type = m.Association(RequirementType, "requirementType") + type = m.Single(m.Association(RequirementType, "requirementType")) - relations: m.Accessor[AbstractRequirementsRelation] - related: m.Accessor[m.ModelElement] + relations: m.Accessor[m.ElementList[AbstractRequirementsRelation]] + related: m.Accessor[m.ElementList[AbstractRequirementsRelation]] -@m.xtype_handler(None) class Folder(Requirement): """A folder that stores Requirements.""" @@ -313,9 +294,9 @@ class Folder(Requirement): class AbstractRequirementsRelation(ReqIFElement): _required_attrs = frozenset({"source", "target"}) - type = m.Association(RelationType, "relationType") - source = m.Association(Requirement, "source") - target = m.Association(m.ModelElement, "target") + type = m.Single(m.Association(RelationType, "relationType")) + source = m.Single(m.Association(Requirement, "source")) + target = m.Single(m.Association(m.ModelElement, "target")) def _short_repr_(self) -> str: direction = "" @@ -329,7 +310,6 @@ def _short_repr_(self) -> str: ) -@m.xtype_handler(None) class InternalRelation(AbstractRequirementsRelation): """A Relation between two requirements.""" diff --git a/capellambse/extensions/reqif/exporter.py b/capellambse/extensions/reqif/exporter.py index 22c53b347..ed219caa5 100644 --- a/capellambse/extensions/reqif/exporter.py +++ b/capellambse/extensions/reqif/exporter.py @@ -221,7 +221,7 @@ def _build_datatypes( if isinstance(attrdef, rq.AttributeDefinitionEnumeration): values = etree.Element("SPECIFIED-VALUES") - for i in attrdef.data_type.values: + for i in getattr(attrdef.data_type, "values", ()): v = etree.Element("ENUM-VALUE") v.set("IDENTIFIER", "_" + i.uuid.upper()) v.set("LAST-CHANGE", timestamp) diff --git a/capellambse/extensions/validation/__init__.py b/capellambse/extensions/validation/__init__.py index 3edb9ba51..b1adb28c9 100644 --- a/capellambse/extensions/validation/__init__.py +++ b/capellambse/extensions/validation/__init__.py @@ -27,14 +27,6 @@ def init() -> None: lambda self: self.validation.validate ) - m.set_accessor( - m.ModelElement, "validation", m.AlternateAccessor(ElementValidation) - ) - m.ModelElement.validate = property( # type: ignore[attr-defined] - lambda self: self.validation.validate - ) - m.set_accessor( - cs.ComponentArchitecture, - "validation", - m.AlternateAccessor(LayerValidation), - ) + m.ModelElement.validation = m.AlternateAccessor(ElementValidation) + m.ModelElement.validate = property(lambda self: self.validation.validate) + cs.ComponentArchitecture.validation = m.AlternateAccessor(LayerValidation) diff --git a/capellambse/loader/core.py b/capellambse/loader/core.py index 682d6715e..f34095218 100644 --- a/capellambse/loader/core.py +++ b/capellambse/loader/core.py @@ -9,6 +9,7 @@ "FragmentType", "MelodyLoader", "ModelFile", + "qtype_of", ] import collections @@ -55,6 +56,7 @@ ) VALID_EXTS = VISUAL_EXTS | SEMANTIC_EXTS | {".afm"} +TAG_XMI = etree.QName(_n.NAMESPACES["xmi"], "XMI") IDTYPES = frozenset({"id", "uid", "xmi:id"}) IDTYPES_RESOLVED = frozenset(helpers.resolve_namespace(t) for t in IDTYPES) IDTYPES_PER_FILETYPE: t.Final[dict[str, frozenset]] = { @@ -158,6 +160,28 @@ def _round_version(v: str, prec: int) -> str: return v[:pos] + re.sub(r"[^.]+", "0", v[pos:]) +def qtype_of(element: etree._Element) -> etree.QName | None: + """Get the qualified type of the element.""" + parent = element.getparent() + if parent is None or ( + parent.getparent() is None and parent.tag == TAG_XMI + ): + return etree.QName(element) + + xtype = element.get(helpers.ATT_XT) + if not xtype or ":" not in xtype: + xtype = element.get(helpers.ATT_XMT) + if not xtype or ":" not in xtype: + return None + nsalias, clsname = xtype.rsplit(":", 1) + try: + nsuri = element.nsmap[nsalias] + except KeyError: + LOGGER.error("Namespace %r not found on element %r", nsalias, element) + return None + return etree.QName(nsuri, clsname) + + class FragmentType(enum.Enum): """The type of an XML fragment.""" @@ -187,6 +211,7 @@ def __missing__(self, key: str) -> t.NoReturn: class ModelFile: """Represents a single file in the model (i.e. a fragment).""" + __qtypecache: dict[etree.QName, dict[int, etree._Element]] __xtypecache: dict[str, dict[int, etree._Element]] __idcache: dict[str, etree._Element | None] __hrefsources: dict[str, etree._Element] @@ -237,6 +262,9 @@ def idcache_index(self, subtree: etree._Element) -> None: xtype = helpers.xtype_of(elm) if xtype is not None: self.__xtypecache[xtype][id(elm)] = elm + qtype = qtype_of(elm) + if qtype is not None: + self.__qtypecache[qtype][id(elm)] = elm for idtype in idtypes: elm_id = elm.get(idtype, None) @@ -269,6 +297,9 @@ def idcache_remove(self, source: str | etree._Element) -> None: xtype = helpers.xtype_of(elm) if xtype: del self.__xtypecache[xtype][id(elm)] + qtype = qtype_of(elm) + if qtype is not None: + del self.__qtypecache[qtype][id(elm)] for idtype in IDTYPES_RESOLVED: elm_id = elm.get(idtype, None) if elm_id is None: @@ -283,6 +314,7 @@ def idcache_remove(self, source: str | etree._Element) -> None: def idcache_rebuild(self) -> None: """Invalidate and rebuild this file's ID cache.""" LOGGER.debug("Indexing file %s...", self.filename) + self.__qtypecache = collections.defaultdict(dict) self.__xtypecache = collections.defaultdict(dict) self.__idcache = {} self.__hrefsources = {} @@ -293,6 +325,35 @@ def idcache_reserve(self, new_id: str) -> None: """Reserve the given ID for an element to be inserted later.""" self.__idcache[new_id] = None + def add_namespace(self, uri: str, alias: str) -> str: + """Add the given namespace to the root of this fragment. + + If a namespace with the same URI is already registered, no + changes will be made. + + If the alias is already in use for a namespace with a different + URI, a numeric suffix will be added to it. + + This method returns the actual alias in use for the namespace, + after applying the aforementioned rules. + """ + new_nsmap = dict(self.root.nsmap) + for k, v in new_nsmap.items(): + if v == uri: + assert k is not None + return k + + if alias in new_nsmap: + for count in range(1, sys.maxsize): + alternative = f"{alias}_{count}" + if alternative not in new_nsmap: + alias = alternative + break + + new_nsmap[alias] = uri + self.__replace_nsmap(new_nsmap) + return alias + def update_namespaces(self, viewpoints: cabc.Mapping[str, str]) -> None: """Update the current namespace map. @@ -339,7 +400,9 @@ def update_namespaces(self, viewpoints: cabc.Mapping[str, str]) -> None: assert new_nsmap.get(ns) in (None, uri) new_nsmap[ns] = uri + self.__replace_nsmap(new_nsmap) + def __replace_nsmap(self, new_nsmap: dict[str | None, str]) -> None: assert new_nsmap LOGGER.debug("New nsmap: %s", new_nsmap) if self.root.nsmap == new_nsmap: @@ -370,6 +433,21 @@ def iterall_xt( if xtype in xtypes: yield from elms.values() + def iterall(self) -> cabc.Iterator[etree._Element]: + """Iterate over all elements in this tree.""" + for qt in self.iter_qtypes(): + yield from self.iter_qtype(qt) + + def iter_qtypes(self) -> cabc.Iterator[etree.QName]: + """Iterate over all qualified types used in this fragment.""" + for qtype, elems in self.__qtypecache.items(): + if elems: + yield qtype + + def iter_qtype(self, qtype: etree.QName) -> cabc.Iterator[etree._Element]: + """Iterate over all elements of the given qualified type.""" + yield from self.__qtypecache.get(qtype, {}).values() + def write_xml( self, file: t.BinaryIO, @@ -997,6 +1075,26 @@ def iterancestors( if element.tag in tagset: yield element + def iterchildren( + self, element: etree._Element, *tags: str + ) -> cabc.Iterator[etree._Element]: + """Iterate over the element's children. + + This method will follow links into different fragment files and + yield those elements as if they were direct children. + + Parameters + ---------- + element + The parent element under which to search for children. + tags + Only consider children which have one of these tags. + """ + tagset = self._nonempty_hashset(tags) + for child in element.iterchildren(): + if child.tag in tagset: + yield self._follow_href(child) + def iterchildren_xt( self, element: etree._Element, *xtypes: str ) -> cabc.Iterator[etree._Element]: diff --git a/capellambse/metamodel/__init__.py b/capellambse/metamodel/__init__.py index fc52f7801..e18c93ae6 100644 --- a/capellambse/metamodel/__init__.py +++ b/capellambse/metamodel/__init__.py @@ -1,41 +1,40 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG # SPDX-License-Identifier: Apache-2.0 +"""Metamodel definitions for Capella models.""" + +from . import activity as activity +from . import behavior as behavior from . import capellacommon as capellacommon from . import capellacore as capellacore from . import capellamodeller as capellamodeller from . import cs as cs +from . import epbs as epbs from . import fa as fa +from . import information as information from . import interaction as interaction from . import la as la +from . import libraries as libraries from . import modellingcore as modellingcore from . import oa as oa from . import pa as pa +from . import pa_deployment as pa_deployment from . import sa as sa import capellambse.model as m # isort: skip -m.set_accessor( - capellacommon.State, - "functions", - m.Backref( - ( - oa.OperationalActivity, - sa.SystemFunction, - la.LogicalFunction, - pa.PhysicalFunction, - ), - "available_in_states", - aslist=m.ElementList, +capellacommon.State.functions = m.Backref( + ( + oa.OperationalActivity, + sa.SystemFunction, + la.LogicalFunction, + pa.PhysicalFunction, ), + "available_in_states", ) -m.set_accessor( - m.ModelElement, - "property_value_packages", - m.DirectProxyAccessor( - capellacore.PropertyValuePkg, - aslist=m.ElementList, - mapkey="name", - ), +m.ModelElement.property_value_packages = m.DirectProxyAccessor( + capellacore.PropertyValuePkg, + aslist=m.ElementList, + mapkey="name", ) del m diff --git a/capellambse/metamodel/activity.py b/capellambse/metamodel/activity.py new file mode 100644 index 000000000..574023428 --- /dev/null +++ b/capellambse/metamodel/activity.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from . import namespaces as ns + +NS = ns.ACTIVITY diff --git a/capellambse/metamodel/behavior.py b/capellambse/metamodel/behavior.py new file mode 100644 index 000000000..d7ddadd6c --- /dev/null +++ b/capellambse/metamodel/behavior.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from . import namespaces as ns + +NS = ns.BEHAVIOR diff --git a/capellambse/metamodel/capellacommon.py b/capellambse/metamodel/capellacommon.py index c3eb3e371..1b0cc7144 100644 --- a/capellambse/metamodel/capellacommon.py +++ b/capellambse/metamodel/capellacommon.py @@ -2,9 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 """Classes handling Mode/State-Machines and related content.""" +from __future__ import annotations + +import typing as t + import capellambse.model as m from . import capellacore, modellingcore +from . import namespaces as ns + +NS = ns.CAPELLACOMMON class AbstractStateRealization(m.ModelElement): ... @@ -16,7 +23,6 @@ class TransfoLink(m.ModelElement): ... class CapabilityRealizationInvolvement(m.ModelElement): ... -@m.xtype_handler(None) class Region(m.ModelElement): """A region inside a state machine or state/mode.""" @@ -35,15 +41,12 @@ class AbstractStateMode(m.ModelElement): regions = m.DirectProxyAccessor(Region, aslist=m.ElementList) -@m.xtype_handler(None) class State(AbstractStateMode): """A state.""" - entries = m.Association(m.ModelElement, "entry", aslist=m.MixedElementList) - do_activity = m.Association( - m.ModelElement, "doActivity", aslist=m.MixedElementList - ) - exits = m.Association(m.ModelElement, "exit", aslist=m.MixedElementList) + entries = m.Association(m.ModelElement, "entry") + do_activity = m.Association(m.ModelElement, "doActivity") + exits = m.Association(m.ModelElement, "exit") incoming_transitions = m.Accessor outgoing_transitions = m.Accessor @@ -51,47 +54,38 @@ class State(AbstractStateMode): functions: m.Accessor -@m.xtype_handler(None) class Mode(AbstractStateMode): """A mode.""" -@m.xtype_handler(None) class DeepHistoryPseudoState(AbstractStateMode): """A deep history pseudo state.""" -@m.xtype_handler(None) class FinalState(AbstractStateMode): """A final state.""" -@m.xtype_handler(None) class ForkPseudoState(AbstractStateMode): """A fork pseudo state.""" -@m.xtype_handler(None) class InitialPseudoState(AbstractStateMode): """An initial pseudo state.""" -@m.xtype_handler(None) class JoinPseudoState(AbstractStateMode): """A join pseudo state.""" -@m.xtype_handler(None) class ShallowHistoryPseudoState(AbstractStateMode): """A shallow history pseudo state.""" -@m.xtype_handler(None) class TerminatePseudoState(AbstractStateMode): """A terminate pseudo state.""" -@m.xtype_handler(None) class StateMachine(m.ModelElement): """A state machine.""" @@ -100,24 +94,19 @@ class StateMachine(m.ModelElement): regions = m.DirectProxyAccessor(Region, aslist=m.ElementList) -@m.xtype_handler(None) class StateTransition(m.ModelElement): r"""A transition between :class:`State`\ s or :class:`Mode`\ s.""" _xmltag = "ownedTransitions" - source = m.Association(m.ModelElement, "source") - destination = m.Association(m.ModelElement, "target") - triggers = m.Association( - m.ModelElement, "triggers", aslist=m.MixedElementList - ) - effects = m.Association( - m.ModelElement, "effect", aslist=m.MixedElementList - ) - guard = m.Association(capellacore.Constraint, "guard") + source = m.Single(m.Association(m.ModelElement, "source")) + target = m.Single(m.Association(m.ModelElement, "target")) + destination = m.DeprecatedAccessor[t.Any]("target") + triggers = m.Association(m.ModelElement, "triggers") + effects = m.Association(m.ModelElement, "effect") + guard = m.Single(m.Association(capellacore.Constraint, "guard")) -@m.xtype_handler(None) class GenericTrace(modellingcore.TraceableElement): """A trace between two elements.""" @@ -131,15 +120,10 @@ def name(self) -> str: # type: ignore[override] return f"[{type(self).__name__}]{direction}" -m.set_accessor( - AbstractStateMode, - "realized_states", - m.Allocation( - None, # FIXME fill in tag - AbstractStateRealization, - aslist=m.ElementList, - attr="targetElement", - ), +AbstractStateMode.realized_states = m.Allocation( + None, # FIXME fill in tag + AbstractStateRealization, + attr="targetElement", ) for cls in [ State, @@ -152,11 +136,7 @@ def name(self) -> str: # type: ignore[override] ShallowHistoryPseudoState, TerminatePseudoState, ]: - m.set_accessor( - cls, - "realizing_states", - m.Backref(cls, "realized_states", aslist=m.ElementList), - ) + cls.realizing_states = m.Backref(cls, "realized_states") for cls in [ State, @@ -168,11 +148,8 @@ def name(self) -> str: # type: ignore[override] ShallowHistoryPseudoState, TerminatePseudoState, ]: - m.set_accessor( - cls, - "incoming_transitions", - m.Backref(StateTransition, "destination", aslist=m.ElementList), - ) + cls.incoming_transitions = m.Backref(StateTransition, "destination") + for cls in [ State, Mode, @@ -182,27 +159,13 @@ def name(self) -> str: # type: ignore[override] JoinPseudoState, ShallowHistoryPseudoState, ]: - m.set_accessor( - cls, - "outgoing_transitions", - m.Backref(StateTransition, "source", aslist=m.ElementList), - ) - -m.set_accessor( - Region, - "states", - m.Containment(AbstractStateMode._xmltag, aslist=m.ElementList), -) -m.set_accessor( - Region, "modes", m.DirectProxyAccessor(Mode, aslist=m.ElementList) -) -m.set_accessor( - Region, - "transitions", - m.DirectProxyAccessor(StateTransition, aslist=m.ElementList), + cls.outgoing_transitions = m.Backref(StateTransition, "source") + +Region.states = m.Containment(AbstractStateMode._xmltag) +Region.modes = m.DirectProxyAccessor(Mode, aslist=m.ElementList) +Region.transitions = m.DirectProxyAccessor( + StateTransition, aslist=m.ElementList ) -m.set_accessor( - m.ModelElement, - "traces", - m.DirectProxyAccessor(GenericTrace, aslist=m.ElementList), +m.ModelElement.traces = m.DirectProxyAccessor( + GenericTrace, aslist=m.ElementList ) diff --git a/capellambse/metamodel/capellacore.py b/capellambse/metamodel/capellacore.py index 65fd1bb3e..a1a6818d7 100644 --- a/capellambse/metamodel/capellacore.py +++ b/capellambse/metamodel/capellacore.py @@ -2,25 +2,25 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import typing as t + import capellambse.model as m +from . import namespaces as ns + +NS = ns.CAPELLACORE + -@m.xtype_handler(None) class Constraint(m.ModelElement): """A constraint.""" _xmltag = "ownedConstraints" - constrained_elements = m.Association( - m.ModelElement, - "constrainedElements", - aslist=m.MixedElementList, - ) + constrained_elements = m.Association(m.ModelElement, "constrainedElements") specification = m.SpecificationAccessor() -@m.xtype_handler(None) class Generalization(m.ModelElement): """A Generalization.""" @@ -29,14 +29,12 @@ class Generalization(m.ModelElement): super: m.Accessor -@m.xtype_handler(None) class EnumerationPropertyLiteral(m.ModelElement): """A Literal for EnumerationPropertyType.""" _xmltag = "ownedLiterals" -@m.xtype_handler(None) class EnumerationPropertyType(m.ModelElement): """An EnumerationPropertyType.""" @@ -56,46 +54,40 @@ class PropertyValue(m.ModelElement): EnumerationPropertyType, aslist=m.ElementList ) - value: m.BasePOD | m.Association + value: t.ClassVar[m.BasePOD | m.Single] -@m.xtype_handler(None) class BooleanPropertyValue(PropertyValue): """A boolean property value.""" value = m.BoolPOD("value") -@m.xtype_handler(None) class FloatPropertyValue(PropertyValue): """A floating point property value.""" value = m.FloatPOD("value") -@m.xtype_handler(None) class IntegerPropertyValue(PropertyValue): """An integer property value.""" value = m.IntPOD("value") -@m.xtype_handler(None) class StringPropertyValue(PropertyValue): """A string property value.""" value = m.StringPOD("value") -@m.xtype_handler(None) class EnumerationPropertyValue(PropertyValue): """An enumeration property value.""" - type = m.Association(EnumerationPropertyType, "type") - value = m.Association(EnumerationPropertyLiteral, "value") + type = m.Single(m.Association(EnumerationPropertyType, "type")) + value = m.Single(m.Association(EnumerationPropertyLiteral, "value")) -@m.xtype_handler(None) class PropertyValueGroup(m.ModelElement): """A group for PropertyValues.""" @@ -116,7 +108,6 @@ class PropertyValueGroup(m.ModelElement): ) -@m.xtype_handler(None) class PropertyValuePkg(m.ModelElement): """A Package for PropertyValues.""" @@ -125,9 +116,9 @@ class PropertyValuePkg(m.ModelElement): enumeration_property_types = m.DirectProxyAccessor( EnumerationPropertyType, aslist=m.ElementList ) - groups = m.DirectProxyAccessor( + groups = m.Containment["PropertyValueGroup"]( + "ownedPropertyValueGroups", PropertyValueGroup, - aslist=m.ElementList, mapkey="name", mapvalue="values", ) @@ -146,50 +137,36 @@ class PropertyValuePkg(m.ModelElement): ) -m.set_self_references( - (PropertyValuePkg, "packages"), +PropertyValuePkg.packages = m.DirectProxyAccessor( + PropertyValuePkg, aslist=m.ElementList ) -m.set_accessor( - m.ModelElement, - "constraints", - m.DirectProxyAccessor(Constraint, aslist=m.ElementList), +m.ModelElement.constraints = m.DirectProxyAccessor( + Constraint, aslist=m.ElementList ) -m.set_accessor( +m.ModelElement.property_values = m.DirectProxyAccessor( m.ModelElement, - "property_values", - m.DirectProxyAccessor( - m.ModelElement, - ( - BooleanPropertyValue, - EnumerationPropertyValue, - FloatPropertyValue, - IntegerPropertyValue, - StringPropertyValue, - ), - aslist=m.MixedElementList, - mapkey="name", - mapvalue="value", + ( + BooleanPropertyValue, + EnumerationPropertyValue, + FloatPropertyValue, + IntegerPropertyValue, + StringPropertyValue, ), + aslist=m.MixedElementList, + mapkey="name", + mapvalue="value", ) -m.set_accessor( - m.ModelElement, - "property_value_groups", - m.DirectProxyAccessor( - PropertyValueGroup, - aslist=m.ElementList, - mapkey="name", - mapvalue="values", - ), +m.ModelElement.property_value_groups = m.DirectProxyAccessor( + PropertyValueGroup, + aslist=m.ElementList, + mapkey="name", + mapvalue="values", ) -m.set_accessor( - m.ModelElement, - "applied_property_values", - m.Association(None, "appliedPropertyValues", aslist=m.ElementList), +m.ModelElement.applied_property_values = m.Association( + None, "appliedPropertyValues" ) -m.set_accessor( - m.ModelElement, - "applied_property_value_groups", - m.Association(None, "appliedPropertyValueGroups", aslist=m.ElementList), +m.ModelElement.applied_property_value_groups = m.Association( + None, "appliedPropertyValueGroups" ) diff --git a/capellambse/metamodel/capellamodeller.py b/capellambse/metamodel/capellamodeller.py index 648cfe147..97f35fcf8 100644 --- a/capellambse/metamodel/capellamodeller.py +++ b/capellambse/metamodel/capellamodeller.py @@ -4,10 +4,13 @@ import capellambse.model as m -from . import la, oa, pa, sa +from . import la +from . import namespaces as ns +from . import oa, pa, sa + +NS = ns.CAPELLAMODELLER -@m.xtype_handler(None) class SystemEngineering(m.ModelElement): """A system engineering element. @@ -28,9 +31,7 @@ class SystemEngineering(m.ModelElement): [source:MIL-STD 499B standard] """ - architectures = m.Containment( - "ownedArchitectures", m.ModelElement, aslist=m.ElementList - ) + architectures = m.Containment("ownedArchitectures", m.ModelElement) @property def oa(self) -> oa.OperationalAnalysis: @@ -85,11 +86,8 @@ def pa(self) -> pa.PhysicalArchitecture: ) from None -@m.xtype_handler(None) class Project(m.ModelElement): - model_roots = m.Containment( - "ownedModelRoots", SystemEngineering, aslist=m.ElementList - ) + model_roots = m.Containment("ownedModelRoots", SystemEngineering) @property def model_root(self) -> SystemEngineering: @@ -98,6 +96,5 @@ def model_root(self) -> SystemEngineering: return self.model_roots.create() -@m.xtype_handler(None) class Library(Project): """A project that is primarily intended as a library of components.""" diff --git a/capellambse/metamodel/cs.py b/capellambse/metamodel/cs.py index 6abeaafad..9ef6bff03 100644 --- a/capellambse/metamodel/cs.py +++ b/capellambse/metamodel/cs.py @@ -14,27 +14,27 @@ import capellambse.model as m from . import capellacommon, fa, information +from . import namespaces as ns + +NS = ns.CS -@m.xtype_handler(None) class Part(m.ModelElement): """A representation of a physical component.""" _xmltag = "ownedParts" - type = m.Association(m.ModelElement, "abstractType") + type = m.Single(m.Association(m.ModelElement, "abstractType")) deployed_parts: m.Accessor -@m.xtype_handler(None) class ExchangeItemAllocation(m.ModelElement): """An allocation of an ExchangeItem to an Interface.""" - item = m.Association(information.ExchangeItem, "allocatedItem") + item = m.Single(m.Association(information.ExchangeItem, "allocatedItem")) -@m.xtype_handler(None) class Interface(m.ModelElement): """An interface.""" @@ -43,7 +43,6 @@ class Interface(m.ModelElement): ) -@m.xtype_handler(None) class InterfacePkg(m.ModelElement): """A package that can hold interfaces and exchange items.""" @@ -55,17 +54,15 @@ class InterfacePkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class PhysicalPort(m.ModelElement): """A physical port.""" _xmltag = "ownedFeatures" - owner = m.ParentAccessor(m.ModelElement) + owner = m.ParentAccessor() links: m.Accessor -@m.xtype_handler(None) class PhysicalLink(PhysicalPort): """A physical link.""" @@ -75,7 +72,6 @@ class PhysicalLink(PhysicalPort): exchanges = m.Allocation[fa.ComponentExchange]( "ownedComponentExchangeAllocations", fa.ComponentExchangeAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -86,7 +82,6 @@ class PhysicalLink(PhysicalPort): target = m.IndexAccessor[PhysicalPort]("ends", 1) -@m.xtype_handler(None) class PhysicalPath(m.ModelElement): """A physical path.""" @@ -95,13 +90,11 @@ class PhysicalPath(m.ModelElement): involved_items = m.Allocation[m.ModelElement]( None, # FIXME fill in tag "org.polarsys.capella.core.data.cs:PhysicalPathInvolvement", - aslist=m.MixedElementList, attr="involved", ) exchanges = m.Allocation[fa.ComponentExchange]( None, # FIXME fill in tag "org.polarsys.capella.core.data.fa:ComponentExchangeAllocation", - aslist=m.ElementList, attr="targetElement", ) @@ -124,13 +117,13 @@ class Component(m.ModelElement): "ownedFeatures", m.ModelElement, aslist=m.ElementList ) - owner = m.ParentAccessor(m.ModelElement) + owner = m.ParentAccessor() state_machines = m.DirectProxyAccessor( capellacommon.StateMachine, aslist=m.ElementList ) ports = m.DirectProxyAccessor(fa.ComponentPort, aslist=m.ElementList) physical_ports = m.DirectProxyAccessor(PhysicalPort, aslist=m.ElementList) - parts = m.Backref(Part, "type", aslist=m.ElementList) + parts = m.Backref(Part, "type") physical_paths = m.DirectProxyAccessor(PhysicalPath, aslist=m.ElementList) physical_links = m.DirectProxyAccessor(PhysicalLink, aslist=m.ElementList) exchanges = m.DirectProxyAccessor( @@ -141,21 +134,16 @@ class Component(m.ModelElement): fa.ComponentExchange, "source.owner", "target.owner", - aslist=m.ElementList, ) realized_components = m.Allocation["Component"]( "ownedComponentRealizations", "org.polarsys.capella.core.data.cs:ComponentRealization", - aslist=m.ElementList, attr="targetElement", ) - realizing_components = m.Backref["Component"]( - (), "realized_components", aslist=m.ElementList - ) + realizing_components = m.Backref["Component"]((), "realized_components") -@m.xtype_handler(None) class ComponentRealization(m.ModelElement): """A realization that links to a component.""" @@ -185,39 +173,20 @@ class ComponentArchitecture(m.ModelElement): all_interfaces = m.DeepProxyAccessor(Interface, aslist=m.ElementList) -m.set_accessor( - InterfacePkg, - "packages", - m.DirectProxyAccessor(InterfacePkg, aslist=m.ElementList), -) -m.set_accessor( - Part, - "deployed_parts", - m.Allocation( - "ownedDeploymentLinks", - "org.polarsys.capella.core.data.pa.deployment:PartDeploymentLink", - aslist=m.ElementList, - attr="deployedElement", - backattr="location", - ), -) -m.set_accessor( - PhysicalPort, - "links", - m.Backref(PhysicalLink, "ends", aslist=m.ElementList), +InterfacePkg.packages = m.DirectProxyAccessor( + InterfacePkg, aslist=m.ElementList ) -m.set_accessor( - PhysicalLink, - "physical_paths", - m.Backref(PhysicalPath, "involved_items", aslist=m.ElementList), +Part.deployed_parts = m.Allocation( + "ownedDeploymentLinks", + "org.polarsys.capella.core.data.pa.deployment:PartDeploymentLink", + attr="deployedElement", + backattr="location", ) -m.set_accessor( - fa.ComponentExchange, - "allocating_physical_link", - m.Backref(PhysicalLink, "exchanges"), +PhysicalPort.links = m.Backref(PhysicalLink, "ends") +PhysicalLink.physical_paths = m.Backref(PhysicalPath, "involved_items") +fa.ComponentExchange.allocating_physical_link = m.Single( + m.Backref(PhysicalLink, "exchanges") ) -m.set_accessor( - fa.ComponentExchange, - "allocating_physical_paths", - m.Backref(PhysicalPath, "exchanges", aslist=m.ElementList), +fa.ComponentExchange.allocating_physical_paths = m.Backref( + PhysicalPath, "exchanges" ) diff --git a/capellambse/metamodel/epbs.py b/capellambse/metamodel/epbs.py new file mode 100644 index 000000000..dd4770f54 --- /dev/null +++ b/capellambse/metamodel/epbs.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from . import namespaces as ns + +NS = ns.EPBS diff --git a/capellambse/metamodel/fa.py b/capellambse/metamodel/fa.py index 63488d12c..c66f25d5f 100644 --- a/capellambse/metamodel/fa.py +++ b/capellambse/metamodel/fa.py @@ -16,6 +16,9 @@ import capellambse.model as m from . import capellacommon, capellacore, information, interaction, modeltypes +from . import namespaces as ns + +NS = ns.FA class ComponentExchangeAllocation(m.ModelElement): ... @@ -27,21 +30,18 @@ class ComponentExchangeFunctionalExchangeAllocation(m.ModelElement): ... class ComponentFunctionalAllocation(m.ModelElement): ... -@m.xtype_handler(None) class ExchangeCategory(m.ModelElement): _xmltag = "ownedCategories" exchanges: m.Association[FunctionalExchange] -@m.xtype_handler(None) class ComponentExchangeCategory(m.ModelElement): _xmltag = "ownedComponentExchangeCategories" exchanges: m.Association[ComponentExchange] -@m.xtype_handler(None) class ControlNode(m.ModelElement): """A node with a specific control-kind.""" @@ -50,7 +50,6 @@ class ControlNode(m.ModelElement): kind = m.EnumPOD("kind", modeltypes.ControlNodeKind, writable=False) -@m.xtype_handler(None) class FunctionRealization(m.ModelElement): """A realization that links to a function.""" @@ -60,52 +59,48 @@ class FunctionRealization(m.ModelElement): class AbstractExchange(m.ModelElement): """Common code for Exchanges.""" - source = m.Association(m.ModelElement, "source") - target = m.Association(m.ModelElement, "target") + source = m.Single(m.Association(m.ModelElement, "source")) + target = m.Single(m.Association(m.ModelElement, "target")) -@m.xtype_handler(None) class AbstractFunction(m.ModelElement): """An AbstractFunction.""" available_in_states = m.Association( - capellacommon.State, "availableInStates", aslist=m.ElementList + capellacommon.State, "availableInStates" ) scenarios = m.Backref[interaction.Scenario]( (), "related_functions", aslist=m.ElementList ) -@m.xtype_handler(None) class FunctionPort(m.ModelElement): """A function port.""" - owner = m.ParentAccessor(m.ModelElement) + owner = m.ParentAccessor() exchanges: m.Accessor state_machines = m.DirectProxyAccessor( capellacommon.StateMachine, aslist=m.ElementList ) -@m.xtype_handler(None) class FunctionInputPort(FunctionPort): """A function input port.""" _xmltag = "inputs" exchange_items = m.Association( - information.ExchangeItem, "incomingExchangeItems", aslist=m.ElementList + information.ExchangeItem, "incomingExchangeItems" ) -@m.xtype_handler(None) class FunctionOutputPort(FunctionPort): """A function output port.""" _xmltag = "outputs" exchange_items = m.Association( - information.ExchangeItem, "outgoingExchangeItems", aslist=m.ElementList + information.ExchangeItem, "outgoingExchangeItems" ) @@ -121,41 +116,36 @@ class Function(AbstractFunction): inputs = m.DirectProxyAccessor(FunctionInputPort, aslist=m.ElementList) outputs = m.DirectProxyAccessor(FunctionOutputPort, aslist=m.ElementList) - exchanges: m.Accessor[FunctionalExchange] + exchanges: m.Accessor[m.ElementList[FunctionalExchange]] functions: m.Accessor packages: m.Accessor - related_exchanges: m.Accessor[FunctionalExchange] + related_exchanges: m.Accessor[m.ElementList[FunctionalExchange]] realized_functions = m.Allocation[AbstractFunction]( "ownedFunctionRealizations", FunctionRealization, - aslist=m.MixedElementList, attr="targetElement", ) - realizing_functions = m.Backref[AbstractFunction]( - (), "realized_functions", aslist=m.MixedElementList - ) + realizing_functions = m.Backref[AbstractFunction]((), "realized_functions") -@m.xtype_handler(None) class FunctionalExchange(AbstractExchange): """A functional exchange.""" _xmltag = "ownedFunctionalExchanges" - exchange_items = m.Association( - information.ExchangeItem, "exchangedItems", aslist=m.ElementList - ) + exchange_items = m.Association(information.ExchangeItem, "exchangedItems") realized_functional_exchanges = m.Allocation["FunctionalExchange"]( "ownedFunctionalExchangeRealizations", "org.polarsys.capella.core.data.fa:FunctionalExchangeRealization", - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) - realizing_functional_exchanges: m.Accessor[FunctionalExchange] - categories = m.Backref(ExchangeCategory, "exchanges", aslist=m.ElementList) + realizing_functional_exchanges: m.Accessor[ + m.ElementList[FunctionalExchange] + ] + categories = m.Backref(ExchangeCategory, "exchanges") @property def owner(self) -> ComponentExchange | None: @@ -168,35 +158,35 @@ class FunctionalChainInvolvement(interaction.AbstractInvolvement): _xmltag = "ownedFunctionalChainInvolvements" -@m.xtype_handler(None) class FunctionalChainInvolvementLink(FunctionalChainInvolvement): """An element linking a FunctionalChain to an Exchange.""" - exchanged_items = m.Association( - information.ExchangeItem, "exchangedItems", aslist=m.ElementList + exchanged_items = m.Association(information.ExchangeItem, "exchangedItems") + exchange_context = m.Single( + m.Association(capellacore.Constraint, "exchangeContext") ) - exchange_context = m.Association(capellacore.Constraint, "exchangeContext") -@m.xtype_handler(None) class FunctionalChainInvolvementFunction(FunctionalChainInvolvement): """An element linking a FunctionalChain to a Function.""" -@m.xtype_handler(None) class FunctionalChainReference(FunctionalChainInvolvement): """An element linking two related functional chains together.""" -@m.xtype_handler(None) class FunctionalChain(m.ModelElement): """A functional chain.""" _xmltag = "ownedFunctionalChains" kind = m.EnumPOD("kind", modeltypes.FunctionalChainKind, default="SIMPLE") - precondition = m.Association(capellacore.Constraint, "preCondition") - postcondition = m.Association(capellacore.Constraint, "postCondition") + precondition = m.Single( + m.Association(capellacore.Constraint, "preCondition") + ) + postcondition = m.Single( + m.Association(capellacore.Constraint, "postCondition") + ) involvements = m.DirectProxyAccessor( m.ModelElement, @@ -210,57 +200,47 @@ class FunctionalChain(m.ModelElement): involved_functions = m.Allocation[AbstractFunction]( "ownedFunctionalChainInvolvements", FunctionalChainInvolvementFunction, - aslist=m.MixedElementList, attr="involved", ) involved_links = m.Allocation[AbstractExchange]( "ownedFunctionalChainInvolvements", FunctionalChainInvolvementLink, - aslist=m.MixedElementList, attr="involved", ) involved_chains = m.Allocation["FunctionalChain"]( "ownedFunctionalChainInvolvements", "org.polarsys.capella.core.data.fa:FunctionalChainReference", attr="involved", - aslist=m.ElementList, ) - involving_chains: m.Accessor[FunctionalChain] + involving_chains: m.Accessor[m.ElementList[FunctionalChain]] realized_chains = m.Allocation["FunctionalChain"]( "ownedFunctionalChainRealizations", "org.polarsys.capella.core.data.fa:FunctionalChainRealization", attr="targetElement", backattr="sourceElement", - aslist=m.ElementList, ) - realizing_chains: m.Accessor[FunctionalChain] + realizing_chains: m.Accessor[m.ElementList[FunctionalChain]] control_nodes = m.DirectProxyAccessor(ControlNode, aslist=m.ElementList) @property - def involved(self) -> m.MixedElementList: + def involved(self) -> m.ElementList[AbstractFunction]: return self.involved_functions + self.involved_links -@m.xtype_handler(None) class ComponentPort(m.ModelElement): """A component port.""" _xmltag = "ownedFeatures" direction = m.EnumPOD("orientation", modeltypes.OrientationPortKind) - owner = m.ParentAccessor(m.ModelElement) + owner = m.ParentAccessor() exchanges: m.Accessor - provided_interfaces = m.Association( - m.ModelElement, "providedInterfaces", aslist=m.ElementList - ) - required_interfaces = m.Association( - m.ModelElement, "requiredInterfaces", aslist=m.ElementList - ) + provided_interfaces = m.Association(m.ModelElement, "providedInterfaces") + required_interfaces = m.Association(m.ModelElement, "requiredInterfaces") -@m.xtype_handler(None) class ComponentExchange(AbstractExchange): """A functional component exchange.""" @@ -271,13 +251,10 @@ class ComponentExchange(AbstractExchange): allocated_functional_exchanges = m.Allocation[FunctionalExchange]( "ownedComponentExchangeFunctionalExchangeAllocations", ComponentExchangeFunctionalExchangeAllocation, - aslist=m.ElementList, attr="targetElement", ) allocated_exchange_items = m.Association( - information.ExchangeItem, - "convoyedInformations", - aslist=m.ElementList, + information.ExchangeItem, "convoyedInformations" ) categories = m.Backref(ComponentExchangeCategory, "exchanges") @@ -298,86 +275,47 @@ def exchange_items(self) -> m.ElementList[information.ExchangeItem]: (FunctionInputPort, FunctionalExchange), (FunctionOutputPort, FunctionalExchange), ]: - m.set_accessor( - _port, - "exchanges", - m.Backref(_exchange, "source", "target", aslist=m.ElementList), - ) + _port.exchanges = m.Backref(_exchange, "source", "target") del _port, _exchange -m.set_accessor( - ComponentExchange, - "realized_component_exchanges", - m.Allocation[ComponentExchange]( - "ownedComponentExchangeRealizations", - "org.polarsys.capella.core.data.fa:ComponentExchangeRealization", - aslist=m.ElementList, - attr="targetElement", - backattr="sourceElement", - ), +ComponentExchange.realized_component_exchanges = m.Allocation[ + ComponentExchange +]( + "ownedComponentExchangeRealizations", + "org.polarsys.capella.core.data.fa:ComponentExchangeRealization", + attr="targetElement", + backattr="sourceElement", ) -m.set_accessor( - ComponentExchange, - "realizing_component_exchanges", - m.Backref( - ComponentExchange, "realized_component_exchanges", aslist=m.ElementList - ), +ComponentExchange.realizing_component_exchanges = m.Backref( + ComponentExchange, "realized_component_exchanges" ) -m.set_accessor( - FunctionalExchange, - "allocating_component_exchange", - m.Backref(ComponentExchange, "allocated_functional_exchanges"), +FunctionalExchange.allocating_component_exchange = m.Single( + m.Backref(ComponentExchange, "allocated_functional_exchanges") ) -m.set_accessor( - FunctionalExchange, - "realizing_functional_exchanges", - m.Backref( - FunctionalExchange, - "realized_functional_exchanges", - aslist=m.ElementList, - ), +FunctionalExchange.realizing_functional_exchanges = m.Backref( + FunctionalExchange, "realized_functional_exchanges" ) -m.set_accessor( - FunctionalExchange, - "involving_functional_chains", - m.Backref(FunctionalChain, "involved_links", aslist=m.ElementList), +FunctionalExchange.involving_functional_chains = m.Backref( + FunctionalChain, "involved_links" ) -m.set_accessor( - FunctionalChain, - "involving_chains", - m.Backref(FunctionalChain, "involved_chains", aslist=m.ElementList), +FunctionalChain.involving_chains = m.Backref( + FunctionalChain, "involved_chains" ) -m.set_accessor( - FunctionalChain, - "realizing_chains", - m.Backref(FunctionalChain, "realized_chains", aslist=m.ElementList), +FunctionalChain.realizing_chains = m.Backref( + FunctionalChain, "realized_chains" ) -m.set_accessor( - Function, - "exchanges", - m.DirectProxyAccessor(FunctionalExchange, aslist=m.ElementList), +Function.exchanges = m.DirectProxyAccessor( + FunctionalExchange, aslist=m.ElementList ) -m.set_accessor( - Function, - "related_exchanges", - m.Backref( - FunctionalExchange, - "source.owner", - "target.owner", - aslist=m.ElementList, - ), +Function.related_exchanges = m.Backref( + FunctionalExchange, "source.owner", "target.owner" ) -m.set_accessor( - information.ExchangeItem, - "exchanges", - m.Backref( - (ComponentExchange, FunctionalExchange), - "exchange_items", - "allocated_exchange_items", - aslist=m.ElementList, - ), +information.ExchangeItem.exchanges = m.Backref( + (ComponentExchange, FunctionalExchange), + "exchange_items", + "allocated_exchange_items", ) ExchangeCategory.exchanges = m.Association( FunctionalExchange, "exchanges", aslist=m.ElementList diff --git a/capellambse/metamodel/information/__init__.py b/capellambse/metamodel/information/__init__.py index 7a9854f2b..0e44569c4 100644 --- a/capellambse/metamodel/information/__init__.py +++ b/capellambse/metamodel/information/__init__.py @@ -13,27 +13,30 @@ from __future__ import annotations +import typing as t + import capellambse.model as m from .. import capellacommon, capellacore, modellingcore, modeltypes +from .. import namespaces as ns from . import datatype, datavalue +NS = ns.INFORMATION + -@m.xtype_handler(None) class Unit(m.ModelElement): """Unit.""" _xmltag = "ownedUnits" -@m.xtype_handler(None) class Association(m.ModelElement): """An Association.""" _xmltag = "ownedAssociations" - members: m.Accessor[Property] - navigable_members: m.Accessor[Property] + members: m.Accessor[m.ElementList[Property]] + navigable_members: m.Accessor[m.ElementList[Property]] @property def roles(self) -> m.ElementList[Property]: @@ -43,14 +46,12 @@ def roles(self) -> m.ElementList[Property]: return m.ElementList(self._model, roles, Property) -@m.xtype_handler(None) class PortAllocation(modellingcore.TraceableElement): """An exchange between a ComponentPort and FunctionalPort.""" _xmltag = "ownedPortAllocations" -@m.xtype_handler(None) class Property(m.ModelElement): """A Property of a Class.""" @@ -76,17 +77,16 @@ class Property(m.ModelElement): kind = m.EnumPOD( "aggregationKind", modeltypes.AggregationKind, default="UNSET" ) - type = m.Association(m.ModelElement, "abstractType") - default_value = m.Containment("ownedDefaultValue") - min_value = m.Containment("ownedMinValue") - max_value = m.Containment("ownedMaxValue") - null_value = m.Containment("ownedNullValue") - min_card = m.Containment("ownedMinCard") - max_card = m.Containment("ownedMaxCard") - association = m.Backref(Association, "roles") + type = m.Single(m.Association(m.ModelElement, "abstractType")) + default_value = m.Single[t.Any](m.Containment("ownedDefaultValue")) + min_value = m.Single[t.Any](m.Containment("ownedMinValue")) + max_value = m.Single[t.Any](m.Containment("ownedMaxValue")) + null_value = m.Single[t.Any](m.Containment("ownedNullValue")) + min_card = m.Single[t.Any](m.Containment("ownedMinCard")) + max_card = m.Single[t.Any](m.Containment("ownedMaxCard")) + association = m.Single[t.Any](m.Backref(Association, "roles")) -@m.xtype_handler(None) class Class(m.ModelElement): """A Class.""" @@ -121,14 +121,12 @@ def properties(self) -> m.ElementList[Property]: ) -@m.xtype_handler(None) class InformationRealization(modellingcore.TraceableElement): """A realization for a Class.""" _xmltag = "ownedInformationRealizations" -@m.xtype_handler(None) class Union(Class): """A Union.""" @@ -137,7 +135,6 @@ class Union(Class): kind = m.EnumPOD("kind", modeltypes.UnionKind, default="UNION") -@m.xtype_handler(None) class Collection(m.ModelElement): """A Collection.""" @@ -149,7 +146,6 @@ class Collection(m.ModelElement): super: m.Accessor[Collection] -@m.xtype_handler(None) class DataPkg(m.ModelElement): """A data package that can hold classes.""" @@ -181,20 +177,18 @@ class DataPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class ExchangeItemElement(m.ModelElement): """An ExchangeItemElement (proxy link).""" _xmltag = "ownedElements" - abstract_type = m.Association(m.ModelElement, "abstractType") - owner = m.ParentAccessor["ExchangeItem"]() + abstract_type = m.Single(m.Association(m.ModelElement, "abstractType")) + owner = m.ParentAccessor() - min_card = m.Containment("ownedMinCard") - max_card = m.Containment("ownedMaxCard") + min_card = m.Single[t.Any](m.Containment("ownedMinCard")) + max_card = m.Single[t.Any](m.Containment("ownedMaxCard")) -@m.xtype_handler(None) class ExchangeItem(m.ModelElement): """An item that can be exchanged on an Exchange.""" @@ -204,22 +198,17 @@ class ExchangeItem(m.ModelElement): "exchangeMechanism", modeltypes.ExchangeMechanism, default="UNSET" ) elements = m.DirectProxyAccessor(ExchangeItemElement, aslist=m.ElementList) - exchanges: m.Accessor[m.ModelElement] + exchanges: m.Accessor[m.ElementList[m.ModelElement]] instances: m.Containment -@m.xtype_handler(None) class ExchangeItemInstance(Property): pass -m.set_accessor( - capellacore.Generalization, "super", m.Association(None, "super") -) +capellacore.Generalization.super = m.Single(m.Association(None, "super")) for cls in [Class, Union, datatype.Enumeration, Collection]: - m.set_accessor( - cls, - "super", + cls.super = m.Single( m.Allocation( "ownedGeneralizations", capellacore.Generalization, @@ -227,51 +216,20 @@ class ExchangeItemInstance(Property): backattr="sub", ), ) - m.set_accessor( - cls, - "sub", - m.Backref(cls, "super", aslist=m.MixedElementList), - ) - -m.set_accessor( - DataPkg, "packages", m.DirectProxyAccessor(DataPkg, aslist=m.ElementList) -) -m.set_accessor( - Association, - "members", - m.Containment("ownedMembers", aslist=m.ElementList), -) -m.set_accessor( - Association, - "navigable_members", - m.Association(Property, "navigableMembers", aslist=m.ElementList), -) -m.set_accessor( - Class, - "realized_classes", - m.Allocation[Class]( - "ownedInformationRealizations", - InformationRealization, - aslist=m.ElementList, - attr="targetElement", - ), -) -m.set_accessor( - Class, - "realizations", - m.DirectProxyAccessor(InformationRealization, aslist=m.ElementList), + cls.sub = m.Backref(cls, "super") + +DataPkg.packages = m.DirectProxyAccessor(DataPkg, aslist=m.ElementList) +Association.members = m.Containment("ownedMembers") +Association.navigable_members = m.Association(Property, "navigableMembers") +Class.realized_classes = m.Allocation( + "ownedInformationRealizations", + InformationRealization, + attr="targetElement", ) -m.set_accessor( - Class, - "realized_by", - m.Backref(Class, "realized_classes", aslist=m.ElementList), +Class.realizations = m.DirectProxyAccessor( + InformationRealization, aslist=m.ElementList ) -m.set_accessor( - ExchangeItem, - "instances", - m.Containment( - "ownedExchangeItemInstances", - ExchangeItemInstance, - aslist=m.ElementList, - ), +Class.realized_by = m.Backref(Class, "realized_classes") +ExchangeItem.instances = m.Containment( + "ownedExchangeItemInstances", ExchangeItemInstance ) diff --git a/capellambse/metamodel/information/communication.py b/capellambse/metamodel/information/communication.py new file mode 100644 index 000000000..1d4419bad --- /dev/null +++ b/capellambse/metamodel/information/communication.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from .. import namespaces as ns + +NS = ns.INFORMATION_COMMUNICATION diff --git a/capellambse/metamodel/information/datatype.py b/capellambse/metamodel/information/datatype.py index a4430574d..769a8e7e6 100644 --- a/capellambse/metamodel/information/datatype.py +++ b/capellambse/metamodel/information/datatype.py @@ -2,11 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import typing as t + import capellambse.model as m from .. import modeltypes +from .. import namespaces as ns from . import datavalue +NS = ns.INFORMATION_DATATYPE + class DataType(m.ModelElement): _xmltag = "ownedDataTypes" @@ -22,21 +27,19 @@ class DataType(m.ModelElement): ) -@m.xtype_handler(None) class BooleanType(DataType): literals = m.DirectProxyAccessor( datavalue.LiteralBooleanValue, aslist=m.ElementList, fixed_length=2, ) - default = m.Containment("ownedDefaultValue") + default = m.Single[t.Any](m.Containment("ownedDefaultValue")) -@m.xtype_handler(None) class Enumeration(DataType): """An Enumeration.""" - domain_type = m.Association(m.ModelElement, "domainType") + domain_type = m.Single(m.Association(m.ModelElement, "domainType")) owned_literals = m.DirectProxyAccessor( datavalue.EnumerationLiteral, aslist=m.ElementList ) @@ -54,23 +57,20 @@ def literals(self) -> m.ElementList[datavalue.EnumerationLiteral]: ) -@m.xtype_handler(None) class StringType(DataType): - default_value = m.Containment("ownedDefaultValue") - null_value = m.Containment("ownedNullValue") - min_length = m.Containment("ownedMinLength") - max_length = m.Containment("ownedMaxLength") + default_value = m.Single[t.Any](m.Containment("ownedDefaultValue")) + null_value = m.Single[t.Any](m.Containment("ownedNullValue")) + min_length = m.Single[t.Any](m.Containment("ownedMinLength")) + max_length = m.Single[t.Any](m.Containment("ownedMaxLength")) -@m.xtype_handler(None) class NumericType(DataType): kind = m.EnumPOD("kind", modeltypes.NumericTypeKind, default="INTEGER") - default_value = m.Containment("ownedDefaultValue") - null_value = m.Containment("ownedNullValue") - min_value = m.Containment("ownedMinValue") - max_value = m.Containment("ownedMaxValue") + default_value = m.Single[t.Any](m.Containment("ownedDefaultValue")) + null_value = m.Single[t.Any](m.Containment("ownedNullValue")) + min_value = m.Single[t.Any](m.Containment("ownedMinValue")) + max_value = m.Single[t.Any](m.Containment("ownedMaxValue")) -@m.xtype_handler(None) class PhysicalQuantity(NumericType): - unit = m.Containment("ownedUnit") + unit = m.Single[t.Any](m.Containment("ownedUnit")) diff --git a/capellambse/metamodel/information/datavalue.py b/capellambse/metamodel/information/datavalue.py index 1ca52122b..0cf716bef 100644 --- a/capellambse/metamodel/information/datavalue.py +++ b/capellambse/metamodel/information/datavalue.py @@ -1,9 +1,14 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG # SPDX-License-Identifier: Apache-2.0 +import typing as t + import capellambse.model as m +from .. import namespaces as ns + +NS = ns.INFORMATION_DATAVALUE + -@m.xtype_handler(None) class LiteralBooleanValue(m.ModelElement): """A Literal Boolean Value.""" @@ -16,54 +21,49 @@ class LiteralValue(m.ModelElement): is_abstract = m.BoolPOD("abstract") """Indicates if property is abstract.""" value = m.StringPOD("value") - type = m.Association(m.ModelElement, "abstractType") + type = m.Single(m.Association(m.ModelElement, "abstractType")) -@m.xtype_handler(None) class LiteralNumericValue(LiteralValue): value = m.StringPOD("value") - unit = m.Association(m.ModelElement, "unit") + unit = m.Single(m.Association(m.ModelElement, "unit")) -@m.xtype_handler(None) class LiteralStringValue(LiteralValue): """A Literal String Value.""" -@m.xtype_handler(None) class ValuePart(m.ModelElement): """A Value Part of a Complex Value.""" _xmltag = "ownedParts" - referenced_property = m.Association(m.ModelElement, "referencedProperty") - value = m.Containment("ownedValue") + referenced_property = m.Single( + m.Association(m.ModelElement, "referencedProperty") + ) + value = m.Single[t.Any](m.Containment("ownedValue")) -@m.xtype_handler(None) class ComplexValue(m.ModelElement): """A Complex Value.""" _xmltag = "ownedDataValues" - type = m.Association(m.ModelElement, "abstractType") + type = m.Single(m.Association(m.ModelElement, "abstractType")) value_parts = m.DirectProxyAccessor(ValuePart, aslist=m.ElementList) -@m.attr_equal("name") -@m.xtype_handler(None) -class EnumerationLiteral(m.ModelElement): +class EnumerationLiteral(m.ModelElement, eq="name"): _xmltag = "ownedLiterals" - value = m.Containment("domainValue") + value = m.Single[t.Any](m.Containment("domainValue")) - owner = m.ParentAccessor["_dt.Enumeration"]() + owner = m.ParentAccessor() -@m.xtype_handler(None) class EnumerationReference(m.ModelElement): - type = m.Association(m.ModelElement, "abstractType") - value = m.Association(m.ModelElement, "referencedValue") + type = m.Single(m.Association(m.ModelElement, "abstractType")) + value = m.Single(m.Association(m.ModelElement, "referencedValue")) from . import datatype as _dt # noqa: F401 diff --git a/capellambse/metamodel/interaction.py b/capellambse/metamodel/interaction.py index b80f12117..7faf8d4bf 100644 --- a/capellambse/metamodel/interaction.py +++ b/capellambse/metamodel/interaction.py @@ -2,9 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations +import typing as t + import capellambse.model as m from . import capellacore +from . import namespaces as ns + +NS = ns.INTERACTION class FunctionalChainAbstractCapabilityInvolvement(m.ModelElement): ... @@ -13,44 +18,41 @@ class FunctionalChainAbstractCapabilityInvolvement(m.ModelElement): ... class AbstractCapabilityRealization(m.ModelElement): ... -@m.xtype_handler(None) class Execution(m.ModelElement): """An execution.""" - start = m.Association(m.ModelElement, "start") - finish = m.Association(m.ModelElement, "finish") + start = m.Single(m.Association(m.ModelElement, "start")) + finish = m.Single(m.Association(m.ModelElement, "finish")) -@m.xtype_handler(None) class StateFragment(Execution): """A state fragment.""" - function = m.Association(m.ModelElement, "relatedAbstractFunction") + function = m.Single( + m.Association(m.ModelElement, "relatedAbstractFunction") + ) -@m.xtype_handler(None) class CombinedFragment(Execution): """A combined fragment.""" operator = m.StringPOD("operator") - operands = m.Association( - m.ModelElement, "referencedOperands", aslist=m.ElementList - ) + operands = m.Association(m.ModelElement, "referencedOperands") -@m.xtype_handler(None) class InstanceRole(m.ModelElement): """An instance role.""" - instance = m.Association[m.ModelElement](None, "representedInstance") + instance = m.Single( + m.Association[m.ModelElement](None, "representedInstance") + ) -@m.xtype_handler(None) class SequenceMessage(m.ModelElement): """A sequence message.""" - source = m.Association(m.ModelElement, "sendingEnd") - target = m.Association(m.ModelElement, "receivingEnd") + source = m.Single(m.Association(m.ModelElement, "sendingEnd")) + target = m.Single(m.Association(m.ModelElement, "receivingEnd")) class Event(m.ModelElement): @@ -60,25 +62,21 @@ class Event(m.ModelElement): class EventOperation(Event): """Abstract super class for events about operations.""" - operation = m.Association(m.ModelElement, "operation") + operation = m.Single(m.Association(m.ModelElement, "operation")) -@m.xtype_handler(None) class ExecutionEvent(Event): """An execution event.""" -@m.xtype_handler(None) class EventSentOperation(EventOperation): """An event-sent operation.""" -@m.xtype_handler(None) class EventReceiptOperation(EventOperation): """An event-receipt operation.""" -@m.xtype_handler(None) class Scenario(m.ModelElement): """A scenario that holds instance roles.""" @@ -86,19 +84,20 @@ class Scenario(m.ModelElement): InstanceRole, aslist=m.ElementList ) messages = m.DirectProxyAccessor(SequenceMessage, aslist=m.ElementList) - events = m.Containment("ownedEvents", aslist=m.MixedElementList) - fragments = m.Containment( - "ownedInteractionFragments", aslist=m.MixedElementList + events = m.Containment[t.Any]("ownedEvents") + fragments = m.Containment[t.Any]("ownedInteractionFragments") + time_lapses = m.Containment[t.Any]("ownedTimeLapses") + postcondition = m.Single( + m.Association(capellacore.Constraint, "postCondition") + ) + precondition = m.Single( + m.Association(capellacore.Constraint, "preCondition") ) - time_lapses = m.Containment("ownedTimeLapses", aslist=m.MixedElementList) - postcondition = m.Association(capellacore.Constraint, "postCondition") - precondition = m.Association(capellacore.Constraint, "preCondition") realized_scenarios = m.Allocation["Scenario"]( "ownedScenarioRealization", "org.polarsys.capella.core.data.interaction:ScenarioRealization", attr="targetElement", backattr="sourceElement", - aslist=m.ElementList, ) realizing_scenarios: m.Backref[Scenario] @@ -110,88 +109,79 @@ def related_functions(self) -> m.ElementList[fa.AbstractFunction]: class InteractionFragment(m.ModelElement): """Abstract super class of all interaction fragments in a Scenario.""" - covered = m.Association[m.ModelElement]( - None, "coveredInstanceRoles", aslist=m.MixedElementList - ) + covered = m.Association[m.ModelElement](None, "coveredInstanceRoles") -@m.xtype_handler(None) class ExecutionEnd(InteractionFragment): """An end for an execution.""" - event = m.Association[Event](None, "event") + event = m.Single(m.Association[Event](None, "event")) -@m.xtype_handler(None) class FragmentEnd(InteractionFragment): """An end for a fragment.""" -@m.xtype_handler(None) class InteractionOperand(InteractionFragment): """An interaction-operand.""" - guard = m.Association(capellacore.Constraint, "guard") + guard = m.Single(m.Association(capellacore.Constraint, "guard")) -@m.xtype_handler(None) class InteractionState(InteractionFragment): """An interaction-state.""" - state = m.Association(m.ModelElement, "relatedAbstractState") - function = m.Association(m.ModelElement, "relatedAbstractFunction") + state = m.Single(m.Association(m.ModelElement, "relatedAbstractState")) + function = m.Single( + m.Association(m.ModelElement, "relatedAbstractFunction") + ) -@m.xtype_handler(None) class MessageEnd(InteractionFragment): """A message-end.""" - event = m.Association[Event](None, "event") + event = m.Single(m.Association[Event](None, "event")) class Exchange(m.ModelElement): """An abstract Exchange.""" - source = m.ParentAccessor(m.ModelElement) + source = m.ParentAccessor() -@m.xtype_handler(None) class AbstractCapabilityExtend(Exchange): """An AbstractCapabilityExtend.""" _xmltag = "extends" - source = m.ParentAccessor(m.ModelElement) - target = m.Association(m.ModelElement, "extended") + source = m.ParentAccessor() + target = m.Single(m.Association(m.ModelElement, "extended")) -@m.xtype_handler(None) class AbstractCapabilityInclude(Exchange): """An AbstractCapabilityInclude.""" _xmltag = "includes" - source = m.ParentAccessor(m.ModelElement) - target = m.Association(m.ModelElement, "included") + source = m.ParentAccessor() + target = m.Single(m.Association(m.ModelElement, "included")) -@m.xtype_handler(None) class AbstractCapabilityGeneralization(Exchange): """An AbstractCapabilityGeneralization.""" _xmltag = "superGeneralizations" - source = m.ParentAccessor(m.ModelElement) - target = m.Association(m.ModelElement, "super") + source = m.ParentAccessor() + target = m.Single(m.Association(m.ModelElement, "super")) class AbstractInvolvement(m.ModelElement): """An abstract Involvement.""" - source = m.ParentAccessor(m.ModelElement) - target = m.Association(m.ModelElement, "involved") - - involved = m.Association(m.ModelElement, "involved") + source = m.ParentAccessor() + target = m.Alias[t.Any]("involved") + involved = m.Single(m.Association(m.ModelElement, "involved")) @property def name(self) -> str: # type: ignore[override] @@ -203,14 +193,11 @@ def name(self) -> str: # type: ignore[override] return f"[{self.__class__.__name__}]{direction}" -@m.xtype_handler(None) class AbstractFunctionAbstractCapabilityInvolvement(AbstractInvolvement): """An abstract CapabilityInvolvement linking to SystemFunctions.""" -Scenario.realizing_scenarios = m.Backref( - Scenario, "realized_scenarios", aslist=m.ElementList -) +Scenario.realizing_scenarios = m.Backref(Scenario, "realized_scenarios") from . import fa diff --git a/capellambse/metamodel/la.py b/capellambse/metamodel/la.py index 0b1252b72..367cac898 100644 --- a/capellambse/metamodel/la.py +++ b/capellambse/metamodel/la.py @@ -9,28 +9,28 @@ from capellambse import model as m -from . import capellacommon, capellacore, cs, fa, interaction, sa +from . import capellacommon, capellacore, cs, fa, interaction +from . import namespaces as ns +from . import sa + +NS = ns.LA -@m.xtype_handler(None) class LogicalFunction(fa.Function): """A logical function on the Logical Architecture layer.""" realized_system_functions = m.TypecastAccessor( sa.SystemFunction, "realized_functions" ) - owner: m.Accessor[LogicalComponent] + owner: m.Single[LogicalComponent] -@m.xtype_handler(None) class LogicalFunctionPkg(m.ModelElement): """A logical function package.""" _xmltag = "ownedFunctionPkg" - functions = m.Containment( - "ownedLogicalFunctions", LogicalFunction, aslist=m.ElementList - ) + functions = m.Containment("ownedLogicalFunctions", LogicalFunction) packages: m.Accessor categories = m.DirectProxyAccessor( @@ -38,7 +38,6 @@ class LogicalFunctionPkg(m.ModelElement): ) -@m.xtype_handler(None) class LogicalComponent(cs.Component): """A logical component on the Logical Architecture layer.""" @@ -47,7 +46,6 @@ class LogicalComponent(cs.Component): allocated_functions = m.Allocation[LogicalFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -59,7 +57,6 @@ class LogicalComponent(cs.Component): components: m.Accessor -@m.xtype_handler(None) class LogicalComponentPkg(m.ModelElement): """A logical component package.""" @@ -79,7 +76,6 @@ class LogicalComponentPkg(m.ModelElement): ) -@m.xtype_handler(None) class CapabilityRealization(m.ModelElement): """A capability.""" @@ -91,41 +87,38 @@ class CapabilityRealization(m.ModelElement): involved_functions = m.Allocation[LogicalFunction]( "ownedAbstractFunctionAbstractCapabilityInvolvements", interaction.AbstractFunctionAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) involved_chains = m.Allocation[fa.FunctionalChain]( "ownedFunctionalChainAbstractCapabilityInvolvements", interaction.FunctionalChainAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) involved_components = m.Allocation[LogicalComponent]( "ownedCapabilityRealizationInvolvements", capellacommon.CapabilityRealizationInvolvement, - aslist=m.MixedElementList, attr="involved", ) realized_capabilities = m.Allocation[sa.Capability]( "ownedAbstractCapabilityRealizations", interaction.AbstractCapabilityRealization, - aslist=m.ElementList, attr="targetElement", ) - postcondition = m.Association(capellacore.Constraint, "postCondition") - precondition = m.Association(capellacore.Constraint, "preCondition") + postcondition = m.Single( + m.Association(capellacore.Constraint, "postCondition") + ) + precondition = m.Single( + m.Association(capellacore.Constraint, "preCondition") + ) scenarios = m.DirectProxyAccessor( interaction.Scenario, aslist=m.ElementList ) - states = m.Association( - capellacommon.State, "availableInStates", aslist=m.ElementList - ) + states = m.Association(capellacommon.State, "availableInStates") packages: m.Accessor -@m.xtype_handler(None) class CapabilityRealizationPkg(m.ModelElement): """A capability package that can hold capabilities.""" @@ -138,7 +131,6 @@ class CapabilityRealizationPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class LogicalArchitecture(cs.ComponentArchitecture): """Provides access to the LogicalArchitecture layer of the model.""" @@ -163,8 +155,10 @@ class LogicalArchitecture(cs.ComponentArchitecture): all_capabilities = m.DeepProxyAccessor( CapabilityRealization, aslist=m.ElementList ) - all_components = ( # maybe this should exclude .is_actor - m.DeepProxyAccessor(LogicalComponent, aslist=m.ElementList) + all_components = ( + m.DeepProxyAccessor( # maybe this should exclude .is_actor + LogicalComponent, aslist=m.ElementList + ) ) all_actors = property( lambda self: self._model.search(LogicalComponent).by_is_actor(True) @@ -198,48 +192,33 @@ class LogicalArchitecture(cs.ComponentArchitecture): ) -m.set_accessor( - sa.Capability, - "realizing_capabilities", - m.Backref( - CapabilityRealization, "realized_capabilities", aslist=m.ElementList - ), +sa.Capability.realizing_capabilities = m.Backref( + CapabilityRealization, "realized_capabilities" ) -m.set_accessor( - sa.SystemComponent, - "realizing_logical_components", - m.Backref(LogicalComponent, "realized_components", aslist=m.ElementList), +sa.SystemComponent.realizing_logical_components = m.Backref( + LogicalComponent, "realized_components" ) -m.set_accessor( - sa.SystemFunction, - "realizing_logical_functions", - m.Backref( - LogicalFunction, "realized_system_functions", aslist=m.ElementList - ), +sa.SystemFunction.realizing_logical_functions = m.Backref( + LogicalFunction, "realized_system_functions" ) -m.set_accessor( - LogicalFunction, - "owner", - m.Backref(LogicalComponent, "allocated_functions"), +LogicalFunction.owner = m.Single( + m.Backref(LogicalComponent, "allocated_functions") ) -m.set_accessor( - LogicalFunction, - "packages", - m.DirectProxyAccessor( - LogicalFunctionPkg, - aslist=m.ElementList, - ), +LogicalFunction.packages = m.DirectProxyAccessor( + LogicalFunctionPkg, aslist=m.ElementList +) +LogicalFunction.involved_in = m.Backref( + CapabilityRealization, "involved_functions" +) +LogicalComponent.components = m.DirectProxyAccessor( + LogicalComponent, aslist=m.ElementList +) +LogicalComponentPkg.packages = m.DirectProxyAccessor( + LogicalComponentPkg, aslist=m.ElementList ) -m.set_accessor( - LogicalFunction, - "involved_in", - m.Backref( - CapabilityRealization, "involved_functions", aslist=m.ElementList - ), +LogicalFunction.functions = m.DirectProxyAccessor( + LogicalFunction, aslist=m.ElementList ) -m.set_self_references( - (LogicalComponent, "components"), - (LogicalComponentPkg, "packages"), - (LogicalFunction, "functions"), - (LogicalFunctionPkg, "packages"), +LogicalFunctionPkg.packages = m.DirectProxyAccessor( + LogicalFunctionPkg, aslist=m.ElementList ) diff --git a/capellambse/metamodel/libraries.py b/capellambse/metamodel/libraries.py new file mode 100644 index 000000000..9c44bd83c --- /dev/null +++ b/capellambse/metamodel/libraries.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from . import namespaces as ns + +NS = ns.LIBRARIES diff --git a/capellambse/metamodel/modellingcore.py b/capellambse/metamodel/modellingcore.py index f5378b467..8d2aae10f 100644 --- a/capellambse/metamodel/modellingcore.py +++ b/capellambse/metamodel/modellingcore.py @@ -1,15 +1,19 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG # SPDX-License-Identifier: Apache-2.0 -"""Abstract classes acting as templates for concrete classes. +from __future__ import annotations -These base classes are used between different layers. -""" +import capellambse.model as m -from capellambse import model as m +from . import namespaces as ns + +NS = ns.MODELLINGCORE + + +ModelElement = m._obj.ModelElement class TraceableElement(m.ModelElement): """A template for traceable ModelObjects.""" - source = m.Association(m.ModelElement, attr="sourceElement") - target = m.Association(m.ModelElement, attr="targetElement") + source = m.Single(m.Association(m.ModelElement, attr="sourceElement")) + target = m.Single(m.Association(m.ModelElement, attr="targetElement")) diff --git a/capellambse/metamodel/namespaces.py b/capellambse/metamodel/namespaces.py new file mode 100644 index 000000000..788070cf7 --- /dev/null +++ b/capellambse/metamodel/namespaces.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +"""Common namespace definitions for the Capella metamodel.""" + +import capellambse.model as m + +ACTIVITY = m.Namespace( + "http://www.polarsys.org/capella/common/activity/{VERSION}", + "org.polarsys.capella.common.data.activity", + m.CORE_VIEWPOINT, + "7.0.0", +) +BEHAVIOR = m.Namespace( + "http://www.polarsys.org/capella/common/behavior/{VERSION}", + "org.polarsys.capella.common.data.behavior", + m.CORE_VIEWPOINT, + "7.0.0", +) +CAPELLACOMMON = m.Namespace( + "http://www.polarsys.org/capella/core/common/{VERSION}", + "org.polarsys.capella.core.data.capellacommon", + m.CORE_VIEWPOINT, + "7.0.0", +) +CAPELLACORE = m.Namespace( + "http://www.polarsys.org/capella/core/core/{VERSION}", + "org.polarsys.capella.core.data.capellacore", + m.CORE_VIEWPOINT, + "7.0.0", +) +CAPELLAMODELLER = m.Namespace( + "http://www.polarsys.org/capella/core/modeller/{VERSION}", + "org.polarsys.capella.core.data.capellamodeller", + m.CORE_VIEWPOINT, + "7.0.0", +) +CS = m.Namespace( + "http://www.polarsys.org/capella/core/cs/{VERSION}", + "org.polarsys.capella.core.data.cs", + m.CORE_VIEWPOINT, + "7.0.0", +) +EPBS = m.Namespace( + "http://www.polarsys.org/capella/core/epbs/{VERSION}", + "org.polarsys.capella.core.data.epbs", + m.CORE_VIEWPOINT, + "7.0.0", +) +FA = m.Namespace( + "http://www.polarsys.org/capella/core/fa/{VERSION}", + "org.polarsys.capella.core.data.fa", + m.CORE_VIEWPOINT, + "7.0.0", +) +INFORMATION = m.Namespace( + "http://www.polarsys.org/capella/core/information/{VERSION}", + "org.polarsys.capella.core.data.information", + m.CORE_VIEWPOINT, + "7.0.0", +) +INFORMATION_COMMUNICATION = m.Namespace( + "http://www.polarsys.org/capella/core/information/communication/{VERSION}", + "org.polarsys.capella.core.data.information.communication", + m.CORE_VIEWPOINT, + "7.0.0", +) +INFORMATION_DATATYPE = m.Namespace( + "http://www.polarsys.org/capella/core/information/datatype/{VERSION}", + "org.polarsys.capella.core.data.information.datatype", + m.CORE_VIEWPOINT, + "7.0.0", +) +INFORMATION_DATAVALUE = m.Namespace( + "http://www.polarsys.org/capella/core/information/datavalue/{VERSION}", + "org.polarsys.capella.core.data.information.datavalue", + m.CORE_VIEWPOINT, + "7.0.0", +) +INTERACTION = m.Namespace( + "http://www.polarsys.org/capella/core/interaction/{VERSION}", + "org.polarsys.capella.core.data.interaction", + m.CORE_VIEWPOINT, + "7.0.0", +) +MODELLINGCORE = m._obj.NS +LA = m.Namespace( + "http://www.polarsys.org/capella/core/la/{VERSION}", + "org.polarsys.capella.core.data.la", + m.CORE_VIEWPOINT, + "7.0.0", +) +LIBRARIES = m.Namespace( + "http://www.polarsys.org/capella/common/libraries/{VERSION}", + "libraries", + m.CORE_VIEWPOINT, + "7.0.0", +) +OA = m.Namespace( + "http://www.polarsys.org/capella/core/oa/{VERSION}", + "org.polarsys.capella.core.data.oa", + m.CORE_VIEWPOINT, + "7.0.0", +) +PA = m.Namespace( + "http://www.polarsys.org/capella/core/pa/{VERSION}", + "org.polarsys.capella.core.data.pa", + m.CORE_VIEWPOINT, + "7.0.0", +) +PA_DEPLOYMENT = m.Namespace( + "http://www.polarsys.org/capella/core/pa/deployment/{VERSION}", + "org.polarsys.capella.core.data.pa.deployment", + m.CORE_VIEWPOINT, + "7.0.0", +) +SA = m.Namespace( + "http://www.polarsys.org/capella/core/ctx/{VERSION}", + "org.polarsys.capella.core.data.ctx", + m.CORE_VIEWPOINT, + "7.0.0", +) diff --git a/capellambse/metamodel/oa.py b/capellambse/metamodel/oa.py index cd02af21f..90af5967b 100644 --- a/capellambse/metamodel/oa.py +++ b/capellambse/metamodel/oa.py @@ -10,9 +10,11 @@ from capellambse import model as m from . import capellacommon, capellacore, cs, fa, information, interaction +from . import namespaces as ns + +NS = ns.OA -@m.xtype_handler(None) class OperationalActivity(fa.AbstractFunction): """An operational activity.""" @@ -22,10 +24,10 @@ class OperationalActivity(fa.AbstractFunction): fa.FunctionalExchange, aslist=m.ElementList ) - inputs = m.Backref(fa.FunctionalExchange, "target", aslist=m.ElementList) - outputs = m.Backref(fa.FunctionalExchange, "source", aslist=m.ElementList) + inputs = m.Backref(fa.FunctionalExchange, "target") + outputs = m.Backref(fa.FunctionalExchange, "source") - owner: m.Accessor[Entity] + owner: m.Single[Entity] @property def related_exchanges(self) -> m.ElementList[fa.FunctionalExchange]: @@ -38,17 +40,14 @@ def related_exchanges(self) -> m.ElementList[fa.FunctionalExchange]: return self.inputs._newlist(exchanges) -@m.xtype_handler(None) class OperationalProcess(fa.FunctionalChain): """An operational process.""" -@m.xtype_handler(None) class EntityOperationalCapabilityInvolvement(interaction.AbstractInvolvement): """An EntityOperationalCapabilityInvolvement.""" -@m.xtype_handler(None) class OperationalCapability(m.ModelElement): """A capability in the OperationalAnalysis layer.""" @@ -57,15 +56,11 @@ class OperationalCapability(m.ModelElement): extends = m.DirectProxyAccessor( interaction.AbstractCapabilityExtend, aslist=m.ElementList ) - extended_by = m.Backref( - interaction.AbstractCapabilityExtend, "target", aslist=m.ElementList - ) + extended_by = m.Backref(interaction.AbstractCapabilityExtend, "target") includes = m.DirectProxyAccessor( interaction.AbstractCapabilityInclude, aslist=m.ElementList ) - included_by = m.Backref( - interaction.AbstractCapabilityInclude, "target", aslist=m.ElementList - ) + included_by = m.Backref(interaction.AbstractCapabilityInclude, "target") generalizes = m.DirectProxyAccessor( interaction.AbstractCapabilityGeneralization, aslist=m.ElementList ) @@ -77,13 +72,11 @@ class OperationalCapability(m.ModelElement): involved_activities = m.Allocation[OperationalActivity]( "ownedAbstractFunctionAbstractCapabilityInvolvements", interaction.AbstractFunctionAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) involved_entities = m.Allocation[m.ModelElement]( "ownedEntityOperationalCapabilityInvolvements", EntityOperationalCapabilityInvolvement, - aslist=m.MixedElementList, attr="involved", ) entity_involvements = m.DirectProxyAccessor( @@ -92,26 +85,26 @@ class OperationalCapability(m.ModelElement): involved_processes = m.Allocation[OperationalProcess]( "ownedFunctionalChainAbstractCapabilityInvolvements", interaction.FunctionalChainAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) owned_processes = m.DirectProxyAccessor( OperationalProcess, aslist=m.ElementList ) - postcondition = m.Association(capellacore.Constraint, "postCondition") - precondition = m.Association(capellacore.Constraint, "preCondition") + postcondition = m.Single( + m.Association(capellacore.Constraint, "postCondition") + ) + precondition = m.Single( + m.Association(capellacore.Constraint, "preCondition") + ) scenarios = m.DirectProxyAccessor( interaction.Scenario, aslist=m.ElementList ) - states = m.Association( - capellacommon.State, "availableInStates", aslist=m.ElementList - ) + states = m.Association(capellacommon.State, "availableInStates") packages: m.Accessor -@m.xtype_handler(None) class OperationalCapabilityPkg(m.ModelElement): """A package that holds operational capabilities.""" @@ -130,16 +123,12 @@ class AbstractEntity(cs.Component): activities = m.Allocation[OperationalActivity]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) - capabilities = m.Backref( - OperationalCapability, "involved_entities", aslist=m.ElementList - ) + capabilities = m.Backref(OperationalCapability, "involved_entities") -@m.xtype_handler(None) class Entity(AbstractEntity): """An Entity in the OperationalAnalysis layer.""" @@ -156,7 +145,6 @@ def outputs(self) -> m.ElementList[CommunicationMean]: return self._model.search(CommunicationMean).by_source(self) -@m.xtype_handler(None) class OperationalActivityPkg(m.ModelElement): """A package that holds operational entities.""" @@ -169,7 +157,6 @@ class OperationalActivityPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class CommunicationMean(fa.AbstractExchange): """An operational entity exchange.""" @@ -178,19 +165,16 @@ class CommunicationMean(fa.AbstractExchange): allocated_interactions = m.Allocation[fa.FunctionalExchange]( None, # FIXME fill in tag fa.ComponentExchangeFunctionalExchangeAllocation, - aslist=m.ElementList, attr="targetElement", ) allocated_exchange_items = m.Association( information.ExchangeItem, "convoyedInformations", - aslist=m.ElementList, ) exchange_items = fa.ComponentExchange.exchange_items -@m.xtype_handler(None) class EntityPkg(m.ModelElement): """A package that holds operational entities.""" @@ -205,7 +189,6 @@ class EntityPkg(m.ModelElement): exchanges = m.DirectProxyAccessor(CommunicationMean, aslist=m.ElementList) -@m.xtype_handler(None) class OperationalAnalysis(cs.ComponentArchitecture): """Provides access to the OperationalAnalysis layer of the model.""" @@ -256,30 +239,23 @@ class OperationalAnalysis(cs.ComponentArchitecture): ) -m.set_accessor( - OperationalActivity, - "packages", - m.DirectProxyAccessor(OperationalActivityPkg, aslist=m.ElementList), +OperationalActivity.packages = m.DirectProxyAccessor( + OperationalActivityPkg, aslist=m.ElementList ) -m.set_accessor( - OperationalActivity, - "owner", - m.Backref(Entity, "activities"), +OperationalActivity.owner = m.Single(m.Backref(Entity, "activities")) +Entity.exchanges = m.DirectProxyAccessor( + CommunicationMean, # type: ignore[arg-type] # FIXME + aslist=m.ElementList, ) -m.set_accessor( - Entity, - "exchanges", - m.DirectProxyAccessor(CommunicationMean, aslist=m.ElementList), +Entity.related_exchanges = m.Backref(CommunicationMean, "source", "target") # type: ignore[arg-type] # FIXME +OperationalActivity.activities = m.DirectProxyAccessor( + OperationalActivity, aslist=m.ElementList ) -m.set_accessor( - Entity, - "related_exchanges", - m.Backref(CommunicationMean, "source", "target", aslist=m.ElementList), +OperationalActivityPkg.packages = m.DirectProxyAccessor( + OperationalActivityPkg, aslist=m.ElementList ) -m.set_self_references( - (OperationalActivity, "activities"), - (OperationalActivityPkg, "packages"), - (OperationalCapabilityPkg, "packages"), - (Entity, "entities"), - (EntityPkg, "packages"), +OperationalCapabilityPkg.packages = m.DirectProxyAccessor( + OperationalCapabilityPkg, aslist=m.ElementList ) +Entity.entities = m.DirectProxyAccessor(Entity, aslist=m.ElementList) +EntityPkg.packages = m.DirectProxyAccessor(EntityPkg, aslist=m.ElementList) diff --git a/capellambse/metamodel/pa.py b/capellambse/metamodel/pa.py index 8534f9244..45e1c62ec 100644 --- a/capellambse/metamodel/pa.py +++ b/capellambse/metamodel/pa.py @@ -10,27 +10,26 @@ import capellambse.model as m from . import capellacommon, cs, fa, la, modeltypes +from . import namespaces as ns + +NS = ns.PA -@m.xtype_handler(None) class PhysicalFunction(fa.Function): """A physical function on the Physical Architecture layer.""" - owner: m.Accessor[PhysicalComponent] + owner: m.Single[PhysicalComponent] realized_logical_functions = m.TypecastAccessor( la.LogicalFunction, "realized_functions" ) -@m.xtype_handler(None) class PhysicalFunctionPkg(m.ModelElement): """A logical component package.""" _xmltag = "ownedFunctionPkg" - functions = m.Containment( - "ownedPhysicalFunctions", PhysicalFunction, aslist=m.ElementList - ) + functions = m.Containment("ownedPhysicalFunctions", PhysicalFunction) packages: m.Accessor categories = m.DirectProxyAccessor( @@ -38,7 +37,6 @@ class PhysicalFunctionPkg(m.ModelElement): ) -@m.xtype_handler(None) class PhysicalComponent(cs.Component): """A physical component on the Physical Architecture layer.""" @@ -52,7 +50,6 @@ class PhysicalComponent(cs.Component): allocated_functions = m.Allocation[PhysicalFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -80,7 +77,6 @@ def components(self) -> m.ElementList[PhysicalComponent]: return self.deployed_components + self.owned_components -@m.xtype_handler(None) class PhysicalComponentPkg(m.ModelElement): """A logical component package.""" @@ -100,7 +96,6 @@ class PhysicalComponentPkg(m.ModelElement): ) -@m.xtype_handler(None) class PhysicalArchitecture(cs.ComponentArchitecture): """Provides access to the Physical Architecture layer of the model.""" @@ -165,38 +160,30 @@ class PhysicalArchitecture(cs.ComponentArchitecture): ) -m.set_accessor( - la.LogicalComponent, - "realizing_physical_components", - m.Backref( - PhysicalComponent, "realized_logical_components", aslist=m.ElementList - ), +la.LogicalComponent.realizing_physical_components = m.Backref( + PhysicalComponent, "realized_logical_components" +) +la.LogicalFunction.realizing_physical_functions = m.Backref( + PhysicalFunction, "realized_logical_functions" +) +PhysicalComponent.deploying_components = m.Backref( + PhysicalComponent, "deployed_components" +) +PhysicalFunction.owner = m.Single( + m.Backref(PhysicalComponent, "allocated_functions") ) -m.set_accessor( - la.LogicalFunction, - "realizing_physical_functions", - m.Backref( - PhysicalFunction, "realized_logical_functions", aslist=m.ElementList - ), +PhysicalFunction.packages = m.DirectProxyAccessor( + PhysicalFunctionPkg, aslist=m.ElementList ) -m.set_accessor( - PhysicalComponent, - "deploying_components", - m.Backref(PhysicalComponent, "deployed_components", aslist=m.ElementList), +PhysicalComponent.owned_components = m.DirectProxyAccessor( + PhysicalComponent, aslist=m.ElementList ) -m.set_accessor( - PhysicalFunction, - "owner", - m.Backref(PhysicalComponent, "allocated_functions"), +PhysicalComponentPkg.packages = m.DirectProxyAccessor( + PhysicalComponentPkg, aslist=m.ElementList ) -m.set_accessor( - PhysicalFunction, - "packages", - m.DirectProxyAccessor(PhysicalFunctionPkg, aslist=m.ElementList), +PhysicalFunction.functions = m.DirectProxyAccessor( + PhysicalFunction, aslist=m.ElementList ) -m.set_self_references( - (PhysicalComponent, "owned_components"), - (PhysicalComponentPkg, "packages"), - (PhysicalFunction, "functions"), - (PhysicalFunctionPkg, "packages"), +PhysicalFunctionPkg.packages = m.DirectProxyAccessor( + PhysicalFunctionPkg, aslist=m.ElementList ) diff --git a/capellambse/metamodel/pa_deployment.py b/capellambse/metamodel/pa_deployment.py new file mode 100644 index 000000000..7a37c5595 --- /dev/null +++ b/capellambse/metamodel/pa_deployment.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from . import namespaces as ns + +NS = ns.PA_DEPLOYMENT diff --git a/capellambse/metamodel/sa.py b/capellambse/metamodel/sa.py index 6449b425f..be1fe1148 100644 --- a/capellambse/metamodel/sa.py +++ b/capellambse/metamodel/sa.py @@ -10,10 +10,13 @@ import capellambse.model as m -from . import capellacommon, capellacore, cs, fa, interaction, oa +from . import capellacommon, capellacore, cs, fa, interaction +from . import namespaces as ns +from . import oa + +NS = ns.SA -@m.xtype_handler(None) class SystemFunction(fa.Function): """A system function.""" @@ -24,22 +27,18 @@ class SystemFunction(fa.Function): owner: m.Accessor -@m.xtype_handler(None) class SystemFunctionPkg(m.ModelElement): """A function package that can hold functions.""" _xmltag = "ownedFunctionPkg" - functions = m.Containment( - "ownedSystemFunctions", SystemFunction, aslist=m.ElementList - ) + functions = m.Containment("ownedSystemFunctions", SystemFunction) packages: m.Accessor categories = m.DirectProxyAccessor( fa.ExchangeCategory, aslist=m.ElementList ) -@m.xtype_handler(None) class SystemComponent(cs.Component): """A system component.""" @@ -48,7 +47,6 @@ class SystemComponent(cs.Component): allocated_functions = m.Allocation[SystemFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -62,7 +60,6 @@ class SystemComponent(cs.Component): ) -@m.xtype_handler(None) class SystemComponentPkg(m.ModelElement): """A system component package.""" @@ -79,12 +76,10 @@ class SystemComponentPkg(m.ModelElement): ) -@m.xtype_handler(None) class CapabilityInvolvement(interaction.AbstractInvolvement): """A CapabilityInvolvement.""" -@m.xtype_handler(None) class Capability(m.ModelElement): """A capability.""" @@ -93,22 +88,16 @@ class Capability(m.ModelElement): extends = m.DirectProxyAccessor( interaction.AbstractCapabilityExtend, aslist=m.ElementList ) - extended_by = m.Backref( - interaction.AbstractCapabilityExtend, "target", aslist=m.ElementList - ) + extended_by = m.Backref(interaction.AbstractCapabilityExtend, "target") includes = m.DirectProxyAccessor( interaction.AbstractCapabilityInclude, aslist=m.ElementList ) - included_by = m.Backref( - interaction.AbstractCapabilityInclude, "target", aslist=m.ElementList - ) + included_by = m.Backref(interaction.AbstractCapabilityInclude, "target") generalizes = m.DirectProxyAccessor( interaction.AbstractCapabilityGeneralization, aslist=m.ElementList ) generalized_by = m.Backref( - interaction.AbstractCapabilityGeneralization, - "target", - aslist=m.ElementList, + interaction.AbstractCapabilityGeneralization, "target" ) owned_chains = m.DirectProxyAccessor( fa.FunctionalChain, aslist=m.ElementList @@ -116,19 +105,16 @@ class Capability(m.ModelElement): involved_functions = m.Allocation[SystemFunction]( "ownedAbstractFunctionAbstractCapabilityInvolvements", interaction.AbstractFunctionAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) involved_chains = m.Allocation[fa.FunctionalChain]( "ownedFunctionalChainAbstractCapabilityInvolvements", interaction.FunctionalChainAbstractCapabilityInvolvement, - aslist=m.ElementList, attr="involved", ) involved_components = m.Allocation[SystemComponent]( "ownedCapabilityInvolvements", CapabilityInvolvement, - aslist=m.MixedElementList, attr="involved", ) component_involvements = m.DirectProxyAccessor( @@ -137,36 +123,35 @@ class Capability(m.ModelElement): realized_capabilities = m.Allocation[oa.OperationalCapability]( None, # FIXME fill in tag interaction.AbstractCapabilityRealization, - aslist=m.ElementList, attr="targetElement", ) - postcondition = m.Association(capellacore.Constraint, "postCondition") - precondition = m.Association(capellacore.Constraint, "preCondition") + postcondition = m.Single( + m.Association(capellacore.Constraint, "postCondition") + ) + precondition = m.Single( + m.Association(capellacore.Constraint, "preCondition") + ) scenarios = m.DirectProxyAccessor( interaction.Scenario, aslist=m.ElementList ) - states = m.Association( - capellacommon.State, "availableInStates", aslist=m.ElementList - ) + states = m.Association(capellacommon.State, "availableInStates") packages: m.Accessor -@m.xtype_handler(None) class MissionInvolvement(interaction.AbstractInvolvement): """A MissionInvolvement.""" _xmltag = "ownedMissionInvolvements" -@m.xtype_handler(None) class CapabilityExploitation(m.ModelElement): """A CapabilityExploitation.""" _xmltag = "ownedCapabilityExploitations" - capability = m.Association(Capability, "capability") + capability = m.Single(m.Association(Capability, "capability")) @property def name(self) -> str: # type: ignore[override] @@ -178,7 +163,6 @@ def name(self) -> str: # type: ignore[override] return f"[{self.__class__.__name__}]{direction}" -@m.xtype_handler(None) class Mission(m.ModelElement): """A mission.""" @@ -187,13 +171,10 @@ class Mission(m.ModelElement): involvements = m.DirectProxyAccessor( MissionInvolvement, aslist=m.ElementList ) - incoming_involvements = m.Backref( - MissionInvolvement, "target", aslist=m.ElementList - ) + incoming_involvements = m.Backref(MissionInvolvement, "target") exploits = m.Allocation[Capability]( None, # FIXME fill in tag CapabilityExploitation, - aslist=m.ElementList, attr="capability", ) exploitations = m.DirectProxyAccessor( @@ -201,7 +182,6 @@ class Mission(m.ModelElement): ) -@m.xtype_handler(None) class MissionPkg(m.ModelElement): """A system mission package that can hold missions.""" @@ -211,7 +191,6 @@ class MissionPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class CapabilityPkg(m.ModelElement): """A capability package that can hold capabilities.""" @@ -222,7 +201,6 @@ class CapabilityPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class SystemAnalysis(cs.ComponentArchitecture): """Provides access to the SystemAnalysis layer of the model.""" @@ -279,49 +257,35 @@ class SystemAnalysis(cs.ComponentArchitecture): ) -m.set_accessor( - SystemFunction, - "owner", - m.Backref(SystemComponent, "allocated_functions"), +SystemFunction.owner = m.Single( + m.Backref(SystemComponent, "allocated_functions") +) +SystemFunction.packages = m.DirectProxyAccessor( + SystemFunctionPkg, aslist=m.ElementList +) +oa.OperationalCapability.realizing_capabilities = m.Backref( + Capability, "realized_capabilities" ) -m.set_accessor( - SystemFunction, - "packages", - m.DirectProxyAccessor(SystemFunctionPkg, aslist=m.ElementList), +Capability.incoming_exploitations = m.Backref( + CapabilityExploitation, "capability" ) -m.set_accessor( - oa.OperationalCapability, - "realizing_capabilities", - m.Backref(Capability, "realized_capabilities", aslist=m.ElementList), +oa.Entity.realizing_system_components = m.Backref( + SystemComponent, "realized_operational_entities" ) -m.set_accessor( - Capability, - "incoming_exploitations", - m.Backref(CapabilityExploitation, "capability", aslist=m.ElementList), +oa.OperationalActivity.realizing_system_functions = m.Backref( + SystemFunction, "realized_operational_activities" ) -m.set_accessor( - oa.Entity, - "realizing_system_components", - m.Backref( - SystemComponent, "realized_operational_entities", aslist=m.ElementList - ), +SystemFunction.involved_in = m.Backref(Capability, "involved_functions") +MissionPkg.packages = m.DirectProxyAccessor(MissionPkg, aslist=m.ElementList) +SystemComponent.components = m.DirectProxyAccessor( + SystemComponent, aslist=m.ElementList ) -m.set_accessor( - oa.OperationalActivity, - "realizing_system_functions", - m.Backref( - SystemFunction, "realized_operational_activities", aslist=m.ElementList - ), +SystemComponentPkg.packages = m.DirectProxyAccessor( + SystemComponentPkg, aslist=m.ElementList ) -m.set_accessor( - SystemFunction, - "involved_in", - m.Backref(Capability, "involved_functions", aslist=m.ElementList), +SystemFunction.functions = m.DirectProxyAccessor( + SystemFunction, aslist=m.ElementList ) -m.set_self_references( - (MissionPkg, "packages"), - (SystemComponent, "components"), - (SystemComponentPkg, "packages"), - (SystemFunction, "functions"), - (SystemFunctionPkg, "packages"), +SystemFunctionPkg.packages = m.DirectProxyAccessor( + SystemFunctionPkg, aslist=m.ElementList ) diff --git a/capellambse/model/__init__.py b/capellambse/model/__init__.py index 6cac1c4bd..13e96c346 100644 --- a/capellambse/model/__init__.py +++ b/capellambse/model/__init__.py @@ -10,6 +10,10 @@ import typing as t import warnings +VIRTUAL_NAMESPACE_PREFIX = ( + "https://dsd-dbs.github.io/py-capellambse/virtual-namespace/" +) + E = t.TypeVar("E", bound=enum.Enum) """TypeVar for ":py:class:`~enum.Enum`".""" S = t.TypeVar("S", bound=str | None) @@ -20,6 +24,8 @@ """Covariant TypeVar for ":py:class:`capellambse.model.ModelObject`".""" U = t.TypeVar("U") """TypeVar (unbound).""" +U_co = t.TypeVar("U_co", covariant=True) +"""Covariant TypeVar (unbound).""" def set_accessor( @@ -31,7 +37,7 @@ def set_accessor( def set_self_references(*args: tuple[type[ModelObject], str]) -> None: for cls, attr in args: - set_accessor(cls, attr, DirectProxyAccessor(cls, aslist=ElementList)) + setattr(cls, attr, DirectProxyAccessor(cls, aslist=ElementList)) def attr_equal(attr: str) -> cabc.Callable[[type[T]], type[T]]: @@ -87,12 +93,19 @@ def __hash__(self): return et +def reset_entrypoint_caches() -> None: + """Reset all cached data from entrypoints.""" + for i in globals().values(): + if hasattr(i, "cache_clear"): + i.cache_clear() + + from . import diagram from ._descriptors import * +from ._meta import * from ._model import * from ._obj import * from ._pods import * -from ._xtype import * # NOTE: These are not in __all__ to avoid duplicate documentation in Sphinx, # however their docstring should mention the re-export. @@ -100,16 +113,14 @@ def __hash__(self): from .diagram import Diagram as Diagram from .diagram import DiagramAccessor as DiagramAccessor from .diagram import DiagramType as DiagramType - -ModelElement.parent = ParentAccessor(ModelElement) - +from .diagram import DRepresentationDescriptor as DRepresentationDescriptor if not t.TYPE_CHECKING: from ._descriptors import __all__ as _all1 - from ._model import __all__ as _all2 - from ._obj import __all__ as _all3 - from ._pods import __all__ as _all4 - from ._xtype import __all__ as _all5 + from ._meta import __all__ as _all2 + from ._model import __all__ as _all3 + from ._obj import __all__ as _all4 + from ._pods import __all__ as _all5 __all__ = [ "E", @@ -117,8 +128,10 @@ def __hash__(self): "T", "T_co", "U", + "U_co", "attr_equal", "diagram", + "reset_entrypoint_caches", "set_self_references", "stringy_enum", *_all1, @@ -144,4 +157,27 @@ def __getattr__(attr): stacklevel=2, ) return target + + if attr == "XTYPE_ANCHORS": + warnings.warn( + f"{attr} is deprecated, define a Namespace instead", + DeprecationWarning, + stacklevel=2, + ) + return {} + + if attr == "XTYPE_HANDLERS": + warnings.warn( + f"{attr} is deprecated, use Namespace-based discovery instead", + DeprecationWarning, + stacklevel=2, + ) + return { + None: [ + cls[0][0] + for ns in enumerate_namespaces() + for cls in ns._classes.values() + ] + } + raise AttributeError(f"{__name__} has no attribute {attr}") diff --git a/capellambse/model/_descriptors.py b/capellambse/model/_descriptors.py index 42012e3bb..f78195b63 100644 --- a/capellambse/model/_descriptors.py +++ b/capellambse/model/_descriptors.py @@ -11,10 +11,12 @@ "Association", "AttributeMatcherAccessor", "Backref", + "BrokenModelError", "Containment", "DeepProxyAccessor", "DeprecatedAccessor", "DirectProxyAccessor", + "Filter", "IndexAccessor", "InvalidModificationError", "NewObject", @@ -22,9 +24,13 @@ "ParentAccessor", "PhysicalAccessor", "PhysicalLinkEndsAccessor", + "Relationship", + "Single", "SpecificationAccessor", "TypecastAccessor", "WritableAccessor", + "build_xtype", + "xtype_handler", ] import abc @@ -43,15 +49,55 @@ import capellambse from capellambse import helpers +from capellambse.loader import core -from . import T_co, _xtype +from . import T, T_co, U_co -_NOT_SPECIFIED = object() +_NotSpecifiedType = t.NewType("_NotSpecifiedType", object) +_NOT_SPECIFIED = _NotSpecifiedType(object()) "Used to detect unspecified optional arguments" LOGGER = logging.getLogger(__name__) +def xtype_handler( + arch: str | None = None, /, *xtypes: str +) -> cabc.Callable[[type[T]], type[T]]: + """Register a class as handler for a specific ``xsi:type``. + + No longer used. Instead, declare a :class:`capellambse.model.Namespace` + containing your classes and register it as entrypoint. + """ + del arch, xtypes + return lambda i: i + + +def build_xtype(class_: type[_obj.ModelObject]) -> str: + ns: _obj.Namespace | None = getattr(class_, "__capella_namespace__", None) + if ns is None: + raise ValueError(f"Cannot determine namespace of class {class_!r}") + return f"{ns.alias}:{class_.__name__}" + + +class BrokenModelError(RuntimeError): + """Raised when the model is invalid.""" + + +class MissingValueError(BrokenModelError): + """Raised when an enforced Single value is absent.""" + + obj = property(lambda self: self.args[0]) + attr = property(lambda self: self.args[1]) + + def __str__(self) -> str: + if len(self.args) != 2: + return super().__str__() + return ( + f"Missing required value for {self.attr!r}" + f" on {self.obj._short_repr_()}" + ) + + class InvalidModificationError(RuntimeError): """Raised when a modification would result in an invalid model.""" @@ -83,7 +129,7 @@ class NewObject: For the time being, client code should treat this as opaque object. """ - def __init__(self, /, type_hint: str, **kw: t.Any) -> None: + def __init__(self, /, type_hint: str = "", **kw: t.Any) -> None: self._type_hint = type_hint self._kw = kw @@ -92,11 +138,11 @@ def __repr__(self) -> str: return f"" -class Accessor(t.Generic[T_co], metaclass=abc.ABCMeta): +class Accessor(t.Generic[U_co], metaclass=abc.ABCMeta): """Super class for all Accessor types.""" - __objclass__: type[t.Any] __name__: str + __objclass__: type[t.Any] def __init__(self) -> None: super().__init__() @@ -110,19 +156,19 @@ def __get__(self, obj: None, objtype: type[t.Any]) -> te.Self: ... @t.overload def __get__( self, obj: _obj.ModelObject, objtype: type[t.Any] | None = ... - ) -> T_co | _obj.ElementList[T_co]: ... + ) -> U_co: ... @abc.abstractmethod def __get__( self, obj: _obj.ModelObject | None, objtype: type[t.Any] | None = None, - ) -> te.Self | T_co | _obj.ElementList[T_co]: + ) -> te.Self | U_co: pass - def __set__(self, obj: _obj.ModelObject, value: t.Any) -> None: + def __set__(self, obj: t.Any, value: t.Any) -> None: raise TypeError("Cannot set this type of attribute") - def __delete__(self, obj: _obj.ModelObject) -> None: + def __delete__(self, obj: t.Any) -> None: raise TypeError("Cannot delete this type of attribute") def __set_name__(self, owner: type[t.Any], name: str) -> None: @@ -131,8 +177,25 @@ def __set_name__(self, owner: type[t.Any], name: str) -> None: friendly_name = name.replace("_", " ") self.__doc__ = f"The {friendly_name} of this {owner.__name__}." + if isinstance(self, Single): + return + + super_acc = None + for cls in owner.__mro__[1:]: + super_acc = cls.__dict__.get(name) + if super_acc is not None: + break + + if isinstance(super_acc, Single): + super_acc = super_acc.wrapped + + if super_acc is not None and type(super_acc) is type(self): + self._resolve_super_attributes(super_acc) + else: + self._resolve_super_attributes(None) + def __repr__(self) -> str: - return f"<{self._qualname!r} {type(self).__name__}>" + return f"<{type(self).__name__} {self._qualname!r}>" @property def _qualname(self) -> str: @@ -141,8 +204,13 @@ def _qualname(self) -> str: return f"(unknown {type(self).__name__} - call __set_name__)" return f"{self.__objclass__.__name__}.{self.__name__}" + def _resolve_super_attributes( + self, super_acc: Accessor[t.Any] | None + ) -> None: + pass -class Alias(Accessor[T_co]): + +class Alias(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Provides an alias to another attribute. Parameters @@ -170,17 +238,19 @@ def __get__( self, obj: _obj.ModelObject, objtype: type[t.Any] | None = ..., - ) -> T_co | _obj.ElementList[T_co]: ... + ) -> _obj.ElementList[T_co]: ... def __get__( self, obj: _obj.ModelObject | None, objtype: type[t.Any] | None = None, - ) -> te.Self | T_co | _obj.ElementList[T_co]: + ) -> te.Self | _obj.ElementList[T_co]: if obj is None: return self return getattr(obj, self.target) - def __set__(self, obj: _obj.ModelObject, value: t.Any) -> None: + def __set__( + self, obj: _obj.ModelObject, value: cabc.Iterable[T_co] + ) -> None: setattr(obj, self.target, value) def __delete__(self, obj: _obj.ModelObject) -> None: @@ -203,12 +273,12 @@ def __get__( self, obj: _obj.ModelObject, objtype: type[t.Any] | None = ..., - ) -> T_co | _obj.ElementList[T_co]: ... + ) -> T_co: ... def __get__( self, obj: _obj.ModelObject | None, objtype: type[t.Any] | None = None, - ) -> te.Self | T_co | _obj.ElementList[T_co]: + ) -> te.Self | T_co: if obj is None: return self @@ -228,7 +298,290 @@ def __warn(self) -> None: warnings.warn(msg, FutureWarning, stacklevel=3) -class WritableAccessor(Accessor[T_co], metaclass=abc.ABCMeta): +class Single(Accessor[T_co | None], t.Generic[T_co]): + """An Accessor wrapper that ensures there is exactly one value. + + This Accessor is used to wrap other Accessors that return multiple + values, such as :class:`Containment`, :class:`Association` or + :class:`Allocation`. Instead of returning a list, Single ensures + that the list from the wrapped accessor contains exactly one + element, and returns that element directly. + + Parameters + ---------- + wrapped + The accessor to wrap. This accessor must return a list (i.e. it + is not possible to nest *Single* descriptors). The instance + passed here should also not be used anywhere else. + enforce + Whether to enforce that there is exactly one value. + + If enforce False and the list obtained from the wrapped accessor + is empty, this accessor returns None; if there is at least one + element in it, the first element is returned. + + If enforce is True, a list which doesn't have exactly one + element will cause a :class:`MissingValueError` to be raised, + which is a subclass of :class:`BrokenModelError`. + + Defaults to False. + + Examples + -------- + >>> class Foo(capellacore.CapellaElement): + ... bar = Single["Bar"](Containment("bar", (NS, "Bar"))) + """ + + def __init__( + self, + wrapped: Accessor[_obj.ElementList[T_co]], + enforce: bool = False, + ) -> None: + """Create a new single-value descriptor.""" + self.wrapped: t.Final = wrapped + self.enforce: t.Final = enforce + + @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 = None + ) -> T_co | None: ... + def __get__( + self, obj: _obj.ModelObject | None, objtype: t.Any | None = None + ) -> te.Self | T_co | None: + """Retrieve the value of the attribute.""" + if obj is None: + return self + + objs: t.Any = self.wrapped.__get__(obj, type(obj)) + if not isinstance(objs, _obj.ElementList): + raise RuntimeError( + f"Expected a list from wrapped accessor on {self._qualname}," + f" got {type(objs).__name__}" + ) + + if objs: + return objs[0] + if self.enforce: + raise MissingValueError(obj, self.__name__) + return None + + def __set__( + self, obj: _obj.ModelObject, value: _obj.ModelObject | None + ) -> None: + """Set the value of the attribute.""" + self.wrapped.__set__(obj, [value]) + + def __delete__(self, obj: _obj.ModelObject) -> None: + """Delete the attribute.""" + if self.enforce: + raise InvalidModificationError( + f"Cannot delete required attribute {self.__name__!r}" + ) + self.wrapped.__delete__(obj) + + def __set_name__(self, owner: type[_obj.ModelObject], name: str) -> None: + """Set the name and owner of the descriptor.""" + self.wrapped.__set_name__(owner, name) + super().__set_name__(owner, name) + + def __repr__(self) -> str: + if self.enforce: + level = "exactly one" + else: + level = "the first" + wrapped = repr(self.wrapped).replace(" " + repr(self._qualname), "") + return f"" + + def purge_references( + self, obj: _obj.ModelObject, target: _obj.ModelObject + ) -> contextlib.AbstractContextManager[None]: + if hasattr(self.wrapped, "purge_references"): + return self.wrapped.purge_references(obj, target) + return contextlib.nullcontext(None) + + +class Relationship(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): + list_type: type[_obj.ElementListCouplingMixin] + list_extra_args: cabc.Mapping[str, t.Any] + single_attr: str | None + + def __init__( + self, + *, + mapkey: str | None, + mapvalue: str | None, + fixed_length: int, + single_attr: str | None, + ) -> None: + self.list_extra_args = { + "mapkey": mapkey, + "mapvalue": mapvalue, + "fixed_length": fixed_length, + } + self.single_attr = single_attr + self.list_type = make_coupled_list_type(self) + + @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_co]: ... + @abc.abstractmethod + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: + pass + + def __set__( + self, + obj: _obj.ModelObject, + value: cabc.Iterable[T_co | NewObject], + ) -> None: + raise TypeError("Cannot set this type of attribute") + + @abc.abstractmethod + def insert( + self, + elmlist: _obj.ElementListCouplingMixin, + index: int, + value: T_co | NewObject, + ) -> T_co: + """Insert the ``value`` object into the model. + + The object must be inserted at an appropriate place, so that, if + ``elmlist`` were to be created afresh, ``value`` would show up + at index ``index``. + + Returns the value that was just inserted. This is useful if the + incoming value was a :class:`NewObject`, in which case the + return value is the newly created object. + """ + + @abc.abstractmethod + def delete( + self, + elmlist: _obj.ElementListCouplingMixin, + obj: _obj.ModelObject, + ) -> None: + """Delete the ``obj`` from the model.""" + + def purge_references( + self, obj: _obj.ModelObject, target: _obj.ModelObject + ) -> contextlib.AbstractContextManager[None]: + """Purge references to the given object from the model. + + This method is called while deleting physical objects, in order + to get rid of references to that object (and its descendants). + Reference purging is done in two steps, which is why this method + returns a context manager. + + The first step, executed by the ``__enter__`` method, collects + references to the target and ensures that deleting them would + result in a valid model. If any validity constraints would be + violated, an exception is raised to indicate as such, and the + whole operation is aborted. + + Once all ``__enter__`` methods have been called, the target + object is deleted from the model. Then all ``__exit__`` methods + are called, which triggers the actual deletion of all previously + discovered references. + + As per the context manager protocol, ``__exit__`` will always be + called after ``__enter__``, even if the operation is to be + aborted. The ``__exit__`` method must therefore inspect whether + an exception was passed in or not in order to know whether the + operation succeeded. + + In order to not confuse other context managers and keep the + model consistent, ``__exit__`` must not raise any further + exceptions. Exceptions should instead be logged to stderr, for + example by using the :py:meth:`logging.Logger.exception` + facility. + + The ``purge_references`` method will only be called for Accessor + instances that actually contain a reference. + + Parameters + ---------- + obj + The model object to purge references from. + target + The object that is to be deleted; references to this object + will be purged. + + Returns + ------- + contextlib.AbstractContextManager + A context manager that deals with purging references in a + transactional manner. + + Raises + ------ + InvalidModificationError + Raised by the returned context manager's ``__enter__`` + method if the attempted modification would result in an + invalid model. Note that it is generally preferred to allow + the operation and take the necessary steps to keep the model + consistent, if possible. This can be achieved for example by + deleting dependent objects along with the original deletion + target. + Exception + Any exception may be raised before ``__enter__`` returns in + order to abort the transaction and prevent the ``obj`` from + being deleted. No exceptions must be raised by ``__exit__``. + + Examples + -------- + A simple implementation for purging a single object reference + could look like this: + + .. code-block:: python + + @contextlib.contextmanager + def purge_references(self, obj, target): + assert self.__get__(obj, type(obj)) == target + + yield + + try: + self.__delete__(obj) + except Exception: + LOGGER.exception("Could not purge a dangling reference") + """ + del obj, target + return contextlib.nullcontext(None) + + def _resolve_super_attributes( + self, super_acc: Accessor[t.Any] | None + ) -> None: + assert isinstance(super_acc, Relationship | None) + + super()._resolve_super_attributes(super_acc) + if super_acc is None: + return + + if self.list_extra_args["fixed_length"] is None: + self.list_extra_args["fixed_length"] = super_acc.list_extra_args[ # type: ignore[index] + "fixed_length" + ] + if self.list_extra_args["mapkey"] is None: + self.list_extra_args["mapkey"] = super_acc.list_extra_args[ # type: ignore[index] + "mapkey" + ] + if self.list_extra_args["mapvalue"] is None: + self.list_extra_args["mapvalue"] = super_acc.list_extra_args[ # type: ignore[index] + "mapvalue" + ] + if self.single_attr is None: + self.single_attr = super_acc.single_attr + + +class WritableAccessor(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """An Accessor that also provides write support on lists it returns.""" aslist: type[_obj.ElementListCouplingMixin] | None @@ -361,18 +714,8 @@ def _match_xtype(self, hint: str, /) -> tuple[type[T_co], str]: f"Expected str as first type, got {type(hint).__name__!r}" ) - matches: list[tuple[type[T_co], str]] = [] - for i, cls in _xtype.XTYPE_HANDLERS[None].items(): - if hint in {i, i.split(":")[-1], cls.__name__}: - matches.append((cls, i)) - if not matches: - raise ValueError(f"Invalid or unknown xsi:type {hint!r}") - if len(matches) > 1: - raise ValueError( - f"Ambiguous xsi:type {hint!r}, please qualify: " - + ", ".join(repr(i) for _, i in matches) - ) - return matches[0] + (cls,) = t.cast(tuple[type[T_co]], _obj.find_wrapper(hint)) + return (cls, build_xtype(cls)) def _guess_xtype(self) -> tuple[type[T_co], str]: try: @@ -471,7 +814,7 @@ def purge_references(self, obj, target): ) -class PhysicalAccessor(Accessor[T_co]): +class PhysicalAccessor(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Helper super class for accessors that work with real elements.""" __slots__ = ( @@ -504,19 +847,18 @@ def __init__( super().__init__() if xtypes is None: self.xtypes = ( - {_xtype.build_xtype(class_)} + {build_xtype(class_)} if class_ is not _obj.ModelElement else set() ) elif isinstance(xtypes, type): assert issubclass(xtypes, _obj.ModelElement) - self.xtypes = {_xtype.build_xtype(xtypes)} + self.xtypes = {build_xtype(xtypes)} elif isinstance(xtypes, str): self.xtypes = {xtypes} else: self.xtypes = { - i if isinstance(i, str) else _xtype.build_xtype(i) - for i in xtypes + i if isinstance(i, str) else build_xtype(i) for i in xtypes } self.aslist = aslist @@ -644,11 +986,10 @@ def __init__( elif isinstance(rootelem, type) and issubclass( rootelem, _obj.ModelElement ): - self.rootelem = [_xtype.build_xtype(rootelem)] + self.rootelem = [build_xtype(rootelem)] else: self.rootelem = [ - i if isinstance(i, str) else _xtype.build_xtype(i) - for i in rootelem + i if isinstance(i, str) else build_xtype(i) for i in rootelem ] def __get__(self, obj, objtype=None): @@ -719,10 +1060,12 @@ def _delete( if elm.get("id") is None: continue - obj = _obj.ModelElement.from_model(model, elm) + obj = _obj.wrap_xml(model, elm) for ref, attr, _ in model.find_references(obj): acc = getattr(type(ref), attr) - if acc is self or not isinstance(acc, WritableAccessor): + if acc is self or not isinstance( + acc, WritableAccessor | Relationship | Single + ): continue stack.enter_context(acc.purge_references(ref, obj)) @@ -864,9 +1207,9 @@ def __init__( elif isinstance(rootelem, type) and issubclass( rootelem, _obj.ModelElement ): - self.rootelem = (_xtype.build_xtype(rootelem),) + self.rootelem = (build_xtype(rootelem),) elif not isinstance(rootelem, str): # type: ignore[unreachable] - self.rootelem = tuple(_xtype.build_xtype(i) for i in rootelem) + self.rootelem = tuple(build_xtype(i) for i in rootelem) else: raise TypeError( "Invalid 'rootelem', expected a type or list of types: " @@ -907,16 +1250,18 @@ def _getsubelems( yield from ldr.iterdescendants_xt(root, *self.xtypes) -class Allocation(WritableAccessor[T_co], PhysicalAccessor[T_co]): +class Allocation(Relationship[T_co]): """Accesses elements through reference elements.""" - __slots__ = ("backattr", "tag", "unique") + __slots__ = ("alloc_type", "attr", "backattr", "class_", "tag") - aslist: type[_obj.ElementListCouplingMixin] | None - backattr: str | None - class_: type[T_co] tag: str | None + alloc_type: _obj.ClassName | None + class_: _obj.ClassName + attr: str | None + backattr: str | None + @t.overload def __init__( self, tag: str | None, @@ -929,6 +1274,35 @@ def __init__( attr: str, backattr: str | None = None, unique: bool = True, + ) -> None: ... + @t.overload + def __init__( + self, + tag: str | None, + alloc_type: _obj.UnresolvedClassName | None, + class_: _obj.UnresolvedClassName, + /, + *, + mapkey: str | None = None, + mapvalue: str | None = None, + attr: str | None = None, + backattr: str | None = None, + ) -> None: ... + def __init__( + self, + tag: str | None, + alloc_type: ( + str | type[_obj.ModelElement] | _obj.UnresolvedClassName | None + ), + class_: _obj.UnresolvedClassName | _NotSpecifiedType = _NOT_SPECIFIED, + /, + *, + aslist: t.Any = _NOT_SPECIFIED, + mapkey: str | None = None, + mapvalue: str | None = None, + attr: str | None = None, + backattr: str | None = None, + unique: bool = True, ) -> None: """Define an Allocation. @@ -944,9 +1318,12 @@ def __init__( ---------- tag The XML tag that the reference elements will have. - xtype - The ``xsi:type`` that the reference elements will have. This - has no influence on the elements that are referenced. + alloc_type + The type that the allocation element will have, as a + ``(Namespace, "ClassName")`` tuple. + class_ + The type of element that this allocation links to, as a + ``(Namespace, "ClassName")`` tuple. attr The attribute on the reference element that contains the actual link. @@ -974,33 +1351,104 @@ def __init__( be raised. Note that this does not have an effect on lists already existing within the loaded model. """ - if not tag: + if not isinstance(tag, str | None): + raise TypeError(f"tag must be a str, not {type(tag).__name__}") + if aslist is not _NOT_SPECIFIED: warnings.warn( - "Unspecified XML tag is deprecated", + "The aslist argument is deprecated and will be removed soon", DeprecationWarning, stacklevel=2, ) - elif not isinstance(tag, str): - raise TypeError(f"tag must be a str, not {type(tag).__name__}") + super().__init__( - _obj.ModelElement, - xtype, - aslist=aslist, mapkey=mapkey, mapvalue=mapvalue, + fixed_length=0, + single_attr=None, ) - if len(self.xtypes) != 1: - raise TypeError(f"One xtype is required, got {len(self.xtypes)}") - self.follow = attr - self.backattr = backattr self.tag = tag + self.attr = attr + self.backattr = backattr self.unique = unique - def __get__(self, obj, objtype=None): + if alloc_type is None: + self.alloc_type = None + elif isinstance(alloc_type, str): + warnings.warn( + ( + "xsi:type strings for Allocation are deprecated," + " use a (Namespace, 'ClassName') tuple instead" + ), + DeprecationWarning, + stacklevel=2, + ) + if ":" in alloc_type: + alloc_type = t.cast( + tuple[str, str], tuple(alloc_type.rsplit(":", 1)) + ) + self.alloc_type = ( + _obj.find_namespace(alloc_type[0]), + alloc_type[1], + ) + else: + self.alloc_type = _obj.resolve_class_name(("", alloc_type)) + elif isinstance(alloc_type, type): + if not issubclass(alloc_type, _obj.ModelElement): + raise TypeError( + "Allocation class must be a subclass of ModelElement:" + f" {alloc_type.__module__}.{alloc_type.__name__}" + ) + warnings.warn( + ( + "Raw classes for Allocation are deprecated," + " use a (Namespace, 'ClassName') tuple instead" + ), + DeprecationWarning, + stacklevel=2, + ) + self.alloc_type = ( + alloc_type.__capella_namespace__, + alloc_type.__name__, + ) + elif isinstance(alloc_type, tuple) and len(alloc_type) == 2: + self.alloc_type = _obj.resolve_class_name(alloc_type) + else: + raise TypeError( + f"Malformed alloc_type, expected a 2-tuple: {alloc_type!r}" + ) + + if class_ is _NOT_SPECIFIED: + warnings.warn( + "Unspecified target class is deprecated", + DeprecationWarning, + stacklevel=2, + ) + self.class_ = (_obj.NS, "ModelElement") + else: + self.class_ = _obj.resolve_class_name(class_) + + @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_co]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: del objtype if obj is None: # pragma: no cover return self + # TODO extend None check to self.tag when removing deprecated features + if None in (self.alloc_type, self.attr): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + elems: list[etree._Element] = [] seen: set[etree._Element] = set() for i in self.__find_refs(obj): @@ -1010,17 +1458,41 @@ def __get__(self, obj, objtype=None): if e not in seen: elems.append(e) seen.add(e) - return self._make_list(obj, elems) + return self.list_type( + obj._model, elems, parent=obj, **self.list_extra_args + ) - def __set__(self, obj: _obj.ModelObject, value: t.Any) -> None: - if self.aslist is None: - if isinstance(value, cabc.Iterable) and not isinstance(value, str): - raise TypeError(f"{self._qualname} expects a single element") + def __set__( + self, + obj: _obj.ModelObject, + value: T_co | NewObject | cabc.Iterable[T_co | NewObject], + ) -> None: + if not isinstance(value, cabc.Iterable): + warnings.warn( + ( + "Assigning a single value onto Allocation is deprecated." + f" If {self._qualname!r} is supposed to use single values," + " wrap it in a 'm.Single()'." + " Otherwise, update your code to assign a list instead." + ), + DeprecationWarning, + stacklevel=2, + ) value = (value,) - elif isinstance(value, str) or not isinstance(value, cabc.Iterable): - raise TypeError(f"{self._qualname} expects an iterable") + + te.assert_type(value, cabc.Iterable[T_co | NewObject]) + if any(isinstance(i, NewObject) for i in value): + raise TypeError("Cannot create new objects on an Allocation") + value = t.cast(cabc.Iterable[T_co], value) + + # TODO Remove this extra check when removing deprecated features if self.tag is None: - raise NotImplementedError("Cannot set: XML tag not set") + raise TypeError("Cannot set: XML tag not set") + if None in (self.tag, self.alloc_type, self.attr): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) self.__delete__(obj) for v in value: @@ -1035,7 +1507,9 @@ def __delete__(self, obj: _obj.ModelObject) -> None: def __follow_ref( self, obj: _obj.ModelObject, refelm: etree._Element ) -> etree._Element | None: - link = refelm.get(self.follow) + assert self.attr is not None + + link = refelm.get(self.attr) if not link: return None return obj._model._loader.follow_link(obj._element, link) @@ -1043,8 +1517,13 @@ def __follow_ref( def __find_refs( self, obj: _obj.ModelObject ) -> cabc.Iterator[etree._Element]: + assert self.alloc_type is not None + # TODO add None check for self.tag when removing deprecated features + + target_type = obj._model.qualify_classname(self.alloc_type) for refelm in obj._element.iterchildren(tag=self.tag): - if helpers.xtype_of(refelm) in self.xtypes: + elm_type = core.qtype_of(refelm) + if elm_type == target_type: yield refelm def __backref( @@ -1062,23 +1541,21 @@ def __create_link( *, before: _obj.ModelObject | None = None, ) -> etree._Element: + assert self.alloc_type is not None + assert self.attr is not None assert self.tag is not None - loader = parent._model._loader + + model = parent._model if self.unique: for i in self.__find_refs(parent): if self.__follow_ref(parent, i) is target._element: raise NonUniqueMemberError(parent, self.__name__, target) - with loader.new_uuid(parent._element) as obj_id: - (xtype,) = self.xtypes - link = loader.create_link(parent._element, target._element) - refobj = parent._element.makeelement( - self.tag, - {helpers.ATT_XT: xtype, "id": obj_id, self.follow: link}, - ) - if self.backattr and (parent_id := parent._element.get("id")): - refobj.set(self.backattr, f"#{parent_id}") + xtype = parent._model.qualify_classname(self.alloc_type) + with model._loader.new_uuid(parent._element) as obj_id: + link = model._loader.create_link(parent._element, target._element) + refobj = parent._element.makeelement(self.tag) if before is None: parent._element.append(refobj) else: @@ -1086,36 +1563,44 @@ def __create_link( assert before_elm is not None assert before_elm in parent._element before_elm.addprevious(refobj) - loader.idcache_index(refobj) + try: + refobj.set(helpers.ATT_XT, xtype) + refobj.set("id", obj_id) + refobj.set(self.attr, link) + if self.backattr: + backlink = model._loader.create_link( + refobj, parent._element + ) + refobj.set(self.backattr, backlink) + model._loader.idcache_index(refobj) + except: + parent._element.remove(refobj) + raise return refobj def insert( self, elmlist: _obj.ElementListCouplingMixin, index: int, - value: _obj.ModelObject | NewObject, - ) -> None: - if self.aslist is None: - raise TypeError("Cannot insert: This is not a list (bug?)") + value: T_co | NewObject, + ) -> T_co: if self.tag is None: raise NotImplementedError("Cannot insert: XML tag not set") if isinstance(value, NewObject): - raise NotImplementedError("Cannot insert new objects yet") + raise TypeError("Cannot create objects in an Allocation") self.__create_link( elmlist._parent, value, before=elmlist[index] if index < len(elmlist) else None, ) + return value def delete( self, elmlist: _obj.ElementListCouplingMixin, obj: _obj.ModelObject, ) -> None: - if self.aslist is None: - raise TypeError("Cannot delete: This is not a list (bug?)") - parent = elmlist._parent for ref in self.__find_refs(parent): if self.__follow_ref(parent, ref) == obj._element: @@ -1145,21 +1630,50 @@ def purge_references( except Exception: LOGGER.exception("Cannot purge dangling ref object %r", ref) + def _resolve_super_attributes( + self, super_acc: Accessor[t.Any] | None + ) -> None: + assert isinstance(super_acc, Allocation | None) -class Association(WritableAccessor[T_co], PhysicalAccessor[T_co]): - """Provides access to elements that are linked in an attribute.""" + super()._resolve_super_attributes(super_acc) + + if self.tag is None and super_acc is not None: + self.tag = super_acc.tag + if self.tag is None: + warnings.warn( + "Unspecified XML tag is deprecated", + DeprecationWarning, + stacklevel=2, + ) - __slots__ = ("attr",) + if None not in (self.alloc_type, self.attr): + return + if super_acc is None: + raise TypeError( + f"Cannot inherit {type(self).__name__} configuration:" + f" No super class of {self.__objclass__.__name__}" + f" defines {self.__name__!r}" + ) - aslist: type[_obj.ElementListCouplingMixin] | None - class_: type[T_co] + if self.alloc_type is None: + self.alloc_type = super_acc.alloc_type + if self.attr is None: + self.attr = super_acc.attr + if self.backattr is None: + self.backattr = super_acc.backattr + + +class Association(Relationship[T_co]): + """Provides access to elements that are linked in an attribute.""" + + __slots__ = ("attr", "class_") def __init__( self, - class_: type[T_co] | None, - attr: str, + class_: type[T_co] | None | _obj.UnresolvedClassName, + attr: str | None, *, - aslist: type[_obj.ElementList] | None = None, + aslist: t.Any = _NOT_SPECIFIED, mapkey: str | None = None, mapvalue: str | None = None, fixed_length: int = 0, @@ -1173,7 +1687,9 @@ def __init__( class_ The proxy class. Currently only used for type hints. attr - The XML attribute to handle. + The XML attribute to handle. If None, the attribute is + copied from an Association with the same name on the parent + class. aslist If None, the attribute contains at most one element reference, and either None or the constructed proxy will be @@ -1199,74 +1715,157 @@ def __init__( Ignored if *aslist* is not specified. """ - del class_ + if aslist is not _NOT_SPECIFIED: + warnings.warn( + "The aslist argument is deprecated and will be removed soon", + DeprecationWarning, + stacklevel=2, + ) + super().__init__( - _obj.ModelElement, - aslist=aslist, mapkey=mapkey, mapvalue=mapvalue, fixed_length=fixed_length, + single_attr=None, ) + + if class_ is None: + warnings.warn( + ( + "None as class_ argument to Association is deprecated," + " specify a (Namespace, 'ClassName') tuple" + ), + DeprecationWarning, + stacklevel=2, + ) + class_ = (_obj.NS, _obj.ModelElement.__name__) + elif isinstance(class_, type): + warnings.warn( + ( + "Raw classes for Association are deprecated," + " use a (Namespace, 'ClassName') tuple instead" + ), + DeprecationWarning, + stacklevel=2, + ) + try: + ns = class_.__capella_namespace__ + except AttributeError: + raise RuntimeError( + f"Invalid class without namespace: {class_!r}" + ) from None + class_ = (ns, class_.__name__) + self.class_ = _obj.resolve_class_name(class_) self.attr = attr - def __get__(self, obj, objtype=None): + @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_co]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: del objtype if obj is None: # pragma: no cover return self + if self.attr is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + elems = obj._model._loader.follow_links( obj._element, obj._element.get(self.attr, "") ) - return self._make_list(obj, elems) + return self.list_type( + obj._model, elems, parent=obj, **self.list_extra_args + ) def __set__( self, obj: _obj.ModelObject, - values: T_co | NewObject | cabc.Iterable[T_co | NewObject], + value: T_co | NewObject | cabc.Iterable[T_co | NewObject], ) -> None: - if not isinstance(values, cabc.Iterable): - values = (values,) - elif self.aslist is None: - raise TypeError( - f"{self._qualname} requires a single item, not an iterable" + if not isinstance(value, cabc.Iterable): + warnings.warn( + ( + "Assigning a single value onto Association is deprecated." + f" If {self._qualname!r} is supposed to use single values," + " wrap it in a 'm.Single()'." + " Otherwise, update your code to assign a list instead." + ), + DeprecationWarning, + stacklevel=2, ) + value = (value,) - if any(isinstance(i, NewObject) for i in values): - raise NotImplementedError("Cannot create new objects here") + if self.attr is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) - assert isinstance(values, cabc.Iterable) - self.__set_links(obj, values) # type: ignore[arg-type] + te.assert_type(value, cabc.Iterable[T_co | NewObject]) + if any(isinstance(i, NewObject) for i in value): + raise TypeError("Cannot create new objects on an Association") + value = t.cast(cabc.Iterable[T_co], value) + + self.__set_links(obj, value) def __delete__(self, obj: _obj.ModelObject) -> None: + if self.attr is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + del obj._element.attrib[self.attr] def insert( self, elmlist: _obj.ElementListCouplingMixin, index: int, - value: _obj.ModelObject | NewObject, - ) -> None: - assert self.aslist is not None + value: T_co | NewObject, + ) -> T_co: if isinstance(value, NewObject): - raise NotImplementedError("Cannot insert new objects yet") + raise TypeError("Cannot create new objects on an Association") if value._model is not elmlist._parent._model: raise ValueError("Cannot insert elements from different models") objs = [*elmlist[:index], value, *elmlist[index:]] self.__set_links(elmlist._parent, objs) + return value def delete( self, elmlist: _obj.ElementListCouplingMixin, obj: _obj.ModelObject ) -> None: - assert self.aslist is not None objs = [i for i in elmlist if i != obj] self.__set_links(elmlist._parent, objs) def __set_links( self, obj: _obj.ModelObject, values: cabc.Iterable[T_co] ) -> None: + if self.attr is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + class_ = obj._model.resolve_class(self.class_) parts: list[str] = [] for value in values: + if not isinstance(value, class_): + raise TypeError( + f"Cannot insert into {self._qualname}:" + " Objects must be instances of" + f" {self.class_[0].viewpoint}:{self.class_[1]}," + f" not {type(value)!r}" + ) if value._model is not obj._model: raise ValueError( "Cannot insert elements from different models" @@ -1279,36 +1878,56 @@ def __set_links( def purge_references( self, obj: _obj.ModelObject, target: _obj.ModelObject ) -> cabc.Generator[None, None, None]: + if self.attr is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + yield - if self.aslist is not None: - try: - elt = obj._element - links = obj._model._loader.follow_links( - elt, elt.get(self.attr, ""), ignore_broken=True - ) - remaining_links = [ - link for link in links if link is not target._element - ] - self.__set_links(obj, self._make_list(obj, remaining_links)) - except Exception: - LOGGER.exception("Cannot write new list of targets") - else: - try: - del obj._element.attrib[self.attr] - except KeyError: - pass - except Exception: - LOGGER.exception("Cannot update link target") + try: + elt = obj._element + links = obj._model._loader.follow_links( + elt, elt.get(self.attr, ""), ignore_broken=True + ) + remaining_links = [ + link for link in links if link is not target._element + ] + self.__set_links( + obj, _obj.ElementList(obj._model, remaining_links) + ) + except Exception: + LOGGER.exception("Cannot write new list of targets") + + def _resolve_super_attributes( + self, super_acc: Accessor[t.Any] | None + ) -> None: + assert isinstance(super_acc, Association | None) + + super()._resolve_super_attributes(super_acc) + + if self.attr is not None: + return + + if super_acc is None: + raise TypeError( + f"Cannot inherit {type(self).__name__} configuration:" + f" No super class of {self.__objclass__.__name__}" + f" defines {self.__name__}" + ) + + assert isinstance(super_acc, Association) + self.attr = super_acc.attr class PhysicalLinkEndsAccessor(Association[T_co]): def __init__( self, - class_: type[T_co], + class_: type[T_co] | None | _obj.UnresolvedClassName, attr: str, *, - aslist: type[_obj.ElementList], + aslist: t.Any = _NOT_SPECIFIED, mapkey: str | None = None, mapvalue: str | None = None, ) -> None: @@ -1320,17 +1939,9 @@ def __init__( mapvalue=mapvalue, fixed_length=2, ) - assert self.aslist is not None - - @contextlib.contextmanager - def purge_references( - self, obj: _obj.ModelObject, target: _obj.ModelObject - ) -> cabc.Generator[None, None, None]: - # TODO This should instead delete the link - raise NotImplementedError("Cannot purge references from PhysicalLink") -class IndexAccessor(Accessor[T_co]): +class IndexAccessor(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Access a specific index in an ElementList of a fixed size.""" __slots__ = ("index", "wrapped") @@ -1341,9 +1952,11 @@ def __init__(self, wrapped: str, index: int) -> None: self.wrapped = wrapped @t.overload - def __get__(self, obj: None, objtype=None) -> te.Self: ... + def __get__(self, obj: None, objtype: type[t.Any]) -> te.Self: ... @t.overload - def __get__(self, obj, objtype=None) -> T_co | _obj.ElementList[T_co]: ... + def __get__( + self, obj: _obj.ModelObject, objtype: type[t.Any] | None = None + ) -> _obj.ElementList[T_co]: ... def __get__( self, obj: _obj.ModelObject | None, @@ -1391,17 +2004,24 @@ def __get__(self, obj, objtype=None): del objtype if obj is None: # pragma: no cover return self - return self.class_.from_model(obj._model, obj._element) + if self.class_ is _obj.ModelElement: + return _obj.wrap_xml(obj._model, obj._element) + + alt = self.class_.__new__(self.class_) + alt._model = obj._model # type: ignore[misc] + alt._element = obj._element # type: ignore[misc] + return alt -class ParentAccessor(PhysicalAccessor[T_co]): + +class ParentAccessor(Accessor["_obj.ModelObject"]): """Accesses the parent XML element.""" __slots__ = () def __init__(self, class_: type[T_co] | None = None): del class_ - super().__init__(_obj.ModelElement) # type: ignore[arg-type] + super().__init__() def __get__(self, obj, objtype=None): del objtype @@ -1412,7 +2032,7 @@ def __get__(self, obj, objtype=None): if parent is None: objrepr = getattr(obj, "_short_repr_", obj.__repr__)() raise AttributeError(f"Object {objrepr} is orphaned") - return self.class_.from_model(obj._model, parent) + return _obj.wrap_xml(obj._model, parent) class AttributeMatcherAccessor(DirectProxyAccessor[T_co]): @@ -1460,6 +2080,7 @@ def __get__(self, obj, objtype=None): class _Specification(t.MutableMapping[str, str]): + __capella_namespace__: t.ClassVar[_obj.Namespace] _aliases = types.MappingProxyType({"LinkedText": "capella:linkedText"}) _linked_text = frozenset({"capella:linkedText"}) _model: capellambse.MelodyModel @@ -1560,19 +2181,23 @@ def __get__(self, obj, objtype=None): return _Specification(obj._model, spec_elm) -class Backref(PhysicalAccessor[T_co]): +class Backref(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Searches for references to the current element elsewhere.""" __slots__ = ("attrs", "target_classes") attrs: tuple[operator.attrgetter, ...] - target_classes: tuple[type[_obj.ModelObject], ...] + class_: _obj.ClassName def __init__( self, - class_: type[T_co] | tuple[type[_obj.ModelObject], ...], + class_: ( + type[T_co] + | tuple[type[_obj.ModelObject], ...] + | _obj.UnresolvedClassName + ), *attrs: str, - aslist: type[_obj.ElementList] | None = None, + aslist: t.Any = _NOT_SPECIFIED, mapkey: str | None = None, mapvalue: str | None = None, ) -> None: @@ -1605,28 +2230,66 @@ def __init__( Ignored if *aslist* is not specified. """ - if isinstance(class_, tuple): - super().__init__( - _obj.ModelElement, # type: ignore[arg-type] - aslist=aslist, - mapkey=mapkey, - mapvalue=mapvalue, + if aslist is not _NOT_SPECIFIED: + warnings.warn( + "The aslist argument is deprecated and will be removed soon", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__() + if ( + isinstance(class_, tuple) + and len(class_) == 2 + and isinstance(class_[0], _obj.Namespace | str) + and isinstance(class_[1], str) + ): + self.class_ = _obj.resolve_class_name(class_) + elif isinstance(class_, cabc.Sequence): + warnings.warn( + ( + "Multiple classes to Backref are deprecated," + " use a common abstract base class instead" + ), + DeprecationWarning, + stacklevel=2, ) - self.target_classes = class_ + self.class_ = (_obj.NS, "ModelElement") else: - super().__init__( - class_, aslist=aslist, mapkey=mapkey, mapvalue=mapvalue + warnings.warn( + ( + "Raw classes to Backref are deprecated," + " use a (Namespace, 'ClassName') tuple instead" + ), + DeprecationWarning, + stacklevel=2, ) - self.target_classes = (class_,) + if not hasattr(class_, "__capella_namespace__"): + raise TypeError(f"Class does not have a namespace: {class_!r}") + self.class_ = (class_.__capella_namespace__, class_.__name__) self.attrs = tuple(operator.attrgetter(i) for i in attrs) + self.list_extra_args = { + "mapkey": mapkey, + "mapvalue": mapvalue, + } - def __get__(self, obj, objtype=None): + @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_co]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: del objtype if obj is None: # pragma: no cover return self matches: list[etree._Element] = [] - for candidate in obj._model.search(*self.target_classes): + for candidate in obj._model.search(self.class_): for attr in self.attrs: try: value = attr(candidate) @@ -1637,7 +2300,166 @@ def __get__(self, obj, objtype=None): ): matches.append(candidate._element) break - return self._make_list(obj, matches) + return _obj.ElementList( + obj._model, matches, None, **self.list_extra_args + ) + + +class Filter(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): + """Provides access to a filtered subset of another attribute.""" + + __slots__ = ("attr", "class_", "wrapped") + + attr: str + class_: _obj.ClassName + + def __init__(self, attr: str, class_: _obj.UnresolvedClassName, /) -> None: + self.attr = attr + self.class_ = _obj.resolve_class_name(class_) + self.list_type = make_coupled_list_type(self) + self.wrapped: Accessor[_obj.ElementList[T_co]] # type: ignore + + @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_co]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: + if obj is None: # pragma: no cover + return self + + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + cls = obj._model.resolve_class(self.class_) + parent_elts = self.wrapped.__get__(obj, objtype) + if not isinstance(parent_elts, _obj.ElementList): + raise TypeError( + f"Parent accessor {self.wrapped!r}" + f" did not return an ElementList: {parent_elts!r}" + ) + filtered_elts = [ + i + for i in parent_elts._elements + if issubclass(obj._model.resolve_class(i), cls) + ] + + return self.list_type( + obj._model, + filtered_elts, + parent=obj, + ) + + def __set__( + self, + obj: _obj.ModelObject, + value: cabc.Iterable[T_co | NewObject], + ) -> None: + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + children = self.__get__(obj, type(obj)) + assert isinstance(children, _obj.ElementListCouplingMixin) + children[:] = value + + def __delete__(self, obj: _obj.ModelObject) -> None: + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + if not isinstance(self.wrapped, Relationship): + raise AttributeError(f"Cannot delete from {self._qualname}") + + children = self.__get__(obj, type(obj)) + assert isinstance(children, _obj.ElementListCouplingMixin) + children.clear() + + def __set_name__(self, owner: type[t.Any], name: str) -> None: + wrapped = getattr(owner, self.attr) + if not isinstance(wrapped, Relationship): + raise TypeError( + "Can only filter on Relationship accessors, not" + f" {type(wrapped).__name__}" + ) + self.wrapped = wrapped # type: ignore + + super().__set_name__(owner, name) + + def insert( + self, + elmlist: _obj.ElementListCouplingMixin, + index: int, + value: T_co | NewObject, + ) -> T_co: + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + if not isinstance(self.wrapped, Relationship): + raise AttributeError(f"Cannot insert into {self._qualname}") + + unfiltered = self.wrapped.__get__( + elmlist._parent, type(elmlist._parent) + ) + assert isinstance(unfiltered, _obj.ElementListCouplingMixin) + if index < 0: + index += len(elmlist) + + if index >= len(elmlist): + real_index = len(unfiltered) + else: + real_index = unfiltered.index(elmlist[index]) + + return self.wrapped.insert(unfiltered, real_index, value) + + def delete( + self, elmlist: _obj.ElementListCouplingMixin, obj: _obj.ModelObject + ) -> None: + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + if not isinstance(self.wrapped, Relationship): + raise AttributeError(f"Cannot delete from {self._qualname}") + + unfiltered = self.wrapped.__get__( + elmlist._parent, type(elmlist._parent) + ) + assert isinstance(unfiltered, _obj.ElementListCouplingMixin) + self.wrapped.delete(unfiltered, obj) + + @contextlib.contextmanager + def purge_references( + self, obj: _obj.ModelObject, target: _obj.ModelObject + ) -> cabc.Generator[None, None, None]: + if not hasattr(self, "wrapped"): + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + if not isinstance(self.wrapped, Relationship): + yield + else: + with self.wrapped.purge_references(obj, target): + yield class TypecastAccessor(WritableAccessor[T_co], PhysicalAccessor[T_co]): @@ -1723,7 +2545,7 @@ def create( if typehint: raise TypeError(f"{self._qualname} does not support type hints") acc: WritableAccessor = getattr(self.class_, self.attr) - obj = acc.create(elmlist, _xtype.build_xtype(self.class_), **kw) + obj = acc.create(elmlist, build_xtype(self.class_), **kw) assert isinstance(obj, self.class_) return obj @@ -1759,119 +2581,184 @@ def purge_references( yield -class Containment(WritableAccessor, PhysicalAccessor): +class Containment(Relationship[T_co]): __slots__ = ("classes", "role_tag") - aslist: type[_obj.ElementListCouplingMixin] | None - class_: type[_obj.ModelElement] - alternate: type[_obj.ModelElement] | None + alternate: type[_obj.ModelObject] | None + @t.overload + def __init__( + self, + role_tag: str, + classes: type[T_co] | cabc.Iterable[type[_obj.ModelObject]] = ..., + /, + *, + aslist: type[_obj.ElementList[T_co]] | None = ..., + mapkey: str | None = ..., + mapvalue: str | None = ..., + alternate: type[_obj.ModelObject] | None = ..., + single_attr: str | None = ..., + fixed_length: int = ..., + ) -> None: ... + @t.overload def __init__( self, role_tag: str, - classes: ( - type[_obj.ModelObject] | cabc.Iterable[type[_obj.ModelObject]] + class_: _obj.UnresolvedClassName, + /, + *, + mapkey: str | None = ..., + mapvalue: str | None = ..., + alternate: type[_obj.ModelObject] | None = ..., + single_attr: str | None = ..., + fixed_length: int = ..., + ) -> None: ... + def __init__( + self, + role_tag: str, + class_: ( + type[T_co] + | cabc.Iterable[type[_obj.ModelObject]] + | _obj.UnresolvedClassName ) = (), + /, *, - aslist: type[_obj.ElementList[T_co]] | None = None, + aslist: t.Any = _NOT_SPECIFIED, mapkey: str | None = None, mapvalue: str | None = None, - alternate: type[_obj.ModelElement] | None = None, + alternate: type[_obj.ModelObject] | None = None, + single_attr: str | None = None, + fixed_length: int = 0, ) -> None: + if aslist is not _NOT_SPECIFIED: + warnings.warn( + "The aslist argument is deprecated and will be removed soon", + DeprecationWarning, + stacklevel=2, + ) + super().__init__( - _obj.ModelElement, - (), - aslist=aslist, mapkey=mapkey, mapvalue=mapvalue, + fixed_length=fixed_length, + single_attr=single_attr, ) self.role_tag = role_tag - if not isinstance(classes, type): - self.classes = tuple(classes) - else: - self.classes = (classes,) - - if len(self.classes) != 1 and alternate is not None: - raise TypeError("alternate needs exactly 1 possible origin class") self.alternate = alternate - def __get__(self, obj, objtype=None): + if ( + isinstance(class_, tuple) + and len(class_) == 2 + and isinstance(class_[0], _obj.Namespace | str) + and isinstance(class_[1], str) + ): + self.class_ = _obj.resolve_class_name(class_) + elif isinstance(class_, cabc.Iterable) and not isinstance(class_, str): + warnings.warn( + ( + "Multiple classes for Containment are deprecated," + " use a common abstract base class instead" + ), + DeprecationWarning, + stacklevel=2, + ) + self.class_ = (_obj.NS, "ModelElement") + elif isinstance(class_, type) and issubclass( + class_, _obj.ModelElement + ): + warnings.warn( + ( + "Raw classes in Containment are deprecated," + " use a (Namespace, 'ClassName') tuple instead" + ), + DeprecationWarning, + stacklevel=2, + ) + else: + raise TypeError( + f"Invalid class_ specified, expected a 2-tuple: {class_!r}" + ) + + @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_co]: ... + def __get__( + self, + obj: _obj.ModelObject | None, + objtype: type[t.Any] | None = None, + ) -> te.Self | _obj.ElementList[T_co]: del objtype if obj is None: # pragma: no cover return self + if self.role_tag is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + loader = obj._model._loader - elts = [ - loader.follow_link(i, href) if (href := i.get("href")) else i - for i in obj._element.iterchildren(self.role_tag) - ] - rv = self._make_list(obj, elts) - if self.classes: - for i in rv: - if not isinstance(i, self.classes): - expected = ", ".join(i.__name__ for i in self.classes) - raise RuntimeError( - f"Unexpected element of type {type(i).__name__}" - f" in {self._qualname} on {obj._short_repr_()}," - f" expected one of: {expected}" - ) - if self.alternate is not None: - assert isinstance(rv, _obj.ElementList) - assert not isinstance(rv, _obj.MixedElementList) - rv._elemclass = self.alternate - return rv + elts = list(loader.iterchildren(obj._element, self.role_tag)) + return self.list_type( + obj._model, + elts, + self.alternate, + parent=obj, + **self.list_extra_args, + ) def __set__( self, obj: _obj.ModelObject, - value: ( - str - | _obj.ModelElement - | NewObject - | cabc.Iterable[str | _obj.ModelElement | NewObject] - ), + value: cabc.Iterable[str | T_co | NewObject], ) -> None: - if self.aslist: - raise NotImplementedError( - "Setting lists of model objects is not supported yet" + if isinstance(value, str) or not isinstance(value, cabc.Iterable): + warnings.warn( + ( + "Assigning a single value onto Containment is deprecated." + f" If {self._qualname!r} is supposed to use single values," + " wrap it in a 'm.Single()'." + " Otherwise, update your code to assign a list instead." + ), + DeprecationWarning, + stacklevel=2, ) + value = (value,) - 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 (elem := self.__get__(obj)) is not None: - valueclass, _ = self._match_xtype(value._type_hint) - elemclass = elem.__class__ - if elemclass is not valueclass: - obj._model._loader.idcache_remove(elem._element) - obj._element.remove(elem._element) - else: - for k, v in value._kw.items(): - setattr(elem, k, v) - return - self._create(obj, self.role_tag, value._type_hint, **value._kw) + current = self.__get__(obj) + previous = {id(i): i for i in current} - def create( - self, - elmlist: _obj.ElementListCouplingMixin, - typehint: str | None = None, - /, - **kw: t.Any, - ) -> _obj.ModelElement: - return self._create(elmlist._parent, self.role_tag, typehint, **kw) + for i in value: + current.append(i) + if hasattr(i, "_element"): + previous.pop(id(i._element), None) + for i in previous.values(): + current.remove(i) def insert( self, elmlist: _obj.ElementListCouplingMixin, index: int, - value: _obj.ModelObject | NewObject, - ) -> None: - if isinstance(value, NewObject): - raise NotImplementedError("Cannot insert new objects yet") - if value._model is not elmlist._model: + value: T_co | NewObject, + ) -> T_co: + if self.role_tag is None: + raise RuntimeError( + f"{type(self).__name__} was not initialized properly;" + " make sure that __set_name__ gets called" + ) + + if self.alternate is not None: + raise NotImplementedError( + "Cannot mutate lists with 'alternate' set" + ) + + if ( + isinstance(value, _obj.ModelObject) + and value._model is not elmlist._model + ): raise ValueError("Cannot move elements between models") try: indexof = elmlist._parent._element.index @@ -1883,8 +2770,35 @@ def insert( parent_index = index except ValueError: parent_index = len(elmlist._parent._element) + + if isinstance(value, NewObject): + if not value._type_hint: + cls = self._guess_xtype(elmlist._model) + else: + cls = self._match_xtype(elmlist._model, value._type_hint) + attrs = dict(value._kw) + if not hasattr(cls, "__capella_namespace__"): + raise TypeError( + f"Invalid class without associated namespace: {cls!r}" + ) + with elmlist._model._loader.new_uuid( + elmlist._parent._element, want=attrs.pop("uuid", None) + ) as uuid: + value = cls( + elmlist._model, + elmlist._parent._element, + self.role_tag, + uuid=uuid, + **attrs, + ) + + else: + assert isinstance(value, _obj.ModelObject) + value._element.tag = self.role_tag + elmlist._parent._element.insert(parent_index, value._element) elmlist._model._loader.idcache_index(value._element) + return value def delete( self, @@ -1902,10 +2816,12 @@ def delete( if elm.get("id") is None: continue - obj = _obj.ModelElement.from_model(model, elm) + obj = _obj.wrap_xml(model, elm) for ref, attr, _ in model.find_references(obj): acc = getattr(type(ref), attr) - if acc is self or not isinstance(acc, WritableAccessor): + if acc is self or not isinstance( + acc, WritableAccessor | Relationship | Single + ): continue stack.enter_context(acc.purge_references(ref, obj)) @@ -1922,26 +2838,23 @@ def purge_references( del obj, target yield - def _match_xtype(self, hint: str, /) -> tuple[type[_obj.ModelObject], str]: - if not self.classes: - return super()._match_xtype(hint) + def _match_xtype( + self, model: capellambse.MelodyModel, hint: str, / + ) -> type[T_co]: + cls = model.resolve_class(hint) + if cls is _obj.ModelElement: + raise TypeError(f"Cannot insert elements into {self._qualname!r}") + return t.cast(type[T_co], cls) - cls = next((i for i in self.classes if i.__name__ == hint), None) - if cls is None: - raise ValueError(f"Invalid class for {self._qualname}: {hint}") - return cls, _xtype.build_xtype(cls) + def _guess_xtype(self, model: capellambse.MelodyModel) -> type[T_co]: + return t.cast(type[T_co], model.resolve_class(self.class_)) - def _guess_xtype(self) -> tuple[type[_obj.ModelObject], str]: - if len(self.classes) == 1: - return self.classes[0], _xtype.build_xtype(self.classes[0]) + def _resolve_super_attributes( + self, super_acc: Accessor[t.Any] | None + ) -> None: + assert isinstance(super_acc, Containment | None) - if len(self.classes) > 1: - hint = ", valid values: " + ", ".join( - i.__name__ for i in self.classes - ) - else: - hint = "" - raise ValueError(f"{self._qualname} requires a type hint{hint}") + super()._resolve_super_attributes(super_acc) def no_list( @@ -1963,13 +2876,31 @@ def no_list( class_ The ``ModelElement`` subclass to instantiate """ + del class_ + if not elems: # pragma: no cover return None if len(elems) > 1: # pragma: no cover raise RuntimeError( f"Expected 1 object for {desc._qualname}, got {len(elems)}" ) - return class_.from_model(model, elems[0]) + return _obj.wrap_xml(model, elems[0]) + + +def make_coupled_list_type( + parent: Accessor[t.Any], +) -> type[_obj.ElementListCouplingMixin]: + list_type: type[_obj.ElementListCouplingMixin] = type( + "CoupledElementList", + (_obj.ElementListCouplingMixin, _obj.ElementList), + {}, + ) + list_type._accessor = parent + list_type.__module__ = __name__ + return list_type from . import _obj + +# HACK to make _Specification objects cooperate in the new world order +_Specification.__capella_namespace__ = _obj.NS diff --git a/capellambse/model/_meta.py b/capellambse/model/_meta.py new file mode 100644 index 000000000..533719961 --- /dev/null +++ b/capellambse/model/_meta.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +__all__ = [ + "Metadata", + "Viewpoint", + "ViewpointReferences", +] + +from . import _obj + +NS = _obj.Namespace( + "http://www.polarsys.org/kitalpha/ad/metadata/1.0.0", + "metadata", + _obj.CORE_VIEWPOINT, +) + + +class Metadata(_obj.ModelElement): + """Metadata about a Capella model. + + This class stores metadata about a Capella model, such as the + Capella version that was used to create it, and the active + viewpoints and their versions. It is tightly coupled to its parent + :class:`Model` instance. + """ + + +class Viewpoint(_obj.ModelElement): + """A viewpoint.""" + + +class ViewpointReferences: ... diff --git a/capellambse/model/_model.py b/capellambse/model/_model.py index 28ac0f736..a1c87cdec 100644 --- a/capellambse/model/_model.py +++ b/capellambse/model/_model.py @@ -10,6 +10,7 @@ import logging import os import typing as t +import warnings from lxml import etree @@ -17,7 +18,7 @@ import capellambse.helpers from capellambse import _diagram_cache, aird, filehandler, loader -from . import _descriptors, _obj, _xtype, diagram +from . import _descriptors, _obj, diagram # `pathlib` is referenced by the `dataclasses` auto-generated init. # Sphinx-autodoc-typehints needs this import here to resolve the forward ref. @@ -39,10 +40,13 @@ class MelodyModel: def project(self) -> capellambse.metamodel.capellamodeller.Project: import capellambse.metamodel as mm - target_xt = { - _xtype.build_xtype(mm.capellamodeller.Project), - _xtype.build_xtype(mm.capellamodeller.Library), - } + cls_project = self.resolve_class((mm.capellamodeller.NS, "Project")) + if __debug__: + cls_library = self.resolve_class( + (mm.capellamodeller.NS, "Library") + ) + assert issubclass(cls_library, cls_project) + roots: list[etree._Element] = [] for fname, frag in self._loader.trees.items(): if fname.parts[0] != "\x00": @@ -53,7 +57,14 @@ def project(self) -> capellambse.metamodel.capellamodeller.Project: elem = next(iter(elem)) except StopIteration: continue - if capellambse.helpers.xtype_of(elem) not in target_xt: + qname = loader.qtype_of(elem) + if qname is None: + continue + try: + elemcls = self.resolve_class(qname) + except (_obj.UnknownNamespaceError, _obj.MissingClassError): + continue + if not issubclass(elemcls, cls_project): continue roots.append(elem) @@ -61,7 +72,7 @@ def project(self) -> capellambse.metamodel.capellamodeller.Project: raise RuntimeError("No root project or library found") if len(roots) > 1: raise RuntimeError(f"Found {len(roots)} Project/Library objects") - obj = _obj.ModelElement.from_model(self, roots[0]) + obj = _obj.wrap_xml(self, roots[0]) assert isinstance(obj, mm.capellamodeller.Project) return obj @@ -259,6 +270,7 @@ def __init__( capellambse.load_model_extensions() self._loader = loader.MelodyLoader(path, **kwargs) + self.__viewpoints = dict(self._loader.referenced_viewpoints()) self._fallback_render_aird = fallback_render_aird if diagram_cache: @@ -308,59 +320,110 @@ def save(self, **kw: t.Any) -> None: def search( self, - *xtypes: str | type[_obj.ModelObject], + *clsnames: str | type[_obj.ModelObject] | _obj.UnresolvedClassName, below: _obj.ModelElement | None = None, ) -> _obj.ElementList: - r"""Search for all elements with any of the given ``xsi:type``\ s. - - If only one xtype is given, the return type will be - :class:`~capellambse.model.ElementList`, otherwise it will be - :class:`~capellambse.model.MixedElementList`. - - If no ``xtypes`` are given at all, this method will return an - exhaustive list of all (semantic) model objects that have an - ``xsi:type`` set. + """Search for all elements with any of the given types. Parameters ---------- - xtypes - The ``xsi:type``\ s to search for, or the classes - corresponding to them (or a mix of both). + clsnames + The classes to search for. Instances of subclasses of any of + the given types will also be returned. If no class is + specified, all model elements are returned. + + Each element can be: + + - A string containing the name of a class. All namespaces + belonging to an activated viewpoint are searched for + classes with that name. + - A "namespace.alias:ClassName" string. + - A (Namespace, 'ClassName') tuple. + - A ('namespace.alias', 'ClassName') tuple. + - A Class object. It must have the ``__capella_namespace__`` + attribute set to its namespace, which is automatically + done for subclasses of + :class:`~capellambse.metamodel.capellamodeller.ModelElement`. + + .. note:: + + This method treats the 'capellamodeller:ModelElement' + class as the superclass of every concrete model element + class. This means that any search query including it will + return all model elements regardless of their type. below A model element to constrain the search. If given, only those elements will be returned that are (immediate or nested) children of this element. This option takes into account model fragmentation, but it does not treat link elements specially. + + Notes + ----- + For performance reasons, this method only takes into account + semantic fragments and diagram descriptors. + + Examples + -------- + The following calls are functionally identical, and will all + return a list of every Logical Component in the model: + + >>> model.search("LogicalComponent") + >>> model.search("org.polarsys.capella.core.data.la:LogicalComponent") + >>> model.search( (capellambse.metamodel.la.NS, "LogicalComponent") ) + >>> model.search( ("org.polarsys.capella.core.data.la", "LogicalComponent") ) """ - xtypes_: list[str] = [] - for xtype in xtypes: - if isinstance(xtype, type): - xtypes_.append(_xtype.build_xtype(xtype)) - elif ":" in xtype: - xtypes_.append(xtype) - elif xtype in {"GenericElement", "ModelElement", "ModelObject"}: - xtypes_.clear() - break + classes: set[type[_obj.ModelObject]] = set() + for clsname in clsnames: + if isinstance(clsname, type): + if not hasattr(clsname, "__capella_namespace__"): + raise TypeError( + f"Class does not belong to a namespace: {clsname!r}" + ) + resolved = self.resolve_class( + (clsname.__capella_namespace__, clsname.__name__) + ) + elif clsname == "GenericElement": + warnings.warn( + "GenericElement has been renamed to ModelElement", + DeprecationWarning, + stacklevel=2, + ) + resolved = _obj.ModelElement + elif clsname == "ModelObject": + resolved = _obj.ModelElement else: - suffix = ":" + xtype - matching_types: list[str] = [] - for i in _xtype.XTYPE_HANDLERS.values(): - matching_types.extend(c for c in i if c.endswith(suffix)) - if not matching_types: - raise ValueError(f"Unknown incomplete type name: {xtype}") - xtypes_.extend(matching_types) - - cls = (_obj.MixedElementList, _obj.ElementList)[len(xtypes_) == 1] - - trees = { - k - for k, v in self._loader.trees.items() - if v.fragment_type is loader.FragmentType.SEMANTIC - } - matches = self._loader.iterall_xt(*xtypes_, trees=trees) - - if not xtypes_ or "viewpoint:DRepresentationDescriptor" in xtypes_: + resolved = self.resolve_class(clsname) + if resolved is _obj.ModelElement: + classes.clear() + break + classes.add(resolved) + + trees = [ + t + for t in self._loader.trees.values() + if t.fragment_type is loader.FragmentType.SEMANTIC + ] + matches: cabc.Iterable[etree._Element] + if not classes: + matches = itertools.chain.from_iterable( + tree.iterall() for tree in trees + ) + else: + matches = [] + for tree in trees: + for qtype in tree.iter_qtypes(): + try: + cls = self.resolve_class(qtype) + except ( + _obj.UnknownNamespaceError, + _obj.MissingClassError, + ): + continue + if any(issubclass(cls, i) for i in classes): + matches.extend(tree.iter_qtype(qtype)) + + if not classes or diagram.Diagram in classes: matches = itertools.chain( matches, aird.enumerate_descriptors(self._loader), @@ -372,11 +435,19 @@ def search( for i in matches if below._element in self._loader.iterancestors(i) ) - return cls(self, list(matches), _obj.ModelElement) + seen: set[int] = set() + elements: list[etree._Element] = [] + for elem in matches: + if id(elem) not in seen: + elements.append(elem) + seen.add(id(elem)) + else: + LOGGER.warning("Found element twice (bad caches?): %r", elem) + return _obj.ElementList(self, elements, _obj.ModelElement) def by_uuid(self, uuid: str) -> t.Any: """Search the entire model for an element with the given UUID.""" - return _obj.ModelElement.from_model(self, self._loader[uuid]) + return _obj.wrap_xml(self, self._loader[uuid]) def find_references( self, target: _obj.ModelObject | str, / @@ -420,7 +491,7 @@ def find_references( if i.fragment_type != loader.FragmentType.VISUAL ], ): - obj = _obj.ModelElement.from_model(self, elem) + obj = _obj.wrap_xml(self, elem) for attr in _reference_attributes(type(obj)): if attr.startswith("_"): continue @@ -606,11 +677,108 @@ def description_badge(self) -> str: return metrics.get_summary_badge(self) def referenced_viewpoints(self) -> dict[str, str]: - return dict(self._loader.referenced_viewpoints()) + return self.__viewpoints.copy() def activate_viewpoint(self, name: str, version: str) -> None: + self.__viewpoints[name] = version self._loader.activate_viewpoint(name, version) + def resolve_class( + self, + typehint: ( + str | _obj.UnresolvedClassName | etree.QName | etree._Element + ), + ) -> type[_obj.ModelObject]: + """Resolve a class based on a type hint. + + The type hint can be any one of: + + - A string like ``'namespace.alias:ClassName'``, as used in the + ``xsi:type`` XML attribute + - A string like ``'{http://name/space/url}ClassName'``, as used + in XML tags for fragment roots. For versioned namespaces, + the version included in the URL is ignored. Future versions + may raise an error if the version doesn't match the activated + viewpoint. + - A :class:`~lxml.etree.QName` object, as obtained by + :func:`~capellambse.loader.qtype_of`. + - An :class:`~lxml.etree._Element` object. + - A simple ClassName string, which will be searched across all + namespaces. It is an error if multiple namespaces provide a + class with that name; to avoid such errors, always use a form + that explicitly provides the namespace. + - A tuple of ``('name.space.alias', 'ClassName')`` + - A tuple of ``(NamespaceInstance, 'ClassName')`` + """ + if isinstance(typehint, etree._Element): + qtype = loader.qtype_of(typehint) + if qtype is None: + raise ValueError( + f"Element is not a proper model element: {typehint!r}" + ) + typehint = qtype + + if isinstance(typehint, etree.QName): + ns, _ = _obj.find_namespace_by_uri(typehint.namespace) + clsname = typehint.localname + + elif isinstance(typehint, str): + if typehint.startswith("{"): + qn = etree.QName(typehint) + ns, _ = _obj.find_namespace_by_uri(qn.namespace) + clsname = qn.localname + elif ":" in typehint: + nsalias, clsname = typehint.rsplit(":", 1) + ns = _obj.find_namespace(nsalias) + else: + clsname = typehint + providers = [ + i for i in _obj.enumerate_namespaces() if clsname in i + ] + if len(providers) < 1: + raise _obj.MissingClassError(None, None, clsname) + if len(providers) > 1: + raise ValueError( + f"Multiple namespaces providing class {clsname!r}:" + f" {', '.join(i.alias for i in providers)}" + ) + (ns,) = providers + + elif isinstance(typehint, tuple) and len(typehint) == 2: + clsname = typehint[1] + if isinstance(typehint[0], str): + ns = _obj.find_namespace(typehint[0]) + else: + ns = typehint[0] + + else: + raise TypeError( + f"Invalid typehint, expected a str or 2-tuple: {typehint!r}" + ) + + if ns.viewpoint is None or "{VERSION}" not in ns.uri: + return ns.get_class(clsname) + viewpoint = self.referenced_viewpoints().get(ns.viewpoint) + if viewpoint is None: + raise RuntimeError( + f"Required viewpoint not activated: {viewpoint!r}" + ) + return ns.get_class(clsname, viewpoint) + + def qualify_classname(self, cls: _obj.ClassName, /) -> etree.QName: + """Qualify a ClassName based on the activated viewpoints.""" + ns, clsname = cls + if "{VERSION}" not in ns.uri or not ns.viewpoint: + return etree.QName(ns.uri, clsname) + + vp = self.referenced_viewpoints().get(ns.viewpoint) + if vp is None: + raise _descriptors.InvalidModificationError( + f"Required viewpoint is not activated: {ns.viewpoint}" + ) + vp = ns.trim_version(vp) + return etree.QName(ns.uri.format(VERSION=vp), clsname) + if t.TYPE_CHECKING: def __getattr__(self, attr: str) -> t.Any: @@ -633,6 +801,7 @@ def _reference_attributes( objtype: type[_obj.ModelObject], / ) -> tuple[str, ...]: ignored_accessors: tuple[type[_descriptors.Accessor], ...] = ( + _descriptors.AlternateAccessor, _descriptors.DeepProxyAccessor, _descriptors.DeprecatedAccessor, _descriptors.ParentAccessor, @@ -644,8 +813,17 @@ def _reference_attributes( if i.startswith("_") or i == "parent": continue acc = getattr(objtype, i, None) - if isinstance(acc, _descriptors.Accessor) and not isinstance( - acc, ignored_accessors + if ( + isinstance(acc, _descriptors.Relationship) + or ( + isinstance(acc, _descriptors.Single) + and isinstance(acc.wrapped, _descriptors.Relationship) + ) + # TODO remove checks for deprecated Accessor classes + or ( + isinstance(acc, _descriptors.Accessor) + and not isinstance(acc, ignored_accessors) + ) ): attrs.append(i) return tuple(attrs) diff --git a/capellambse/model/_obj.py b/capellambse/model/_obj.py index ced9c7c4f..6840d3f35 100644 --- a/capellambse/model/_obj.py +++ b/capellambse/model/_obj.py @@ -4,26 +4,45 @@ from __future__ import annotations __all__ = [ + "CORE_VIEWPOINT", "CachedElementList", + "ClassName", "ElementList", "ElementListCouplingMixin", "ElementListMapItemsView", "ElementListMapKeyView", + "MissingClassError", "MixedElementList", "ModelElement", "ModelObject", + "Namespace", + "UnresolvedClassName", + "enumerate_namespaces", + "find_namespace", + "find_namespace_by_uri", + "find_wrapper", + "resolve_class_name", + "wrap_xml", ] +import abc +import collections import collections.abc as cabc import contextlib +import dataclasses import enum +import functools +import importlib +import importlib.metadata as imm import inspect import logging import operator import re import textwrap import typing as t +import warnings +import awesomeversion as av import markupsafe import typing_extensions as te from lxml import etree @@ -31,9 +50,10 @@ import capellambse from capellambse import helpers -from . import T, U, _descriptors, _pods, _styleclass, _xtype +from . import VIRTUAL_NAMESPACE_PREFIX, T, U, _descriptors, _pods, _styleclass LOGGER = logging.getLogger(__name__) +CORE_VIEWPOINT = "org.polarsys.capella.core.viewpoint" _NOT_SPECIFIED = object() @@ -47,6 +67,267 @@ _ICON_CACHE: dict[tuple[str, str, int], t.Any] = {} +UnresolvedClassName: te.TypeAlias = "tuple[str | Namespace, str]" +"""A tuple of namespace URI and class name.""" + +ClassName: te.TypeAlias = "tuple[Namespace, str]" +"""A tuple of Namespace object and class name.""" + + +class UnknownNamespaceError(KeyError): + """Raised when a requested namespace cannot be found.""" + + def __init__(self, name: str, /) -> None: + super().__init__(name) + + @property + def name(self) -> str: + """The name or URI that was searched for.""" + return self.args[0] + + def __str__(self) -> str: + return f"Namespace not found: {self.name}" + + +class MissingClassError(KeyError): + """Raised when a requested class is not found.""" + + def __init__( + self, + ns: Namespace | None, + nsver: av.AwesomeVersion | str | None, + clsname: str, + ) -> None: + if isinstance(nsver, str): + nsver = av.AwesomeVersion(nsver) + super().__init__(ns, nsver, clsname) + + @property + def ns(self) -> Namespace | None: + """The namespace that was searched, or None for all namespaces.""" + return self.args[0] + + @property + def ns_version(self) -> av.AwesomeVersion | None: + """The namespace version, if the namespace is versioned.""" + return self.args[1] + + @property + def clsname(self) -> str: + """The class name that was searched for.""" + return self.args[2] + + def __str__(self) -> str: + if self.ns and self.ns_version: + return ( + f"No class {self.clsname!r} in v{self.ns_version} of" + f" namespace {self.ns.uri!r}" + ) + if self.ns: + return f"No class {self.clsname!r} in namespace {self.ns.uri!r}" + return f"No class {self.clsname!r} found in any known namespace" + + +@dataclasses.dataclass(init=False, frozen=True) +class Namespace: + """The interface between the model and a namespace containing classes. + + Instances of this class represent the different namespaces used to + organize types of Capella model objects. They are also the entry + point into the namespace when a loaded model has to interact with + it, e.g. for looking up a class to load or create. + + For a more higher-level overview of the interactions, and how to + make use of this and related classes, read the documentation on + `Extending the metamodel <_model-extensions>`__. + + Parameters + ---------- + uri + The URI of the namespace. This is used to identify the + namespace in the XML files. It usually looks like a URL, but + does not have to be one. + alias + The preferred alias of the namespace. This is the type name + prefix used in an XML file. + + If the preferred alias is not available because another + namespace already uses it, a numeric suffix will be appended + to the alias to make it unique. + maxver + The maximum version of the namespace that is supported by this + implementation. If a model uses a higher version, it cannot be + loaded and an exception will be raised. + """ + + uri: str + alias: str + viewpoint: str | None + maxver: av.AwesomeVersion | None + version_precision: int + """Number of significant parts in the version number for namespaces. + + When qualifying a versioned namespace based on the model's activated + viewpoint, only use this many components for the namespace URL. + Components after that are set to zero. + + Example: A viewpoint version of "1.2.3" with a version precision of + 2 will result in the namespace version "1.2.0". + """ + + def __init__( + self, + uri: str, + alias: str, + viewpoint: str | None = None, + maxver: str | None = None, + *, + version_precision: int = 1, + ) -> None: + if version_precision <= 0: + raise ValueError("Version precision cannot be negative") + + object.__setattr__(self, "uri", uri) + object.__setattr__(self, "alias", alias) + object.__setattr__(self, "viewpoint", viewpoint) + object.__setattr__(self, "version_precision", version_precision) + + is_versioned = "{VERSION}" in uri + if is_versioned and maxver is None: + raise TypeError( + "Versioned namespaces must declare their supported 'maxver'" + ) + if not is_versioned and maxver is not None: + raise TypeError( + "Unversioned namespaces cannot declare a supported 'maxver'" + ) + + if maxver is not None: + maxver = av.AwesomeVersion(maxver) + object.__setattr__(self, "maxver", maxver) + else: + object.__setattr__(self, "maxver", None) + + clstuple: te.TypeAlias = """tuple[ + type[ModelObject], + av.AwesomeVersion, + av.AwesomeVersion | None, + ]""" + self._classes: dict[str, list[clstuple]] + object.__setattr__(self, "_classes", collections.defaultdict(list)) + + def match_uri(self, uri: str) -> bool | av.AwesomeVersion | None: + """Match a (potentially versioned) URI against this namespace. + + The return type depends on whether this namespace is versioned. + + Unversioned Namespaces return a simple boolean flag indicating + whether the URI exactly matches this Namespace. + + Versioned Namespaces return one of: + + - ``False``, if the URI did not match + - ``None``, if the URI did match, but the version field was + empty or the literal ``{VERSION}`` placeholder + - Otherwise, an :class:~`awesomeversion.AwesomeVersion` object + with the version number contained in the URL + + Values other than True and False can then be passed on to + :meth:`get_class`, to obtain a class object appropriate for the + namespace and version described by the URI. + """ + if "{VERSION}" not in self.uri: + return uri == self.uri + + prefix, _, suffix = self.uri.partition("{VERSION}") + if ( + len(uri) >= len(prefix) + len(suffix) + and uri.startswith(prefix) + and uri.endswith(suffix) + ): + v = uri[len(prefix) : -len(suffix) or None] + if "/" in v: + return False + if v in ("", "{VERSION}"): + return None + return self.trim_version(v) + + return False + + def get_class( + self, clsname: str, version: str | None = None + ) -> type[ModelObject]: + if "{VERSION}" in self.uri and not version: + raise TypeError( + f"Versioned namespace, but no version requested: {self.uri}" + ) + + classes = self._classes.get(clsname) + if not classes: + raise MissingClassError(self, version, clsname) + + eligible: list[tuple[av.AwesomeVersion, type[ModelObject]]] = [] + for cls, minver, maxver in classes: + if version and (version < minver or (maxver and version > maxver)): + continue + eligible.append((minver, cls)) + + if not eligible: + raise MissingClassError(self, version, clsname) + eligible.sort(key=lambda i: i[0], reverse=True) + return eligible[0][1] + + def register( + self, + cls: type[ModelObject], + minver: str | None, + maxver: str | None, + ) -> None: + if cls.__capella_namespace__ is not self: + raise ValueError( + f"Cannot register class {cls.__name__!r}" + f" in Namespace {self.uri!r}," + f" because it belongs to {cls.__capella_namespace__.uri!r}" + ) + + classes = self._classes[cls.__name__] + if minver is not None: + minver = av.AwesomeVersion(minver) + else: + minver = av.AwesomeVersion(0) + if maxver is not None: + maxver = av.AwesomeVersion(maxver) + classes.append((cls, minver, maxver)) + + def trim_version( + self, version: str | av.AwesomeVersion, / + ) -> av.AwesomeVersion: + assert self.version_precision > 0 + pos = dots = 0 + while pos < len(version) and dots < self.version_precision: + try: + pos = version.index(".", pos) + 1 + except ValueError: + return av.AwesomeVersion(version) + else: + dots += 1 + trimmed = version[:pos] + re.sub(r"[^.]+", "0", version[pos:]) + return av.AwesomeVersion(trimmed) + + def __contains__(self, clsname: str) -> bool: + """Return whether this Namespace has a class with the given name.""" + return clsname in self._classes + + +NS = Namespace( + "http://www.polarsys.org/capella/common/core/{VERSION}", + "org.polarsys.capella.common.data.core", + CORE_VIEWPOINT, + "7.0.0", +) + + +@t.runtime_checkable class ModelObject(t.Protocol): """A class that wraps a specific model object. @@ -58,6 +339,8 @@ class ModelObject(t.Protocol): mentioned special cases. """ + __capella_namespace__: t.ClassVar[Namespace] + @property def _model(self) -> capellambse.MelodyModel: ... @@ -89,14 +372,133 @@ def __init__( (commonly e.g. ``uuid``). """ - @classmethod - def from_model( - cls, model: capellambse.MelodyModel, element: t.Any - ) -> ModelObject: - """Instantiate a ModelObject from existing model elements.""" +class _ModelElementMeta(abc.ABCMeta): + def __setattr__(cls, attr, value): + super().__setattr__(attr, value) + setname = getattr(value, "__set_name__", None) + if setname is not None: + setname(cls, attr) + + def __new__( + mcs, + name: str, + bases: tuple[type, ...], + namespace: dict[str, t.Any], + *, + ns: Namespace | None = None, + minver: str | None = None, + maxver: str | None = None, + eq: str | None = None, + abstract: bool = False, + ) -> type[ModelElement]: + """Create a new model object class. + + This method automatically registers the class with the + namespace, taking care of the ``minver`` and ``maxver`` + constraints. -class ModelElement: + Parameters + ---------- + name + The name of the class. + bases + The base classes of the class. + namespace + The class' namespace, as defined by Python. + ns + The metamodel namespace to register the class in. If not + specified, the namespace is looked up in the module that + defines the class. + minver + The minimum version of the namespace that this class is + compatible with. If not specified, the minimum version is + assumed to be 0. + maxver + The maximum version of the namespace that this class is + compatible with. If not specified, there is no maximum + version. + eq + When comparing instances of this class with non-model + classes, this attribute is used to determine equality. If + not specified, the standard Python equality rules apply. + abstract + Mark the class as abstract. Only subclasses of abstract + classes can be instantiated, not the abstract class itself. + """ + del abstract # FIXME prohibit instantiation of abstract classes + + if "__capella_namespace__" in namespace: + raise TypeError( + f"Cannot create class {name!r}:" + " Invalid declaration of __capella_namespace__ in class body" + ) + + if ( + "_xmltag" in namespace + and namespace["_xmltag"] is not None + and not namespace["__module__"].startswith("capellambse.") + ): + warnings.warn( + ( + "The '_xmltag' declaration in the class body is deprecated," + " define a Containment on the containing class instead" + ), + DeprecationWarning, + stacklevel=2, + ) + + if eq is not None: + if "__eq__" in namespace: + raise TypeError( + f"Cannot generate __eq__ for {name!r}:" + f" method already defined in class body" + ) + + def __eq__(self, other): + if not isinstance(other, ModelElement): + value = getattr(self, eq) # type: ignore[arg-type] + return value.__eq__(other) + return super(cls, self).__eq__(other) # type: ignore[misc] + + namespace["__eq__"] = __eq__ + + if ns is None: + modname: str = namespace["__module__"] + cls_mod = importlib.import_module(modname) + auto_ns = getattr(cls_mod, "NS", None) + if not isinstance(auto_ns, Namespace): + raise TypeError( + f"Cannot create class {name!r}: No namespace\n" + "\n" + f"No Namespace found at {modname}.NS,\n" + "and no `ns` passed explicitly while subclassing.\n" + "\n" + "Declare a module-wide namespace with:\n" + "\n" + " from capellambse import ModelElement, Namespace\n" + " NS = Namespace(...)\n" + f" class {name}(ModelElement): ...\n" + "\n" + "Or specify it explicitly for each class:\n" + "\n" + " from capellambse import ModelElement, Namespace\n" + " MY_NS = Namespace(...)\n" + f" class {name}(ModelElement, ns=MY_NS): ...\n" + ) + ns = auto_ns + + assert isinstance(ns, Namespace) + namespace["__capella_namespace__"] = ns + cls = t.cast( + "type[ModelElement]", + super().__new__(mcs, name, bases, namespace), + ) + ns.register(cls, minver=minver, maxver=maxver) + return cls + + +class ModelElement(metaclass=_ModelElementMeta): """A model element. This is the common base class for all elements of a model. In terms @@ -105,6 +507,8 @@ class ModelElement: to any superclass should be modified to use this class instead. """ + __capella_namespace__: t.ClassVar[Namespace] + uuid = _pods.StringPOD("id", writable=False) """The universally unique identifier of this object. @@ -133,9 +537,12 @@ class ModelElement: lambda self: self._model.diagrams.by_semantic_nodes(self) ) - parent: _descriptors.ParentAccessor - constraints: _descriptors.Accessor - property_value_packages: _descriptors.Accessor + parent = _descriptors.ParentAccessor() + constraints: _descriptors.Accessor[ElementList[t.Any]] + property_values: _descriptors.Accessor[ElementList[t.Any]] + property_value_groups: _descriptors.Accessor[ElementList[t.Any]] + applied_property_values: _descriptors.Accessor[ElementList[t.Any]] + applied_property_value_groups: _descriptors.Accessor[ElementList[t.Any]] _required_attrs = frozenset({"uuid", "xtype"}) _xmltag: str | None = None @@ -167,7 +574,7 @@ def progress_status(self) -> str: if uuid is None: return "NOT_SET" - return self.from_model(self._model, self._model._loader[uuid]).name + return wrap_xml(self._model, self._model._loader[uuid]).name @classmethod def from_model( @@ -188,18 +595,7 @@ def from_model( An instance of ModelElement (or a more appropriate subclass, if any) that wraps the given XML element. """ - class_ = cls - if class_ is ModelElement: - xtype = helpers.xtype_of(element) - if xtype is not None: - with contextlib.suppress(KeyError): - class_ = _xtype.XTYPE_HANDLERS[None][xtype] - if class_ is not cls: - return class_.from_model(model, element) - self = class_.__new__(class_) - self._model = model - self._element = element - return self + return wrap_xml(model, element, cls) @property def layer(self) -> capellambse.metamodel.cs.ComponentArchitecture: @@ -232,6 +628,7 @@ def __init__( /, *, uuid: str, + xtype: str | None = None, **kw: t.Any, ) -> None: all_required_attrs: set[str] = set() @@ -239,7 +636,7 @@ def __init__( all_required_attrs |= getattr( basecls, "_required_attrs", frozenset() ) - missing_attrs = all_required_attrs - frozenset(kw) - {"uuid"} + missing_attrs = all_required_attrs - frozenset(kw) - {"uuid", "xtype"} if missing_attrs: mattrs = ", ".join(sorted(missing_attrs)) raise TypeError(f"Missing required keyword arguments: {mattrs}") @@ -251,23 +648,35 @@ def __init__( raise TypeError( f"Cannot instantiate {type(self).__name__} directly" ) + + fragment_name = model._loader.find_fragment(parent) + fragment = model._loader.trees[fragment_name] + self._model = model self._element: etree._Element = etree.Element(xmltag) parent.append(self._element) try: self.uuid = uuid + if xtype is not None: + warnings.warn( + "Passing 'xtype' during ModelElement construction is deprecated and no longer needed", + DeprecationWarning, + stacklevel=2, + ) + ns = self.__capella_namespace__ + qtype = model.qualify_classname((ns, type(self).__name__)) + assert qtype.namespace is not None + fragment.add_namespace(qtype.namespace, ns.alias) + self._element.set(helpers.ATT_XT, qtype) for key, val in kw.items(): - if key == "xtype": - self._element.set(helpers.ATT_XT, val) - elif not isinstance( + if not isinstance( getattr(type(self), key), _descriptors.Accessor | _pods.BasePOD, ): raise TypeError( f"Cannot set {key!r} on {type(self).__name__}" ) - else: - setattr(self, key, val) + setattr(self, key, val) self._model._loader.idcache_index(self._element) except BaseException: parent.remove(self._element) @@ -316,10 +725,16 @@ def __repr__(self) -> str: # pragma: no cover continue acc = getattr(type(self), attr, None) + backref: str | None = None if isinstance(acc, _descriptors.Backref): - classes = ", ".join(i.__name__ for i in acc.target_classes) + backref = str(acc.class_[1]) + elif isinstance(acc, _descriptors.Single) and isinstance( + acc.wrapped, _descriptors.Backref + ): + backref = str(acc.wrapped.class_[1]) + if backref: attrs.append( - f".{attr} = ... # backreference to {classes}" + f".{attr} = ... # backreference to {backref}" " - omitted: can be slow to compute" ) continue @@ -406,12 +821,18 @@ def __html__(self) -> markupsafe.Markup: continue acc = getattr(type(self), attr, None) + backref: str | None = None if isinstance(acc, _descriptors.Backref): - classes = ", ".join(i.__name__ for i in acc.target_classes) + backref = escape(acc.class_[1]) + elif isinstance(acc, _descriptors.Single) and isinstance( + acc.wrapped, _descriptors.Backref + ): + backref = escape(acc.wrapped.class_[1]) + if backref: fragments.append('') fragments.append(escape(attr)) fragments.append('') - fragments.append(f"Backreference to {escape(classes)}") + fragments.append(f"Backreference to {backref}") fragments.append(" - omitted: can be slow to compute.") fragments.append(" Display this property directly to show.") fragments.append("\n") @@ -556,6 +977,21 @@ def __init__( assert None not in elements self._model = model self._elements = elements + + if ( + __debug__ + and elemclass is not None + and elemclass is not ModelElement + ): + for i, e in enumerate(self._elements): + ecls = model.resolve_class(e) + if not issubclass(elemclass, ecls): + raise TypeError( + f"BUG: Configured elemclass {elemclass.__name__!r}" + f" is not a subclass of {ecls.__name__!r}" + f" (found at index {i})" + ) + if elemclass is not None: self._elemclass = elemclass else: @@ -653,10 +1089,16 @@ def __getitem__(self, idx: int | slice | str) -> t.Any: if isinstance(idx, str): obj = self._map_find(idx) return self._map_getvalue(obj) - return self._elemclass.from_model(self._model, self._elements[idx]) + + if self._elemclass is not ModelElement: + obj = self._elemclass.__new__(self._elemclass) + obj._model = self._model # type: ignore[misc] + obj._element = self._elements[idx] # type: ignore[misc] + return obj + return wrap_xml(self._model, self._elements[idx], self._elemclass) @t.overload - def __setitem__(self, index: int, value: T) -> None: ... + def __setitem__(self, index: int, value: t.Any) -> None: ... @t.overload def __setitem__(self, index: slice, value: cabc.Iterable[T]) -> None: ... @t.overload @@ -830,7 +1272,9 @@ def get(self, key: str, default: t.Any = None) -> t.Any: except KeyError: return default - def insert(self, index: int, value: T) -> None: + def insert(self, index: int, value: t.Any) -> None: + if not isinstance(value, ModelObject): + raise TypeError("Cannot create elements: List is not coupled") elm: etree._Element = value._element self._elements.insert(index, elm) @@ -932,7 +1376,12 @@ def map(self, attr: str | _MapFunction[T]) -> ElementList: if len(classes) == 1: return ElementList(self._model, newelems, classes.pop()) - return MixedElementList(self._model, newelems) + return ElementList(self._model, newelems) + + if t.TYPE_CHECKING: + + def append(self, value: t.Any) -> None: ... + def extend(self, values: cabc.Iterable[t.Any]) -> None: ... class _ListFilter(t.Generic[T]): @@ -987,6 +1436,9 @@ def make_values_container(self, *values: t.Any) -> cabc.Iterable[t.Any]: return values def ismatch(self, element: T, valueset: cabc.Iterable[t.Any]) -> bool: + if self._attr == "type" and type(element).__name__ in valueset: + return self._positive + try: value = self.extract_key(element) except AttributeError: @@ -1154,7 +1606,7 @@ def __init__( Additional arguments are passed to the superclass. """ del elemclass - super().__init__(model, elements, ModelElement, **kw) + super().__init__(model, elements, None, **kw) def __getattr__(self, attr: str) -> _ListFilter[ModelElement]: if attr == "by_type": @@ -1220,7 +1672,7 @@ class ElementListCouplingMixin(ElementList[T], t.Generic[T]): modifications to the Accessor. """ - _accessor: _descriptors.WritableAccessor[T] + _accessor: _descriptors.Accessor[ElementList[T]] def is_coupled(self) -> bool: return True @@ -1232,8 +1684,6 @@ def __init__( fixed_length: int = 0, **kw: t.Any, ) -> None: - assert type(self)._accessor - super().__init__(*args, **kw) self._parent = parent self.fixed_length = fixed_length @@ -1246,8 +1696,14 @@ def __setitem__(self, index: slice, value: cabc.Iterable[T]) -> None: ... def __setitem__(self, index: str, value: t.Any) -> None: ... def __setitem__(self, index: int | slice | str, value: t.Any) -> None: assert self._parent is not None - accessor = type(self)._accessor - assert isinstance(accessor, _descriptors.WritableAccessor) + acc = type(self)._accessor + if not ( + isinstance(acc, _descriptors.WritableAccessor) + or hasattr(acc, "__set__") + ): + raise TypeError( + f"Parent accessor does not support overwriting: {acc!r}" + ) if not isinstance(index, int | slice): super().__setitem__(index, value) @@ -1261,19 +1717,25 @@ def __setitem__(self, index: int | slice | str, value: t.Any) -> None: f"Cannot set: List must stay at length {self.fixed_length}" ) - accessor.__set__(self._parent, new_objs) + acc.__set__(self._parent, new_objs) def __delitem__(self, index: int | slice) -> None: if self.fixed_length and len(self) <= self.fixed_length: raise TypeError("Cannot delete from a fixed-length list") assert self._parent is not None - accessor = type(self)._accessor - assert isinstance(accessor, _descriptors.WritableAccessor) + acc = type(self)._accessor + if not ( + isinstance(acc, _descriptors.WritableAccessor) + or hasattr(acc, "delete") + ): + raise TypeError( + f"Parent accessor does not support deleting items: {acc!r}" + ) if not isinstance(index, slice): index = slice(index, index + 1 or None) for obj in self[index]: - accessor.delete(self, obj) + acc.delete(self, obj) super().__delitem__(index) def _newlist_type(self) -> type[ElementList[T]]: @@ -1311,15 +1773,29 @@ def create(self, typehint: str | None = None, /, **kw: t.Any) -> T: assert self._parent is not None acc = type(self)._accessor - assert isinstance(acc, _descriptors.WritableAccessor) - newobj = acc.create(self, typehint, **kw) - try: - acc.insert(self, len(self), newobj) - super().insert(len(self), newobj) - except: - self._parent._element.remove(newobj._element) - raise - return newobj + if isinstance(acc, _descriptors.WritableAccessor): + newobj = acc.create(self, typehint, **kw) + try: + acc.insert(self, len(self), newobj) + super().insert(len(self), newobj) + except: + self._parent._element.remove(newobj._element) + raise + return newobj + + if hasattr(acc, "insert"): + newobj = _descriptors.NewObject(typehint or "", **kw) + value = acc.insert(self, len(self), newobj) + try: + super().insert(len(self), value) + except: + self._parent._element.remove(value._element) + raise + return value + + raise AssertionError( + f"Parent accessor does not support item insertion: {acc!r}" + ) def create_singleattr(self, arg: t.Any) -> T: """Make a new model object (instance of ModelElement). @@ -1339,22 +1815,252 @@ def create_singleattr(self, arg: t.Any) -> T: assert self._parent is not None acc = type(self)._accessor - assert isinstance(acc, _descriptors.WritableAccessor) - newobj = acc.create_singleattr(self, arg) - try: - acc.insert(self, len(self), newobj) - super().insert(len(self), newobj) - except: - self._parent._element.remove(newobj._element) - raise - return newobj - - def insert(self, index: int, value: T) -> None: + if isinstance(acc, _descriptors.WritableAccessor): + newobj = acc.create_singleattr(self, arg) + try: + acc.insert(self, len(self), newobj) + super().insert(len(self), newobj) + except: + self._parent._element.remove(newobj._element) + raise + return newobj + + single_attr = getattr(acc, "single_attr", None) + if not isinstance(single_attr, str): + raise TypeError("Cannot create object from a single attribute") + return self.create(**{single_attr: arg}) + + def insert(self, index: int, value: T | _descriptors.NewObject) -> None: if self.fixed_length and len(self) >= self.fixed_length: raise TypeError("Cannot insert into a fixed-length list") assert self._parent is not None acc = type(self)._accessor - assert isinstance(acc, _descriptors.WritableAccessor) - acc.insert(self, index, value) + + if isinstance(acc, _descriptors.WritableAccessor): + if isinstance(value, _descriptors.NewObject): + value = acc.create(self, value._type_hint, **value._kw) + acc.insert(self, index, value) + + elif hasattr(acc, "insert"): + value = acc.insert(self, index, value) + + else: + raise TypeError( + f"Parent accessor does not support item insertion: {acc!r}" + ) + super().insert(index, value) + + +@functools.cache +def enumerate_namespaces() -> tuple[Namespace, ...]: + has_base_metamodel = False + namespaces: list[Namespace] = [] + for i in imm.entry_points(group="capellambse.namespaces"): + if i.value.startswith("capellambse.metamodel."): + has_base_metamodel = True + + nsobj = i.load() + if not isinstance(nsobj, Namespace): + raise TypeError( + "Found non-Namespace object at entrypoint" + f" {i.name!r} in group {i.group!r}: {nsobj!r}" + ) + namespaces.append(nsobj) + + if not has_base_metamodel: + raise RuntimeError( + "Did not find the base metamodel in enumerate_namespaces()!" + " Check that capellambse is installed properly." + ) + assert len(namespaces) > 1 + return tuple(namespaces) + + +@functools.lru_cache(maxsize=128) +def find_namespace(name: str, /) -> Namespace: + try: + return next(i for i in enumerate_namespaces() if i.alias == name) + except StopIteration: + raise UnknownNamespaceError(name) from None + + +@functools.lru_cache(maxsize=128) +def find_namespace_by_uri( + uri: str, / +) -> tuple[Namespace, av.AwesomeVersion | None]: + """Find a namespace by its URL. + + If the namespace is versioned, the second element of the returned + tuple is the version indicated in the URL. For unversioned + namespaces, the second element is always None. + """ + for i in enumerate_namespaces(): + result = i.match_uri(uri) + if result is False: + continue + if result is True: + return (i, None) + return (i, result) + raise UnknownNamespaceError(uri) + + +@functools.lru_cache(maxsize=128) +def resolve_class_name(uclsname: UnresolvedClassName, /) -> ClassName: + """Resolve an unresolved classname to a resolved ClassName tuple. + + Note that this method does not check whether the requested class + name actually exists in the resolved namespace. This helps to avoid + problems with circular dependencies between metamodel modules, where + the first point of use is initialized before the class gets + registered in the namespace. + + However, if the namespace part of the UnresolvedClassName input is + the empty string, the class must already be registered in its + namespace, as there would be no way to find the correct namespace + otherwise. + """ + ns, clsname = uclsname + + if isinstance(ns, Namespace): + return (ns, clsname) + + if isinstance(ns, str) and ns: + try: + ns_obj = next( + i + for i in enumerate_namespaces() + if i.alias == ns or i.match_uri(ns) + ) + except StopIteration: + raise ValueError(f"Namespace not found: {ns}") from None + else: + return (ns_obj, clsname) + + if not ns: + classes: list[ClassName] = [] + for ns_obj in enumerate_namespaces(): + if clsname in ns_obj: + classes.append((ns_obj, clsname)) + if len(classes) < 1: + raise ValueError(f"Class not found: {uclsname!r}") + if len(classes) > 1: + if not ns: + raise ValueError( + f"Multiple classes {clsname!r} found, specify namespace" + ) + raise RuntimeError( + f"Multiple classes {clsname!r} found in namespace {ns}" + ) + return classes[0] + + raise TypeError(f"Malformed class name: {uclsname!r}") + + +@t.overload +def wrap_xml( + model: capellambse.MelodyModel, element: etree._Element, / +) -> t.Any: ... +@t.overload +def wrap_xml( + model: capellambse.MelodyModel, element: etree._Element, /, type: type[T] +) -> T: ... +def wrap_xml( + model: capellambse.MelodyModel, + element: etree._Element, + /, + type: type[T] | None = None, +) -> T | t.Any: + """Wrap an XML element with the appropriate high-level proxy class. + + If *type* is a subclass of the element's declared type, and it belongs to a + namespace whose URL starts with the value of + :data:`capellambse.model.VIRTUAL_NAMESPACE_PREFIX`, it is used instead of + the declared type. + + Otherwise, *type* will be verified to be a superclass of (or exactly match) + the element's declared type. A mismatch will result in an error being + raised at runtime. This may be used to benefit more from static type + checkers. + """ + try: + cls = model.resolve_class(element) + except (UnknownNamespaceError, MissingClassError): + cls = ModelElement + + if type is not None: + try: + ns: Namespace = type.__capella_namespace__ + except AttributeError: + raise TypeError( + f"Class does not belong to a namespace: {type.__name__}" + ) from None + + if ns.uri.startswith(VIRTUAL_NAMESPACE_PREFIX): + if not issubclass(type, cls): + raise TypeError( + f"Requested virtual type {type.__name__!r}" + f" is not a subtype of declared type {cls.__name__!r}" + f" for element with ID {element.get('id')!r}" + ) + cls = type + elif type is not ModelElement and not issubclass(cls, type): + raise RuntimeError( + f"Class mismatch: requested {type!r}, but found {cls!r} in XML" + ) + + obj = cls.__new__(cls) + obj._model = model # type: ignore[misc] + obj._element = element # type: ignore[misc] + return obj + + +@functools.cache +def find_wrapper(typehint: str) -> tuple[type[ModelObject], ...]: + """Find the possible wrapper classes for the hinted type. + + The typehint is either a single class name, or a namespace prefix + and class name separated by ``:``. This function searches for all + known wrapper classes that match the given namespace prefix (if any) + and which have the given name, and returns them as a tuple. If no + matching wrapper classes are found, an empty tuple is returned. + """ + namespaces: list[tuple[Namespace, av.AwesomeVersion | None]] + if typehint.startswith("{"): + qname = etree.QName(typehint) + assert qname.namespace is not None + clsname = qname.localname + namespaces = [] + for i in enumerate_namespaces(): + v = i.match_uri(qname.namespace) + if v is True: + namespaces.append((i, None)) + elif v: + namespaces.append((i, v)) + if not namespaces: + raise ValueError( + f"Unknown namespace: {qname.namespace!r}." + " Check that relevant extensions are installed properly." + ) + + elif ":" in typehint: + nsname, clsname = typehint.rsplit(":", 1) + namespaces = [ + (i, i.maxver) for i in enumerate_namespaces() if i.alias == nsname + ] + if not namespaces: + raise ValueError( + f"Unknown namespace alias: {nsname!r}." + " Check that relevant extensions are installed properly." + ) + + else: + namespaces = [(i, i.maxver) for i in enumerate_namespaces()] + clsname = typehint + + candidates: list[type[ModelObject]] = [] + for ns, nsver in namespaces: + with contextlib.suppress(MissingClassError): + candidates.append(ns.get_class(clsname, nsver)) + return tuple(candidates) diff --git a/capellambse/model/_pods.py b/capellambse/model/_pods.py index 1582f7404..2531651bc 100644 --- a/capellambse/model/_pods.py +++ b/capellambse/model/_pods.py @@ -97,6 +97,10 @@ def __delete__(self, obj: t.Any) -> None: self.__set__(obj, None) def __set_name__(self, owner: type[t.Any], name: str) -> None: + if self.__objclass__ is not None: + raise RuntimeError( + f"__set_name__ called twice on {self._qualname}" + ) self.__name__ = name self.__objclass__ = owner diff --git a/capellambse/model/_styleclass.py b/capellambse/model/_styleclass.py index baaa4304b..955152440 100644 --- a/capellambse/model/_styleclass.py +++ b/capellambse/model/_styleclass.py @@ -74,7 +74,7 @@ def _functional_exchange(obj: _obj.ModelObject) -> str: assert isinstance(obj, mm.fa.FunctionalExchange) styleclass = _default(obj) - if get_styleclass(obj.target) == "OperationalActivity": + if obj.target and get_styleclass(obj.target) == "OperationalActivity": return styleclass.replace("Functional", "Operational") return styleclass @@ -108,7 +108,7 @@ def _part(obj: _obj.ModelObject) -> str: from . import _obj assert isinstance(obj, mm.cs.Part) - assert not isinstance(obj.type, _obj.ElementList) + assert isinstance(obj.type, _obj.ModelElement) xclass = _default(obj.type) if xclass == "PhysicalComponent": return _physical_component(obj.type) @@ -126,7 +126,8 @@ def _port_allocation(obj: _obj.ModelObject) -> str: styleclasses = { get_styleclass(p) for p in (obj.source, obj.target) - if not isinstance(p, mm.fa.ComponentPort | _obj.ElementList) + if isinstance(p, _obj.ModelElement) + and not isinstance(p, mm.fa.ComponentPort) } return f"{'_'.join(sorted(styleclasses))}Allocation" diff --git a/capellambse/model/_xtype.py b/capellambse/model/_xtype.py deleted file mode 100644 index 56c43c67b..000000000 --- a/capellambse/model/_xtype.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -__all__ = [ - "XTYPE_ANCHORS", - "XTYPE_HANDLERS", - "build_xtype", - "find_wrapper", - "xtype_handler", -] - -import collections.abc as cabc -import typing as t - -from . import T, _obj - -XTYPE_ANCHORS = { - "capellambse.metamodel": "org.polarsys.capella.core.data", - "capellambse.metamodel.sa": "org.polarsys.capella.core.data.ctx", - "capellambse.model.diagram": "viewpoint", -} -"""A mapping from anchor modules to Capella packages. - -This dictionary maps Python modules and packages to the Capella packages -they represent. :func:`build_xtype` and related functions/classes can then -use this information to automatically derive an ``xsi:type`` from any -class that is defined in such an anchor module (or a submodule of one). -""" -XTYPE_HANDLERS: dict[None, dict[str, type[t.Any]]] = {None: {}} -r"""Defines a mapping between ``xsi:type``\ s and wrapper classes. - -The first layer's keys can be either ``None`` or the ``xsi:type`` of the -architectural layer that the wrapper should be applied to. In the case -of ``None``, the wrapper will be applied to all layers. Note that -layer-specific wrappers have precedence over layer-agnostic ones. - -These keys map to a further dictionary. This second layer maps from the -``xsi:type``\ (s) that each wrapper handles to the wrapper class. -""" - - -def xtype_handler( - arch: str | None = None, /, *xtypes: str -) -> cabc.Callable[[type[T]], type[T]]: - """Register a class as handler for a specific ``xsi:type``. - - ``arch`` is the ``xsi:type`` of the desired architecture. It must - always be a simple string or None. In the latter case the definition - applies to all elements regardless of their architectural layer. - Architecture-specific definitions will always win over - architecture-independent ones. - - Each string given in ``xtypes`` notes an ``xsi:type`` of elements - that this class handles. It is possible to specify multiple values, - in which case the class will be registered for each ``xsi:type`` - under the architectural layer given in ``arch``. - - Handler classes' ``__init__`` methods must accept two positional - arguments. The first argument is the - :class:`~capellambse.model.MelodyModel` instance which loaded the - corresponding model, and the second one is - the LXML element that needs to be handled. - - Example:: - - >>> @xtype_handler(None, 'xtype:1', 'xtype:2') - ... class Test: - ... _xmltag = "ownedTests" - ... def from_model(self, model, element, /): - ... ... # Instantiate from model XML element - """ - if arch is not None: # pragma: no cover - raise TypeError( - "xtype_handler with non-None 'arch' is no longer supported" - ) - - # Compile a list of all xtype strings - xtype_strs = [] - for xtype in xtypes: - if isinstance(xtype, str): - xtype_strs.append(xtype) - else: # pragma: no cover - raise ValueError( - f"All `xtype`s must be str, not {type(xtype).__name__!r}" - ) - - def register_xtype_handler(cls: type[T]) -> type[T]: - # Avoid double registration when executing an extension as module - if cls.__module__ == "__main__": - return cls - - if not xtype_strs: - xtype_strs.append(build_xtype(cls)) - - for xtype in xtype_strs: - if xtype in XTYPE_HANDLERS[None]: # pragma: no cover - raise LookupError(f"Duplicate xsi:type {xtype} in {arch}") - XTYPE_HANDLERS[None][xtype] = cls - return cls - - return register_xtype_handler - - -def build_xtype(class_: type[_obj.ModelObject]) -> str: - anchor = package = "" - for a, p in XTYPE_ANCHORS.items(): - if len(a) > len(anchor) and class_.__module__.startswith(a): - anchor = a - package = p - - if not anchor: - raise TypeError(f"Module is not an xtype anchor: {class_.__module__}") - - module = class_.__module__[len(anchor) :] - clsname = class_.__name__ - return f"{package}{module}:{clsname}" - - -def find_wrapper(typehint: str) -> tuple[type[_obj.ModelObject], ...]: - """Find the possible wrapper classes for the hinted type. - - The typehint is either a single class name, or a namespace prefix - and class name separated by ``:``. This function searches for all - known wrapper classes that match the given namespace prefix (if any) - and which have the given name, and returns them as a tuple. If no - matching wrapper classes are found, an empty tuple is returned. - """ - return tuple( - v - for k, v in XTYPE_HANDLERS[None].items() - if k.endswith(f":{typehint}") or k == typehint - ) diff --git a/capellambse/model/diagram.py b/capellambse/model/diagram.py index 28ce0170e..af578a8ae 100644 --- a/capellambse/model/diagram.py +++ b/capellambse/model/diagram.py @@ -42,7 +42,12 @@ import capellambse from capellambse import aird, diagram, helpers, svg -from . import _descriptors, _obj, _pods, _xtype, stringy_enum +from . import _descriptors, _obj, _pods, stringy_enum + +VIEWPOINT_NS = _obj.Namespace( + "http://www.eclipse.org/sirius/1.1.0", + "viewpoint", +) @stringy_enum @@ -188,14 +193,14 @@ def uuid(self) -> str: ... @property def name(self) -> str: ... @property - def target(self) -> _obj.ModelElement: ... + def target(self) -> _obj.ModelObject: ... else: uuid: str """Unique ID of this diagram.""" name: str """Human-readable name for this diagram.""" - target: _obj.ModelElement + target: _obj.ModelObject """This diagram's "target". The target of a diagram is usually: @@ -238,7 +243,6 @@ def _allow_render(self) -> bool: ... def __init__(self, model: capellambse.MelodyModel) -> None: self._model = model - self._last_render_params = {} def __dir__(self) -> list[str]: return dir(type(self)) + [ @@ -341,7 +345,7 @@ def _repr_mimebundle_( return bundle @property - def nodes(self) -> _obj.MixedElementList: + def nodes(self) -> _obj.ElementList: """Return a list of all nodes visible in this diagram.""" allids = {e.uuid for e in self.render(None) if not e.hidden} elems = [] @@ -353,7 +357,7 @@ def nodes(self) -> _obj.MixedElementList: continue elems.append(elem._element) - return _obj.MixedElementList(self._model, elems, _obj.ModelElement) + return _obj.ElementList(self._model, elems) @t.overload def render(self, fmt: None, /, **params) -> diagram.Diagram: ... @@ -561,7 +565,10 @@ def __load_cache(self, chain: list[DiagramConverter]) -> t.Any: return data def __render_fresh(self, params: dict[str, t.Any]) -> diagram.Diagram: - if not hasattr(self, "_render") or self._last_render_params != params: + if ( + not hasattr(self, "_render") + or getattr(self, "_last_render_params", {}) != params + ): self.invalidate_cache() try: self._render = self._create_diagram(params) @@ -582,28 +589,22 @@ def invalidate_cache(self) -> None: del self._error -@_xtype.xtype_handler( - None, - "viewpoint:DRepresentationDescriptor", - "diagram:DSemanticDiagram", - "sequence:SequenceDDiagram", -) -class Diagram(AbstractDiagram): +class DRepresentationDescriptor(AbstractDiagram): """Provides access to a single diagram. Also directly exposed as ``capellambse.model.Diagram``. """ - uuid: str = _pods.StringPOD( # type: ignore[assignment] - "uid", writable=False - ) + __capella_namespace__: t.ClassVar[_obj.Namespace] = VIEWPOINT_NS + + uuid: str = _pods.StringPOD("uid", writable=False) # type: ignore[assignment] xtype = property(lambda self: helpers.xtype_of(self._element)) name: str = _pods.StringPOD("name") # type: ignore[assignment] description = _pods.HTMLStringPOD("documentation") _element: aird.DRepresentationDescriptor - __nodes: list[etree._Element] | None + _node_cache: list[etree._Element] @classmethod def from_model( @@ -617,7 +618,6 @@ def from_model( self._model = model self._element = element self._last_render_params = {} - self.__nodes = None return self target_id = element.get("uid") @@ -637,27 +637,25 @@ def __eq__(self, other: object) -> bool: return self._model is other._model and self._element == other._element @property - def nodes(self) -> _obj.MixedElementList: - if self.__nodes is None: - self.__nodes = list( + def nodes(self) -> _obj.ElementList: + if not hasattr(self, "_node_cache"): + self._node_cache = list( aird.iter_visible(self._model._loader, self._element) ) - return _obj.MixedElementList( - self._model, self.__nodes.copy(), _obj.ModelElement - ) + return _obj.ElementList(self._model, self._node_cache.copy()) @property - def semantic_nodes(self) -> _obj.MixedElementList: - if self.__nodes is None: - self.__nodes = list( + def semantic_nodes(self) -> _obj.ElementList: + if not hasattr(self, "_node_cache"): + self._node_cache = list( aird.iter_visible(self._model._loader, self._element) ) from capellambse.metamodel import cs, interaction elems: list[etree._Element] = [] - for i in self.__nodes: - obj = _obj.ModelElement.from_model(self._model, i) + for i in self._node_cache: + obj: _obj.ModelElement | None = _obj.wrap_xml(self._model, i) if isinstance(obj, cs.Part): obj = obj.type elif isinstance(obj, interaction.AbstractInvolvement): @@ -667,7 +665,7 @@ def semantic_nodes(self) -> _obj.MixedElementList: if obj is not None: elems.append(obj._element) - return _obj.MixedElementList(self._model, elems) + return _obj.ElementList(self._model, elems) @property def _allow_render(self) -> bool: @@ -690,9 +688,9 @@ def representation_path(self) -> str: return self._element.attrib["repPath"] @property - def target(self) -> _obj.ModelElement: + def target(self) -> _obj.ModelObject: target = aird.find_target(self._model._loader, self._element) - return _obj.ModelElement.from_model(self._model, target) + return _obj.wrap_xml(self._model, target) @property def type(self) -> DiagramType: @@ -724,6 +722,10 @@ def invalidate_cache(self) -> None: super().invalidate_cache() +VIEWPOINT_NS.register(DRepresentationDescriptor, None, None) +Diagram = DRepresentationDescriptor + + class DiagramAccessor(_descriptors.Accessor): """Provides access to a list of diagrams below the specified viewpoint. diff --git a/docs/source/examples/01 Introduction.ipynb b/docs/source/examples/01 Introduction.ipynb index c91dd8270..bd2b32d21 100644 --- a/docs/source/examples/01 Introduction.ipynb +++ b/docs/source/examples/01 Introduction.ipynb @@ -147,7 +147,7 @@ "realization_viewRealization view of Prof. S. Snape (uuid: 6f463eed-c77b-4568-8078-beec0536f243_realization_view)\n", "realized_components

(Empty list)

\n", "realized_system_components

(Empty list)

\n", - "realizing_componentsBackreference to - omitted: can be slow to compute. Display this property directly to show.\n", + "realizing_componentsBackreference to ModelElement - omitted: can be slow to compute. Display this property directly to show.\n", "realizing_physical_componentsBackreference to PhysicalComponent - omitted: can be slow to compute. Display this property directly to show.\n", "related_exchangesBackreference to ComponentExchange - omitted: can be slow to compute. Display this property directly to show.\n", "requirements

(Empty list)

\n", @@ -200,7 +200,7 @@ ".realization_view = \n", ".realized_components = []\n", ".realized_system_components = []\n", - ".realizing_components = ... # backreference to - omitted: can be slow to compute\n", + ".realizing_components = ... # backreference to ModelElement - omitted: can be slow to compute\n", ".realizing_physical_components = ... # backreference to PhysicalComponent - omitted: can be slow to compute\n", ".related_exchanges = ... # backreference to ComponentExchange - omitted: can be slow to compute\n", ".requirements = []\n", diff --git a/docs/source/examples/02 Intro to Physical Architecture API.ipynb b/docs/source/examples/02 Intro to Physical Architecture API.ipynb index a5b119394..939ec221b 100644 --- a/docs/source/examples/02 Intro to Physical Architecture API.ipynb +++ b/docs/source/examples/02 Intro to Physical Architecture API.ipynb @@ -230,7 +230,7 @@ "realization_viewRealization view of Cooling Fan (uuid: 65e82f3f-c5b7-44c1-bfea-8e20bb0230be_realization_view)\n", "realized_components

(Empty list)

\n", "realized_logical_components

(Empty list)

\n", - "realizing_componentsBackreference to - omitted: can be slow to compute. Display this property directly to show.\n", + "realizing_componentsBackreference to ModelElement - omitted: can be slow to compute. Display this property directly to show.\n", "related_exchangesBackreference to ComponentExchange - omitted: can be slow to compute. Display this property directly to show.\n", "requirements

(Empty list)

\n", "sid\n", @@ -283,7 +283,7 @@ ".realization_view = \n", ".realized_components = []\n", ".realized_logical_components = []\n", - ".realizing_components = ... # backreference to - omitted: can be slow to compute\n", + ".realizing_components = ... # backreference to ModelElement - omitted: can be slow to compute\n", ".related_exchanges = ... # backreference to ComponentExchange - omitted: can be slow to compute\n", ".requirements = []\n", ".sid = ''\n", diff --git a/docs/source/examples/08 Property Values.ipynb b/docs/source/examples/08 Property Values.ipynb index c871eecfc..83c0eb7d0 100644 --- a/docs/source/examples/08 Property Values.ipynb +++ b/docs/source/examples/08 Property Values.ipynb @@ -398,8 +398,13 @@ "domains
    \n", "
  1. ManagedDomain "DarkMagic" (3763dd54-a878-446a-9330-b5d9c7121865)
  2. \n", "
\n", + "enumeration_property_types

(Empty list)

\n", "filtering_criteria

(Empty list)

\n", + "groups

(Empty list)

\n", "nameEXTENSIONS\n", + "packages
    \n", + "
  1. PropertyValuePkg "DarkMagic" (3763dd54-a878-446a-9330-b5d9c7121865)
  2. \n", + "
\n", "parentProject "Melody Model Test" (af2196ac-49d3-4063-885c-9fa29adc39a8)\n", "progress_statusNOT_SET\n", "property_value_groups

(Empty list)

\n", @@ -414,6 +419,7 @@ "traces

(Empty list)

\n", "uuidfdbeb3b8-e1e2-4c6d-aff4-bccb1b9f437f\n", "validation<capellambse.extensions.validation._validate.ElementValidation object at 0x762147ff7cb0>\n", + "values

(Empty list)

\n", "visible_on_diagrams

(Empty list)

\n", "xtypeorg.polarsys.capella.core.data.capellacore:PropertyValuePkg\n", "" @@ -426,8 +432,11 @@ ".description = Markup('')\n", ".diagrams = []\n", ".domains = [0] \n", + ".enumeration_property_types = []\n", ".filtering_criteria = []\n", + ".groups = []\n", ".name = 'EXTENSIONS'\n", + ".packages = [0] \n", ".parent = \n", ".progress_status = 'NOT_SET'\n", ".property_value_groups = []\n", @@ -440,6 +449,7 @@ ".traces = []\n", ".uuid = 'fdbeb3b8-e1e2-4c6d-aff4-bccb1b9f437f'\n", ".validation = \n", + ".values = []\n", ".visible_on_diagrams = []\n", ".xtype = 'org.polarsys.capella.core.data.capellacore:PropertyValuePkg'" ] @@ -540,6 +550,8 @@ "applied_property_value_groups

(Empty list)

\n", "applied_property_values

(Empty list)

\n", "constraints

(Empty list)

\n", + "description[PROPERTY]DarkMagic.Power.Max<10000.0[/PROPERTY]\n", + "[ARCHITECTURE]LOGICAL[/ARCHITECTURE]\n", "diagrams

(Empty list)

\n", "filtering_criteria

(Empty list)

\n", "fullnameDarkMagic.Power Level\n", @@ -559,6 +571,9 @@ "traces

(Empty list)

\n", "uuida0cb5a23-955e-43d6-a633-2c4e66991364\n", "validation<capellambse.extensions.validation._validate.ElementValidation object at 0x762150164380>\n", + "values
    \n", + "
  1. EnumerationPropertyValue "Skill": EnumerationPropertyLiteral "TheOneWhoShallNotBeNamed" (4da42696-28e2-4f90-bfb6-90ba1fc52d01) (7367c731-aabb-4807-9d74-7ef2c6482ec9)
  2. \n", + "
\n", "visible_on_diagrams

(Empty list)

\n", "xtypeorg.polarsys.capella.core.data.capellacore:PropertyValueGroup\n", "" @@ -568,6 +583,7 @@ ".applied_property_value_groups = []\n", ".applied_property_values = []\n", ".constraints = []\n", + ".description = Markup('[PROPERTY]DarkMagic.Power.Max<10000.0[/PROPERTY]\\n[ARCHITECTURE]LOGICAL[/ARCHITECTURE]')\n", ".diagrams = []\n", ".filtering_criteria = []\n", ".fullname = 'DarkMagic.Power Level'\n", @@ -585,6 +601,7 @@ ".traces = []\n", ".uuid = 'a0cb5a23-955e-43d6-a633-2c4e66991364'\n", ".validation = \n", + ".values = [0] \n", ".visible_on_diagrams = []\n", ".xtype = 'org.polarsys.capella.core.data.capellacore:PropertyValueGroup'" ] diff --git a/pyproject.toml b/pyproject.toml index 01fe2aeca..a0bfb7f60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,34 @@ pvmt = "capellambse.extensions.pvmt:init" reqif = "capellambse.extensions.reqif:init" validation = "capellambse.extensions.validation:init" +[project.entry-points."capellambse.namespaces"] +metadata = "capellambse.model._meta:NS" +viewpoint = "capellambse.model.diagram:VIEWPOINT_NS" + +activity = "capellambse.metamodel.namespaces:ACTIVITY" +behavior = "capellambse.metamodel.namespaces:BEHAVIOR" +capellacommon = "capellambse.metamodel.namespaces:CAPELLACOMMON" +capellacore = "capellambse.metamodel.namespaces:CAPELLACORE" +capellamodeller = "capellambse.metamodel.namespaces:CAPELLAMODELLER" +cs = "capellambse.metamodel.namespaces:CS" +epbs = "capellambse.metamodel.namespaces:EPBS" +fa = "capellambse.metamodel.namespaces:FA" +information = "capellambse.metamodel.namespaces:INFORMATION" +information_communication = "capellambse.metamodel.namespaces:INFORMATION_COMMUNICATION" +information_datatype = "capellambse.metamodel.namespaces:INFORMATION_DATATYPE" +information_datavalue = "capellambse.metamodel.namespaces:INFORMATION_DATAVALUE" +interaction = "capellambse.metamodel.namespaces:INTERACTION" +la = "capellambse.metamodel.namespaces:LA" +libraries = "capellambse.metamodel.namespaces:LIBRARIES" +modellingcore = "capellambse.metamodel.namespaces:MODELLINGCORE" +oa = "capellambse.metamodel.namespaces:OA" +pa = "capellambse.metamodel.namespaces:PA" +pa_deployment = "capellambse.metamodel.namespaces:PA_DEPLOYMENT" +sa = "capellambse.metamodel.namespaces:SA" + +capellarequirements = "capellambse.extensions.reqif:CapellaRequirementsNS" +requirements = "capellambse.extensions.reqif:RequirementsNS" + [tool.coverage.run] branch = true command_line = "-m pytest" diff --git a/tests/test_model_creation_deletion.py b/tests/test_model_creation_deletion.py index 551f1ec06..312fd1c4d 100644 --- a/tests/test_model_creation_deletion.py +++ b/tests/test_model_creation_deletion.py @@ -24,16 +24,33 @@ def model(): return m.MelodyModel(TEST_ROOT / TEST_MODEL) -def test_created_elements_can_be_accessed_in_model( +def test_DirectProxyAccessor_created_elements_can_be_accessed_in_model( model: m.MelodyModel, ): - newobj = model.la.root_component.components.create(name="TestComponent") + parent = model.la.root_component + assert isinstance(type(parent).components, m.DirectProxyAccessor) + + newobj = parent.components.create(name="TestComponent") assert newobj is not None assert isinstance(newobj, mm.la.LogicalComponent) assert newobj in model.la.root_component.components +def test_Containment_created_elements_can_be_accessed_in_model( + model: m.MelodyModel, +): + parent = model.by_uuid("df30d27f-0efb-4896-b6b6-0757145c7ad5") + assert isinstance(parent, mm.capellacommon.Region) + assert isinstance(type(parent).states, m.Containment) + + newobj = parent.states.create("State", name="Booting up") + + assert newobj is not None + assert isinstance(newobj, mm.capellacommon.State) + assert newobj in parent.states + + def test_created_elements_show_up_in_xml_after_adding_them( model: m.MelodyModel, ): @@ -109,15 +126,19 @@ def test_adding_a_namespace_preserves_the_capella_version_comment( assert len(prev_elements) == 1 -def test_deleting_an_object_purges_references_from_AttrProxyAccessor( +def test_deleting_an_object_purges_references_from_Association( model: m.MelodyModel, caplog ) -> None: part = model.by_uuid("1bd59e23-3d45-4e39-88b4-33a11c56d4e3") assert isinstance(part, mm.cs.Part) - assert isinstance(type(part).type, m.Association) + acc = type(part).type + assert isinstance(acc, m.Single) + assert isinstance(acc.wrapped, m.Association) component = model.by_uuid("ea5f09e6-a0ec-46b2-bd3e-b572f9bf99d6") + parent = component.parent - component.parent.components.remove(component) + parent.components.remove(component) + parent.components.append(component) assert not list(model.find_references(component)) assert part.type is None diff --git a/tests/test_model_layers.py b/tests/test_model_layers.py index 2fea7d493..b52e5e2c5 100644 --- a/tests/test_model_layers.py +++ b/tests/test_model_layers.py @@ -58,7 +58,7 @@ def test_model_compatibility(folder: str, aird: str) -> None: ( "91dc2eec-c878-4fdb-91d8-8f4a4527424e", [ - ("88d7f9a7-1fae-4884-8233-7582153cc5a7", "destination", None), + ("88d7f9a7-1fae-4884-8233-7582153cc5a7", "target", None), ("a94806d8-71bb-4eb8-987b-bdce6ca99cb8", "modes", 0), ("a94806d8-71bb-4eb8-987b-bdce6ca99cb8", "states", 0), ("d0ea4afa-4231-4a3d-b1db-03655738dab8", "source", None), @@ -222,7 +222,9 @@ def test_Capabilities_conditions_markup_escapes(model: m.MelodyModel): " is near the actor" ) - assert markupsafe.escape(elm.precondition.specification) == expected + assert elm.precondition is not None + spec = elm.precondition.specification + assert markupsafe.escape(spec) == expected @pytest.mark.parametrize( @@ -252,7 +254,8 @@ def test_model_elements_have_pre_or_post_conditions( condition = elm.precondition or elm.postcondition assert condition - assert condition.specification["capella:linkedText"] + spec = condition.specification + assert spec["capella:linkedText"] @pytest.mark.parametrize( @@ -595,6 +598,7 @@ def test_model_search_finds_elements( session_shared_model: m.MelodyModel, searchkey ): expected = { + # Classes "0fef2887-04ce-4406-b1a1-a1b35e1ce0f3", "1adf8097-18f9-474e-b136-6c845fc6d9e9", "2a923851-a4ca-4fd2-a4b3-302edb8ac178", @@ -610,6 +614,11 @@ def test_model_search_finds_elements( "c89849fd-0643-4708-a4da-74c9ea9ca7b1", "ca79bf38-5e82-4104-8c49-e6e16b3748e9", "d2b4a93c-73ef-4f01-8b59-f86c074ec521", + # Unions + "246ab250-2a80-487f-b022-1123c02e33ff", + "2f34192f-088b-4511-95ea-b1a29fb6028b", + "31c5c280-64e1-4d11-b874-412966aa547c", + "be06f4a0-aff2-4475-a8a8-766e5fa26006", } found = session_shared_model.search(searchkey) @@ -635,17 +644,15 @@ def test_model_search_below_filters_elements_by_ancestor( assert actual == expected -@pytest.mark.parametrize( - "xtype", - {i for map in m.XTYPE_HANDLERS.values() for i in map.values()}, -) def test_model_search_does_not_contain_duplicates( - session_shared_model: m.MelodyModel, xtype: type[t.Any] + session_shared_model: m.MelodyModel, ) -> None: - results = session_shared_model.search(xtype) - uuids = [i.uuid for i in results] + results = session_shared_model.search() + uuids = sorted(i.uuid for i in results) + uuids_dedup = sorted(set(uuids)) - assert len(uuids) == len(set(uuids)) + assert len(uuids) > 0 + assert len(uuids) == len(uuids_dedup) def test_CommunicationMean(model: m.MelodyModel) -> None: diff --git a/tests/test_model_pvmt.py b/tests/test_model_pvmt.py index 8a659c23c..df0680226 100644 --- a/tests/test_model_pvmt.py +++ b/tests/test_model_pvmt.py @@ -91,12 +91,15 @@ def test_groups(self, model): "Property (String, ends with)", ], ) - def test_apply_outofscope(self, model, group): + def test_apply_outofscope(self, model, group: str): obj = model.by_uuid("d32caffc-b9a1-448e-8e96-65a36ba06292") domain = model.pvmt.domains["Out of scope"] - group = domain.groups[group] + assert isinstance(domain, pvmt.ManagedDomain) + group_obj = domain.groups[group] + assert isinstance(group_obj, pvmt.ManagedGroup) + with pytest.raises(pvmt.ScopeError): - group.apply(obj) + group_obj.apply(obj) @pytest.mark.parametrize( "group", diff --git a/tests/test_reqif.py b/tests/test_reqif.py index 3a66b0488..09af062af 100644 --- a/tests/test_reqif.py +++ b/tests/test_reqif.py @@ -375,6 +375,7 @@ def test_RequirementsModule_attributes(self, model: m.MelodyModel): assert len(mod.folders) == 1 assert len(mod.requirements) == 1 + assert mod.type is not None assert mod.type.long_name == "ModuleType" for attr, expected in { "identifier": "1", @@ -391,6 +392,7 @@ def test_RequirementFolder_attributes(self, model: m.MelodyModel): assert len(folder.folders) == 1 assert len(folder.requirements) == 2 + assert folder.type is not None assert folder.type.long_name == "ReqType" for attr, expected in { "identifier": "1", @@ -407,6 +409,7 @@ def test_RequirementFolder_attributes(self, model: m.MelodyModel): def test_Requirement_attributes(self, model: m.MelodyModel): req = model.by_uuid("3c2d312c-37c9-41b5-8c32-67578fa52dc3") assert isinstance(req, reqif.Requirement) + assert req.type is not None assert req.type.long_name == "ReqType" for attr, expected in {