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
« 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.
34.. seealso::
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
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
48@export
49class AttributeContext(Enum):
50 """
51 Enumeration of all attribute contexts.
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()
61 def __str__(self) -> str:
62 return f"{self.name.lower()}"
65@export
66class AttributeTypes(Enum):
67 """
68 Enumeration of all attribute types.
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()
79 def __str__(self) -> str:
80 return f"{self.name.lower()}"
83@export
84class EdgeDefault(Enum):
85 """An enumeration describing the default edge direction."""
86 Undirected = auto()
87 Directed = auto()
89 def __str__(self) -> str:
90 return f"{self.name.lower()}"
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()
100 def __str__(self) -> str:
101 return f"{self.name.lower()}"
104@export
105class IDStyle(Enum):
106 """An enumeration describing the style of identifiers (IDs)."""
107 Canonical = auto()
108 Free = auto()
110 def __str__(self) -> str:
111 return f"{self.name.lower()}"
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.
124 :returns: ``True``, if the element needs a closing tag.
125 """
126 return True
128 def Tag(self, indent: int = 0) -> str:
129 raise NotImplementedError()
131 def OpeningTag(self, indent: int = 0) -> str:
132 raise NotImplementedError()
134 def ClosingTag(self, indent: int = 0) -> str:
135 raise NotImplementedError()
137 def ToStringLines(self, indent: int = 0) -> List[str]:
138 raise NotImplementedError()
141@export
142class BaseWithID(Base):
143 _id: str
145 def __init__(self, identifier: str) -> None:
146 super().__init__()
147 self._id = identifier
149 @readonly
150 def ID(self) -> str:
151 """
152 Read-only property to access the element's unique ID (:attr:`_id`).
154 :returns: Unique ID of the element.
155 """
156 return self._id
159@export
160class BaseWithData(BaseWithID):
161 _data: List['Data']
163 def __init__(self, identifier: str) -> None:
164 super().__init__(identifier)
166 self._data = []
168 @readonly
169 def Data(self) -> List['Data']:
170 """
171 Read-only property to access the data elements attached to this element (:attr:`_data`).
173 :returns: List of data elements.
174 """
175 return self._data
177 def AddData(self, data: Data) -> Data:
178 self._data.append(data)
179 return data
182@export
183class Key(BaseWithID):
184 _context: AttributeContext
185 _attributeName: str
186 _attributeType: AttributeTypes
188 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None:
189 super().__init__(identifier)
191 self._context = context
192 self._attributeName = name
193 self._attributeType = type
195 @readonly
196 def Context(self) -> AttributeContext:
197 """
198 Read-only property to access the context this key applies to (:attr:`_context`).
200 :returns: The attribute's context (graph, node, edge, ...).
201 """
202 return self._context
204 @readonly
205 def AttributeName(self) -> str:
206 """
207 Read-only property to access the name of the described attribute (:attr:`_attributeName`).
209 :returns: Name of the attribute.
210 """
211 return self._attributeName
213 @readonly
214 def AttributeType(self) -> AttributeTypes:
215 """
216 Read-only property to access the type of the described attribute (:attr:`_attributeType`).
218 :returns: Type of the attribute.
219 """
220 return self._attributeType
222 @readonly
223 def HasClosingTag(self) -> bool:
224 """
225 Check if this XML element is written with a separate closing tag.
227 A key is always written as a self-closing tag.
229 :returns: ``False``, because a key never has a closing tag.
230 """
231 return False
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"""
236 def ToStringLines(self, indent: int = 2) -> List[str]:
237 return [self.Tag(indent)]
240@export
241class Data(Base):
242 _key: Key
243 _data: Any
245 def __init__(self, key: Key, data: Any) -> None:
246 super().__init__()
248 self._key = key
249 self._data = data
251 @readonly
252 def Key(self) -> Key:
253 """
254 Read-only property to access the key describing this data element (:attr:`_key`).
256 :returns: The key this data element refers to.
257 """
258 return self._key
260 @readonly
261 def Data(self) -> Any:
262 """
263 Read-only property to access the data element's value (:attr:`_data`).
265 :returns: Value of the data element.
266 """
267 return self._data
269 @readonly
270 def HasClosingTag(self) -> bool:
271 """
272 Check if this XML element is written with a separate closing tag.
274 :returns: ``False``, because a data element is written inline.
275 """
276 return False
278 def Tag(self, indent: int = 2) -> str:
279 data = str(self._data)
280 data = data.replace("&", "&")
281 data = data.replace("<", "<")
282 data = data.replace(">", ">")
283 data = data.replace("\n", "\\n")
284 return f"""{' '*indent}<data key="{self._key._id}">{data}</data>\n"""
286 def ToStringLines(self, indent: int = 2) -> List[str]:
287 return [self.Tag(indent)]
290@export
291class Node(BaseWithData):
292 def __init__(self, identifier: str) -> None:
293 super().__init__(identifier)
295 @readonly
296 def HasClosingTag(self) -> bool:
297 """
298 Check if this XML element is written with a separate closing tag.
300 :returns: ``True``, if the node carries data elements, otherwise ``False``.
301 """
302 return len(self._data) > 0
304 def Tag(self, indent: int = 2) -> str:
305 return f"""{' '*indent}<node id="{self._id}" />\n"""
307 def OpeningTag(self, indent: int = 2) -> str:
308 return f"""{' '*indent}<node id="{self._id}">\n"""
310 def ClosingTag(self, indent: int = 2) -> str:
311 return f"""{' ' * indent}</node>\n"""
313 def ToStringLines(self, indent: int = 2) -> List[str]:
314 if not self.HasClosingTag:
315 return [self.Tag(indent)]
317 lines = [self.OpeningTag(indent)]
318 for data in self._data:
319 lines.extend(data.ToStringLines(indent + 1))
320 lines.append(self.ClosingTag(indent))
322 return lines
325@export
326class Edge(BaseWithData):
327 _source: Node
328 _target: Node
330 def __init__(self, identifier: str, source: Node, target: Node) -> None:
331 super().__init__(identifier)
333 self._source = source
334 self._target = target
336 @readonly
337 def Source(self) -> Node:
338 """
339 Read-only property to access the edge's source node (:attr:`_source`).
341 :returns: Source node of the edge.
342 """
343 return self._source
345 @readonly
346 def Target(self) -> Node:
347 """
348 Read-only property to access the edge's target node (:attr:`_target`).
350 :returns: Target node of the edge.
351 """
352 return self._target
354 @readonly
355 def HasClosingTag(self) -> bool:
356 """
357 Check if this XML element is written with a separate closing tag.
359 :returns: ``True``, if the edge carries data elements, otherwise ``False``.
360 """
361 return len(self._data) > 0
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"""
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"""
369 def ClosingTag(self, indent: int = 2) -> str:
370 return f"""{' ' * indent}</edge>\n"""
372 def ToStringLines(self, indent: int = 2) -> List[str]:
373 if not self.HasClosingTag:
374 return [self.Tag(indent)]
376 lines = [self.OpeningTag(indent)]
377 for data in self._data:
378 lines.extend(data.ToStringLines(indent + 1))
379 lines.append(self.ClosingTag(indent))
381 return lines
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
394 def __init__(self, identifier: Nullable[str] = None) -> None:
395 super().__init__(identifier)
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
405 @readonly
406 def Subgraphs(self) -> Dict[str, 'Subgraph']:
407 """
408 Read-only property to access the graph's subgraphs (:attr:`_subgraphs`).
410 :returns: Dictionary of subgraph IDs and subgraphs.
411 """
412 return self._subgraphs
414 @readonly
415 def Nodes(self) -> Dict[str, Node]:
416 """
417 Read-only property to access the graph's nodes (:attr:`_nodes`).
419 :returns: Dictionary of node IDs and nodes.
420 """
421 return self._nodes
423 @readonly
424 def Edges(self) -> Dict[str, Edge]:
425 """
426 Read-only property to access the graph's edges (:attr:`_edges`).
428 :returns: Dictionary of edge IDs and edges.
429 """
430 return self._edges
432 def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph':
433 self._subgraphs[subgraph._subgraphID] = subgraph
434 self._nodes[subgraph._id] = subgraph
435 return subgraph
437 def GetSubgraph(self, subgraphName: str) -> 'Subgraph':
438 return self._subgraphs[subgraphName]
440 def AddNode(self, node: Node) -> Node:
441 self._nodes[node._id] = node
442 return node
444 def GetNode(self, nodeName: str) -> Node:
445 return self._nodes[nodeName]
447 def AddEdge(self, edge: Edge) -> Edge:
448 self._edges[edge._id] = edge
449 return edge
451 def GetEdge(self, edgeName: str) -> Edge:
452 return self._edges[edgeName]
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"""
465 def ClosingTag(self, indent: int = 1) -> str:
466 return f"{' '*indent}</graph>\n"
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))
478 return lines
481@export
482class Graph(BaseGraph):
483 _document: 'GraphMLDocument'
484 _ids: Dict[str, Union[Node, Edge, 'Subgraph']]
486 def __init__(self, document: 'GraphMLDocument', identifier: str) -> None:
487 super().__init__(identifier)
488 self._document = document
489 self._ids = {}
491 def GetByID(self, identifier: str) -> Union[Node, Edge, 'Subgraph']:
492 return self._ids[identifier]
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
500 def AddNode(self, node: Node) -> Node:
501 result = super().AddNode(node)
502 self._ids[node._id] = node
503 return result
505 def AddEdge(self, edge: Edge) -> Edge:
506 result = super().AddEdge(edge)
507 self._ids[edge._id] = edge
508 return result
511@export
512class Subgraph(Node, BaseGraph):
513 _subgraphID: str
514 _root: Nullable[Graph]
516 def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None:
517 super().__init__(nodeIdentifier)
518 BaseGraph.__init__(self, nodeIdentifier)
520 self._subgraphID = graphIdentifier
521 self._root = None
523 @readonly
524 def RootGraph(self) -> Graph:
525 """
526 Read-only property to access the graph this subgraph is embedded in (:attr:`_root`).
528 :returns: The root graph.
529 """
530 return self._root
532 @readonly
533 def SubgraphID(self) -> str:
534 """
535 Read-only property to access the subgraph's ID (:attr:`_subgraphID`).
537 :returns: ID of the subgraph.
538 """
539 return self._subgraphID
541 @readonly
542 def HasClosingTag(self) -> bool:
543 """
544 Check if this XML element is written with a separate closing tag.
546 :returns: ``True``, because a subgraph always has a closing tag.
547 """
548 return True
550 def AddNode(self, node: Node) -> Node:
551 result = super().AddNode(node)
552 self._root._ids[node._id] = node
553 return result
555 def AddEdge(self, edge: Edge) -> Edge:
556 result = super().AddEdge(edge)
557 self._root._ids[edge._id] = edge
558 return result
560 def Tag(self, indent: int = 2) -> str:
561 raise NotImplementedError()
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"""
574 def ClosingTag(self, indent: int = 2) -> str:
575 return BaseGraph.ClosingTag(self, indent)
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))
592 return lines
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 }
605 _graph: Graph
606 _keys: Dict[str, Key]
608 def __init__(self, identifier: str = "G") -> None:
609 super().__init__()
611 self._graph = Graph(self, identifier)
612 self._keys = {}
614 @readonly
615 def Graph(self) -> BaseGraph:
616 """
617 Read-only property to access the document's graph (:attr:`_graph`).
619 :returns: The graph described by this document.
620 """
621 return self._graph
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`).
628 :returns: Dictionary of key IDs and keys.
629 """
630 return self._keys
632 def AddKey(self, key: Key) -> Key:
633 self._keys[key._id] = key
634 return key
636 def GetKey(self, keyName: str) -> Key:
637 return self._keys[keyName]
639 def HasKey(self, keyName: str) -> bool:
640 return keyName in self._keys
642 def FromGraph(self, graph: pyToolingGraph) -> None:
643 document = self
644 self._graph._id = graph._name
646 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
647 edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String))
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))
660 rootGraph.AddNode(newNode)
662 for edge in pyTGraph.IterateEdges():
663 source = rootGraph.GetByID(edge._source._id)
664 target = rootGraph.GetByID(edge._destination._id)
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))
675 rootGraph.AddEdge(newEdge)
677 for link in pyTGraph.IterateLinks():
678 source = rootGraph.GetByID(link._source._id)
679 target = rootGraph.GetByID(link._destination._id)
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))
690 rootGraph.AddEdge(newEdge)
692 def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph):
693 rootGraph = nodeGraph.RootGraph
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))
705 nodeGraph.AddNode(newNode)
707 for edge in pyTSubgraph.IterateEdges():
708 source = nodeGraph.GetNode(edge._source._id)
709 target = nodeGraph.GetNode(edge._destination._id)
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))
720 nodeGraph.AddEdge(newEdge)
722 for subgraph in graph.Subgraphs:
723 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name)
724 self._graph.AddSubgraph(nodeGraph)
725 translateSubgraph(nodeGraph, subgraph)
727 translateGraph(self._graph, graph)
729 def FromTree(self, tree: pyToolingNode) -> None:
730 self._graph._id = tree._id
732 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
734 rootNode = self._graph.AddNode(Node(tree._id))
735 rootNode.AddData(Data(nodeValue, tree._value))
737 for i, node in enumerate(tree.GetDescendants()):
738 newNode = self._graph.AddNode(Node(node._id))
739 newNode.AddData(Data(nodeValue, node._value))
741 newEdge = self._graph.AddEdge(Edge(f"e{i}", newNode, self._graph.GetNode(node._parent._id)))
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"""
750 def ClosingTag(self, indent: int = 0) -> str:
751 return f"{' '*indent}</graphml>\n"
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))
760 return lines
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())