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

412 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 22:22 +0000

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

2# _____ _ _ ____ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2017-2025 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, List, Dict, Union, Optional as Nullable 

41 

42try: 

43 from pyTooling.Decorators import export, readonly 

44 from pyTooling.MetaClasses import ExtendedType 

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

46 from pyTooling.Tree import Node as pyToolingNode 

47except (ImportError, ModuleNotFoundError): # pragma: no cover 

48 print("[pyTooling.Graph.GraphML] Could not import from 'pyTooling.*'!") 

49 

50 try: 

51 from Decorators import export, readonly 

52 from MetaClasses import ExtendedType, mixin 

53 from Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph 

54 from Tree import Node as pyToolingNode 

55 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

56 print("[pyTooling.Graph.GraphML] Could not import directly!") 

57 raise ex 

58 

59 

60@export 

61class AttributeContext(Enum): 

62 """ 

63 Enumeration of all attribute contexts. 

64 

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

66 """ 

67 GraphML = auto() 

68 Graph = auto() 

69 Node = auto() 

70 Edge = auto() 

71 Port = auto() 

72 

73 def __str__(self) -> str: 

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

75 

76 

77@export 

78class AttributeTypes(Enum): 

79 """ 

80 Enumeration of all attribute types. 

81 

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

83 """ 

84 Boolean = auto() 

85 Int = auto() 

86 Long = auto() 

87 Float = auto() 

88 Double = auto() 

89 String = auto() 

90 

91 def __str__(self) -> str: 

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

93 

94 

95@export 

96class EdgeDefault(Enum): 

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

98 Undirected = auto() 

99 Directed = auto() 

100 

101 def __str__(self) -> str: 

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

103 

104 

105@export 

106class ParsingOrder(Enum): 

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

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

109 AdjacencyList = auto() 

110 Free = auto() 

111 

112 def __str__(self) -> str: 

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

114 

115 

116@export 

117class IDStyle(Enum): 

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

119 Canonical = auto() 

120 Free = auto() 

121 

122 def __str__(self) -> str: 

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

124 

125 

126@export 

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

128 """ 

129 Base-class for all GraphML data model classes. 

130 """ 

131 @readonly 

132 def HasClosingTag(self) -> bool: 

133 return True 

134 

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

136 raise NotImplementedError() 

137 

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

139 raise NotImplementedError() 

140 

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

142 raise NotImplementedError() 

143 

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

145 raise NotImplementedError() 

146 

147 

148@export 

149class BaseWithID(Base): 

150 _id: str 

151 

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

153 super().__init__() 

154 self._id = identifier 

155 

156 @readonly 

157 def ID(self) -> str: 

158 return self._id 

159 

160 

161@export 

162class BaseWithData(BaseWithID): 

163 _data: List['Data'] 

164 

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

166 super().__init__(identifier) 

167 

168 self._data = [] 

169 

170 @readonly 

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

172 return self._data 

173 

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

175 self._data.append(data) 

176 return data 

177 

178 

179@export 

180class Key(BaseWithID): 

181 _context: AttributeContext 

182 _attributeName: str 

183 _attributeType: AttributeTypes 

184 

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

186 super().__init__(identifier) 

187 

188 self._context = context 

189 self._attributeName = name 

190 self._attributeType = type 

191 

192 @readonly 

193 def Context(self) -> AttributeContext: 

194 return self._context 

195 

196 @readonly 

197 def AttributeName(self) -> str: 

198 return self._attributeName 

199 

200 @readonly 

201 def AttributeType(self) -> AttributeTypes: 

202 return self._attributeType 

203 

204 @readonly 

205 def HasClosingTag(self) -> bool: 

206 return False 

207 

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

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

210 

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

212 return [self.Tag(indent)] 

213 

214 

215@export 

216class Data(Base): 

217 _key: Key 

218 _data: Any 

219 

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

221 super().__init__() 

222 

223 self._key = key 

224 self._data = data 

225 

226 @readonly 

227 def Key(self) -> Key: 

228 return self._key 

229 

230 @readonly 

231 def Data(self) -> Any: 

232 return self._data 

233 

234 @readonly 

235 def HasClosingTag(self) -> bool: 

236 return False 

237 

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

239 data = str(self._data) 

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

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

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

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

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

245 

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

247 return [self.Tag(indent)] 

248 

249 

250@export 

251class Node(BaseWithData): 

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

253 super().__init__(identifier) 

254 

255 @readonly 

256 def HasClosingTag(self) -> bool: 

257 return len(self._data) > 0 

258 

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

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

261 

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

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

264 

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

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

267 

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

269 if not self.HasClosingTag: 

270 return [self.Tag(indent)] 

271 

272 lines = [self.OpeningTag(indent)] 

273 for data in self._data: 

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

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

276 

277 return lines 

278 

279 

280@export 

281class Edge(BaseWithData): 

282 _source: Node 

283 _target: Node 

284 

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

286 super().__init__(identifier) 

287 

288 self._source = source 

289 self._target = target 

290 

291 @readonly 

292 def Source(self) -> Node: 

293 return self._source 

294 

295 @readonly 

296 def Target(self) -> Node: 

297 return self._target 

298 

299 @readonly 

300 def HasClosingTag(self) -> bool: 

301 return len(self._data) > 0 

302 

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

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

305 

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

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

308 

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

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

311 

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

313 if not self.HasClosingTag: 

314 return [self.Tag(indent)] 

315 

316 lines = [self.OpeningTag(indent)] 

317 for data in self._data: 

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

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

320 

321 return lines 

322 

323 

324@export 

325class BaseGraph(BaseWithData, mixin=True): 

326 _subgraphs: Dict[str, 'Subgraph'] 

327 _nodes: Dict[str, Node] 

328 _edges: Dict[str, Edge] 

329 _edgeDefault: EdgeDefault 

330 _parseOrder: ParsingOrder 

331 _nodeIDStyle: IDStyle 

332 _edgeIDStyle: IDStyle 

333 

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

335 super().__init__(identifier) 

336 

337 self._subgraphs = {} 

338 self._nodes = {} 

339 self._edges = {} 

340 self._edgeDefault = EdgeDefault.Directed 

341 self._parseOrder = ParsingOrder.NodesFirst 

342 self._nodeIDStyle = IDStyle.Free 

343 self._edgeIDStyle = IDStyle.Free 

344 

345 @readonly 

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

347 return self._subgraphs 

348 

349 @readonly 

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

351 return self._nodes 

352 

353 @readonly 

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

355 return self._edges 

356 

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

358 self._subgraphs[subgraph._subgraphID] = subgraph 

359 self._nodes[subgraph._id] = subgraph 

360 return subgraph 

361 

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

363 return self._subgraphs[subgraphName] 

364 

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

366 self._nodes[node._id] = node 

367 return node 

368 

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

370 return self._nodes[nodeName] 

371 

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

373 self._edges[edge._id] = edge 

374 return edge 

375 

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

377 return self._edges[edgeName] 

378 

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

380 return f"""\ 

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

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

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

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

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

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

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

388""" 

389 

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

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

392 

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

394 lines = [self.OpeningTag(indent)] 

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

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

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

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

399 # for data in self._data: 

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

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

402 

403 return lines 

404 

405 

406@export 

407class Graph(BaseGraph): 

408 _document: 'GraphMLDocument' 

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

410 

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

412 super().__init__(identifier) 

413 self._document = document 

414 self._ids = {} 

415 

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

417 return self._ids[identifier] 

418 

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

420 result = super().AddSubgraph(subgraph) 

421 self._ids[subgraph._subgraphID] = subgraph 

422 subgraph._root = self 

423 return result 

424 

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

426 result = super().AddNode(node) 

427 self._ids[node._id] = node 

428 return result 

429 

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

431 result = super().AddEdge(edge) 

432 self._ids[edge._id] = edge 

433 return result 

434 

435 

436@export 

437class Subgraph(Node, BaseGraph): 

438 _subgraphID: str 

439 _root: Nullable[Graph] 

440 

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

442 super().__init__(nodeIdentifier) 

443 BaseGraph.__init__(self, nodeIdentifier) 

444 

445 self._subgraphID = graphIdentifier 

446 self._root = None 

447 

448 @readonly 

449 def RootGraph(self) -> Graph: 

450 return self._root 

451 

452 @readonly 

453 def SubgraphID(self) -> str: 

454 return self._subgraphID 

455 

456 @readonly 

457 def HasClosingTag(self) -> bool: 

458 return True 

459 

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

461 result = super().AddNode(node) 

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

463 return result 

464 

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

466 result = super().AddEdge(edge) 

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

468 return result 

469 

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

471 raise NotImplementedError() 

472 

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

474 return f"""\ 

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

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

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

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

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

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

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

482""" 

483 

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

485 return BaseGraph.ClosingTag(self, indent) 

486 

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

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

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

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

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

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

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

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

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

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

497 # for data in self._data: 

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

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

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

501 

502 return lines 

503 

504 

505@export 

506class GraphMLDocument(Base): 

507 xmlNS = { 

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

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

510 } 

511 xsi = { 

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

513 } 

514 

515 _graph: Graph 

516 _keys: Dict[str, Key] 

517 

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

519 super().__init__() 

520 

521 self._graph = Graph(self, identifier) 

522 self._keys = {} 

523 

524 @readonly 

525 def Graph(self) -> BaseGraph: 

526 return self._graph 

527 

528 @readonly 

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

530 return self._keys 

531 

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

533 self._keys[key._id] = key 

534 return key 

535 

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

537 return self._keys[keyName] 

538 

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

540 return keyName in self._keys 

541 

542 def FromGraph(self, graph: pyToolingGraph): 

543 document = self 

544 self._graph._id = graph._name 

545 

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

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

548 

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

550 for vertex in pyTGraph.IterateVertices(): 

551 newNode = Node(vertex._id) 

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

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

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

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

556 else: 

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

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

559 

560 rootGraph.AddNode(newNode) 

561 

562 for edge in pyTGraph.IterateEdges(): 

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

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

565 

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

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

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

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

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

571 else: 

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

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

574 

575 rootGraph.AddEdge(newEdge) 

576 

577 for link in pyTGraph.IterateLinks(): 

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

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

580 

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

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

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

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

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

586 else: 

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

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

589 

590 rootGraph.AddEdge(newEdge) 

591 

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

593 rootGraph = nodeGraph.RootGraph 

594 

595 for vertex in pyTSubgraph.IterateVertices(): 

596 newNode = Node(vertex._id) 

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

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

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

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

601 else: 

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

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

604 

605 nodeGraph.AddNode(newNode) 

606 

607 for edge in pyTSubgraph.IterateEdges(): 

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

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

610 

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

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

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

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

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

616 else: 

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

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

619 

620 nodeGraph.AddEdge(newEdge) 

621 

622 for subgraph in graph.Subgraphs: 

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

624 self._graph.AddSubgraph(nodeGraph) 

625 translateSubgraph(nodeGraph, subgraph) 

626 

627 translateGraph(self._graph, graph) 

628 

629 def FromTree(self, tree: pyToolingNode): 

630 self._graph._id = tree._id 

631 

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

633 

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

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

636 

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

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

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

640 

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

642 

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

644 return f"""\ 

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

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

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

648""" 

649 

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

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

652 

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

654 lines = [self.OpeningTag(indent)] 

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

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

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

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

659 

660 return lines 

661 

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

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

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

665 f.writelines(self.ToStringLines())