pyTooling.Graph.GraphML

pyTooling/Graph/GraphML.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
# ==================================================================================================================== #
#             _____           _ _               ____                 _                                                 #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  / ___|_ __ __ _ _ __ | |__                                              #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || |  _| '__/ _` | '_ \| '_ \                                             #
# | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | |                                            #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_|  \__,_| .__/|_| |_|                                            #
# |_|    |___/                          |___/                 |_|                                                      #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
A data model to write out GraphML XML files.

.. seealso::

   `GraphML Primer <http://graphml.graphdrawing.org/primer/graphml-primer.html>`__
      |rarr| The format's own introduction, describing the elements this module writes.
"""
from __future__            import annotations

from enum                  import Enum, auto
from pathlib               import Path
from typing                import Any, ClassVar, Union, Optional as Nullable

from pyTooling.Decorators  import export, notimplemented, readonly
from pyTooling.MetaClasses import ExtendedType
from pyTooling.Graph       import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph
from pyTooling.Tree        import Node as pyToolingNode


@export
class AttributeContext(Enum):
	"""
	Enumeration of all attribute contexts.

	An attribute context describes to what kind of GraphML node an attribute can be applied.
	"""
	GraphML = auto()
	Graph = auto()
	Node = auto()
	Edge = auto()
	Port = auto()

	def __str__(self) -> str:
		"""
		Return the enumeration value's name as it is written in a GraphML document.

		:returns: Name of the enumeration value in lower case.
		"""
		return f"{self.name.lower()}"


@export
class AttributeTypes(Enum):
	"""
	Enumeration of all attribute types.

	An attribute type describes what datatype can be applied to an attribute.
	"""
	Boolean = auto()
	Int = auto()
	Long = auto()
	Float = auto()
	Double = auto()
	String = auto()

	def __str__(self) -> str:
		"""
		Return the enumeration value's name as it is written in a GraphML document.

		:returns: Name of the enumeration value in lower case.
		"""
		return f"{self.name.lower()}"


@export
class EdgeDefault(Enum):
	"""An enumeration describing the default edge direction."""
	Undirected = auto()
	Directed = auto()

	def __str__(self) -> str:
		"""
		Return the enumeration value's name as it is written in a GraphML document.

		:returns: Name of the enumeration value in lower case.
		"""
		return f"{self.name.lower()}"


@export
class ParsingOrder(Enum):
	"""An enumeration describing the parsing order of the graph's representation."""
	NodesFirst = auto()     #: First, all nodes are given, then followed by all edges.
	AdjacencyList = auto()
	Free = auto()

	def __str__(self) -> str:
		"""
		Return the enumeration value's name as it is written in a GraphML document.

		:returns: Name of the enumeration value in lower case.
		"""
		return f"{self.name.lower()}"


@export
class IDStyle(Enum):
	"""An enumeration describing the style of identifiers (IDs)."""
	Canonical = auto()
	Free = auto()

	def __str__(self) -> str:
		"""
		Return the enumeration value's name as it is written in a GraphML document.

		:returns: Name of the enumeration value in lower case.
		"""
		return f"{self.name.lower()}"


@export
class Base(metaclass=ExtendedType, slots=True):
	"""
	Base-class for all GraphML data model classes.
	"""
	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

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

	def Tag(self, indent: int = 0) -> str:
		"""
		Return this element as a self-closing XML tag.

		:param indent:               Optional, indentation level of the XML element.
		:returns:                    The XML tag, indented and terminated by a newline.
		:raises NotImplementedError: If this abstract method is not overridden by a derived class.
		"""
		raise NotImplementedError()

	def OpeningTag(self, indent: int = 0) -> str:
		"""
		Return the opening XML tag of this element.

		:param indent:               Optional, indentation level of the XML element.
		:returns:                    The opening XML tag, indented and terminated by a newline.
		:raises NotImplementedError: If this abstract method is not overridden by a derived class.
		"""
		raise NotImplementedError()

	def ClosingTag(self, indent: int = 0) -> str:
		"""
		Return the closing XML tag of this element.

		:param indent:               Optional, indentation level of the XML element.
		:returns:                    The closing XML tag, indented and terminated by a newline.
		:raises NotImplementedError: If this abstract method is not overridden by a derived class.
		"""
		raise NotImplementedError()

	def ToStringLines(self, indent: int = 0) -> list[str]:
		"""
		Render this element as a list of XML lines.

		:param indent:               Optional, indentation level of the XML element.
		:returns:                    List of XML lines describing this element.
		:raises NotImplementedError: If this abstract method is not overridden by a derived class.
		"""
		raise NotImplementedError()


@export
class BaseWithID(Base):
	"""Base-class for all GraphML elements carrying a document-wide unique ID."""
	_id: str  #: Unique identifier of this GraphML element.

	def __init__(self, identifier: str) -> None:
		"""
		Initialize a GraphML element with its unique ID.

		:param identifier: Optional, unique ID of the element within the GraphML document.
		"""
		super().__init__()
		self._id = identifier

	@readonly
	def ID(self) -> str:
		"""
		Read-only property to access the element's unique ID (:attr:`_id`).

		:returns: Unique ID of the element.
		"""
		return self._id


@export
class BaseWithData(BaseWithID):
	"""Base-class for all GraphML elements that can carry attached data items (key-value-pairs)."""
	_data: list[Data]  #: Data items (key-value-pairs) attached to this GraphML element.

	def __init__(self, identifier: str) -> None:
		"""
		Initialize a GraphML element with its unique ID and an empty list of data items.

		:param identifier: Optional, unique ID of the element within the GraphML document.
		"""
		super().__init__(identifier)

		self._data = []

	@readonly
	def Data(self) -> list[Data]:
		"""
		Read-only property to access the data elements attached to this element (:attr:`_data`).

		:returns: List of data elements.
		"""
		return self._data

	def AddData(self, data: Data) -> Data:
		"""
		Attach a data item (key-value-pair) to this element.

		:param data: The data item to attach.
		:returns:    The attached data item, so it can be used in the calling expression.
		"""
		self._data.append(data)
		return data


@export
class Key(BaseWithID):
	"""
	Declares an attribute that data items can refer to.

	A GraphML document declares its attributes once - name, data type, and the element kind they apply to - and every
	:class:`Data` item then references such a key by ID.
	"""
	_context:       AttributeContext  #: GraphML element kind this key can be used on.
	_attributeName: str               #: Name of the attribute described by this key.
	_attributeType: AttributeTypes    #: Data type of the attribute described by this key.

	def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None:
		"""
		Initialize a key declaring an attribute.

		:param identifier: Optional, unique ID of the key within the GraphML document.
		:param context:    GraphML element kind this key can be used on.
		:param name:       Name of the declared attribute.
		:param type:       Data type of the declared attribute.
		"""
		super().__init__(identifier)

		self._context = context
		self._attributeName = name
		self._attributeType = type

	@readonly
	def Context(self) -> AttributeContext:
		"""
		Read-only property to access the context this key applies to (:attr:`_context`).

		:returns: The attribute's context (graph, node, edge, ...).
		"""
		return self._context

	@readonly
	def AttributeName(self) -> str:
		"""
		Read-only property to access the name of the described attribute (:attr:`_attributeName`).

		:returns: Name of the attribute.
		"""
		return self._attributeName

	@readonly
	def AttributeType(self) -> AttributeTypes:
		"""
		Read-only property to access the type of the described attribute (:attr:`_attributeType`).

		:returns: Type of the attribute.
		"""
		return self._attributeType

	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

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

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

	def Tag(self, indent: int = 2) -> str:
		"""
		Return this key as a self-closing XML tag.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The XML tag, indented and terminated by a newline.
		"""
		return f"""{'  '*indent}<key id="{self._id}" for="{self._context}" attr.name="{self._attributeName}" attr.type="{self._attributeType}" />\n"""

	def ToStringLines(self, indent: int = 2) -> list[str]:
		"""
		Render this key as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this key and everything attached to it.
		"""
		return [self.Tag(indent)]


@export
class Data(Base):
	"""A single attached attribute: a value and the :class:`Key` describing it."""
	_key:  Key  #: Key describing name and type of this data item.
	_data: Any  #: Value of this data item.

	def __init__(self, key: Key, data: Any) -> None:
		"""
		Initialize a data item with the key describing it and its value.

		:param key:  Key declaring name and type of this attribute.
		:param data: Value of this attribute.
		"""
		super().__init__()

		self._key = key
		self._data = data

	@readonly
	def Key(self) -> Key:
		"""
		Read-only property to access the key describing this data element (:attr:`_key`).

		:returns: The key this data element refers to.
		"""
		return self._key

	@readonly
	def Data(self) -> Any:
		"""
		Read-only property to access the data element's value (:attr:`_data`).

		:returns: Value of the data element.
		"""
		return self._data

	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

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

	def Tag(self, indent: int = 2) -> str:
		"""
		Return this data item as a self-closing XML tag.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The XML tag, indented and terminated by a newline.
		"""
		data = str(self._data)
		data = data.replace("&", "&amp;")
		data = data.replace("<", "&lt;")
		data = data.replace(">", "&gt;")
		data = data.replace("\n", "\\n")
		return f"""{'  '*indent}<data key="{self._key._id}">{data}</data>\n"""

	def ToStringLines(self, indent: int = 2) -> list[str]:
		"""
		Render this data item as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this data item and everything attached to it.
		"""
		return [self.Tag(indent)]


@export
class Node(BaseWithData):
	"""A node (vertex) of a GraphML graph."""

	def __init__(self, identifier: str) -> None:
		"""
		Initialize a node.

		:param identifier: Optional, unique ID of the node within the GraphML document.
		"""
		super().__init__(identifier)

	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

		:returns: ``True``, if the node carries data elements, otherwise ``False``.
		"""
		return len(self._data) > 0

	def Tag(self, indent: int = 2) -> str:
		"""
		Return this node as a self-closing XML tag.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The XML tag, indented and terminated by a newline.
		"""
		return f"""{'  '*indent}<node id="{self._id}" />\n"""

	def OpeningTag(self, indent: int = 2) -> str:
		"""
		Return the opening XML tag of this node.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The opening XML tag, indented and terminated by a newline.
		"""
		return f"""{'  '*indent}<node id="{self._id}">\n"""

	def ClosingTag(self, indent: int = 2) -> str:
		"""
		Return the closing XML tag of this node.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The closing XML tag, indented and terminated by a newline.
		"""
		return f"""{'  ' * indent}</node>\n"""

	def ToStringLines(self, indent: int = 2) -> list[str]:
		"""
		Render this node as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this node and everything attached to it.
		"""
		if not self.HasClosingTag:
			return [self.Tag(indent)]

		lines = [self.OpeningTag(indent)]
		for data in self._data:
			lines.extend(data.ToStringLines(indent + 1))
		lines.append(self.ClosingTag(indent))

		return lines


@export
class Edge(BaseWithData):
	"""An edge of a GraphML graph, connecting a source node to a target node."""
	_source: Node  #: Node the edge starts at.
	_target: Node  #: Node the edge ends at.

	def __init__(self, identifier: str, source: Node, target: Node) -> None:
		"""
		Initialize an edge between two nodes.

		:param identifier: Optional, unique ID of the edge within the GraphML document.
		:param source:     Node the edge starts at.
		:param target:     Node the edge ends at.
		"""
		super().__init__(identifier)

		self._source = source
		self._target = target

	@readonly
	def Source(self) -> Node:
		"""
		Read-only property to access the edge's source node (:attr:`_source`).

		:returns: Source node of the edge.
		"""
		return self._source

	@readonly
	def Target(self) -> Node:
		"""
		Read-only property to access the edge's target node (:attr:`_target`).

		:returns: Target node of the edge.
		"""
		return self._target

	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

		:returns: ``True``, if the edge carries data elements, otherwise ``False``.
		"""
		return len(self._data) > 0

	def Tag(self, indent: int = 2) -> str:
		"""
		Return this edge as a self-closing XML tag.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The XML tag, indented and terminated by a newline.
		"""
		return f"""{'  ' * indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}" />\n"""

	def OpeningTag(self, indent: int = 2) -> str:
		"""
		Return the opening XML tag of this edge.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The opening XML tag, indented and terminated by a newline.
		"""
		return f"""{'  '*indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}">\n"""

	def ClosingTag(self, indent: int = 2) -> str:
		"""
		Return the closing XML tag of this edge.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The closing XML tag, indented and terminated by a newline.
		"""
		return f"""{'  ' * indent}</edge>\n"""

	def ToStringLines(self, indent: int = 2) -> list[str]:
		"""
		Render this edge as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this edge and everything attached to it.
		"""
		if not self.HasClosingTag:
			return [self.Tag(indent)]

		lines = [self.OpeningTag(indent)]
		for data in self._data:
			lines.extend(data.ToStringLines(indent + 1))
		lines.append(self.ClosingTag(indent))

		return lines


@export
class BaseGraph(BaseWithData, mixin=True):
	"""
	Mixin-class for everything that contains nodes, edges and subgraphs - a graph as well as a subgraph.

	Beside the elements themselves, it carries the document-level settings applied while writing them: the default edge
	direction, the parsing order, and the ID styles for nodes and edges.
	"""
	_subgraphs:   dict[str, Subgraph]  #: Subgraphs of this graph, by ID.
	_nodes:       dict[str, Node]      #: Nodes of this graph, by ID.
	_edges:       dict[str, Edge]      #: Edges of this graph, by ID.
	_edgeDefault: EdgeDefault          #: Direction applied to edges that don't specify one.
	_parseOrder:  ParsingOrder         #: Order in which nodes and edges may appear in the XML document.
	_nodeIDStyle: IDStyle              #: Whether node IDs are free-form or canonical.
	_edgeIDStyle: IDStyle              #: Whether edge IDs are free-form or canonical.

	def __init__(self, identifier: Nullable[str] = None) -> None:
		"""
		Initialize an empty graph with the default document settings.

		Edges are directed, nodes are written before edges, and both ID styles are free-form until they are changed.

		:param identifier: Optional, unique ID of the graph within the GraphML document.
		"""
		super().__init__(identifier)

		self._subgraphs = {}
		self._nodes = {}
		self._edges = {}
		self._edgeDefault = EdgeDefault.Directed
		self._parseOrder = ParsingOrder.NodesFirst
		self._nodeIDStyle = IDStyle.Free
		self._edgeIDStyle = IDStyle.Free

	@readonly
	def Subgraphs(self) -> dict[str, Subgraph]:
		"""
		Read-only property to access the graph's subgraphs (:attr:`_subgraphs`).

		:returns: Dictionary of subgraph IDs and subgraphs.
		"""
		return self._subgraphs

	@readonly
	def Nodes(self) -> dict[str, Node]:
		"""
		Read-only property to access the graph's nodes (:attr:`_nodes`).

		:returns: Dictionary of node IDs and nodes.
		"""
		return self._nodes

	@readonly
	def Edges(self) -> dict[str, Edge]:
		"""
		Read-only property to access the graph's edges (:attr:`_edges`).

		:returns: Dictionary of edge IDs and edges.
		"""
		return self._edges

	def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
		"""
		Add a subgraph to this graph, which is a node of this graph as well.

		:param subgraph: The subgraph to add.
		:returns:        The added subgraph, so it can be used in the calling expression.
		"""
		self._subgraphs[subgraph._subgraphID] = subgraph
		self._nodes[subgraph._id] = subgraph
		return subgraph

	def GetSubgraph(self, subgraphName: str) -> Subgraph:
		"""
		Return the subgraph with the given ID.

		:param subgraphName: ID of the subgraph.
		:returns:            The subgraph with that ID.
		:raises KeyError:    If no subgraph has that ID.
		"""
		return self._subgraphs[subgraphName]

	def AddNode(self, node: Node) -> Node:
		"""
		Add a node to this graph.

		:param node: The node to add.
		:returns:    The added node, so it can be used in the calling expression.
		"""
		self._nodes[node._id] = node
		return node

	def GetNode(self, nodeName: str) -> Node:
		"""
		Return the node with the given ID.

		:param nodeName:  ID of the node.
		:returns:         The node with that ID.
		:raises KeyError: If no node has that ID.
		"""
		return self._nodes[nodeName]

	def AddEdge(self, edge: Edge) -> Edge:
		"""
		Add an edge to this graph.

		:param edge: The edge to add.
		:returns:    The added edge, so it can be used in the calling expression.
		"""
		self._edges[edge._id] = edge
		return edge

	def GetEdge(self, edgeName: str) -> Edge:
		"""
		Return the edge with the given ID.

		:param edgeName:  ID of the edge.
		:returns:         The edge with that ID.
		:raises KeyError: If no edge has that ID.
		"""
		return self._edges[edgeName]

	def OpeningTag(self, indent: int = 1) -> str:
		"""
		Return the opening XML tag of this graph.

		Beside the graph's ID, the tag carries the parsing hints a reader needs: the default edge direction, the
number of nodes and edges, the parsing order and both ID styles.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The opening XML tag, indented and terminated by a newline.
		"""
		return f"""\
{'  '*indent}<graph id="{self._id}"
{'  '*indent}  edgedefault="{self._edgeDefault!s}"
{'  '*indent}  parse.nodes="{len(self._nodes)}"
{'  '*indent}  parse.edges="{len(self._edges)}"
{'  '*indent}  parse.order="{self._parseOrder!s}"
{'  '*indent}  parse.nodeids="{self._nodeIDStyle!s}"
{'  '*indent}  parse.edgeids="{self._edgeIDStyle!s}">
"""

	def ClosingTag(self, indent: int = 1) -> str:
		"""
		Return the closing XML tag of this graph.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The closing XML tag, indented and terminated by a newline.
		"""
		return f"{'  '*indent}</graph>\n"

	def ToStringLines(self, indent: int = 1) -> list[str]:
		"""
		Render this graph as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this graph and everything it contains.
		"""
		lines = [self.OpeningTag(indent)]
		for node in self._nodes.values():
			lines.extend(node.ToStringLines(indent + 1))
		for edge in self._edges.values():
			lines.extend(edge.ToStringLines(indent + 1))
		# for data in self._data:
		# 	lines.extend(data.ToStringLines(indent + 1))
		lines.append(self.ClosingTag(indent))

		return lines


@export
class Graph(BaseGraph):
	"""
	The root graph of a GraphML document.

	It owns the ID space: every node, edge and subgraph registers itself here, so an ID is used only once per document.
	"""
	_document: GraphMLDocument                         #: The GraphML document this graph belongs to.
	_ids:      dict[str, Union[Node, Edge, Subgraph]]  #: Every element of this graph by ID, used to keep IDs unique.

	def __init__(self, document: GraphMLDocument, identifier: str) -> None:
		"""
		Initialize the root graph of a GraphML document.

		:param document:   The GraphML document this graph belongs to.
		:param identifier: Optional, unique ID of the graph within the GraphML document.
		"""
		super().__init__(identifier)
		self._document = document
		self._ids = {}

	def GetByID(self, identifier: str) -> Union[Node, Edge, Subgraph]:
		"""
		Return the element with the given ID, whichever kind it is.

		:param identifier: Optional, ID of the node, edge or subgraph.
		:returns:          The element registered under that ID.
		:raises KeyError:  If no element has that ID.
		"""
		return self._ids[identifier]

	def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
		"""
		Add a subgraph to the root graph and register its ID.

		:param subgraph: The subgraph to add.
		:returns:        The added subgraph, so it can be used in the calling expression.
		"""
		result = super().AddSubgraph(subgraph)
		self._ids[subgraph._subgraphID] = subgraph
		subgraph._root = self
		return result

	def AddNode(self, node: Node) -> Node:
		"""
		Add a node to the root graph and register its ID.

		:param node: The node to add.
		:returns:    The added node, so it can be used in the calling expression.
		"""
		result = super().AddNode(node)
		self._ids[node._id] = node
		return result

	def AddEdge(self, edge: Edge) -> Edge:
		"""
		Add an edge to the root graph and register its ID.

		:param edge: The edge to add.
		:returns:    The added edge, so it can be used in the calling expression.
		"""
		result = super().AddEdge(edge)
		self._ids[edge._id] = edge
		return result


@export
class Subgraph(Node, BaseGraph):
	"""
	A nested graph, which is a node of its parent graph and a graph of its own.

	It therefore carries two identifiers: the node's ID it is referenced by, and :attr:`_subgraphID` for the graph it
	contains.
	"""
	_subgraphID: str              #: ID of the subgraph, which is distinct from the node's own ID.
	_root:       Nullable[Graph]  #: The graph this subgraph is nested in.

	def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None:
		"""
		Initialize a subgraph, which is a node in its parent graph and a graph of its own.

		:param nodeIdentifier:  Unique ID of the node representing the subgraph.
		:param graphIdentifier: Unique ID of the graph contained in that node.
		"""
		super().__init__(nodeIdentifier)
		BaseGraph.__init__(self, nodeIdentifier)

		self._subgraphID = graphIdentifier
		self._root = None

	@readonly
	def RootGraph(self) -> Graph:
		"""
		Read-only property to access the graph this subgraph is embedded in (:attr:`_root`).

		:returns: The root graph.
		"""
		return self._root

	@readonly
	def SubgraphID(self) -> str:
		"""
		Read-only property to access the subgraph's ID (:attr:`_subgraphID`).

		:returns: ID of the subgraph.
		"""
		return self._subgraphID

	@readonly
	def HasClosingTag(self) -> bool:
		"""
		Check if this XML element is written with a separate closing tag.

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

	def AddNode(self, node: Node) -> Node:
		"""
		Add a node to this subgraph and register its ID at the root graph.

		:param node: The node to add.
		:returns:    The added node, so it can be used in the calling expression.
		"""
		result = super().AddNode(node)
		self._root._ids[node._id] = node
		return result

	def AddEdge(self, edge: Edge) -> Edge:
		"""
		Add an edge to this subgraph and register its ID at the root graph.

		:param edge: The edge to add.
		:returns:    The added edge, so it can be used in the calling expression.
		"""
		result = super().AddEdge(edge)
		self._root._ids[edge._id] = edge
		return result

	@notimplemented("A subgraph is always written with an opening and a closing tag.")
	def Tag(self, indent: int = 2) -> str:
		"""
		A subgraph always contains a graph, so it is never written as a self-closing tag.

		:param indent: Optional, indentation level of the XML element.
		"""

	def OpeningTag(self, indent: int = 1) -> str:
		"""
		Return the opening XML tag of this subgraph.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The opening XML tag, indented and terminated by a newline.
		"""
		return f"""\
{'  ' * indent}<graph id="{self._subgraphID}"
{'  ' * indent}  edgedefault="{self._edgeDefault!s}"
{'  ' * indent}  parse.nodes="{len(self._nodes)}"
{'  ' * indent}  parse.edges="{len(self._edges)}"
{'  ' * indent}  parse.order="{self._parseOrder!s}"
{'  ' * indent}  parse.nodeids="{self._nodeIDStyle!s}"
{'  ' * indent}  parse.edgeids="{self._edgeIDStyle!s}">
"""

	def ClosingTag(self, indent: int = 2) -> str:
		"""
		Return the closing XML tag of this subgraph.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The closing XML tag, indented and terminated by a newline.
		"""
		return BaseGraph.ClosingTag(self, indent)

	def ToStringLines(self, indent: int = 2) -> list[str]:
		"""
		Render this subgraph as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this subgraph and everything it contains.
		"""
		lines = [super().OpeningTag(indent)]
		for data in self._data:
			lines.extend(data.ToStringLines(indent + 1))
		# lines.extend(Graph.ToStringLines(self, indent + 1))
		lines.append(self.OpeningTag(indent + 1))
		for node in self._nodes.values():
			lines.extend(node.ToStringLines(indent + 2))
		for edge in self._edges.values():
			lines.extend(edge.ToStringLines(indent + 2))
		# for data in self._data:
		# 	lines.extend(data.ToStringLines(indent + 1))
		lines.append(self.ClosingTag(indent + 1))
		lines.append(super().ClosingTag(indent))

		return lines


@export
class GraphMLDocument(Base):
	"""
	A GraphML document: the root graph, the keys it declares, and the XML boilerplate to write it out.
	"""

	xmlNS: ClassVar[dict[Nullable[str], str]] = {
		None:  "http://graphml.graphdrawing.org/xmlns",
		"xsi": "http://www.w3.org/2001/XMLSchema-instance"
	}  #: XML namespaces of a GraphML document.
	xsi: ClassVar[dict[str, str]] = {
		"schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"
	}  #: XML schema instance attributes of a GraphML document.

	_graph: Graph           #: The document's root graph.
	_keys:  dict[str, Key]  #: Keys declared by this document, by ID.

	def __init__(self, identifier: str = "G") -> None:
		"""
		Initialize a GraphML document with an empty root graph.

		:param identifier: Optional, unique ID of the root graph.
		"""
		super().__init__()

		self._graph = Graph(self, identifier)
		self._keys = {}

	@readonly
	def Graph(self) -> BaseGraph:
		"""
		Read-only property to access the document's graph (:attr:`_graph`).

		:returns: The graph described by this document.
		"""
		return self._graph

	@readonly
	def Keys(self) -> dict[str, Key]:
		"""
		Read-only property to access the attribute keys declared in this document (:attr:`_keys`).

		:returns: Dictionary of key IDs and keys.
		"""
		return self._keys

	def AddKey(self, key: Key) -> Key:
		"""
		Declare an attribute, so data items can refer to it.

		:param key: The key to declare.
		:returns:   The declared key, so it can be used in the calling expression.
		"""
		self._keys[key._id] = key
		return key

	def GetKey(self, keyName: str) -> Key:
		"""
		Return the declared key with the given ID.

		:param keyName:   ID of the key.
		:returns:         The key with that ID.
		:raises KeyError: If no key has that ID.
		"""
		return self._keys[keyName]

	def HasKey(self, keyName: str) -> bool:
		"""
		Check if a key with the given ID was declared.

		:param keyName: ID of the key.
		:returns:       ``True``, if such a key exists.
		"""
		return keyName in self._keys

	def FromGraph(self, graph: pyToolingGraph) -> None:
		"""
		Fill this document from a :class:`pyTooling.Graph.Graph`.

		Vertices become nodes, edges become edges, and the vertex and edge values are attached as data items,
		declared by two keys this method adds. Subgraphs are translated recursively.

		:param graph: The graph to translate into this document.
		"""
		document = self
		self._graph._id = graph._name

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

		def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph):
			"""
			Nested function for recursion.

			It translates the vertices and edges of one pyTooling graph into GraphML nodes and edges, and recurses into the
			subgraphs it finds.

			:param rootGraph: The GraphML graph the elements are added to.
			:param pyTGraph:  The pyTooling graph to translate.
			"""
			for vertex in pyTGraph.IterateVertices():
				newNode = Node(vertex._id)
				newNode.AddData(Data(nodeValue, vertex._value))
				for key, value in vertex._dict.items():
					if document.HasKey(str(key)):
						nodeKey = document.GetKey(f"node{key!s}")
					else:
						nodeKey = document.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
					newNode.AddData(Data(nodeKey, value))

				rootGraph.AddNode(newNode)

			for edge in pyTGraph.IterateEdges():
				source = rootGraph.GetByID(edge._source._id)
				target = rootGraph.GetByID(edge._destination._id)

				newEdge = Edge(edge._id, source, target)
				newEdge.AddData(Data(edgeValue, edge._value))
				for key, value in edge._dict.items():
					if self.HasKey(str(key)):
						edgeKey = self.GetBy(f"edge{key!s}")
					else:
						edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
					newEdge.AddData(Data(edgeKey, value))

				rootGraph.AddEdge(newEdge)

			for link in pyTGraph.IterateLinks():
				source = rootGraph.GetByID(link._source._id)
				target = rootGraph.GetByID(link._destination._id)

				newEdge = Edge(link._id, source, target)
				newEdge.AddData(Data(edgeValue, link._value))
				for key, value in link._dict.items():
					if self.HasKey(str(key)):
						edgeKey = self.GetKey(f"link{key!s}")
					else:
						edgeKey = self.AddKey(Key(f"link{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
					newEdge.AddData(Data(edgeKey, value))

				rootGraph.AddEdge(newEdge)

		def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph):
			"""
			Nested function for recursion.

			It translates one pyTooling subgraph into a GraphML subgraph.

			:param nodeGraph:   The GraphML subgraph the elements are added to.
			:param pyTSubgraph: The pyTooling subgraph to translate.
			"""
			rootGraph = nodeGraph.RootGraph

			for vertex in pyTSubgraph.IterateVertices():
				newNode = Node(vertex._id)
				newNode.AddData(Data(nodeValue, vertex._value))
				for key, value in vertex._dict.items():
					if self.HasKey(str(key)):
						nodeKey = self.GetKey(f"node{key!s}")
					else:
						nodeKey = self.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
					newNode.AddData(Data(nodeKey, value))

				nodeGraph.AddNode(newNode)

			for edge in pyTSubgraph.IterateEdges():
				source = nodeGraph.GetNode(edge._source._id)
				target = nodeGraph.GetNode(edge._destination._id)

				newEdge = Edge(edge._id, source, target)
				newEdge.AddData(Data(edgeValue, edge._value))
				for key, value in edge._dict.items():
					if self.HasKey(str(key)):
						edgeKey = self.GetKey(f"edge{key!s}")
					else:
						edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
					newEdge.AddData(Data(edgeKey, value))

				nodeGraph.AddEdge(newEdge)

		for subgraph in graph.Subgraphs:
			nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name)
			self._graph.AddSubgraph(nodeGraph)
			translateSubgraph(nodeGraph, subgraph)

		translateGraph(self._graph, graph)

	def FromTree(self, tree: pyToolingNode) -> None:
		"""
		Fill this document from a :class:`pyTooling.Tree.Node`.

		Every node of the tree becomes a GraphML node, and every parent-child relation becomes an edge.

		:param tree: The root node of the tree to translate into this document.
		"""
		self._graph._id = tree._id

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

		rootNode = self._graph.AddNode(Node(tree._id))
		rootNode.AddData(Data(nodeValue, tree._value))

		for i, node in enumerate(tree.GetDescendants()):
			newNode = self._graph.AddNode(Node(node._id))
			newNode.AddData(Data(nodeValue, node._value))

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

	def OpeningTag(self, indent: int = 0) -> str:
		"""
		Return the opening XML tag of this document.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The opening XML tag, indented and terminated by a newline.
		"""
		return f"""\
{'  '*indent}<graphml xmlns="{self.xmlNS[None]}"
{'  '*indent}         xmlns:xsi="{self.xmlNS["xsi"]}"
{'  '*indent}         xsi:schemaLocation="{self.xsi["schemaLocation"]}">
"""

	def ClosingTag(self, indent: int = 0) -> str:
		"""
		Return the closing XML tag of this document.

		:param indent: Optional, indentation level of the XML element.
		:returns:      The closing XML tag, indented and terminated by a newline.
		"""
		return f"{'  '*indent}</graphml>\n"

	def ToStringLines(self, indent: int = 0) -> list[str]:
		"""
		Render this document as a list of XML lines.

		:param indent: Optional, indentation level of the XML element.
		:returns:      List of XML lines describing this document and everything it contains.
		"""
		lines = [self.OpeningTag(indent)]
		for key in self._keys.values():
			lines.extend(key.ToStringLines(indent + 1))
		lines.extend(self._graph.ToStringLines(indent + 1))
		lines.append(self.ClosingTag(indent))

		return lines

	def WriteToFile(self, file: Path) -> None:
		"""
		Write this document as a GraphML file.

		:param file: Path of the file to write.
		"""
		with file.open("w", encoding="utf-8") as f:
			f.write("""<?xml version="1.0" encoding="utf-8"?>""")
			f.writelines(self.ToStringLines())