Coverage for pyTooling/Graph/GraphML.py: 88%
413 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +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 `GraphML Primer <http://graphml.graphdrawing.org/primer/graphml-primer.html>`__
37 |rarr| The format's own introduction, describing the elements this module writes.
38"""
39from __future__ import annotations
41from enum import Enum, auto
42from pathlib import Path
43from typing import Any, ClassVar, Union, Optional as Nullable
45from pyTooling.Decorators import export, notimplemented, readonly
46from pyTooling.MetaClasses import ExtendedType
47from pyTooling.Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph
48from pyTooling.Tree import Node as pyToolingNode
51@export
52class AttributeContext(Enum):
53 """
54 Enumeration of all attribute contexts.
56 An attribute context describes to what kind of GraphML node an attribute can be applied.
57 """
58 GraphML = auto()
59 Graph = auto()
60 Node = auto()
61 Edge = auto()
62 Port = auto()
64 def __str__(self) -> str:
65 """
66 Return the enumeration value's name as it is written in a GraphML document.
68 :returns: Name of the enumeration value in lower case.
69 """
70 return f"{self.name.lower()}"
73@export
74class AttributeTypes(Enum):
75 """
76 Enumeration of all attribute types.
78 An attribute type describes what datatype can be applied to an attribute.
79 """
80 Boolean = auto()
81 Int = auto()
82 Long = auto()
83 Float = auto()
84 Double = auto()
85 String = auto()
87 def __str__(self) -> str:
88 """
89 Return the enumeration value's name as it is written in a GraphML document.
91 :returns: Name of the enumeration value in lower case.
92 """
93 return f"{self.name.lower()}"
96@export
97class EdgeDefault(Enum):
98 """An enumeration describing the default edge direction."""
99 Undirected = auto()
100 Directed = auto()
102 def __str__(self) -> str:
103 """
104 Return the enumeration value's name as it is written in a GraphML document.
106 :returns: Name of the enumeration value in lower case.
107 """
108 return f"{self.name.lower()}"
111@export
112class ParsingOrder(Enum):
113 """An enumeration describing the parsing order of the graph's representation."""
114 NodesFirst = auto() #: First, all nodes are given, then followed by all edges.
115 AdjacencyList = auto()
116 Free = auto()
118 def __str__(self) -> str:
119 """
120 Return the enumeration value's name as it is written in a GraphML document.
122 :returns: Name of the enumeration value in lower case.
123 """
124 return f"{self.name.lower()}"
127@export
128class IDStyle(Enum):
129 """An enumeration describing the style of identifiers (IDs)."""
130 Canonical = auto()
131 Free = auto()
133 def __str__(self) -> str:
134 """
135 Return the enumeration value's name as it is written in a GraphML document.
137 :returns: Name of the enumeration value in lower case.
138 """
139 return f"{self.name.lower()}"
142@export
143class Base(metaclass=ExtendedType, slots=True):
144 """
145 Base-class for all GraphML data model classes.
146 """
147 @readonly
148 def HasClosingTag(self) -> bool:
149 """
150 Check if this XML element is written with a separate closing tag.
152 :returns: ``True``, if the element needs a closing tag.
153 """
154 return True
156 def Tag(self, indent: int = 0) -> str:
157 """
158 Return this element as a self-closing XML tag.
160 :param indent: Optional, indentation level of the XML element.
161 :returns: The XML tag, indented and terminated by a newline.
162 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
163 """
164 raise NotImplementedError()
166 def OpeningTag(self, indent: int = 0) -> str:
167 """
168 Return the opening XML tag of this element.
170 :param indent: Optional, indentation level of the XML element.
171 :returns: The opening XML tag, indented and terminated by a newline.
172 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
173 """
174 raise NotImplementedError()
176 def ClosingTag(self, indent: int = 0) -> str:
177 """
178 Return the closing XML tag of this element.
180 :param indent: Optional, indentation level of the XML element.
181 :returns: The closing XML tag, indented and terminated by a newline.
182 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
183 """
184 raise NotImplementedError()
186 def ToStringLines(self, indent: int = 0) -> list[str]:
187 """
188 Render this element as a list of XML lines.
190 :param indent: Optional, indentation level of the XML element.
191 :returns: List of XML lines describing this element.
192 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
193 """
194 raise NotImplementedError()
197@export
198class BaseWithID(Base):
199 """Base-class for all GraphML elements carrying a document-wide unique ID."""
200 _id: str #: Unique identifier of this GraphML element.
202 def __init__(self, identifier: str) -> None:
203 """
204 Initialize a GraphML element with its unique ID.
206 :param identifier: Optional, unique ID of the element within the GraphML document.
207 """
208 super().__init__()
209 self._id = identifier
211 @readonly
212 def ID(self) -> str:
213 """
214 Read-only property to access the element's unique ID (:attr:`_id`).
216 :returns: Unique ID of the element.
217 """
218 return self._id
221@export
222class BaseWithData(BaseWithID):
223 """Base-class for all GraphML elements that can carry attached data items (key-value-pairs)."""
224 _data: list[Data] #: Data items (key-value-pairs) attached to this GraphML element.
226 def __init__(self, identifier: str) -> None:
227 """
228 Initialize a GraphML element with its unique ID and an empty list of data items.
230 :param identifier: Optional, unique ID of the element within the GraphML document.
231 """
232 super().__init__(identifier)
234 self._data = []
236 @readonly
237 def Data(self) -> list[Data]:
238 """
239 Read-only property to access the data elements attached to this element (:attr:`_data`).
241 :returns: List of data elements.
242 """
243 return self._data
245 def AddData(self, data: Data) -> Data:
246 """
247 Attach a data item (key-value-pair) to this element.
249 :param data: The data item to attach.
250 :returns: The attached data item, so it can be used in the calling expression.
251 """
252 self._data.append(data)
253 return data
256@export
257class Key(BaseWithID):
258 """
259 Declares an attribute that data items can refer to.
261 A GraphML document declares its attributes once - name, data type, and the element kind they apply to - and every
262 :class:`Data` item then references such a key by ID.
263 """
264 _context: AttributeContext #: GraphML element kind this key can be used on.
265 _attributeName: str #: Name of the attribute described by this key.
266 _attributeType: AttributeTypes #: Data type of the attribute described by this key.
268 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None:
269 """
270 Initialize a key declaring an attribute.
272 :param identifier: Optional, unique ID of the key within the GraphML document.
273 :param context: GraphML element kind this key can be used on.
274 :param name: Name of the declared attribute.
275 :param type: Data type of the declared attribute.
276 """
277 super().__init__(identifier)
279 self._context = context
280 self._attributeName = name
281 self._attributeType = type
283 @readonly
284 def Context(self) -> AttributeContext:
285 """
286 Read-only property to access the context this key applies to (:attr:`_context`).
288 :returns: The attribute's context (graph, node, edge, ...).
289 """
290 return self._context
292 @readonly
293 def AttributeName(self) -> str:
294 """
295 Read-only property to access the name of the described attribute (:attr:`_attributeName`).
297 :returns: Name of the attribute.
298 """
299 return self._attributeName
301 @readonly
302 def AttributeType(self) -> AttributeTypes:
303 """
304 Read-only property to access the type of the described attribute (:attr:`_attributeType`).
306 :returns: Type of the attribute.
307 """
308 return self._attributeType
310 @readonly
311 def HasClosingTag(self) -> bool:
312 """
313 Check if this XML element is written with a separate closing tag.
315 A key is always written as a self-closing tag.
317 :returns: ``False``, because a key never has a closing tag.
318 """
319 return False
321 def Tag(self, indent: int = 2) -> str:
322 """
323 Return this key as a self-closing XML tag.
325 :param indent: Optional, indentation level of the XML element.
326 :returns: The XML tag, indented and terminated by a newline.
327 """
328 return f"""{' '*indent}<key id="{self._id}" for="{self._context}" attr.name="{self._attributeName}" attr.type="{self._attributeType}" />\n"""
330 def ToStringLines(self, indent: int = 2) -> list[str]:
331 """
332 Render this key as a list of XML lines.
334 :param indent: Optional, indentation level of the XML element.
335 :returns: List of XML lines describing this key and everything attached to it.
336 """
337 return [self.Tag(indent)]
340@export
341class Data(Base):
342 """A single attached attribute: a value and the :class:`Key` describing it."""
343 _key: Key #: Key describing name and type of this data item.
344 _data: Any #: Value of this data item.
346 def __init__(self, key: Key, data: Any) -> None:
347 """
348 Initialize a data item with the key describing it and its value.
350 :param key: Key declaring name and type of this attribute.
351 :param data: Value of this attribute.
352 """
353 super().__init__()
355 self._key = key
356 self._data = data
358 @readonly
359 def Key(self) -> Key:
360 """
361 Read-only property to access the key describing this data element (:attr:`_key`).
363 :returns: The key this data element refers to.
364 """
365 return self._key
367 @readonly
368 def Data(self) -> Any:
369 """
370 Read-only property to access the data element's value (:attr:`_data`).
372 :returns: Value of the data element.
373 """
374 return self._data
376 @readonly
377 def HasClosingTag(self) -> bool:
378 """
379 Check if this XML element is written with a separate closing tag.
381 :returns: ``False``, because a data element is written inline.
382 """
383 return False
385 def Tag(self, indent: int = 2) -> str:
386 """
387 Return this data item as a self-closing XML tag.
389 :param indent: Optional, indentation level of the XML element.
390 :returns: The XML tag, indented and terminated by a newline.
391 """
392 data = str(self._data)
393 data = data.replace("&", "&")
394 data = data.replace("<", "<")
395 data = data.replace(">", ">")
396 data = data.replace("\n", "\\n")
397 return f"""{' '*indent}<data key="{self._key._id}">{data}</data>\n"""
399 def ToStringLines(self, indent: int = 2) -> list[str]:
400 """
401 Render this data item as a list of XML lines.
403 :param indent: Optional, indentation level of the XML element.
404 :returns: List of XML lines describing this data item and everything attached to it.
405 """
406 return [self.Tag(indent)]
409@export
410class Node(BaseWithData):
411 """A node (vertex) of a GraphML graph."""
413 def __init__(self, identifier: str) -> None:
414 """
415 Initialize a node.
417 :param identifier: Optional, unique ID of the node within the GraphML document.
418 """
419 super().__init__(identifier)
421 @readonly
422 def HasClosingTag(self) -> bool:
423 """
424 Check if this XML element is written with a separate closing tag.
426 :returns: ``True``, if the node carries data elements, otherwise ``False``.
427 """
428 return len(self._data) > 0
430 def Tag(self, indent: int = 2) -> str:
431 """
432 Return this node as a self-closing XML tag.
434 :param indent: Optional, indentation level of the XML element.
435 :returns: The XML tag, indented and terminated by a newline.
436 """
437 return f"""{' '*indent}<node id="{self._id}" />\n"""
439 def OpeningTag(self, indent: int = 2) -> str:
440 """
441 Return the opening XML tag of this node.
443 :param indent: Optional, indentation level of the XML element.
444 :returns: The opening XML tag, indented and terminated by a newline.
445 """
446 return f"""{' '*indent}<node id="{self._id}">\n"""
448 def ClosingTag(self, indent: int = 2) -> str:
449 """
450 Return the closing XML tag of this node.
452 :param indent: Optional, indentation level of the XML element.
453 :returns: The closing XML tag, indented and terminated by a newline.
454 """
455 return f"""{' ' * indent}</node>\n"""
457 def ToStringLines(self, indent: int = 2) -> list[str]:
458 """
459 Render this node as a list of XML lines.
461 :param indent: Optional, indentation level of the XML element.
462 :returns: List of XML lines describing this node and everything attached to it.
463 """
464 if not self.HasClosingTag:
465 return [self.Tag(indent)]
467 lines = [self.OpeningTag(indent)]
468 for data in self._data:
469 lines.extend(data.ToStringLines(indent + 1))
470 lines.append(self.ClosingTag(indent))
472 return lines
475@export
476class Edge(BaseWithData):
477 """An edge of a GraphML graph, connecting a source node to a target node."""
478 _source: Node #: Node the edge starts at.
479 _target: Node #: Node the edge ends at.
481 def __init__(self, identifier: str, source: Node, target: Node) -> None:
482 """
483 Initialize an edge between two nodes.
485 :param identifier: Optional, unique ID of the edge within the GraphML document.
486 :param source: Node the edge starts at.
487 :param target: Node the edge ends at.
488 """
489 super().__init__(identifier)
491 self._source = source
492 self._target = target
494 @readonly
495 def Source(self) -> Node:
496 """
497 Read-only property to access the edge's source node (:attr:`_source`).
499 :returns: Source node of the edge.
500 """
501 return self._source
503 @readonly
504 def Target(self) -> Node:
505 """
506 Read-only property to access the edge's target node (:attr:`_target`).
508 :returns: Target node of the edge.
509 """
510 return self._target
512 @readonly
513 def HasClosingTag(self) -> bool:
514 """
515 Check if this XML element is written with a separate closing tag.
517 :returns: ``True``, if the edge carries data elements, otherwise ``False``.
518 """
519 return len(self._data) > 0
521 def Tag(self, indent: int = 2) -> str:
522 """
523 Return this edge as a self-closing XML tag.
525 :param indent: Optional, indentation level of the XML element.
526 :returns: The XML tag, indented and terminated by a newline.
527 """
528 return f"""{' ' * indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}" />\n"""
530 def OpeningTag(self, indent: int = 2) -> str:
531 """
532 Return the opening XML tag of this edge.
534 :param indent: Optional, indentation level of the XML element.
535 :returns: The opening XML tag, indented and terminated by a newline.
536 """
537 return f"""{' '*indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}">\n"""
539 def ClosingTag(self, indent: int = 2) -> str:
540 """
541 Return the closing XML tag of this edge.
543 :param indent: Optional, indentation level of the XML element.
544 :returns: The closing XML tag, indented and terminated by a newline.
545 """
546 return f"""{' ' * indent}</edge>\n"""
548 def ToStringLines(self, indent: int = 2) -> list[str]:
549 """
550 Render this edge as a list of XML lines.
552 :param indent: Optional, indentation level of the XML element.
553 :returns: List of XML lines describing this edge and everything attached to it.
554 """
555 if not self.HasClosingTag:
556 return [self.Tag(indent)]
558 lines = [self.OpeningTag(indent)]
559 for data in self._data:
560 lines.extend(data.ToStringLines(indent + 1))
561 lines.append(self.ClosingTag(indent))
563 return lines
566@export
567class BaseGraph(BaseWithData, mixin=True):
568 """
569 Mixin-class for everything that contains nodes, edges and subgraphs - a graph as well as a subgraph.
571 Beside the elements themselves, it carries the document-level settings applied while writing them: the default edge
572 direction, the parsing order, and the ID styles for nodes and edges.
573 """
574 _subgraphs: dict[str, Subgraph] #: Subgraphs of this graph, by ID.
575 _nodes: dict[str, Node] #: Nodes of this graph, by ID.
576 _edges: dict[str, Edge] #: Edges of this graph, by ID.
577 _edgeDefault: EdgeDefault #: Direction applied to edges that don't specify one.
578 _parseOrder: ParsingOrder #: Order in which nodes and edges may appear in the XML document.
579 _nodeIDStyle: IDStyle #: Whether node IDs are free-form or canonical.
580 _edgeIDStyle: IDStyle #: Whether edge IDs are free-form or canonical.
582 def __init__(self, identifier: Nullable[str] = None) -> None:
583 """
584 Initialize an empty graph with the default document settings.
586 Edges are directed, nodes are written before edges, and both ID styles are free-form until they are changed.
588 :param identifier: Optional, unique ID of the graph within the GraphML document.
589 """
590 super().__init__(identifier)
592 self._subgraphs = {}
593 self._nodes = {}
594 self._edges = {}
595 self._edgeDefault = EdgeDefault.Directed
596 self._parseOrder = ParsingOrder.NodesFirst
597 self._nodeIDStyle = IDStyle.Free
598 self._edgeIDStyle = IDStyle.Free
600 @readonly
601 def Subgraphs(self) -> dict[str, Subgraph]:
602 """
603 Read-only property to access the graph's subgraphs (:attr:`_subgraphs`).
605 :returns: Dictionary of subgraph IDs and subgraphs.
606 """
607 return self._subgraphs
609 @readonly
610 def Nodes(self) -> dict[str, Node]:
611 """
612 Read-only property to access the graph's nodes (:attr:`_nodes`).
614 :returns: Dictionary of node IDs and nodes.
615 """
616 return self._nodes
618 @readonly
619 def Edges(self) -> dict[str, Edge]:
620 """
621 Read-only property to access the graph's edges (:attr:`_edges`).
623 :returns: Dictionary of edge IDs and edges.
624 """
625 return self._edges
627 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
628 """
629 Add a subgraph to this graph, which is a node of this graph as well.
631 :param subgraph: The subgraph to add.
632 :returns: The added subgraph, so it can be used in the calling expression.
633 """
634 self._subgraphs[subgraph._subgraphID] = subgraph
635 self._nodes[subgraph._id] = subgraph
636 return subgraph
638 def GetSubgraph(self, subgraphName: str) -> Subgraph:
639 """
640 Return the subgraph with the given ID.
642 :param subgraphName: ID of the subgraph.
643 :returns: The subgraph with that ID.
644 :raises KeyError: If no subgraph has that ID.
645 """
646 return self._subgraphs[subgraphName]
648 def AddNode(self, node: Node) -> Node:
649 """
650 Add a node to this graph.
652 :param node: The node to add.
653 :returns: The added node, so it can be used in the calling expression.
654 """
655 self._nodes[node._id] = node
656 return node
658 def GetNode(self, nodeName: str) -> Node:
659 """
660 Return the node with the given ID.
662 :param nodeName: ID of the node.
663 :returns: The node with that ID.
664 :raises KeyError: If no node has that ID.
665 """
666 return self._nodes[nodeName]
668 def AddEdge(self, edge: Edge) -> Edge:
669 """
670 Add an edge to this graph.
672 :param edge: The edge to add.
673 :returns: The added edge, so it can be used in the calling expression.
674 """
675 self._edges[edge._id] = edge
676 return edge
678 def GetEdge(self, edgeName: str) -> Edge:
679 """
680 Return the edge with the given ID.
682 :param edgeName: ID of the edge.
683 :returns: The edge with that ID.
684 :raises KeyError: If no edge has that ID.
685 """
686 return self._edges[edgeName]
688 def OpeningTag(self, indent: int = 1) -> str:
689 """
690 Return the opening XML tag of this graph.
692 Beside the graph's ID, the tag carries the parsing hints a reader needs: the default edge direction, the
693number of nodes and edges, the parsing order and both ID styles.
695 :param indent: Optional, indentation level of the XML element.
696 :returns: The opening XML tag, indented and terminated by a newline.
697 """
698 return f"""\
699{' '*indent}<graph id="{self._id}"
700{' '*indent} edgedefault="{self._edgeDefault!s}"
701{' '*indent} parse.nodes="{len(self._nodes)}"
702{' '*indent} parse.edges="{len(self._edges)}"
703{' '*indent} parse.order="{self._parseOrder!s}"
704{' '*indent} parse.nodeids="{self._nodeIDStyle!s}"
705{' '*indent} parse.edgeids="{self._edgeIDStyle!s}">
706"""
708 def ClosingTag(self, indent: int = 1) -> str:
709 """
710 Return the closing XML tag of this graph.
712 :param indent: Optional, indentation level of the XML element.
713 :returns: The closing XML tag, indented and terminated by a newline.
714 """
715 return f"{' '*indent}</graph>\n"
717 def ToStringLines(self, indent: int = 1) -> list[str]:
718 """
719 Render this graph as a list of XML lines.
721 :param indent: Optional, indentation level of the XML element.
722 :returns: List of XML lines describing this graph and everything it contains.
723 """
724 lines = [self.OpeningTag(indent)]
725 for node in self._nodes.values():
726 lines.extend(node.ToStringLines(indent + 1))
727 for edge in self._edges.values():
728 lines.extend(edge.ToStringLines(indent + 1))
729 # for data in self._data:
730 # lines.extend(data.ToStringLines(indent + 1))
731 lines.append(self.ClosingTag(indent))
733 return lines
736@export
737class Graph(BaseGraph):
738 """
739 The root graph of a GraphML document.
741 It owns the ID space: every node, edge and subgraph registers itself here, so an ID is used only once per document.
742 """
743 _document: GraphMLDocument #: The GraphML document this graph belongs to.
744 _ids: dict[str, Union[Node, Edge, Subgraph]] #: Every element of this graph by ID, used to keep IDs unique.
746 def __init__(self, document: GraphMLDocument, identifier: str) -> None:
747 """
748 Initialize the root graph of a GraphML document.
750 :param document: The GraphML document this graph belongs to.
751 :param identifier: Optional, unique ID of the graph within the GraphML document.
752 """
753 super().__init__(identifier)
754 self._document = document
755 self._ids = {}
757 def GetByID(self, identifier: str) -> Union[Node, Edge, Subgraph]:
758 """
759 Return the element with the given ID, whichever kind it is.
761 :param identifier: Optional, ID of the node, edge or subgraph.
762 :returns: The element registered under that ID.
763 :raises KeyError: If no element has that ID.
764 """
765 return self._ids[identifier]
767 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
768 """
769 Add a subgraph to the root graph and register its ID.
771 :param subgraph: The subgraph to add.
772 :returns: The added subgraph, so it can be used in the calling expression.
773 """
774 result = super().AddSubgraph(subgraph)
775 self._ids[subgraph._subgraphID] = subgraph
776 subgraph._root = self
777 return result
779 def AddNode(self, node: Node) -> Node:
780 """
781 Add a node to the root graph and register its ID.
783 :param node: The node to add.
784 :returns: The added node, so it can be used in the calling expression.
785 """
786 result = super().AddNode(node)
787 self._ids[node._id] = node
788 return result
790 def AddEdge(self, edge: Edge) -> Edge:
791 """
792 Add an edge to the root graph and register its ID.
794 :param edge: The edge to add.
795 :returns: The added edge, so it can be used in the calling expression.
796 """
797 result = super().AddEdge(edge)
798 self._ids[edge._id] = edge
799 return result
802@export
803class Subgraph(Node, BaseGraph):
804 """
805 A nested graph, which is a node of its parent graph and a graph of its own.
807 It therefore carries two identifiers: the node's ID it is referenced by, and :attr:`_subgraphID` for the graph it
808 contains.
809 """
810 _subgraphID: str #: ID of the subgraph, which is distinct from the node's own ID.
811 _root: Nullable[Graph] #: The graph this subgraph is nested in.
813 def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None:
814 """
815 Initialize a subgraph, which is a node in its parent graph and a graph of its own.
817 :param nodeIdentifier: Unique ID of the node representing the subgraph.
818 :param graphIdentifier: Unique ID of the graph contained in that node.
819 """
820 super().__init__(nodeIdentifier)
821 BaseGraph.__init__(self, nodeIdentifier)
823 self._subgraphID = graphIdentifier
824 self._root = None
826 @readonly
827 def RootGraph(self) -> Graph:
828 """
829 Read-only property to access the graph this subgraph is embedded in (:attr:`_root`).
831 :returns: The root graph.
832 """
833 return self._root
835 @readonly
836 def SubgraphID(self) -> str:
837 """
838 Read-only property to access the subgraph's ID (:attr:`_subgraphID`).
840 :returns: ID of the subgraph.
841 """
842 return self._subgraphID
844 @readonly
845 def HasClosingTag(self) -> bool:
846 """
847 Check if this XML element is written with a separate closing tag.
849 :returns: ``True``, because a subgraph always has a closing tag.
850 """
851 return True
853 def AddNode(self, node: Node) -> Node:
854 """
855 Add a node to this subgraph and register its ID at the root graph.
857 :param node: The node to add.
858 :returns: The added node, so it can be used in the calling expression.
859 """
860 result = super().AddNode(node)
861 self._root._ids[node._id] = node
862 return result
864 def AddEdge(self, edge: Edge) -> Edge:
865 """
866 Add an edge to this subgraph and register its ID at the root graph.
868 :param edge: The edge to add.
869 :returns: The added edge, so it can be used in the calling expression.
870 """
871 result = super().AddEdge(edge)
872 self._root._ids[edge._id] = edge
873 return result
875 @notimplemented("A subgraph is always written with an opening and a closing tag.")
876 def Tag(self, indent: int = 2) -> str:
877 """
878 A subgraph always contains a graph, so it is never written as a self-closing tag.
880 :param indent: Optional, indentation level of the XML element.
881 """
883 def OpeningTag(self, indent: int = 1) -> str:
884 """
885 Return the opening XML tag of this subgraph.
887 :param indent: Optional, indentation level of the XML element.
888 :returns: The opening XML tag, indented and terminated by a newline.
889 """
890 return f"""\
891{' ' * indent}<graph id="{self._subgraphID}"
892{' ' * indent} edgedefault="{self._edgeDefault!s}"
893{' ' * indent} parse.nodes="{len(self._nodes)}"
894{' ' * indent} parse.edges="{len(self._edges)}"
895{' ' * indent} parse.order="{self._parseOrder!s}"
896{' ' * indent} parse.nodeids="{self._nodeIDStyle!s}"
897{' ' * indent} parse.edgeids="{self._edgeIDStyle!s}">
898"""
900 def ClosingTag(self, indent: int = 2) -> str:
901 """
902 Return the closing XML tag of this subgraph.
904 :param indent: Optional, indentation level of the XML element.
905 :returns: The closing XML tag, indented and terminated by a newline.
906 """
907 return BaseGraph.ClosingTag(self, indent)
909 def ToStringLines(self, indent: int = 2) -> list[str]:
910 """
911 Render this subgraph as a list of XML lines.
913 :param indent: Optional, indentation level of the XML element.
914 :returns: List of XML lines describing this subgraph and everything it contains.
915 """
916 lines = [super().OpeningTag(indent)]
917 for data in self._data: 917 ↛ 918line 917 didn't jump to line 918 because the loop on line 917 never started
918 lines.extend(data.ToStringLines(indent + 1))
919 # lines.extend(Graph.ToStringLines(self, indent + 1))
920 lines.append(self.OpeningTag(indent + 1))
921 for node in self._nodes.values():
922 lines.extend(node.ToStringLines(indent + 2))
923 for edge in self._edges.values():
924 lines.extend(edge.ToStringLines(indent + 2))
925 # for data in self._data:
926 # lines.extend(data.ToStringLines(indent + 1))
927 lines.append(self.ClosingTag(indent + 1))
928 lines.append(super().ClosingTag(indent))
930 return lines
933@export
934class GraphMLDocument(Base):
935 """
936 A GraphML document: the root graph, the keys it declares, and the XML boilerplate to write it out.
937 """
939 xmlNS: ClassVar[dict[Nullable[str], str]] = {
940 None: "http://graphml.graphdrawing.org/xmlns",
941 "xsi": "http://www.w3.org/2001/XMLSchema-instance"
942 } #: XML namespaces of a GraphML document.
943 xsi: ClassVar[dict[str, str]] = {
944 "schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"
945 } #: XML schema instance attributes of a GraphML document.
947 _graph: Graph #: The document's root graph.
948 _keys: dict[str, Key] #: Keys declared by this document, by ID.
950 def __init__(self, identifier: str = "G") -> None:
951 """
952 Initialize a GraphML document with an empty root graph.
954 :param identifier: Optional, unique ID of the root graph.
955 """
956 super().__init__()
958 self._graph = Graph(self, identifier)
959 self._keys = {}
961 @readonly
962 def Graph(self) -> BaseGraph:
963 """
964 Read-only property to access the document's graph (:attr:`_graph`).
966 :returns: The graph described by this document.
967 """
968 return self._graph
970 @readonly
971 def Keys(self) -> dict[str, Key]:
972 """
973 Read-only property to access the attribute keys declared in this document (:attr:`_keys`).
975 :returns: Dictionary of key IDs and keys.
976 """
977 return self._keys
979 def AddKey(self, key: Key) -> Key:
980 """
981 Declare an attribute, so data items can refer to it.
983 :param key: The key to declare.
984 :returns: The declared key, so it can be used in the calling expression.
985 """
986 self._keys[key._id] = key
987 return key
989 def GetKey(self, keyName: str) -> Key:
990 """
991 Return the declared key with the given ID.
993 :param keyName: ID of the key.
994 :returns: The key with that ID.
995 :raises KeyError: If no key has that ID.
996 """
997 return self._keys[keyName]
999 def HasKey(self, keyName: str) -> bool:
1000 """
1001 Check if a key with the given ID was declared.
1003 :param keyName: ID of the key.
1004 :returns: ``True``, if such a key exists.
1005 """
1006 return keyName in self._keys
1008 def FromGraph(self, graph: pyToolingGraph) -> None:
1009 """
1010 Fill this document from a :class:`pyTooling.Graph.Graph`.
1012 Vertices become nodes, edges become edges, and the vertex and edge values are attached as data items,
1013 declared by two keys this method adds. Subgraphs are translated recursively.
1015 :param graph: The graph to translate into this document.
1016 """
1017 document = self
1018 self._graph._id = graph._name
1020 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
1021 edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String))
1023 def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph):
1024 """
1025 Nested function for recursion.
1027 It translates the vertices and edges of one pyTooling graph into GraphML nodes and edges, and recurses into the
1028 subgraphs it finds.
1030 :param rootGraph: The GraphML graph the elements are added to.
1031 :param pyTGraph: The pyTooling graph to translate.
1032 """
1033 for vertex in pyTGraph.IterateVertices():
1034 newNode = Node(vertex._id)
1035 newNode.AddData(Data(nodeValue, vertex._value))
1036 for key, value in vertex._dict.items(): 1036 ↛ 1037line 1036 didn't jump to line 1037 because the loop on line 1036 never started
1037 if document.HasKey(str(key)):
1038 nodeKey = document.GetKey(f"node{key!s}")
1039 else:
1040 nodeKey = document.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
1041 newNode.AddData(Data(nodeKey, value))
1043 rootGraph.AddNode(newNode)
1045 for edge in pyTGraph.IterateEdges():
1046 source = rootGraph.GetByID(edge._source._id)
1047 target = rootGraph.GetByID(edge._destination._id)
1049 newEdge = Edge(edge._id, source, target)
1050 newEdge.AddData(Data(edgeValue, edge._value))
1051 for key, value in edge._dict.items(): 1051 ↛ 1052line 1051 didn't jump to line 1052 because the loop on line 1051 never started
1052 if self.HasKey(str(key)):
1053 edgeKey = self.GetBy(f"edge{key!s}")
1054 else:
1055 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
1056 newEdge.AddData(Data(edgeKey, value))
1058 rootGraph.AddEdge(newEdge)
1060 for link in pyTGraph.IterateLinks():
1061 source = rootGraph.GetByID(link._source._id)
1062 target = rootGraph.GetByID(link._destination._id)
1064 newEdge = Edge(link._id, source, target)
1065 newEdge.AddData(Data(edgeValue, link._value))
1066 for key, value in link._dict.items(): 1066 ↛ 1067line 1066 didn't jump to line 1067 because the loop on line 1066 never started
1067 if self.HasKey(str(key)):
1068 edgeKey = self.GetKey(f"link{key!s}")
1069 else:
1070 edgeKey = self.AddKey(Key(f"link{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
1071 newEdge.AddData(Data(edgeKey, value))
1073 rootGraph.AddEdge(newEdge)
1075 def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph):
1076 """
1077 Nested function for recursion.
1079 It translates one pyTooling subgraph into a GraphML subgraph.
1081 :param nodeGraph: The GraphML subgraph the elements are added to.
1082 :param pyTSubgraph: The pyTooling subgraph to translate.
1083 """
1084 rootGraph = nodeGraph.RootGraph
1086 for vertex in pyTSubgraph.IterateVertices():
1087 newNode = Node(vertex._id)
1088 newNode.AddData(Data(nodeValue, vertex._value))
1089 for key, value in vertex._dict.items(): 1089 ↛ 1090line 1089 didn't jump to line 1090 because the loop on line 1089 never started
1090 if self.HasKey(str(key)):
1091 nodeKey = self.GetKey(f"node{key!s}")
1092 else:
1093 nodeKey = self.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
1094 newNode.AddData(Data(nodeKey, value))
1096 nodeGraph.AddNode(newNode)
1098 for edge in pyTSubgraph.IterateEdges():
1099 source = nodeGraph.GetNode(edge._source._id)
1100 target = nodeGraph.GetNode(edge._destination._id)
1102 newEdge = Edge(edge._id, source, target)
1103 newEdge.AddData(Data(edgeValue, edge._value))
1104 for key, value in edge._dict.items(): 1104 ↛ 1105line 1104 didn't jump to line 1105 because the loop on line 1104 never started
1105 if self.HasKey(str(key)):
1106 edgeKey = self.GetKey(f"edge{key!s}")
1107 else:
1108 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
1109 newEdge.AddData(Data(edgeKey, value))
1111 nodeGraph.AddEdge(newEdge)
1113 for subgraph in graph.Subgraphs:
1114 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name)
1115 self._graph.AddSubgraph(nodeGraph)
1116 translateSubgraph(nodeGraph, subgraph)
1118 translateGraph(self._graph, graph)
1120 def FromTree(self, tree: pyToolingNode) -> None:
1121 """
1122 Fill this document from a :class:`pyTooling.Tree.Node`.
1124 Every node of the tree becomes a GraphML node, and every parent-child relation becomes an edge.
1126 :param tree: The root node of the tree to translate into this document.
1127 """
1128 self._graph._id = tree._id
1130 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
1132 rootNode = self._graph.AddNode(Node(tree._id))
1133 rootNode.AddData(Data(nodeValue, tree._value))
1135 for i, node in enumerate(tree.GetDescendants()):
1136 newNode = self._graph.AddNode(Node(node._id))
1137 newNode.AddData(Data(nodeValue, node._value))
1139 newEdge = self._graph.AddEdge(Edge(f"e{i}", newNode, self._graph.GetNode(node._parent._id)))
1141 def OpeningTag(self, indent: int = 0) -> str:
1142 """
1143 Return the opening XML tag of this document.
1145 :param indent: Optional, indentation level of the XML element.
1146 :returns: The opening XML tag, indented and terminated by a newline.
1147 """
1148 return f"""\
1149{' '*indent}<graphml xmlns="{self.xmlNS[None]}"
1150{' '*indent} xmlns:xsi="{self.xmlNS["xsi"]}"
1151{' '*indent} xsi:schemaLocation="{self.xsi["schemaLocation"]}">
1152"""
1154 def ClosingTag(self, indent: int = 0) -> str:
1155 """
1156 Return the closing XML tag of this document.
1158 :param indent: Optional, indentation level of the XML element.
1159 :returns: The closing XML tag, indented and terminated by a newline.
1160 """
1161 return f"{' '*indent}</graphml>\n"
1163 def ToStringLines(self, indent: int = 0) -> list[str]:
1164 """
1165 Render this document as a list of XML lines.
1167 :param indent: Optional, indentation level of the XML element.
1168 :returns: List of XML lines describing this document and everything it contains.
1169 """
1170 lines = [self.OpeningTag(indent)]
1171 for key in self._keys.values():
1172 lines.extend(key.ToStringLines(indent + 1))
1173 lines.extend(self._graph.ToStringLines(indent + 1))
1174 lines.append(self.ClosingTag(indent))
1176 return lines
1178 def WriteToFile(self, file: Path) -> None:
1179 """
1180 Write this document as a GraphML file.
1182 :param file: Path of the file to write.
1183 """
1184 with file.open("w", encoding="utf-8") as f:
1185 f.write(f"""<?xml version="1.0" encoding="utf-8"?>""")
1186 f.writelines(self.ToStringLines())