Skip to content

Commit 3d87153

Browse files
committed
feat: Re-type MelodyLoader uses in preparation for alternative backends
1 parent c92295b commit 3d87153

File tree

17 files changed

+292
-86
lines changed

17 files changed

+292
-86
lines changed

src/capellambse/aird/__init__.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,23 @@ class _DiagramDescriptor(t.NamedTuple):
6262
target: etree._Element
6363

6464

65+
def _iter_views(model: loader.Loader, /) -> cabc.Iterator:
66+
# cf. C.XP_VIEWS
67+
for tree in model.trees.values():
68+
if tree.fragment_type != loader.FragmentType.VISUAL:
69+
continue
70+
if tree.root.tag == helpers.TAG_XMI:
71+
roots = tree.root.iterchildren()
72+
else:
73+
roots = iter([tree.root])
74+
for root in roots:
75+
if root.tag != f"{{{_n.NAMESPACES['viewpoint']}}}DAnalysis":
76+
continue
77+
yield from root.iterchildren("ownedViews")
78+
79+
6580
def enumerate_descriptors(
66-
model: loader.MelodyLoader,
81+
model: loader.Loader,
6782
*,
6883
viewpoint: str | None = None,
6984
) -> cabc.Iterator[DRepresentationDescriptor]:
@@ -77,7 +92,7 @@ def enumerate_descriptors(
7792
Only return diagrams of the given viewpoint. If not given, all
7893
diagrams are returned.
7994
"""
80-
for view in model.xpath(C.XP_VIEWS):
95+
for view in _iter_views(model):
8196
if viewpoint and _viewpoint_of(view) != viewpoint:
8297
continue
8398

@@ -89,7 +104,7 @@ def enumerate_descriptors(
89104
raise RuntimeError(
90105
f"Malformed diagram reference: {rep_path!r}"
91106
)
92-
diag_root = model[rep_path]
107+
diag_root = model.follow_link(d, rep_path)
93108
if diag_root.tag not in DIAGRAM_ROOTS:
94109
continue
95110

@@ -130,12 +145,12 @@ def parse_diagrams(
130145

131146

132147
def _build_descriptor(
133-
model: loader.MelodyLoader,
148+
model: loader.Loader,
134149
descriptor: DRepresentationDescriptor,
135150
) -> _DiagramDescriptor:
136151
assert isinstance(descriptor, etree._Element)
137152

138-
diag_root = model[descriptor.attrib["repPath"]]
153+
diag_root = model.follow_link(descriptor, descriptor.attrib["repPath"])
139154
styleclass = get_styleclass(descriptor)
140155
target = find_target(model, descriptor)
141156

@@ -151,7 +166,7 @@ def _build_descriptor(
151166

152167

153168
def find_target(
154-
model: loader.MelodyLoader, descriptor: DRepresentationDescriptor
169+
model: loader.Loader, descriptor: DRepresentationDescriptor
155170
) -> etree._Element:
156171
assert isinstance(descriptor, etree._Element)
157172
target_anchors = list(descriptor.iterchildren("target"))
@@ -180,7 +195,7 @@ def get_styleclass(descriptor: DRepresentationDescriptor) -> str | None:
180195

181196

182197
def parse_diagram(
183-
model: loader.MelodyLoader,
198+
model: loader.Loader,
184199
descriptor: DRepresentationDescriptor,
185200
**params: t.Any,
186201
) -> diagram.Diagram:
@@ -255,7 +270,11 @@ def parse_diagram(
255270
def _element_from_xml(ebd: C.ElementBuilder) -> diagram.DiagramElement:
256271
"""Construct a single diagram element from the model XML."""
257272
element = ebd.data_element.get("element")
258-
tag = ebd.melodyloader[element].tag if element else None
273+
tag = (
274+
ebd.melodyloader.follow_link(ebd.data_element, element).tag
275+
if element
276+
else None
277+
)
259278
if element is not None and tag != "ownedRepresentationDescriptors":
260279
factory = _semantic.from_xml
261280
else:
@@ -264,7 +283,7 @@ def _element_from_xml(ebd: C.ElementBuilder) -> diagram.DiagramElement:
264283

265284

266285
def iter_visible(
267-
model: loader.MelodyLoader,
286+
model: loader.Loader,
268287
descriptor: DRepresentationDescriptor,
269288
) -> cabc.Iterator[etree._Element]:
270289
r"""Iterate over all semantic elements that are visible in a diagram.

src/capellambse/aird/_box_factories.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,9 @@ def statemode_activities_factory(seb: C.SemanticElementBuilder) -> diagram.Box:
427427
for elm in seb.diag_element.iterchildren("ownedElements"):
428428
elm_id = elm.get("uid")
429429
try:
430-
target_id = next(elm.iterchildren("target")).attrib["href"]
431-
target = seb.melodyloader[target_id]
430+
target_link = next(elm.iterchildren("target"))
431+
target_id = target_link.attrib["href"]
432+
target = seb.melodyloader.follow_link(target_link, target_id)
432433
mapping_id = next(elm.iterchildren("actualMapping")).attrib["href"]
433434
except (KeyError, StopIteration):
434435
C.LOGGER.error("No usable target or mapping for %r", elm_id)

src/capellambse/aird/_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ElementBuilder:
5252
target_diagram: diagram.Diagram
5353
diagram_tree: etree._Element
5454
data_element: etree._Element
55-
melodyloader: capellambse.loader.MelodyLoader
55+
melodyloader: capellambse.loader.Loader
5656
fragment: pathlib.PurePosixPath
5757

5858

src/capellambse/aird/_edge_factories.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,9 @@ def req_relation_factory(seb: C.SemanticElementBuilder) -> diagram.Edge:
608608
if not label:
609609
try:
610610
reltype_id = seb.melodyobjs[0].attrib["relationType"]
611-
reltype = seb.melodyloader[reltype_id]
611+
reltype = seb.melodyloader.follow_link(
612+
seb.melodyobjs[0], reltype_id
613+
)
612614
label = reltype.attrib["ReqIFLongName"]
613615
except KeyError:
614616
C.LOGGER.warning(

src/capellambse/aird/_filters/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class FilterArguments:
142142

143143
target_diagram: diagram.Diagram
144144
diagram_root: etree._Element
145-
melodyloader: capellambse.loader.MelodyLoader
145+
melodyloader: capellambse.loader.Loader
146146
params: dict[str, t.Any]
147147

148148

@@ -174,7 +174,7 @@ def applyfilters(args: FilterArguments) -> None:
174174
dgobject.styleclass or dgobject.__class__.__name__,
175175
dgobject.uuid,
176176
)
177-
data_element = args.melodyloader[dgobject.uuid]
177+
data_element = args.melodyloader.follow_link(None, dgobject.uuid)
178178
p2flt(
179179
c.ElementBuilder(
180180
target_diagram=args.target_diagram,
@@ -280,7 +280,9 @@ def __init__(
280280
self._model = model
281281
self._diagram = diagram
282282
assert isinstance(diagram._element, etree._Element)
283-
self._target = self._model._loader[diagram._element.attrib["repPath"]]
283+
self._target = self._model._loader.follow_link(
284+
diagram._element, diagram._element.attrib["repPath"]
285+
)
284286

285287
@property
286288
def _elements(self) -> t.Iterator[etree._Element]:

src/capellambse/aird/_filters/global.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ def hide_alloc_func_exch(
179179
for cex in component_exchanges:
180180
assert cex.uuid is not None
181181
# Find all allocated functional exchanges
182-
for fex in args.melodyloader[cex.uuid].iterchildren(
182+
cex_elem = args.melodyloader.follow_link(None, cex.uuid)
183+
for fex in cex_elem.iterchildren(
183184
"ownedComponentExchangeFunctionalExchangeAllocations"
184185
):
185186
target = fex.attrib["targetElement"].rsplit("#", 1)[-1]
@@ -189,7 +190,7 @@ def hide_alloc_func_exch(
189190

190191
def _stringify_exchange_items(
191192
obj: diagram.DiagramElement,
192-
melodyloader: capellambse.loader.MelodyLoader,
193+
melodyloader: capellambse.loader.Loader,
193194
sort_items: bool = False,
194195
) -> str:
195196
assert obj.uuid is not None
@@ -209,11 +210,11 @@ def _stringify_exchange_items(
209210
def _get_allocated_exchangeitem_names(
210211
*try_ids: str,
211212
alloc_attr: str,
212-
melodyloader: capellambse.loader.MelodyLoader,
213+
melodyloader: capellambse.loader.Loader,
213214
) -> tuple[lxml.etree._Element | None, list[str]]:
214215
for obj_id in try_ids:
215216
try:
216-
elm = melodyloader[obj_id]
217+
elm = melodyloader.follow_link(None, obj_id)
217218
except KeyError:
218219
pass
219220
else:
@@ -223,7 +224,7 @@ def _get_allocated_exchangeitem_names(
223224

224225
if elm.tag == "ownedDiagramElements":
225226
targetlink = next(elm.iterchildren("target"))
226-
elm = melodyloader[targetlink.attrib["href"]]
227+
elm = melodyloader.follow_link(targetlink, targetlink.attrib["href"])
227228

228229
names = []
229230
for elem in melodyloader.follow_links(

src/capellambse/aird/_visual.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ def shape_factory(ebd: c.ElementBuilder) -> diagram.Box:
6666
assert ebd.target_diagram.styleclass is not None
6767

6868
uid = ebd.data_element.attrib[c.ATT_XMID]
69-
element = ebd.data_element.get("element")
70-
if element is not None:
71-
label = ebd.melodyloader[element].attrib["name"]
69+
element_id = ebd.data_element.get("element")
70+
if element_id is not None:
71+
element = ebd.melodyloader.follow_link(ebd.data_element, element_id)
72+
label = element.attrib["name"]
7273
description = ebd.data_element.get("description", "")
7374
else:
7475
label = ebd.data_element.get("description", "")
@@ -108,7 +109,7 @@ def shape_factory(ebd: c.ElementBuilder) -> diagram.Box:
108109
int(layout.attrib.get("height", "0")),
109110
)
110111

111-
if element is not None:
112+
if element_id is not None:
112113
styleclass = "RepresentationLink"
113114
else:
114115
styleclass = "Note"

src/capellambse/helpers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ def resolve_namespace(tag: str) -> str:
845845

846846

847847
def unescape_linked_text(
848-
loader: capellambse.loader.MelodyLoader, attr_text: str | None
848+
loader: capellambse.loader.Loader, attr_text: str | None
849849
) -> markupsafe.Markup:
850850
"""Transform the ``linkedText`` into regular HTML."""
851851

@@ -863,7 +863,7 @@ def flatten_element(
863863
ehref = html.escape(href)
864864

865865
try:
866-
target = loader[href]
866+
target = loader.follow_link(None, href)
867867
except KeyError:
868868
yield f"<deleted element {ehref}>"
869869
else:
@@ -887,7 +887,7 @@ def flatten_element(
887887

888888

889889
def escape_linked_text(
890-
loader: capellambse.loader.MelodyLoader, attr_text: str
890+
loader: capellambse.loader.Loader, attr_text: str
891891
) -> str:
892892
"""Transform simple HTML with object links into ``LinkedText``.
893893

src/capellambse/loader/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
.. _LXML Documentation: https://lxml.de/
1010
"""
1111

12+
from ._typing import *
1213
from .core import *
13-
from .modelinfo import ModelInfo as ModelInfo

src/capellambse/loader/_typing.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# SPDX-FileCopyrightText: Copyright DB InfraGO AG
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
__all__ = [
5+
"Element",
6+
"FragmentType",
7+
"Loader",
8+
"ModelInfo",
9+
]
10+
11+
import dataclasses
12+
import enum
13+
import pathlib
14+
from collections.abc import Iterator, Mapping
15+
from contextlib import AbstractContextManager
16+
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeAlias, TypeVar
17+
18+
from lxml import etree
19+
from typing_extensions import Self
20+
21+
from capellambse import filehandler
22+
23+
_E_co = TypeVar("_E_co", covariant=True, bound="Element")
24+
_E = TypeVar("_E", bound="Element")
25+
_Q = TypeVar("_Q")
26+
Loader: TypeAlias = (
27+
"_Loader[Element, etree.QName] | _Loader[etree._Element, etree.QName]"
28+
)
29+
30+
31+
class FragmentType(enum.Enum):
32+
"""The type of an XML fragment."""
33+
34+
SEMANTIC = enum.auto()
35+
VISUAL = enum.auto()
36+
OTHER = enum.auto()
37+
38+
39+
@dataclasses.dataclass
40+
class ModelInfo:
41+
url: str | None
42+
title: str | None
43+
entrypoint: pathlib.PurePosixPath
44+
resources: dict[str, filehandler.abc.HandlerInfo]
45+
capella_version: str
46+
viewpoints: dict[str, str]
47+
48+
49+
class _Tree(Protocol[_E_co, _Q]):
50+
@property
51+
def root(self) -> _E_co: ...
52+
@property
53+
def fragment_type(self) -> FragmentType: ...
54+
55+
def iterall(self, /) -> Iterator[_E_co]: ...
56+
def iter_qtypes(self, /) -> Iterator[_Q]: ...
57+
def iter_qtype(self, qtype: _Q, /) -> Iterator[_E_co]: ...
58+
59+
def add_namespace(self, uri: str, alias: str, /) -> str: ...
60+
61+
62+
class _Loader(Protocol, Generic[_E, _Q]):
63+
@property
64+
def trees(self) -> Mapping[pathlib.PurePosixPath, _Tree[_E, _Q]]: ...
65+
@property
66+
def resources(self) -> dict[str, filehandler.FileHandler]: ...
67+
68+
def get_model_info(self, /) -> ModelInfo: ...
69+
70+
def find_fragment(self, elem: _E, /) -> pathlib.PurePosixPath: ...
71+
def iterancestors(self, elem: _E, /) -> Iterator[_E]: ...
72+
def iterdescendants(self, elem: _E, /) -> Iterator[_E]: ...
73+
def iterchildren(self, elem: _E, tag: str, /) -> Iterator[_E]: ...
74+
def find_references(self, target_id: str, /) -> Iterator[_E]: ...
75+
76+
def create_link(
77+
self,
78+
source: _E,
79+
target: _E,
80+
*,
81+
include_target_type: bool | None = None,
82+
) -> str: ...
83+
def follow_link(self, source: _E | None, id: str, /) -> _E: ...
84+
def follow_links(
85+
self,
86+
source: _E,
87+
id_list: str,
88+
/,
89+
*,
90+
ignore_broken: bool = ...,
91+
) -> list[_E]: ...
92+
93+
def new_uuid(
94+
self,
95+
parent: _E,
96+
/,
97+
*,
98+
want: str | None = ...,
99+
) -> AbstractContextManager[str]: ...
100+
def idcache_index(self, subtree: _E, /) -> None: ...
101+
def idcache_remove(self, subtree: _E, /) -> None: ...
102+
def idcache_rebuild(self, /) -> None: ...
103+
104+
def activate_viewpoint(self, name: str, version: str, /) -> None: ...
105+
def update_namespaces(self, /) -> None: ...
106+
def save(self, /, **kw: Any) -> None: ...
107+
108+
def write_tmp_project_dir(
109+
self, /
110+
) -> AbstractContextManager[pathlib.Path]: ...
111+
112+
113+
class Element(Protocol):
114+
@property
115+
def tag(self) -> str: ...
116+
117+
def iterchildren(self, tag: str = ..., /) -> Iterator[Self]: ...
118+
119+
120+
if TYPE_CHECKING:
121+
122+
def __protocol_compliance_check() -> None:
123+
from capellambse import loader # noqa: PLC0415
124+
125+
tree: _Tree
126+
tree = loader.ModelFile() # type: ignore[call-arg]
127+
del tree
128+
129+
elm: Element
130+
elm = etree._Element()
131+
del elm
132+
133+
ldr: Loader
134+
ldr = loader.MelodyLoader() # type: ignore[call-arg]
135+
del ldr

0 commit comments

Comments
 (0)