Coverage for pyTooling/Graph/GraphML.py: 88%

389 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 07:24 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| # 

7# |_| |___/ |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32A data model to write out GraphML XML files. 

33 

34.. seealso:: 

35 

36 * http://graphml.graphdrawing.org/primer/graphml-primer.html 

37""" 

38from enum import Enum, auto 

39from pathlib import Path 

40from typing import Any, ClassVar, List, Dict, Union, Optional as Nullable 

41 

42from pyTooling.Decorators import export, readonly 

43from pyTooling.MetaClasses import ExtendedType 

44from pyTooling.Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph 

45from pyTooling.Tree import Node as pyToolingNode 

46 

47 

48@export 

49class AttributeContext(Enum): 

50 """ 

51 Enumeration of all attribute contexts. 

52 

53 An attribute context describes to what kind of GraphML node an attribute can be applied. 

54 """ 

55 GraphML = auto() 

56 Graph = auto() 

57 Node = auto() 

58 Edge = auto() 

59 Port = auto() 

60 

61 def __str__(self) -> str: 

62 return f"{self.name.lower()}" 

63 

64 

65@export 

66class AttributeTypes(Enum): 

67 """ 

68 Enumeration of all attribute types. 

69 

70 An attribute type describes what datatype can be applied to an attribute. 

71 """ 

72 Boolean = auto() 

73 Int = auto() 

74 Long = auto() 

75 Float = auto() 

76 Double = auto() 

77 String = auto() 

78 

79 def __str__(self) -> str: 

80 return f"{self.name.lower()}" 

81 

82 

83@export 

84class EdgeDefault(Enum): 

85 """An enumeration describing the default edge direction.""" 

86 Undirected = auto() 

87 Directed = auto() 

88 

89 def __str__(self) -> str: 

90 return f"{self.name.lower()}" 

91 

92 

93@export 

94class ParsingOrder(Enum): 

95 """An enumeration describing the parsing order of the graph's representation.""" 

96 NodesFirst = auto() #: First, all nodes are given, then followed by all edges. 

97 AdjacencyList = auto() 

98 Free = auto() 

99 

100 def __str__(self) -> str: 

101 return f"{self.name.lower()}" 

102 

103 

104@export 

105class IDStyle(Enum): 

106 """An enumeration describing the style of identifiers (IDs).""" 

107 Canonical = auto() 

108 Free = auto() 

109 

110 def __str__(self) -> str: 

111 return f"{self.name.lower()}" 

112 

113 

114@export 

115class Base(metaclass=ExtendedType, slots=True): 

116 """ 

117 Base-class for all GraphML data model classes. 

118 """ 

119 @readonly 

120 def HasClosingTag(self) -> bool: 

121 """ 

122 Check if this XML element is written with a separate closing tag. 

123 

124 :returns: ``True``, if the element needs a closing tag. 

125 """ 

126 return True 

127 

128 def Tag(self, indent: int = 0) -> str: 

129 raise NotImplementedError() 

130 

131 def OpeningTag(self, indent: int = 0) -> str: 

132 raise NotImplementedError() 

133 

134 def ClosingTag(self, indent: int = 0) -> str: 

135 raise NotImplementedError() 

136 

137 def ToStringLines(self, indent: int = 0) -> List[str]: 

138 raise NotImplementedError() 

139 

140 

141@export 

142class BaseWithID(Base): 

143 _id: str 

144 

145 def __init__(self, identifier: str) -> None: 

146 super().__init__() 

147 self._id = identifier 

148 

149 @readonly 

150 def ID(self) -> str: 

151 """ 

152 Read-only property to access the element's unique ID (:attr:`_id`). 

153 

154 :returns: Unique ID of the element. 

155 """ 

156 return self._id 

157 

158 

159@export 

160class BaseWithData(BaseWithID): 

161 _data: List['Data'] 

162 

163 def __init__(self, identifier: str) -> None: 

164 super().__init__(identifier) 

165 

166 self._data = [] 

167 

168 @readonly 

169 def Data(self) -> List['Data']: 

170 """ 

171 Read-only property to access the data elements attached to this element (:attr:`_data`). 

172 

173 :returns: List of data elements. 

174 """ 

175 return self._data 

176 

177 def AddData(self, data: Data) -> Data: 

178 self._data.append(data) 

179 return data 

180 

181 

182@export 

183class Key(BaseWithID): 

184 _context: AttributeContext 

185 _attributeName: str 

186 _attributeType: AttributeTypes 

187 

188 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None: 

189 super().__init__(identifier) 

190 

191 self._context = context 

192 self._attributeName = name 

193 self._attributeType = type 

194 

195 @readonly 

196 def Context(self) -> AttributeContext: 

197 """ 

198 Read-only property to access the context this key applies to (:attr:`_context`). 

199 

200 :returns: The attribute's context (graph, node, edge, ...). 

201 """ 

202 return self._context 

203 

204 @readonly 

205 def AttributeName(self) -> str: 

206 """ 

207 Read-only property to access the name of the described attribute (:attr:`_attributeName`). 

208 

209 :returns: Name of the attribute. 

210 """ 

211 return self._attributeName 

212 

213 @readonly 

214 def AttributeType(self) -> AttributeTypes: 

215 """ 

216 Read-only property to access the type of the described attribute (:attr:`_attributeType`). 

217 

218 :returns: Type of the attribute. 

219 """ 

220 return self._attributeType 

221 

222 @readonly 

223 def HasClosingTag(self) -> bool: 

224 """ 

225 Check if this XML element is written with a separate closing tag. 

226 

227 A key is always written as a self-closing tag. 

228 

229 :returns: ``False``, because a key never has a closing tag. 

230 """ 

231 return False 

232 

233 def Tag(self, indent: int = 2) -> str: 

234 return f"""{' '*indent}<key id="{self._id}" for="{self._context}" attr.name="{self._attributeName}" attr.type="{self._attributeType}" />\n""" 

235 

236 def ToStringLines(self, indent: int = 2) -> List[str]: 

237 return [self.Tag(indent)] 

238 

239 

240@export 

241class Data(Base): 

242 _key: Key 

243 _data: Any 

244 

245 def __init__(self, key: Key, data: Any) -> None: 

246 super().__init__() 

247 

248 self._key = key 

249 self._data = data 

250 

251 @readonly 

252 def Key(self) -> Key: 

253 """ 

254 Read-only property to access the key describing this data element (:attr:`_key`). 

255 

256 :returns: The key this data element refers to. 

257 """ 

258 return self._key 

259 

260 @readonly 

261 def Data(self) -> Any: 

262 """ 

263 Read-only property to access the data element's value (:attr:`_data`). 

264 

265 :returns: Value of the data element. 

266 """ 

267 return self._data 

268 

269 @readonly 

270 def HasClosingTag(self) -> bool: 

271 """ 

272 Check if this XML element is written with a separate closing tag. 

273 

274 :returns: ``False``, because a data element is written inline. 

275 """ 

276 return False 

277 

278 def Tag(self, indent: int = 2) -> str: 

279 data = str(self._data) 

280 data = data.replace("&", "&amp;") 

281 data = data.replace("<", "&lt;") 

282 data = data.replace(">", "&gt;") 

283 data = data.replace("\n", "\\n") 

284 return f"""{' '*indent}<data key="{self._key._id}">{data}</data>\n""" 

285 

286 def ToStringLines(self, indent: int = 2) -> List[str]: 

287 return [self.Tag(indent)] 

288 

289 

290@export 

291class Node(BaseWithData): 

292 def __init__(self, identifier: str) -> None: 

293 super().__init__(identifier) 

294 

295 @readonly 

296 def HasClosingTag(self) -> bool: 

297 """ 

298 Check if this XML element is written with a separate closing tag. 

299 

300 :returns: ``True``, if the node carries data elements, otherwise ``False``. 

301 """ 

302 return len(self._data) > 0 

303 

304 def Tag(self, indent: int = 2) -> str: 

305 return f"""{' '*indent}<node id="{self._id}" />\n""" 

306 

307 def OpeningTag(self, indent: int = 2) -> str: 

308 return f"""{' '*indent}<node id="{self._id}">\n""" 

309 

310 def ClosingTag(self, indent: int = 2) -> str: 

311 return f"""{' ' * indent}</node>\n""" 

312 

313 def ToStringLines(self, indent: int = 2) -> List[str]: 

314 if not self.HasClosingTag: 

315 return [self.Tag(indent)] 

316 

317 lines = [self.OpeningTag(indent)] 

318 for data in self._data: 

319 lines.extend(data.ToStringLines(indent + 1)) 

320 lines.append(self.ClosingTag(indent)) 

321 

322 return lines 

323 

324 

325@export 

326class Edge(BaseWithData): 

327 _source: Node 

328 _target: Node 

329 

330 def __init__(self, identifier: str, source: Node, target: Node) -> None: 

331 super().__init__(identifier) 

332 

333 self._source = source 

334 self._target = target 

335 

336 @readonly 

337 def Source(self) -> Node: 

338 """ 

339 Read-only property to access the edge's source node (:attr:`_source`). 

340 

341 :returns: Source node of the edge. 

342 """ 

343 return self._source 

344 

345 @readonly 

346 def Target(self) -> Node: 

347 """ 

348 Read-only property to access the edge's target node (:attr:`_target`). 

349 

350 :returns: Target node of the edge. 

351 """ 

352 return self._target 

353 

354 @readonly 

355 def HasClosingTag(self) -> bool: 

356 """ 

357 Check if this XML element is written with a separate closing tag. 

358 

359 :returns: ``True``, if the edge carries data elements, otherwise ``False``. 

360 """ 

361 return len(self._data) > 0 

362 

363 def Tag(self, indent: int = 2) -> str: 

364 return f"""{' ' * indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}" />\n""" 

365 

366 def OpeningTag(self, indent: int = 2) -> str: 

367 return f"""{' '*indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}">\n""" 

368 

369 def ClosingTag(self, indent: int = 2) -> str: 

370 return f"""{' ' * indent}</edge>\n""" 

371 

372 def ToStringLines(self, indent: int = 2) -> List[str]: 

373 if not self.HasClosingTag: 

374 return [self.Tag(indent)] 

375 

376 lines = [self.OpeningTag(indent)] 

377 for data in self._data: 

378 lines.extend(data.ToStringLines(indent + 1)) 

379 lines.append(self.ClosingTag(indent)) 

380 

381 return lines 

382 

383 

384@export 

385class BaseGraph(BaseWithData, mixin=True): 

386 _subgraphs: Dict[str, 'Subgraph'] 

387 _nodes: Dict[str, Node] 

388 _edges: Dict[str, Edge] 

389 _edgeDefault: EdgeDefault 

390 _parseOrder: ParsingOrder 

391 _nodeIDStyle: IDStyle 

392 _edgeIDStyle: IDStyle 

393 

394 def __init__(self, identifier: Nullable[str] = None) -> None: 

395 super().__init__(identifier) 

396 

397 self._subgraphs = {} 

398 self._nodes = {} 

399 self._edges = {} 

400 self._edgeDefault = EdgeDefault.Directed 

401 self._parseOrder = ParsingOrder.NodesFirst 

402 self._nodeIDStyle = IDStyle.Free 

403 self._edgeIDStyle = IDStyle.Free 

404 

405 @readonly 

406 def Subgraphs(self) -> Dict[str, 'Subgraph']: 

407 """ 

408 Read-only property to access the graph's subgraphs (:attr:`_subgraphs`). 

409 

410 :returns: Dictionary of subgraph IDs and subgraphs. 

411 """ 

412 return self._subgraphs 

413 

414 @readonly 

415 def Nodes(self) -> Dict[str, Node]: 

416 """ 

417 Read-only property to access the graph's nodes (:attr:`_nodes`). 

418 

419 :returns: Dictionary of node IDs and nodes. 

420 """ 

421 return self._nodes 

422 

423 @readonly 

424 def Edges(self) -> Dict[str, Edge]: 

425 """ 

426 Read-only property to access the graph's edges (:attr:`_edges`). 

427 

428 :returns: Dictionary of edge IDs and edges. 

429 """ 

430 return self._edges 

431 

432 def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph': 

433 self._subgraphs[subgraph._subgraphID] = subgraph 

434 self._nodes[subgraph._id] = subgraph 

435 return subgraph 

436 

437 def GetSubgraph(self, subgraphName: str) -> 'Subgraph': 

438 return self._subgraphs[subgraphName] 

439 

440 def AddNode(self, node: Node) -> Node: 

441 self._nodes[node._id] = node 

442 return node 

443 

444 def GetNode(self, nodeName: str) -> Node: 

445 return self._nodes[nodeName] 

446 

447 def AddEdge(self, edge: Edge) -> Edge: 

448 self._edges[edge._id] = edge 

449 return edge 

450 

451 def GetEdge(self, edgeName: str) -> Edge: 

452 return self._edges[edgeName] 

453 

454 def OpeningTag(self, indent: int = 1) -> str: 

455 return f"""\ 

456{' '*indent}<graph id="{self._id}" 

457{' '*indent} edgedefault="{self._edgeDefault!s}" 

458{' '*indent} parse.nodes="{len(self._nodes)}" 

459{' '*indent} parse.edges="{len(self._edges)}" 

460{' '*indent} parse.order="{self._parseOrder!s}" 

461{' '*indent} parse.nodeids="{self._nodeIDStyle!s}" 

462{' '*indent} parse.edgeids="{self._edgeIDStyle!s}"> 

463""" 

464 

465 def ClosingTag(self, indent: int = 1) -> str: 

466 return f"{' '*indent}</graph>\n" 

467 

468 def ToStringLines(self, indent: int = 1) -> List[str]: 

469 lines = [self.OpeningTag(indent)] 

470 for node in self._nodes.values(): 

471 lines.extend(node.ToStringLines(indent + 1)) 

472 for edge in self._edges.values(): 

473 lines.extend(edge.ToStringLines(indent + 1)) 

474 # for data in self._data: 

475 # lines.extend(data.ToStringLines(indent + 1)) 

476 lines.append(self.ClosingTag(indent)) 

477 

478 return lines 

479 

480 

481@export 

482class Graph(BaseGraph): 

483 _document: 'GraphMLDocument' 

484 _ids: Dict[str, Union[Node, Edge, 'Subgraph']] 

485 

486 def __init__(self, document: 'GraphMLDocument', identifier: str) -> None: 

487 super().__init__(identifier) 

488 self._document = document 

489 self._ids = {} 

490 

491 def GetByID(self, identifier: str) -> Union[Node, Edge, 'Subgraph']: 

492 return self._ids[identifier] 

493 

494 def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph': 

495 result = super().AddSubgraph(subgraph) 

496 self._ids[subgraph._subgraphID] = subgraph 

497 subgraph._root = self 

498 return result 

499 

500 def AddNode(self, node: Node) -> Node: 

501 result = super().AddNode(node) 

502 self._ids[node._id] = node 

503 return result 

504 

505 def AddEdge(self, edge: Edge) -> Edge: 

506 result = super().AddEdge(edge) 

507 self._ids[edge._id] = edge 

508 return result 

509 

510 

511@export 

512class Subgraph(Node, BaseGraph): 

513 _subgraphID: str 

514 _root: Nullable[Graph] 

515 

516 def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None: 

517 super().__init__(nodeIdentifier) 

518 BaseGraph.__init__(self, nodeIdentifier) 

519 

520 self._subgraphID = graphIdentifier 

521 self._root = None 

522 

523 @readonly 

524 def RootGraph(self) -> Graph: 

525 """ 

526 Read-only property to access the graph this subgraph is embedded in (:attr:`_root`). 

527 

528 :returns: The root graph. 

529 """ 

530 return self._root 

531 

532 @readonly 

533 def SubgraphID(self) -> str: 

534 """ 

535 Read-only property to access the subgraph's ID (:attr:`_subgraphID`). 

536 

537 :returns: ID of the subgraph. 

538 """ 

539 return self._subgraphID 

540 

541 @readonly 

542 def HasClosingTag(self) -> bool: 

543 """ 

544 Check if this XML element is written with a separate closing tag. 

545 

546 :returns: ``True``, because a subgraph always has a closing tag. 

547 """ 

548 return True 

549 

550 def AddNode(self, node: Node) -> Node: 

551 result = super().AddNode(node) 

552 self._root._ids[node._id] = node 

553 return result 

554 

555 def AddEdge(self, edge: Edge) -> Edge: 

556 result = super().AddEdge(edge) 

557 self._root._ids[edge._id] = edge 

558 return result 

559 

560 def Tag(self, indent: int = 2) -> str: 

561 raise NotImplementedError() 

562 

563 def OpeningTag(self, indent: int = 1) -> str: 

564 return f"""\ 

565{' ' * indent}<graph id="{self._subgraphID}" 

566{' ' * indent} edgedefault="{self._edgeDefault!s}" 

567{' ' * indent} parse.nodes="{len(self._nodes)}" 

568{' ' * indent} parse.edges="{len(self._edges)}" 

569{' ' * indent} parse.order="{self._parseOrder!s}" 

570{' ' * indent} parse.nodeids="{self._nodeIDStyle!s}" 

571{' ' * indent} parse.edgeids="{self._edgeIDStyle!s}"> 

572""" 

573 

574 def ClosingTag(self, indent: int = 2) -> str: 

575 return BaseGraph.ClosingTag(self, indent) 

576 

577 def ToStringLines(self, indent: int = 2) -> List[str]: 

578 lines = [super().OpeningTag(indent)] 

579 for data in self._data: 579 ↛ 580line 579 didn't jump to line 580 because the loop on line 579 never started

580 lines.extend(data.ToStringLines(indent + 1)) 

581 # lines.extend(Graph.ToStringLines(self, indent + 1)) 

582 lines.append(self.OpeningTag(indent + 1)) 

583 for node in self._nodes.values(): 

584 lines.extend(node.ToStringLines(indent + 2)) 

585 for edge in self._edges.values(): 

586 lines.extend(edge.ToStringLines(indent + 2)) 

587 # for data in self._data: 

588 # lines.extend(data.ToStringLines(indent + 1)) 

589 lines.append(self.ClosingTag(indent + 1)) 

590 lines.append(super().ClosingTag(indent)) 

591 

592 return lines 

593 

594 

595@export 

596class GraphMLDocument(Base): 

597 xmlNS: ClassVar[Dict[Nullable[str], str]] = { 

598 None: "http://graphml.graphdrawing.org/xmlns", 

599 "xsi": "http://www.w3.org/2001/XMLSchema-instance" 

600 } 

601 xsi: ClassVar[Dict[str, str]] = { 

602 "schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd" 

603 } 

604 

605 _graph: Graph 

606 _keys: Dict[str, Key] 

607 

608 def __init__(self, identifier: str = "G") -> None: 

609 super().__init__() 

610 

611 self._graph = Graph(self, identifier) 

612 self._keys = {} 

613 

614 @readonly 

615 def Graph(self) -> BaseGraph: 

616 """ 

617 Read-only property to access the document's graph (:attr:`_graph`). 

618 

619 :returns: The graph described by this document. 

620 """ 

621 return self._graph 

622 

623 @readonly 

624 def Keys(self) -> Dict[str, Key]: 

625 """ 

626 Read-only property to access the attribute keys declared in this document (:attr:`_keys`). 

627 

628 :returns: Dictionary of key IDs and keys. 

629 """ 

630 return self._keys 

631 

632 def AddKey(self, key: Key) -> Key: 

633 self._keys[key._id] = key 

634 return key 

635 

636 def GetKey(self, keyName: str) -> Key: 

637 return self._keys[keyName] 

638 

639 def HasKey(self, keyName: str) -> bool: 

640 return keyName in self._keys 

641 

642 def FromGraph(self, graph: pyToolingGraph) -> None: 

643 document = self 

644 self._graph._id = graph._name 

645 

646 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String)) 

647 edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String)) 

648 

649 def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph): 

650 for vertex in pyTGraph.IterateVertices(): 

651 newNode = Node(vertex._id) 

652 newNode.AddData(Data(nodeValue, vertex._value)) 

653 for key, value in vertex._dict.items(): 653 ↛ 654line 653 didn't jump to line 654 because the loop on line 653 never started

654 if document.HasKey(str(key)): 

655 nodeKey = document.GetKey(f"node{key!s}") 

656 else: 

657 nodeKey = document.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String)) 

658 newNode.AddData(Data(nodeKey, value)) 

659 

660 rootGraph.AddNode(newNode) 

661 

662 for edge in pyTGraph.IterateEdges(): 

663 source = rootGraph.GetByID(edge._source._id) 

664 target = rootGraph.GetByID(edge._destination._id) 

665 

666 newEdge = Edge(edge._id, source, target) 

667 newEdge.AddData(Data(edgeValue, edge._value)) 

668 for key, value in edge._dict.items(): 668 ↛ 669line 668 didn't jump to line 669 because the loop on line 668 never started

669 if self.HasKey(str(key)): 

670 edgeKey = self.GetBy(f"edge{key!s}") 

671 else: 

672 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) 

673 newEdge.AddData(Data(edgeKey, value)) 

674 

675 rootGraph.AddEdge(newEdge) 

676 

677 for link in pyTGraph.IterateLinks(): 

678 source = rootGraph.GetByID(link._source._id) 

679 target = rootGraph.GetByID(link._destination._id) 

680 

681 newEdge = Edge(link._id, source, target) 

682 newEdge.AddData(Data(edgeValue, link._value)) 

683 for key, value in link._dict.items(): 683 ↛ 684line 683 didn't jump to line 684 because the loop on line 683 never started

684 if self.HasKey(str(key)): 

685 edgeKey = self.GetKey(f"link{key!s}") 

686 else: 

687 edgeKey = self.AddKey(Key(f"link{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) 

688 newEdge.AddData(Data(edgeKey, value)) 

689 

690 rootGraph.AddEdge(newEdge) 

691 

692 def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph): 

693 rootGraph = nodeGraph.RootGraph 

694 

695 for vertex in pyTSubgraph.IterateVertices(): 

696 newNode = Node(vertex._id) 

697 newNode.AddData(Data(nodeValue, vertex._value)) 

698 for key, value in vertex._dict.items(): 698 ↛ 699line 698 didn't jump to line 699 because the loop on line 698 never started

699 if self.HasKey(str(key)): 

700 nodeKey = self.GetKey(f"node{key!s}") 

701 else: 

702 nodeKey = self.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String)) 

703 newNode.AddData(Data(nodeKey, value)) 

704 

705 nodeGraph.AddNode(newNode) 

706 

707 for edge in pyTSubgraph.IterateEdges(): 

708 source = nodeGraph.GetNode(edge._source._id) 

709 target = nodeGraph.GetNode(edge._destination._id) 

710 

711 newEdge = Edge(edge._id, source, target) 

712 newEdge.AddData(Data(edgeValue, edge._value)) 

713 for key, value in edge._dict.items(): 713 ↛ 714line 713 didn't jump to line 714 because the loop on line 713 never started

714 if self.HasKey(str(key)): 

715 edgeKey = self.GetKey(f"edge{key!s}") 

716 else: 

717 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String)) 

718 newEdge.AddData(Data(edgeKey, value)) 

719 

720 nodeGraph.AddEdge(newEdge) 

721 

722 for subgraph in graph.Subgraphs: 

723 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name) 

724 self._graph.AddSubgraph(nodeGraph) 

725 translateSubgraph(nodeGraph, subgraph) 

726 

727 translateGraph(self._graph, graph) 

728 

729 def FromTree(self, tree: pyToolingNode) -> None: 

730 self._graph._id = tree._id 

731 

732 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String)) 

733 

734 rootNode = self._graph.AddNode(Node(tree._id)) 

735 rootNode.AddData(Data(nodeValue, tree._value)) 

736 

737 for i, node in enumerate(tree.GetDescendants()): 

738 newNode = self._graph.AddNode(Node(node._id)) 

739 newNode.AddData(Data(nodeValue, node._value)) 

740 

741 newEdge = self._graph.AddEdge(Edge(f"e{i}", newNode, self._graph.GetNode(node._parent._id))) 

742 

743 def OpeningTag(self, indent: int = 0) -> str: 

744 return f"""\ 

745{' '*indent}<graphml xmlns="{self.xmlNS[None]}" 

746{' '*indent} xmlns:xsi="{self.xmlNS["xsi"]}" 

747{' '*indent} xsi:schemaLocation="{self.xsi["schemaLocation"]}"> 

748""" 

749 

750 def ClosingTag(self, indent: int = 0) -> str: 

751 return f"{' '*indent}</graphml>\n" 

752 

753 def ToStringLines(self, indent: int = 0) -> List[str]: 

754 lines = [self.OpeningTag(indent)] 

755 for key in self._keys.values(): 

756 lines.extend(key.ToStringLines(indent + 1)) 

757 lines.extend(self._graph.ToStringLines(indent + 1)) 

758 lines.append(self.ClosingTag(indent)) 

759 

760 return lines 

761 

762 def WriteToFile(self, file: Path) -> None: 

763 with file.open("w", encoding="utf-8") as f: 

764 f.write(f"""<?xml version="1.0" encoding="utf-8"?>""") 

765 f.writelines(self.ToStringLines())