From 608ba689c5e1fa20fd1e4c73e5edb9ca367b8a11 Mon Sep 17 00:00:00 2001 From: Martin Lehmann Date: Fri, 23 Aug 2024 11:17:24 +0200 Subject: [PATCH] feat: Implement new Namespace-based class discovery This commit reworks the discovery of metamodel classes and namespaces. It introduces the `Namespace` object, whose instances keep track of the contained classes. Namespace objects are discovered through the Python entrypoint system. This makes extending the metamodel significantly easier. It's no longer necessary to add weirdly formatted entries into barely documented dictionaries, with errors only appearing at some arbitrary later point. Discovery of classes now happens via a custom metaclass by simply inheriting from `ModelElement`. This also allows additional metadata to be specified at subclassing time, which is especially relevant for namespace versioning. Due to this, the following decorators are now no longer needed and have been deprecated: - `@xtype_handler`: Was used to register classes for discovery, which is now done by the metaclass. - `@attr_equal`: This overwrites the `__eq__` method to compare against an attribute. Now handled by the `eq=...` class keyword argument. --- .pre-commit-config.yaml | 1 + capellambse/extensions/filtering.py | 33 +- capellambse/extensions/pvmt/__init__.py | 2 +- capellambse/extensions/pvmt/_config.py | 56 +- capellambse/extensions/reqif/__init__.py | 10 +- capellambse/extensions/reqif/_capellareq.py | 25 +- capellambse/extensions/reqif/_glue.py | 68 +- capellambse/extensions/reqif/_requirements.py | 67 +- capellambse/extensions/reqif/exporter.py | 2 +- capellambse/extensions/validation/__init__.py | 14 +- capellambse/loader/core.py | 107 +- capellambse/metamodel/__init__.py | 39 +- capellambse/metamodel/activity.py | 5 + capellambse/metamodel/behavior.py | 5 + capellambse/metamodel/capellacommon.py | 95 +- capellambse/metamodel/capellacore.py | 89 +- capellambse/metamodel/capellamodeller.py | 17 +- capellambse/metamodel/cs.py | 75 +- capellambse/metamodel/epbs.py | 5 + capellambse/metamodel/fa.py | 172 +-- capellambse/metamodel/information/__init__.py | 99 +- .../metamodel/information/communication.py | 5 + capellambse/metamodel/information/datatype.py | 30 +- .../metamodel/information/datavalue.py | 34 +- capellambse/metamodel/interaction.py | 94 +- capellambse/metamodel/la.py | 103 +- capellambse/metamodel/libraries.py | 5 + capellambse/metamodel/modellingcore.py | 16 +- capellambse/metamodel/namespaces.py | 121 ++ capellambse/metamodel/oa.py | 86 +- capellambse/metamodel/pa.py | 65 +- capellambse/metamodel/pa_deployment.py | 5 + capellambse/metamodel/sa.py | 120 +- capellambse/model/__init__.py | 62 +- capellambse/model/_descriptors.py | 1167 +++++++++++++---- capellambse/model/_meta.py | 34 + capellambse/model/_model.py | 266 +++- capellambse/model/_obj.py | 783 ++++++++++- capellambse/model/_pods.py | 4 + capellambse/model/_styleclass.py | 7 +- capellambse/model/_xtype.py | 134 -- capellambse/model/diagram.py | 62 +- pyproject.toml | 29 + tests/static_assertions.py | 1 + tests/test_model_creation_deletion.py | 10 +- tests/test_model_layers.py | 22 +- tests/test_model_pvmt.py | 3 + 47 files changed, 2807 insertions(+), 1447 deletions(-) create mode 100644 capellambse/metamodel/activity.py create mode 100644 capellambse/metamodel/behavior.py create mode 100644 capellambse/metamodel/epbs.py create mode 100644 capellambse/metamodel/information/communication.py create mode 100644 capellambse/metamodel/libraries.py create mode 100644 capellambse/metamodel/namespaces.py create mode 100644 capellambse/metamodel/pa_deployment.py create mode 100644 capellambse/model/_meta.py delete mode 100644 capellambse/model/_xtype.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81ce8ed11..66d976532 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -106,6 +106,7 @@ repos: additional_dependencies: - mypypp==0.1.1 + - awesomeversion==24.2.0 - click==8.1.7 - diskcache==5.0 - jinja2==3.1.3 diff --git a/capellambse/extensions/filtering.py b/capellambse/extensions/filtering.py index 8d548253b..bffb11fec 100644 --- a/capellambse/extensions/filtering.py +++ b/capellambse/extensions/filtering.py @@ -20,16 +20,15 @@ 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/6.0.0", + "filtering", + "org.polarsys.capella.filtering", +) -@m.xtype_handler(None) class FilteringCriterion(m.ModelElement): """A single filtering criterion.""" @@ -40,17 +39,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 +80,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 +91,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 b855a20dc..1acc6338a 100644 --- a/capellambse/extensions/pvmt/_config.py +++ b/capellambse/extensions/pvmt/_config.py @@ -173,13 +173,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(_PVMTBase): """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 +263,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: groupobj.property_values.create( @@ -257,7 +286,7 @@ def _short_html_(self) -> markupsafe.Markup: ) -class ManagedDomain(m.ModelElement): +class ManagedDomain(_PVMTBase): """A "domain" in the property value management extension.""" _required_attrs = frozenset({"name"}) @@ -269,18 +298,15 @@ class ManagedDomain(m.ModelElement): "ownedEnumerationPropertyTypes", mm.capellacore.EnumerationPropertyType, ) - 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, - ) + enumeration_property_types = m.Containment[ + mm.capellacore.EnumerationPropertyType + ]("ownedEnumerationPropertyTypes", mm.capellacore.EnumerationPropertyType) def __init__( self, @@ -313,16 +339,12 @@ def from_model( return self -class PVMTConfiguration(m.ModelElement): +class PVMTConfiguration(_PVMTBase): """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/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..59c1ffd66 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( @@ -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..c50f4e216 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,7 +199,6 @@ def __str__(self) -> str: return self.long_name -@m.xtype_handler(None) class EnumerationDataTypeDefinition(ReqIFElement): """An enumeration data type definition for requirement types.""" @@ -217,24 +209,26 @@ class EnumerationDataTypeDefinition(ReqIFElement): ) -@m.xtype_handler(None) class AttributeDefinitionEnumeration(ReqIFElement): """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, # type: ignore[arg-type] + "definition", + ) ) values = m.Association(EnumValue, "values", aslist=m.ElementList) @@ -251,9 +245,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 +254,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[m.ModelElement]] -@m.xtype_handler(None) class Folder(Requirement): """A folder that stores Requirements.""" @@ -313,9 +301,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 +317,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 714b78902..a4e2c3a3c 100644 --- a/capellambse/loader/core.py +++ b/capellambse/loader/core.py @@ -9,6 +9,7 @@ "FragmentType", "MelodyLoader", "ModelFile", + "qtype_of", ] import collections @@ -35,6 +36,11 @@ from capellambse.loader import exs from capellambse.loader.modelinfo import ModelInfo +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + E = builder.ElementMaker() LOGGER = logging.getLogger(__name__) PROJECT_NATURE = "org.polarsys.capella.project.nature" @@ -55,6 +61,7 @@ VALID_EXTS = VISUAL_EXTS | SEMANTIC_EXTS | {".afm"} ERR_BAD_EXT = "Model file {} has an unsupported extension: {}" +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]] = { @@ -163,6 +170,26 @@ 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.""" @@ -192,6 +219,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] @@ -243,6 +271,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) @@ -275,6 +306,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: @@ -289,6 +323,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 = {} @@ -299,6 +334,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. @@ -312,7 +376,7 @@ def update_namespaces(self, viewpoints: cabc.Mapping[str, str]) -> None: the Plugin's viewpoint is not activated in the model, an error is raised and no update is performed. """ - new_nsmap: dict[str, str] = { + new_nsmap: dict[str | None, str] = { "xmi": _n.NAMESPACES["xmi"], "xsi": _n.NAMESPACES["xsi"], } @@ -344,7 +408,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: @@ -367,6 +433,10 @@ def update_namespaces(self, viewpoints: cabc.Mapping[str, str]) -> None: self.root = new_root + @deprecated( + "iterall_xt() is deprecated," + " use iterall() or iter_qtypes() + iter_qtype() instead" + ) def iterall_xt( self, xtypes: cabc.Container[str] ) -> cabc.Iterator[etree._Element]: @@ -375,6 +445,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, @@ -988,6 +1073,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..d8b3c8f84 100644 --- a/capellambse/metamodel/capellacommon.py +++ b/capellambse/metamodel/capellacommon.py @@ -5,6 +5,9 @@ import capellambse.model as m from . import capellacore, modellingcore +from . import namespaces as ns + +NS = ns.CAPELLACOMMON class AbstractStateRealization(m.ModelElement): ... @@ -16,7 +19,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 +37,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 +50,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 +90,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("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 +116,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 +132,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 +144,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 +155,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..70e9c4019 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.""" @@ -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 08f311fc1..dfa4a079a 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", ) @@ -120,13 +113,13 @@ class Component(m.ModelElement): is_actor = m.BoolPOD("actor") """Boolean flag for an actor Component.""" - 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( @@ -137,21 +130,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.""" @@ -178,39 +166,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 45a10aa2c..8d4fc235a 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,7 +30,6 @@ class ComponentExchangeFunctionalExchangeAllocation(m.ModelElement): ... class ComponentFunctionalAllocation(m.ModelElement): ... -@m.xtype_handler(None) class ControlNode(m.ModelElement): """A node with a specific control-kind.""" @@ -36,7 +38,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.""" @@ -46,49 +47,45 @@ 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" ) -@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" ) @@ -104,40 +101,35 @@ 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] + realizing_functional_exchanges: m.Accessor[ + m.ElementList[FunctionalExchange] + ] @property def owner(self) -> ComponentExchange | None: @@ -150,27 +142,23 @@ 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.""" @@ -190,57 +178,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.""" @@ -251,13 +229,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" ) @property @@ -277,86 +252,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", ) diff --git a/capellambse/metamodel/information/__init__.py b/capellambse/metamodel/information/__init__.py index cea1d8d2f..2f79ec2a2 100644 --- a/capellambse/metamodel/information/__init__.py +++ b/capellambse/metamodel/information/__init__.py @@ -16,24 +16,25 @@ 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 +44,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 +75,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(m.Containment("ownedDefaultValue")) + min_value = m.Single(m.Containment("ownedMinValue")) + max_value = m.Single(m.Containment("ownedMaxValue")) + null_value = m.Single(m.Containment("ownedNullValue")) + min_card = m.Single(m.Containment("ownedMinCard")) + max_card = m.Single(m.Containment("ownedMaxCard")) + association = m.Single(m.Backref(Association, "roles")) -@m.xtype_handler(None) class Class(m.ModelElement): """A Class.""" @@ -121,14 +119,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 +133,6 @@ class Union(Class): kind = m.EnumPOD("kind", modeltypes.UnionKind, default="UNION") -@m.xtype_handler(None) class Collection(m.ModelElement): """A Collection.""" @@ -149,7 +144,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 +175,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(m.Containment("ownedMinCard")) + max_card = m.Single(m.Containment("ownedMaxCard")) -@m.xtype_handler(None) class ExchangeItem(m.ModelElement): """An item that can be exchanged on an Exchange.""" @@ -204,16 +196,12 @@ 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]] -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, @@ -221,42 +209,17 @@ class ExchangeItem(m.ModelElement): 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) + 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( - 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", +Class.realizations = ( m.DirectProxyAccessor(InformationRealization, aslist=m.ElementList), ) -m.set_accessor( - Class, - "realized_by", - m.Backref(Class, "realized_classes", aslist=m.ElementList), -) +Class.realized_by = m.Backref(Class, "realized_classes") 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..bbfeec53f 100644 --- a/capellambse/metamodel/information/datatype.py +++ b/capellambse/metamodel/information/datatype.py @@ -5,8 +5,11 @@ 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 +25,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(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 +55,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(m.Containment("ownedDefaultValue")) + null_value = m.Single(m.Containment("ownedNullValue")) + min_length = m.Single(m.Containment("ownedMinLength")) + max_length = m.Single(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(m.Containment("ownedDefaultValue")) + null_value = m.Single(m.Containment("ownedNullValue")) + min_value = m.Single(m.Containment("ownedMinValue")) + max_value = m.Single(m.Containment("ownedMaxValue")) -@m.xtype_handler(None) class PhysicalQuantity(NumericType): - unit = m.Containment("ownedUnit") + unit = m.Single(m.Containment("ownedUnit")) diff --git a/capellambse/metamodel/information/datavalue.py b/capellambse/metamodel/information/datavalue.py index 1ca52122b..977a21e80 100644 --- a/capellambse/metamodel/information/datavalue.py +++ b/capellambse/metamodel/information/datavalue.py @@ -2,8 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 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 +19,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(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(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 709232767..d72f0107a 100644 --- a/capellambse/metamodel/interaction.py +++ b/capellambse/metamodel/interaction.py @@ -3,6 +3,9 @@ import capellambse.model as m from . import capellacore +from . import namespaces as ns + +NS = ns.INTERACTION class FunctionalChainAbstractCapabilityInvolvement(m.ModelElement): ... @@ -11,44 +14,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): @@ -58,25 +58,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.""" @@ -84,100 +80,93 @@ 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("ownedEvents") + fragments = m.Containment("ownedInteractionFragments") + time_lapses = m.Containment("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") 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("involved") + involved = m.Single(m.Association(m.ModelElement, "involved")) @property def name(self) -> str: # type: ignore[override] @@ -189,6 +178,5 @@ 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.""" diff --git a/capellambse/metamodel/la.py b/capellambse/metamodel/la.py index 16a5dc72d..cad4ecc39 100644 --- a/capellambse/metamodel/la.py +++ b/capellambse/metamodel/la.py @@ -9,33 +9,32 @@ 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 -@m.xtype_handler(None) class LogicalComponent(cs.Component): """A logical component on the Logical Architecture layer.""" @@ -44,7 +43,6 @@ class LogicalComponent(cs.Component): allocated_functions = m.Allocation[LogicalFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -56,7 +54,6 @@ class LogicalComponent(cs.Component): components: m.Accessor -@m.xtype_handler(None) class LogicalComponentPkg(m.ModelElement): """A logical component package.""" @@ -73,7 +70,6 @@ class LogicalComponentPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class CapabilityRealization(m.ModelElement): """A capability.""" @@ -85,41 +81,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.""" @@ -132,7 +125,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.""" @@ -157,8 +149,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) @@ -192,48 +186,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 04d07ba93..cff3a6e97 100644 --- a/capellambse/metamodel/pa.py +++ b/capellambse/metamodel/pa.py @@ -10,32 +10,30 @@ 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 -@m.xtype_handler(None) class PhysicalComponent(cs.Component): """A physical component on the Physical Architecture layer.""" @@ -49,7 +47,6 @@ class PhysicalComponent(cs.Component): allocated_functions = m.Allocation[PhysicalFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -77,7 +74,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.""" @@ -94,7 +90,6 @@ class PhysicalComponentPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class PhysicalArchitecture(cs.ComponentArchitecture): """Provides access to the Physical Architecture layer of the model.""" @@ -159,38 +154,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 2d8fbbb06..2d55a1a2a 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,19 +27,15 @@ 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 -@m.xtype_handler(None) class SystemComponent(cs.Component): """A system component.""" @@ -45,7 +44,6 @@ class SystemComponent(cs.Component): allocated_functions = m.Allocation[SystemFunction]( "ownedFunctionalAllocation", fa.ComponentFunctionalAllocation, - aslist=m.ElementList, attr="targetElement", backattr="sourceElement", ) @@ -59,7 +57,6 @@ class SystemComponent(cs.Component): ) -@m.xtype_handler(None) class SystemComponentPkg(m.ModelElement): """A system component package.""" @@ -73,12 +70,10 @@ class SystemComponentPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class CapabilityInvolvement(interaction.AbstractInvolvement): """A CapabilityInvolvement.""" -@m.xtype_handler(None) class Capability(m.ModelElement): """A capability.""" @@ -87,22 +82,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 @@ -110,19 +99,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( @@ -131,36 +117,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] @@ -172,7 +157,6 @@ def name(self) -> str: # type: ignore[override] return f"[{self.__class__.__name__}]{direction}" -@m.xtype_handler(None) class Mission(m.ModelElement): """A mission.""" @@ -181,13 +165,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( @@ -195,7 +176,6 @@ class Mission(m.ModelElement): ) -@m.xtype_handler(None) class MissionPkg(m.ModelElement): """A system mission package that can hold missions.""" @@ -205,7 +185,6 @@ class MissionPkg(m.ModelElement): packages: m.Accessor -@m.xtype_handler(None) class CapabilityPkg(m.ModelElement): """A capability package that can hold capabilities.""" @@ -216,7 +195,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.""" @@ -273,49 +251,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..279697251 100644 --- a/capellambse/model/__init__.py +++ b/capellambse/model/__init__.py @@ -7,9 +7,15 @@ import collections.abc as cabc import enum import functools +import sys import typing as t import warnings +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + E = t.TypeVar("E", bound=enum.Enum) """TypeVar for ":py:class:`~enum.Enum`".""" S = t.TypeVar("S", bound=str | None) @@ -20,8 +26,11 @@ """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).""" +@deprecated("set_accessor is deprecated and no longer needed") def set_accessor( cls: type[ModelObject], attr: str, accessor: Accessor ) -> None: @@ -29,11 +38,16 @@ def set_accessor( accessor.__set_name__(cls, attr) +@deprecated("set_self_references is deprecated, use a 'Containment' instead") 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)) +@deprecated( + '`@attr_equal("...")` is deprecated,' + ' use `class X(ModelElement, eq="...")` instead' +) def attr_equal(attr: str) -> cabc.Callable[[type[T]], type[T]]: def add_wrapped_eq(cls: type[T]) -> type[T]: orig_eq = cls.__eq__ @@ -87,12 +101,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 +121,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 +136,10 @@ def __hash__(self): "T", "T_co", "U", + "U_co", "attr_equal", "diagram", + "reset_entrypoint_caches", "set_self_references", "stringy_enum", *_all1, @@ -144,4 +165,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 f10dc2dd7..67f73d7ff 100644 --- a/capellambse/model/_descriptors.py +++ b/capellambse/model/_descriptors.py @@ -9,8 +9,7 @@ "NonUniqueMemberError", # descriptor ABCs "Accessor", - "WritableAccessor", - "PhysicalAccessor", + "Relationship", # relationship descriptors "Allocation", "Association", @@ -18,17 +17,22 @@ "Containment", # misc descriptors "Alias", + "AlternateAccessor", "DeprecatedAccessor", - "DeepProxyAccessor", - "PhysicalLinkEndsAccessor", "IndexAccessor", - "AlternateAccessor", "ParentAccessor", - "AttributeMatcherAccessor", + "Single", "SpecificationAccessor", - "TypecastAccessor", # legacy + "AttributeMatcherAccessor", + "DeepProxyAccessor", "DirectProxyAccessor", + "WritableAccessor", + "PhysicalAccessor", + "PhysicalLinkEndsAccessor", + "TypecastAccessor", + "build_xtype", + "xtype_handler", # helpers "NewObject", ] @@ -50,20 +54,67 @@ import capellambse from capellambse import helpers +from capellambse.loader import core -from . import T_co, _xtype +from . import T, T_co, U_co + +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated if sys.version_info >= (3, 13): from warnings import deprecated else: from typing_extensions import deprecated -_NOT_SPECIFIED = object() +_NotSpecifiedType = t.NewType("_NotSpecifiedType", object) +_NOT_SPECIFIED = _NotSpecifiedType(object()) "Used to detect unspecified optional arguments" LOGGER = logging.getLogger(__name__) +@deprecated("@xtype_handler is deprecated and no longer used") +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 + + +@deprecated("build_xtype is deprecated") +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.""" @@ -95,7 +146,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 @@ -104,11 +155,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__() @@ -122,19 +173,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: @@ -144,7 +195,7 @@ def __set_name__(self, owner: type[t.Any], name: str) -> None: self.__doc__ = f"The {friendly_name} of this {owner.__name__}." 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: @@ -154,7 +205,7 @@ def _qualname(self) -> str: return f"{self.__objclass__.__name__}.{self.__name__}" -class Alias(Accessor[T_co]): +class Alias(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Provides an alias to another attribute. Parameters @@ -182,17 +233,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: @@ -215,12 +268,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 @@ -240,7 +293,272 @@ 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 = type( + "CoupledElementList", + (_obj.ElementListCouplingMixin, _obj.ElementList), + {"_accessor": self}, + ) + self.list_type.__module__ = __name__ + + @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) + + +@deprecated("WritableAccessor is deprecated, use Relation instead") +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 @@ -373,18 +691,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)) # type: ignore[deprecated] def _guess_xtype(self) -> tuple[type[T_co], str]: try: @@ -483,7 +791,8 @@ def purge_references(self, obj, target): ) -class PhysicalAccessor(Accessor[T_co]): +@deprecated("PhysicalAccessor is deprecated, use Relation instead") +class PhysicalAccessor(Accessor["_obj.ElementList[T_co]"], t.Generic[T_co]): """Helper super class for accessors that work with real elements.""" __slots__ = ( @@ -516,18 +825,18 @@ def __init__( super().__init__() if xtypes is None: self.xtypes = ( - {_xtype.build_xtype(class_)} + {build_xtype(class_)} # type: ignore[deprecated] 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)} # type: ignore[deprecated] elif isinstance(xtypes, str): self.xtypes = {xtypes} else: self.xtypes = { - i if isinstance(i, str) else _xtype.build_xtype(i) + i if isinstance(i, str) else build_xtype(i) # type: ignore[deprecated] for i in xtypes } @@ -657,10 +966,10 @@ def __init__( elif isinstance(rootelem, type) and issubclass( rootelem, _obj.ModelElement ): - self.rootelem = [_xtype.build_xtype(rootelem)] + self.rootelem = [build_xtype(rootelem)] # type: ignore[deprecated] else: self.rootelem = [ - i if isinstance(i, str) else _xtype.build_xtype(i) + i if isinstance(i, str) else build_xtype(i) # type: ignore[deprecated] for i in rootelem ] @@ -732,10 +1041,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)) @@ -828,6 +1139,9 @@ def purge_references( yield +@deprecated( + "DeepProxyAccessor is deprecated, use @property and model.search() instead" +) class DeepProxyAccessor(PhysicalAccessor[T_co]): """A DirectProxyAccessor that searches recursively through the tree.""" @@ -877,9 +1191,9 @@ def __init__( elif isinstance(rootelem, type) and issubclass( rootelem, _obj.ModelElement ): - self.rootelem = (_xtype.build_xtype(rootelem),) + self.rootelem = (build_xtype(rootelem),) # type: ignore[deprecated] 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) # type: ignore[deprecated] else: raise TypeError( "Invalid 'rootelem', expected a type or list of types: " @@ -920,16 +1234,22 @@ 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 + class_: _obj.ClassName + attr: str + backattr: str | None + @t.overload + @deprecated( + "Raw classes, xsi:type strings and 'aslist' are deprecated," + " migrate to (Namespace, 'ClassName') tuples and drop aslist=..." + ) def __init__( self, tag: str | None, @@ -942,6 +1262,33 @@ def __init__( attr: str, backattr: str | None = None, unique: bool = True, + ) -> None: ... + @t.overload + def __init__( + self, + tag: str, + alloc_type: _obj.UnresolvedClassName, + class_: _obj.UnresolvedClassName, + /, + *, + mapkey: str | None = None, + mapvalue: str | None = None, + attr: str, + backattr: str | None = None, + ) -> None: ... + def __init__( + self, + tag: str | None, + alloc_type: str | type[_obj.ModelElement] | _obj.UnresolvedClassName, + class_: _obj.UnresolvedClassName | _NotSpecifiedType = _NOT_SPECIFIED, + /, + *, + aslist: t.Any = _NOT_SPECIFIED, + mapkey: str | None = None, + mapvalue: str | None = None, + attr: str, + backattr: str | None = None, + unique: bool = True, ) -> None: """Define an Allocation. @@ -957,9 +1304,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. @@ -995,21 +1345,89 @@ def __init__( ) 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, + if aslist is not _NOT_SPECIFIED: + warnings.warn( + "The aslist argument is deprecated and will be removed soon", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__( + 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 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 @@ -1023,15 +1441,33 @@ 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) + if self.tag is None: raise NotImplementedError("Cannot set: XML tag not set") @@ -1048,7 +1484,7 @@ 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) + link = refelm.get(self.attr) if not link: return None return obj._model._loader.follow_link(obj._element, link) @@ -1056,8 +1492,10 @@ def __follow_ref( def __find_refs( self, obj: _obj.ModelObject ) -> cabc.Iterator[etree._Element]: + 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( @@ -1076,22 +1514,17 @@ def __create_link( before: _obj.ModelObject | None = None, ) -> etree._Element: 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: @@ -1099,36 +1532,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: @@ -1159,20 +1600,17 @@ def purge_references( LOGGER.exception("Cannot purge dangling ref object %r", ref) -class Association(WritableAccessor[T_co], PhysicalAccessor[T_co]): +class Association(Relationship[T_co]): """Provides access to elements that are linked in an attribute.""" - __slots__ = ("attr",) - - aslist: type[_obj.ElementListCouplingMixin] | None - class_: type[T_co] + __slots__ = ("attr", "class_") def __init__( self, - class_: type[T_co] | None, + class_: type[T_co] | None | _obj.UnresolvedClassName, attr: str, *, - aslist: type[_obj.ElementList] | None = None, + aslist: t.Any = _NOT_SPECIFIED, mapkey: str | None = None, mapvalue: str | None = None, fixed_length: int = 0, @@ -1212,17 +1650,60 @@ 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__ # type: ignore[attr-defined] + 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 @@ -1231,25 +1712,34 @@ def __get__(self, obj, objtype=None): 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") + 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) - assert isinstance(values, cabc.Iterable) - self.__set_links(obj, values) # type: ignore[arg-type] + self.__set_links(obj, value) def __delete__(self, obj: _obj.ModelObject) -> None: del obj._element.attrib[self.attr] @@ -1258,28 +1748,35 @@ 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: + 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" @@ -1294,34 +1791,32 @@ def purge_references( ) -> cabc.Generator[None, None, None]: 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") +@deprecated( + "PhysicalLinkEndsAccessor is deprecated," + " use Association with fixed_length=2 instead" +) 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: @@ -1333,17 +1828,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") @@ -1354,9 +1841,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, @@ -1404,17 +1893,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 @@ -1425,9 +1921,12 @@ 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) +@deprecated( + "AttributeMatcherAccessor is deprecated, use FilterAccessor instead" +) class AttributeMatcherAccessor(DirectProxyAccessor[T_co]): __slots__ = ( "_AttributeMatcherAccessor__aslist", @@ -1573,19 +2072,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: @@ -1618,28 +2121,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, ) - self.target_classes = class_ + + 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.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__) # type: ignore[attr-defined] 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) @@ -1653,9 +2194,16 @@ 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 + ) +@deprecated( + "TypecastAccessor is deprecated," + " use FilterAccessor to perform filtering" + " or Alias to create an unfiltered Alias" +) class TypecastAccessor(WritableAccessor[T_co], PhysicalAccessor[T_co]): """Changes the static type of the value of another accessor. @@ -1739,7 +2287,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) # type: ignore[deprecated] assert isinstance(obj, self.class_) return obj @@ -1775,119 +2323,172 @@ 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 + aslist: type[_obj.ElementListCouplingMixin] + alternate: type[_obj.ModelObject] | None + @t.overload + @deprecated( + "Raw classes, xsi:type strings and 'aslist' are deprecated," + " migrate to (Namespace, 'ClassName') tuples and drop aslist=..." + ) def __init__( self, role_tag: str, - classes: ( - type[_obj.ModelObject] | cabc.Iterable[type[_obj.ModelObject]] - ) = (), + classes: type[T_co] | cabc.Iterable[type[_obj.ModelObject]] = (), + /, *, aslist: type[_obj.ElementList[T_co]] | None = None, mapkey: str | None = None, mapvalue: str | None = None, - alternate: type[_obj.ModelElement] | None = None, + alternate: type[_obj.ModelObject] | None = None, + ) -> None: ... + @t.overload + def __init__( + self, + role_tag: str, + class_: _obj.UnresolvedClassName, + /, + *, + mapkey: str | None = None, + mapvalue: str | None = None, + alternate: type[_obj.ModelObject] | None = None, + ) -> None: ... + def __init__( + self, + role_tag: str, + class_: ( + type[T_co] + | cabc.Iterable[type[_obj.ModelObject]] + | _obj.UnresolvedClassName + ) = (), + /, + *, + aslist: t.Any = _NOT_SPECIFIED, + mapkey: str | None = None, + mapvalue: str | None = None, + alternate: type[_obj.ModelObject] | None = None, + single_attr: str | None = None, ) -> 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=0, + 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 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.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 @@ -1899,8 +2500,34 @@ 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}" + ) + ns = cls.__capella_namespace__ # type: ignore[attr-defined] + xtype = elmlist._model.qualify_classname((ns, cls.__name__)) + 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, + xtype=xtype, + **attrs, + ) + elmlist._parent._element.insert(parent_index, value._element) elmlist._model._loader.idcache_index(value._element) + return value def delete( self, @@ -1918,10 +2545,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)) @@ -1938,26 +2567,16 @@ 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) - - 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 _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) - def _guess_xtype(self) -> tuple[type[_obj.ModelObject], str]: - if len(self.classes) == 1: - return self.classes[0], _xtype.build_xtype(self.classes[0]) - - 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}") + def _guess_xtype(self, model: capellambse.MelodyModel) -> type[T_co]: + return t.cast(type[T_co], model.resolve_class(self.class_)) def no_list( @@ -1979,13 +2598,15 @@ 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]) from . import _obj 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..864c41982 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,94 @@ 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 + ) -> type[_obj.ModelObject]: + """Resolve a class based on a type hint. + + The type hint can be 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 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.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 +787,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 +799,13 @@ 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 167aa8ee2..d1fa5e450 100644 --- a/capellambse/model/_obj.py +++ b/capellambse/model/_obj.py @@ -4,6 +4,18 @@ from __future__ import annotations __all__ = [ + # namespaces and discovery + "CORE_VIEWPOINT", + "ClassName", + "MissingClassError", + "Namespace", + "UnresolvedClassName", + "enumerate_namespaces", + "find_namespace", + "find_namespace_by_uri", + "resolve_class_name", + "wrap_xml", + # model elements "ModelObject", "ModelElement", "ElementList", @@ -12,28 +24,45 @@ "ElementListMapKeyView", "ElementListMapItemsView", "ElementListCouplingMixin", + # legacy + "find_wrapper", ] +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 sys import textwrap import typing as t +import warnings +import awesomeversion as av import markupsafe import typing_extensions as te from lxml import etree import capellambse -from capellambse import helpers +from capellambse import helpers, loader -from . import T, U, _descriptors, _pods, _styleclass, _xtype +from . import T, U, _descriptors, _pods, _styleclass + +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated LOGGER = logging.getLogger(__name__) +CORE_VIEWPOINT = "org.polarsys.capella.core.viewpoint" _NOT_SPECIFIED = object() @@ -47,6 +76,260 @@ _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: + 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. @@ -89,14 +372,136 @@ 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): + __capella_namespace__: Namespace + + 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. + + 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 ns is not NS + and name != "ModelElement" + and namespace["_xmltag"] is not None + ): + 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: + +class ModelElement(metaclass=_ModelElementMeta): """A model element. This is the common base class for all elements of a model. In terms @@ -133,9 +538,10 @@ class ModelElement: lambda self: self._model.diagrams.by_semantic_nodes(self) ) - parent: _descriptors.ParentAccessor + parent = _descriptors.ParentAccessor() constraints: _descriptors.Accessor property_value_packages: _descriptors.Accessor + property_values: _descriptors.Accessor _required_attrs = frozenset({"uuid", "xtype"}) _xmltag: str | None = None @@ -167,12 +573,13 @@ 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 + @deprecated("ModelElement.from_model is deprecated, use wrap_xml instead") def from_model( cls, model: capellambse.MelodyModel, element: etree._Element - ) -> te.Self: + ) -> ModelObject: """Wrap an existing model object. Parameters @@ -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) @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) @@ -317,9 +726,8 @@ def __repr__(self) -> str: # pragma: no cover acc = getattr(type(self), attr, None) if isinstance(acc, _descriptors.Backref): - classes = ", ".join(i.__name__ for i in acc.target_classes) attrs.append( - f".{attr} = ... # backreference to {classes}" + f".{attr} = ... # backreference to {acc.class_[1]}" " - omitted: can be slow to compute" ) continue @@ -407,11 +815,10 @@ def __html__(self) -> markupsafe.Markup: acc = getattr(type(self), attr, None) if isinstance(acc, _descriptors.Backref): - classes = ", ".join(i.__name__ for i in acc.target_classes) fragments.append('') fragments.append(escape(attr)) fragments.append('') - fragments.append(f"Backreference to {escape(classes)}") + fragments.append(f"Backreference to {escape(acc.class_[1])}") fragments.append(" - omitted: can be slow to compute.") fragments.append(" Display this property directly to show.") fragments.append("") @@ -653,10 +1060,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]) @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 +1243,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 +1347,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 +1407,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: @@ -1130,6 +1553,7 @@ def _newlist(self, elements: list[etree._Element]) -> ElementList[T]: return newlist +@deprecated("MixedElementList is deprecated, use base ElementList instead") class MixedElementList(ElementList[ModelElement]): """ElementList that handles proxies using ``XTYPE_HANDLERS``.""" @@ -1220,7 +1644,7 @@ class ElementListCouplingMixin(ElementList[T], t.Generic[T]): modifications to the Accessor. """ - _accessor: _descriptors.WritableAccessor[T] + _accessor: _descriptors.WritableAccessor[T] | _descriptors.Relationship[T] def is_coupled(self) -> bool: return True @@ -1247,7 +1671,9 @@ 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) + assert isinstance( + accessor, _descriptors.WritableAccessor | _descriptors.Relationship + ) if not isinstance(index, int | slice): super().__setitem__(index, value) @@ -1269,7 +1695,9 @@ def __delitem__(self, index: int | slice) -> None: assert self._parent is not None accessor = type(self)._accessor - assert isinstance(accessor, _descriptors.WritableAccessor) + assert isinstance( + accessor, _descriptors.WritableAccessor | _descriptors.Relationship + ) if not isinstance(index, slice): index = slice(index, index + 1 or None) for obj in self[index]: @@ -1311,15 +1739,27 @@ 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 isinstance(acc, _descriptors.Relationship): + 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("Parent Accessor is not a Relationship?") def create_singleattr(self, arg: t.Any) -> T: """Make a new model object (instance of ModelElement). @@ -1339,22 +1779,243 @@ 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 + 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 + + if isinstance(acc, _descriptors.Relationship): + if acc.single_attr is None: + raise TypeError("Cannot create object from a single string") + return self.create(**{acc.single_attr: arg}) - def insert(self, index: int, value: T) -> None: + raise AssertionError("Parent Accessor is not a Relationship?") + + 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 isinstance(acc, _descriptors.Relationship): + value = acc.insert(self, index, value) + + else: + raise AssertionError( + f"Unsupported parent accessor type: {type(acc).__name__!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 ns and ns_obj.alias != ns and not ns_obj.match_uri(ns): + continue + 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. + + The proxy class to use will be automatically determined based on the + type declaration in the XML. If the ``type`` argument is specified, + it must be a superclass of the declared class, otherwise an + exception is raised. This may be used to benefit static type + checkers. + """ + typehint = loader.qtype_of(element) + if typehint is None: + raise ValueError(f"Element is not a proper model element: {element!r}") + + try: + cls = model.resolve_class(typehint) + except (UnknownNamespaceError, MissingClassError) as err: + LOGGER.warning("Current metamodel is incomplete: %s", err) + cls = ModelElement + if type is not None 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 + + +@deprecated( + "find_wrapper is deprecated," + " use resolve_class_name or MelodyModel.resolve_class instead" +) +@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 f5509e8a3..6858be999 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 619c712bb..0b9775588 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) @@ -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 27fa783eb..e5c8eef2d 100644 --- a/capellambse/model/diagram.py +++ b/capellambse/model/diagram.py @@ -46,7 +46,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 @@ -192,14 +197,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: @@ -242,7 +247,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)) + [ @@ -345,7 +349,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 = [] @@ -357,7 +361,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: ... @@ -565,7 +569,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) @@ -586,28 +593,20 @@ 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 - ) + 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( @@ -621,7 +620,6 @@ def from_model( self._model = model self._element = element self._last_render_params = {} - self.__nodes = None return self target_id = element.get("uid") @@ -641,27 +639,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( + 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): @@ -694,9 +690,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: @@ -728,6 +724,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/pyproject.toml b/pyproject.toml index 75e6026ac..b39b43cd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "awesomeversion>=24.2.0", "diskcache>=5.0", "lxml>=4.5.0", "markupsafe>=2.0", @@ -100,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/static_assertions.py b/tests/static_assertions.py index d5af91e06..0a7ae894c 100644 --- a/tests/static_assertions.py +++ b/tests/static_assertions.py @@ -12,6 +12,7 @@ def protocol_ModelObject_compliance(): mobj = model.ModelElement() # type: ignore[call-arg] mobj = model._descriptors._Specification() # type: ignore[call-arg] + mobj = model.diagram.AbstractDiagram() # type: ignore[call-arg,abstract] mobj = model.diagram.Diagram() del mobj diff --git a/tests/test_model_creation_deletion.py b/tests/test_model_creation_deletion.py index 551f1ec06..cb034f107 100644 --- a/tests/test_model_creation_deletion.py +++ b/tests/test_model_creation_deletion.py @@ -109,15 +109,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 52b286933..6d6a5860e 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), @@ -565,6 +565,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", @@ -580,6 +581,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) @@ -605,17 +611,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 c1d2eeb61..cdf90491f 100644 --- a/tests/test_model_pvmt.py +++ b/tests/test_model_pvmt.py @@ -92,7 +92,10 @@ def test_groups(self, model): def test_apply_outofscope(self, model, group): obj = model.by_uuid("d32caffc-b9a1-448e-8e96-65a36ba06292") domain = model.pvmt.domains["Out of scope"] + assert isinstance(domain, pvmt.ManagedDomain) group = domain.groups[group] + assert isinstance(group, pvmt.ManagedGroup) + with pytest.raises(pvmt.ScopeError): group.apply(obj)