code-constraints 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_constraints/__init__.py +1 -0
- code_constraints/cli/__init__.py +0 -0
- code_constraints/cli/__main__.py +1555 -0
- code_constraints/cli/_assets/agents/cdec-architect.md +468 -0
- code_constraints/cli/_assets/agents/oop-refactor-architect.md +317 -0
- code_constraints/cli/_assets/shims/csharp/CodeConstraintsRules.cs +94 -0
- code_constraints/cli/_assets/shims/julia/CdecRules.jl +129 -0
- code_constraints/cli/_assets/shims/lua/cdec_rules.lua +92 -0
- code_constraints/cli/_assets/shims/odin/cdec_rules.odin +67 -0
- code_constraints/cli/_assets/shims/python/cdec_rules.py +94 -0
- code_constraints/cli/_assets/skills/cdec-architecture-loop/SKILL.md +152 -0
- code_constraints/cli/depstamp.py +118 -0
- code_constraints/cli/detect.py +77 -0
- code_constraints/cli/interactive.py +304 -0
- code_constraints/cli/scaffold.py +602 -0
- code_constraints/cli/update.py +157 -0
- code_constraints/core/__init__.py +41 -0
- code_constraints/core/annotations.py +217 -0
- code_constraints/core/associations.py +134 -0
- code_constraints/core/diff.py +302 -0
- code_constraints/core/editor_io.py +280 -0
- code_constraints/core/graph_model.py +681 -0
- code_constraints/core/keys.py +105 -0
- code_constraints/core/model.py +294 -0
- code_constraints/core/model_io.py +65 -0
- code_constraints/core/receivers.py +34 -0
- code_constraints/core/rules.py +177 -0
- code_constraints/core/rulesdoc.py +208 -0
- code_constraints/core/tags.py +114 -0
- code_constraints/core/ts_fingerprint.py +88 -0
- code_constraints/core/xmi_reader.py +358 -0
- code_constraints/core/xmi_writer.py +373 -0
- code_constraints/csharp/__init__.py +3 -0
- code_constraints/csharp/activity.py +250 -0
- code_constraints/csharp/conformance.py +331 -0
- code_constraints/csharp/fingerprint.py +274 -0
- code_constraints/csharp/parser.py +436 -0
- code_constraints/csharp/rules_extract.py +78 -0
- code_constraints/csharp/sequence.py +295 -0
- code_constraints/enforce/__init__.py +15 -0
- code_constraints/enforce/engine.py +122 -0
- code_constraints/enforce/model.py +74 -0
- code_constraints/julia/__init__.py +5 -0
- code_constraints/julia/conformance.py +282 -0
- code_constraints/julia/fingerprint.py +226 -0
- code_constraints/julia/parser.py +523 -0
- code_constraints/julia/rules_extract.py +216 -0
- code_constraints/lint/__init__.py +10 -0
- code_constraints/lint/baseline.py +96 -0
- code_constraints/lint/config.py +239 -0
- code_constraints/lint/engine.py +179 -0
- code_constraints/lint/pipeline.py +108 -0
- code_constraints/lint/report.py +151 -0
- code_constraints/lint/rules/__init__.py +50 -0
- code_constraints/lint/rules/base.py +200 -0
- code_constraints/lint/rules/cyclic_package_dependencies.py +69 -0
- code_constraints/lint/rules/dangling_classes.py +98 -0
- code_constraints/lint/rules/forbidden_package_references.py +47 -0
- code_constraints/lint/rules/forbidden_references.py +48 -0
- code_constraints/lint/rules/frozen_members.py +67 -0
- code_constraints/lint/rules/frozen_rules.py +105 -0
- code_constraints/lint/rules/implementation_locks.py +156 -0
- code_constraints/lint/rules/layer_dependencies.py +92 -0
- code_constraints/lint/rules/max_class_fanout.py +41 -0
- code_constraints/lint/rules/no_new_classes.py +27 -0
- code_constraints/lint/rules/no_removed_classes.py +27 -0
- code_constraints/lint/rules/reference_architecture.py +111 -0
- code_constraints/lint/rules/subclass_naming.py +71 -0
- code_constraints/lint/rules/tag_conformance.py +76 -0
- code_constraints/lock/__init__.py +73 -0
- code_constraints/lock/engine.py +395 -0
- code_constraints/lock/model.py +235 -0
- code_constraints/lock/store.py +144 -0
- code_constraints/lua/__init__.py +5 -0
- code_constraints/lua/conformance.py +239 -0
- code_constraints/lua/fingerprint.py +252 -0
- code_constraints/lua/parser.py +500 -0
- code_constraints/lua/rules_extract.py +55 -0
- code_constraints/mcp/__init__.py +20 -0
- code_constraints/mcp/__main__.py +73 -0
- code_constraints/mcp/server.py +1203 -0
- code_constraints/odin/__init__.py +5 -0
- code_constraints/odin/conformance.py +244 -0
- code_constraints/odin/fingerprint.py +159 -0
- code_constraints/odin/parser.py +471 -0
- code_constraints/odin/rules_extract.py +38 -0
- code_constraints/python/__init__.py +3 -0
- code_constraints/python/activity.py +278 -0
- code_constraints/python/conformance.py +249 -0
- code_constraints/python/fingerprint.py +231 -0
- code_constraints/python/parser.py +330 -0
- code_constraints/python/rules_extract.py +83 -0
- code_constraints/python/sequence.py +257 -0
- code_constraints/reference/__init__.py +15 -0
- code_constraints/reference/compare.py +356 -0
- code_constraints/reference/report.py +38 -0
- code_constraints/svelte/__init__.py +3 -0
- code_constraints/svelte/parser.py +523 -0
- code_constraints/typescript/__init__.py +3 -0
- code_constraints/typescript/parser.py +590 -0
- code_constraints/waivers/__init__.py +89 -0
- code_constraints/waivers/collect.py +167 -0
- code_constraints/waivers/model.py +90 -0
- code_constraints/waivers/ops.py +150 -0
- code_constraints/waivers/review.py +156 -0
- code_constraints/waivers/store.py +300 -0
- code_constraints/web/__init__.py +0 -0
- code_constraints/web/_static/assets/index-3ivBsYY4.css +1 -0
- code_constraints/web/_static/assets/index-BTzTqGFp.js +9 -0
- code_constraints/web/_static/index.html +13 -0
- code_constraints/web/app.py +1076 -0
- code_constraints-0.1.0.dist-info/METADATA +663 -0
- code_constraints-0.1.0.dist-info/RECORD +116 -0
- code_constraints-0.1.0.dist-info/WHEEL +4 -0
- code_constraints-0.1.0.dist-info/entry_points.txt +3 -0
- code_constraints-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Read XMI 2.1 files produced by `xmi_writer` back into a `Project`.
|
|
2
|
+
|
|
3
|
+
This reader is intentionally limited to documents we wrote ourselves; we do
|
|
4
|
+
not attempt to interpret arbitrary third-party XMI. The reader preserves the
|
|
5
|
+
diff annotations on the `cdec:` namespace.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from lxml import etree
|
|
13
|
+
|
|
14
|
+
from code_constraints.core.model import (
|
|
15
|
+
Activity,
|
|
16
|
+
ActivityEdge,
|
|
17
|
+
ActivityNode,
|
|
18
|
+
ActivityNodeKind,
|
|
19
|
+
Association,
|
|
20
|
+
Attribute,
|
|
21
|
+
Class,
|
|
22
|
+
DiffStatus,
|
|
23
|
+
EdgeLayout,
|
|
24
|
+
Fragment,
|
|
25
|
+
Layout,
|
|
26
|
+
Lifeline,
|
|
27
|
+
Message,
|
|
28
|
+
Operation,
|
|
29
|
+
Package,
|
|
30
|
+
Parameter,
|
|
31
|
+
Project,
|
|
32
|
+
RuleAnnotation,
|
|
33
|
+
Sequence,
|
|
34
|
+
SourceLocation,
|
|
35
|
+
Visibility,
|
|
36
|
+
)
|
|
37
|
+
from code_constraints.core.xmi_writer import NS, UML, CDEC, XMI
|
|
38
|
+
|
|
39
|
+
_REVERSE_NODE_TYPE: dict[str, ActivityNodeKind] = {
|
|
40
|
+
"uml:InitialNode": "initial",
|
|
41
|
+
"uml:ActivityFinalNode": "final",
|
|
42
|
+
"uml:OpaqueAction": "action",
|
|
43
|
+
"uml:DecisionNode": "decision",
|
|
44
|
+
"uml:MergeNode": "merge",
|
|
45
|
+
"uml:ForkNode": "fork",
|
|
46
|
+
"uml:JoinNode": "join",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_project(path: str | Path) -> Project:
|
|
51
|
+
tree = etree.parse(str(path))
|
|
52
|
+
root = tree.getroot()
|
|
53
|
+
model = root.find(f"{UML}Model")
|
|
54
|
+
if model is None:
|
|
55
|
+
raise ValueError(f"{path}: no uml:Model element found")
|
|
56
|
+
|
|
57
|
+
lang = model.get(f"{CDEC}sourceLanguage", "python")
|
|
58
|
+
if lang not in (
|
|
59
|
+
"python", "csharp", "typescript", "svelte", "odin", "lua", "julia",
|
|
60
|
+
):
|
|
61
|
+
lang = "python"
|
|
62
|
+
project = Project(
|
|
63
|
+
source_language=lang, # type: ignore[arg-type]
|
|
64
|
+
root_path=model.get(f"{CDEC}rootPath", ""),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for child in model.findall("packagedElement"):
|
|
68
|
+
xmi_type = child.get(f"{XMI}type")
|
|
69
|
+
if xmi_type == "uml:Package":
|
|
70
|
+
project.packages.append(_read_package(child))
|
|
71
|
+
elif xmi_type == "uml:Activity":
|
|
72
|
+
project.activities.append(_read_activity(child))
|
|
73
|
+
elif xmi_type == "uml:Interaction":
|
|
74
|
+
project.sequences.append(_read_sequence(child))
|
|
75
|
+
elif xmi_type == "uml:Association":
|
|
76
|
+
project.associations.append(_read_association(child))
|
|
77
|
+
# Bare classes outside a package land in an implicit __root__ package
|
|
78
|
+
elif xmi_type in ("uml:Class", "uml:Interface", "uml:Enumeration"):
|
|
79
|
+
_ensure_root_package(project).classes.append(_read_class(child))
|
|
80
|
+
|
|
81
|
+
return project
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_association(el: etree._Element) -> Association:
|
|
85
|
+
return Association(
|
|
86
|
+
source=el.get(f"{CDEC}source", ""),
|
|
87
|
+
target=el.get(f"{CDEC}target", ""),
|
|
88
|
+
name=el.get("name") or None,
|
|
89
|
+
source_multiplicity=el.get(f"{CDEC}sourceMultiplicity") or None,
|
|
90
|
+
target_multiplicity=el.get(f"{CDEC}targetMultiplicity") or None,
|
|
91
|
+
source_role=el.get(f"{CDEC}sourceRole") or None,
|
|
92
|
+
target_role=el.get(f"{CDEC}targetRole") or None,
|
|
93
|
+
status=_read_status(el),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ensure_root_package(project: Project) -> Package:
|
|
98
|
+
for p in project.packages:
|
|
99
|
+
if p.qualified_name == "__root__":
|
|
100
|
+
return p
|
|
101
|
+
pkg = Package(name="__root__", qualified_name="__root__")
|
|
102
|
+
project.packages.append(pkg)
|
|
103
|
+
return pkg
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_package(el: etree._Element) -> Package:
|
|
107
|
+
pkg = Package(
|
|
108
|
+
name=el.get("name", ""),
|
|
109
|
+
qualified_name=el.get(f"{CDEC}qualifiedName", el.get("name", "")),
|
|
110
|
+
status=_read_status(el),
|
|
111
|
+
layout=_read_layout(el),
|
|
112
|
+
description=_read_owned_comment(el),
|
|
113
|
+
)
|
|
114
|
+
for child in el.findall("packagedElement"):
|
|
115
|
+
xmi_type = child.get(f"{XMI}type")
|
|
116
|
+
if xmi_type == "uml:Package":
|
|
117
|
+
pkg.sub_packages.append(_read_package(child))
|
|
118
|
+
elif xmi_type in ("uml:Class", "uml:Interface", "uml:Enumeration"):
|
|
119
|
+
pkg.classes.append(_read_class(child))
|
|
120
|
+
return pkg
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _read_class(el: etree._Element) -> Class:
|
|
124
|
+
cls = Class(
|
|
125
|
+
name=el.get("name", ""),
|
|
126
|
+
qualified_name=el.get(f"{CDEC}qualifiedName", el.get("name", "")),
|
|
127
|
+
kind=el.get(f"{CDEC}kind", "class"), # type: ignore[arg-type]
|
|
128
|
+
status=_read_status(el),
|
|
129
|
+
location=_read_location(el),
|
|
130
|
+
layout=_read_layout(el),
|
|
131
|
+
description=_read_owned_comment(el),
|
|
132
|
+
rules=_read_rules(el),
|
|
133
|
+
)
|
|
134
|
+
for gen in el.findall("generalization"):
|
|
135
|
+
base = gen.get(f"{CDEC}general", "")
|
|
136
|
+
if base:
|
|
137
|
+
cls.bases.append(base)
|
|
138
|
+
for dep_el in el.findall(f"{CDEC}dependency"):
|
|
139
|
+
dep = dep_el.get("type", "")
|
|
140
|
+
if dep:
|
|
141
|
+
cls.dependencies.append(dep)
|
|
142
|
+
for attr_el in el.findall("ownedAttribute"):
|
|
143
|
+
cls.attributes.append(_read_attribute(attr_el))
|
|
144
|
+
for op_el in el.findall("ownedOperation"):
|
|
145
|
+
cls.operations.append(_read_operation(op_el))
|
|
146
|
+
return cls
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _read_attribute(el: etree._Element) -> Attribute:
|
|
150
|
+
return Attribute(
|
|
151
|
+
name=el.get("name", ""),
|
|
152
|
+
type=el.get(f"{CDEC}type", ""),
|
|
153
|
+
visibility=_read_visibility(el.get("visibility", "public")),
|
|
154
|
+
is_static=el.get("isStatic") == "true",
|
|
155
|
+
is_readonly=el.get("isReadOnly") == "true",
|
|
156
|
+
default=el.get(f"{CDEC}default"),
|
|
157
|
+
description=_read_owned_comment(el),
|
|
158
|
+
status=_read_status(el),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _read_operation(el: etree._Element) -> Operation:
|
|
163
|
+
params: list[Parameter] = []
|
|
164
|
+
return_type = ""
|
|
165
|
+
for p_el in el.findall("ownedParameter"):
|
|
166
|
+
direction = p_el.get("direction", "in")
|
|
167
|
+
if direction == "return":
|
|
168
|
+
return_type = p_el.get(f"{CDEC}type", "")
|
|
169
|
+
else:
|
|
170
|
+
params.append(
|
|
171
|
+
Parameter(
|
|
172
|
+
name=p_el.get("name", ""),
|
|
173
|
+
type=p_el.get(f"{CDEC}type", ""),
|
|
174
|
+
default=p_el.get(f"{CDEC}default"),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return Operation(
|
|
178
|
+
name=el.get("name", ""),
|
|
179
|
+
parameters=params,
|
|
180
|
+
return_type=return_type,
|
|
181
|
+
visibility=_read_visibility(el.get("visibility", "public")),
|
|
182
|
+
is_static=el.get("isStatic") == "true",
|
|
183
|
+
is_abstract=el.get("isAbstract") == "true",
|
|
184
|
+
description=_read_owned_comment(el),
|
|
185
|
+
rules=_read_rules(el),
|
|
186
|
+
status=_read_status(el),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _read_rules(el: etree._Element) -> list[RuleAnnotation]:
|
|
191
|
+
rules: list[RuleAnnotation] = []
|
|
192
|
+
for rule_el in el.findall(f"{CDEC}rule"):
|
|
193
|
+
name = rule_el.get("name", "")
|
|
194
|
+
if not name:
|
|
195
|
+
continue
|
|
196
|
+
args = [a.get("value", "") for a in rule_el.findall(f"{CDEC}arg")]
|
|
197
|
+
kwargs = {
|
|
198
|
+
kw.get("key", ""): kw.get("value", "")
|
|
199
|
+
for kw in rule_el.findall(f"{CDEC}kwarg")
|
|
200
|
+
if kw.get("key")
|
|
201
|
+
}
|
|
202
|
+
rules.append(RuleAnnotation(name=name, args=args, kwargs=kwargs))
|
|
203
|
+
return rules
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _read_activity(el: etree._Element) -> Activity:
|
|
207
|
+
act = Activity(
|
|
208
|
+
name=el.get("name", ""),
|
|
209
|
+
granularity=el.get(f"{CDEC}granularity", "control-flow"), # type: ignore[arg-type]
|
|
210
|
+
status=_read_status(el),
|
|
211
|
+
location=_read_location(el),
|
|
212
|
+
)
|
|
213
|
+
for node_el in el.findall("node"):
|
|
214
|
+
kind = _REVERSE_NODE_TYPE.get(node_el.get(f"{XMI}type", ""), "action")
|
|
215
|
+
act.nodes.append(
|
|
216
|
+
ActivityNode(
|
|
217
|
+
id=node_el.get(f"{XMI}id", ""),
|
|
218
|
+
kind=kind,
|
|
219
|
+
label=node_el.get("name", ""),
|
|
220
|
+
status=_read_status(node_el),
|
|
221
|
+
layout=_read_layout(node_el),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
for edge_el in el.findall("edge"):
|
|
225
|
+
act.edges.append(
|
|
226
|
+
ActivityEdge(
|
|
227
|
+
source=edge_el.get("source", ""),
|
|
228
|
+
target=edge_el.get("target", ""),
|
|
229
|
+
guard=edge_el.get(f"{CDEC}guard", ""),
|
|
230
|
+
status=_read_status(edge_el),
|
|
231
|
+
edge_layout=_read_edge_layout(edge_el),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return act
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _read_sequence(el: etree._Element) -> Sequence:
|
|
238
|
+
seq = Sequence(
|
|
239
|
+
name=el.get("name", ""),
|
|
240
|
+
status=_read_status(el),
|
|
241
|
+
location=_read_location(el),
|
|
242
|
+
)
|
|
243
|
+
for ll_el in el.findall("lifeline"):
|
|
244
|
+
col = ll_el.get(f"{CDEC}columnX")
|
|
245
|
+
seq.lifelines.append(
|
|
246
|
+
Lifeline(
|
|
247
|
+
name=ll_el.get("name", ""),
|
|
248
|
+
represents=ll_el.get(f"{CDEC}represents", ""),
|
|
249
|
+
column_x=float(col) if col is not None else None,
|
|
250
|
+
status=_read_status(ll_el),
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
for m_el in el.findall("message"):
|
|
254
|
+
seq.messages.append(
|
|
255
|
+
Message(
|
|
256
|
+
sender=m_el.get(f"{CDEC}sender", ""),
|
|
257
|
+
receiver=m_el.get(f"{CDEC}receiver", ""),
|
|
258
|
+
label=m_el.get("name", ""),
|
|
259
|
+
is_return=m_el.get(f"{CDEC}isReturn") == "true",
|
|
260
|
+
guard=m_el.get(f"{CDEC}guard", ""),
|
|
261
|
+
status=_read_status(m_el),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
for f_el in el.findall("fragment"):
|
|
265
|
+
try:
|
|
266
|
+
start_row = int(f_el.get(f"{CDEC}startRow", "0"))
|
|
267
|
+
end_row = int(f_el.get(f"{CDEC}endRow", "0"))
|
|
268
|
+
except ValueError:
|
|
269
|
+
continue
|
|
270
|
+
kind_raw = f_el.get(f"{CDEC}kind", "alt")
|
|
271
|
+
if kind_raw not in ("alt", "opt", "loop"):
|
|
272
|
+
kind_raw = "alt"
|
|
273
|
+
seq.fragments.append(
|
|
274
|
+
Fragment(
|
|
275
|
+
kind=kind_raw, # type: ignore[arg-type]
|
|
276
|
+
label=f_el.get("name", ""),
|
|
277
|
+
start_row=start_row,
|
|
278
|
+
end_row=end_row,
|
|
279
|
+
status=_read_status(f_el),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return seq
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _read_owned_comment(el: etree._Element) -> str | None:
|
|
286
|
+
"""Return the body text of the first `ownedComment` child, if any.
|
|
287
|
+
|
|
288
|
+
Tolerant of multiple comments (takes the first) and of an absent
|
|
289
|
+
`<body>` element (returns None).
|
|
290
|
+
"""
|
|
291
|
+
comment = el.find("ownedComment")
|
|
292
|
+
if comment is None:
|
|
293
|
+
return None
|
|
294
|
+
body = comment.find("body")
|
|
295
|
+
if body is None or body.text is None:
|
|
296
|
+
return None
|
|
297
|
+
text = body.text.strip()
|
|
298
|
+
return text or None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _read_status(el: etree._Element) -> DiffStatus:
|
|
302
|
+
raw = el.get(f"{CDEC}status", "unchanged")
|
|
303
|
+
try:
|
|
304
|
+
return DiffStatus(raw)
|
|
305
|
+
except ValueError:
|
|
306
|
+
return DiffStatus.UNCHANGED
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _read_visibility(raw: str) -> Visibility:
|
|
310
|
+
try:
|
|
311
|
+
return Visibility(raw)
|
|
312
|
+
except ValueError:
|
|
313
|
+
return Visibility.PUBLIC
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _read_location(el: etree._Element) -> SourceLocation | None:
|
|
317
|
+
file = el.get(f"{CDEC}file")
|
|
318
|
+
if not file:
|
|
319
|
+
return None
|
|
320
|
+
try:
|
|
321
|
+
start = int(el.get(f"{CDEC}startLine", "0"))
|
|
322
|
+
end = int(el.get(f"{CDEC}endLine", "0"))
|
|
323
|
+
except ValueError:
|
|
324
|
+
return None
|
|
325
|
+
return SourceLocation(file=file, start_line=start, end_line=end)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _read_layout(el: etree._Element) -> Layout | None:
|
|
329
|
+
x = el.get(f"{CDEC}x")
|
|
330
|
+
y = el.get(f"{CDEC}y")
|
|
331
|
+
if x is None and y is None:
|
|
332
|
+
return None
|
|
333
|
+
try:
|
|
334
|
+
return Layout(
|
|
335
|
+
x=float(x or "0"),
|
|
336
|
+
y=float(y or "0"),
|
|
337
|
+
width=float(el.get(f"{CDEC}width", "0")),
|
|
338
|
+
height=float(el.get(f"{CDEC}height", "0")),
|
|
339
|
+
collapsed=el.get(f"{CDEC}collapsed") == "true",
|
|
340
|
+
)
|
|
341
|
+
except ValueError:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _read_edge_layout(el: etree._Element) -> EdgeLayout | None:
|
|
346
|
+
raw = el.get(f"{CDEC}waypoints")
|
|
347
|
+
if not raw:
|
|
348
|
+
return None
|
|
349
|
+
waypoints: list[tuple[float, float]] = []
|
|
350
|
+
for pair in raw.split(";"):
|
|
351
|
+
if not pair:
|
|
352
|
+
continue
|
|
353
|
+
try:
|
|
354
|
+
xs, ys = pair.split(",", 1)
|
|
355
|
+
waypoints.append((float(xs), float(ys)))
|
|
356
|
+
except ValueError:
|
|
357
|
+
continue
|
|
358
|
+
return EdgeLayout(waypoints=waypoints) if waypoints else None
|