Coverage for pyTooling/Graph/GraphML.py: 97%
436 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +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, abstractmethod
47from pyTooling.Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph, Edge as pyToolingEdge, \
48 Link as pyToolingLink, GraphViz
49from pyTooling.Tree import Node as pyToolingNode
52_ATTRIBUTE_ESCAPES = str.maketrans({"&": "&", "<": "<", ">": ">", '"': """}) #: Translation table escaping an XML attribute value.
55def _escapeAttribute(value: Any) -> str:
56 """
57 Escape a value for an XML attribute in double quotes: ``&``, ``<``, ``>`` and ``"``.
59 :param value: The value, converted by :func:`str`.
60 :returns: The escaped text.
61 """
62 return str(value).translate(_ATTRIBUTE_ESCAPES)
64@export
65class AttributeContext(Enum):
66 """
67 Enumeration of all attribute contexts.
69 An attribute context describes to what kind of GraphML node an attribute can be applied.
70 """
71 GraphML = auto()
72 Graph = auto()
73 Node = auto()
74 Edge = auto()
75 Port = auto()
77 def __str__(self) -> str:
78 """
79 Return the enumeration value's name as it is written in a GraphML document.
81 :returns: Name of the enumeration value in lower case.
82 """
83 return f"{self.name.lower()}"
86@export
87class AttributeTypes(Enum):
88 """
89 Enumeration of all attribute types.
91 An attribute type describes what datatype can be applied to an attribute.
92 """
93 Boolean = auto()
94 Int = auto()
95 Long = auto()
96 Float = auto()
97 Double = auto()
98 String = auto()
100 def __str__(self) -> str:
101 """
102 Return the enumeration value's name as it is written in a GraphML document.
104 :returns: Name of the enumeration value in lower case.
105 """
106 return f"{self.name.lower()}"
109@export
110class EdgeDefault(Enum):
111 """An enumeration describing the default edge direction."""
112 Undirected = auto()
113 Directed = auto()
115 def __str__(self) -> str:
116 """
117 Return the enumeration value's name as it is written in a GraphML document.
119 :returns: Name of the enumeration value in lower case.
120 """
121 return f"{self.name.lower()}"
124@export
125class ParsingOrder(Enum):
126 """An enumeration describing the parsing order of the graph's representation."""
127 NodesFirst = auto() #: First, all nodes are given, then followed by all edges.
128 AdjacencyList = auto()
129 Free = auto()
131 def __str__(self) -> str:
132 """
133 Return the enumeration value's name as it is written in a GraphML document.
135 :returns: Name of the enumeration value in lower case.
136 """
137 return f"{self.name.lower()}"
140@export
141class IDStyle(Enum):
142 """An enumeration describing the style of identifiers (IDs)."""
143 Canonical = auto()
144 Free = auto()
146 def __str__(self) -> str:
147 """
148 Return the enumeration value's name as it is written in a GraphML document.
150 :returns: Name of the enumeration value in lower case.
151 """
152 return f"{self.name.lower()}"
155@export
156class Base(metaclass=ExtendedType, slots=True):
157 """
158 Base-class for all GraphML data model classes.
159 """
160 @readonly
161 def HasClosingTag(self) -> bool:
162 """
163 Check if this XML element is written with a separate closing tag.
165 :returns: ``True``, if the element needs a closing tag.
166 """
167 return True
169 @abstractmethod
170 def Tag(self, indent: int = 0) -> str:
171 """
172 Return this element as a self-closing XML tag.
174 An element always written with an opening and a closing tag overrides it with
175 :deco:`~pyTooling.Decorators.notimplemented`.
177 :param indent: Optional, indentation level of the XML element.
178 :returns: The XML tag, indented and terminated by a newline.
179 """
181 @abstractmethod
182 def OpeningTag(self, indent: int = 0) -> str:
183 """
184 Return the opening XML tag of this element.
186 An element always written as a single tag overrides it with :deco:`~pyTooling.Decorators.notimplemented`.
188 :param indent: Optional, indentation level of the XML element.
189 :returns: The opening XML tag, indented and terminated by a newline.
190 """
192 @abstractmethod
193 def ClosingTag(self, indent: int = 0) -> str:
194 """
195 Return the closing XML tag of this element.
197 An element always written as a single tag overrides it with :deco:`~pyTooling.Decorators.notimplemented`.
199 :param indent: Optional, indentation level of the XML element.
200 :returns: The closing XML tag, indented and terminated by a newline.
201 """
203 @abstractmethod
204 def ToStringLines(self, indent: int = 0) -> list[str]:
205 """
206 Render this element as a list of XML lines.
208 :param indent: Optional, indentation level of the XML element.
209 :returns: List of XML lines describing this element.
210 """
213@export
214class BaseWithID(Base):
215 """Base-class for all GraphML elements carrying a document-wide unique ID."""
216 _id: Nullable[str] #: Unique identifier of this GraphML element. ``None`` if it has none, e.g. an edge.
218 def __init__(self, identifier: Nullable[str]) -> None:
219 """
220 Initialize a GraphML element with its unique ID.
222 :param identifier: Unique ID of the element within the GraphML document, or ``None``.
223 """
224 super().__init__()
225 self._id = identifier
227 @readonly
228 def ID(self) -> Nullable[str]:
229 """
230 Read-only property to access the element's unique ID (:attr:`_id`).
232 :returns: Unique ID of the element, or ``None`` if it has none.
233 """
234 return self._id
237@export
238class BaseWithData(BaseWithID):
239 """Base-class for all GraphML elements that can carry attached data items (key-value-pairs)."""
240 _data: list[Data] #: Data items (key-value-pairs) attached to this GraphML element.
242 def __init__(self, identifier: Nullable[str]) -> None:
243 """
244 Initialize a GraphML element with its unique ID and an empty list of data items.
246 :param identifier: Unique ID of the element within the GraphML document, or ``None``.
247 """
248 super().__init__(identifier)
250 self._data = []
252 @readonly
253 def Data(self) -> list[Data]:
254 """
255 Read-only property to access the data elements attached to this element (:attr:`_data`).
257 :returns: List of data elements.
258 """
259 return self._data
261 def AddData(self, data: Data) -> Data:
262 """
263 Attach a data item (key-value-pair) to this element.
265 :param data: The data item to attach.
266 :returns: The attached data item, so it can be used in the calling expression.
267 """
268 self._data.append(data)
269 return data
272@export
273class Key(BaseWithID):
274 """
275 Declares an attribute that data items can refer to.
277 A GraphML document declares its attributes once - name, data type, and the element kind they apply to - and every
278 :class:`Data` item then references such a key by ID.
279 """
280 _context: AttributeContext #: GraphML element kind this key can be used on.
281 _attributeName: str #: Name of the attribute described by this key.
282 _attributeType: AttributeTypes #: Data type of the attribute described by this key.
284 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None:
285 """
286 Initialize a key declaring an attribute.
288 :param identifier: Unique ID of the key within the GraphML document.
289 :param context: GraphML element kind this key can be used on.
290 :param name: Name of the declared attribute.
291 :param type: Data type of the declared attribute.
292 """
293 super().__init__(identifier)
295 self._context = context
296 self._attributeName = name
297 self._attributeType = type
299 @readonly
300 def Context(self) -> AttributeContext:
301 """
302 Read-only property to access the context this key applies to (:attr:`_context`).
304 :returns: The attribute's context (graph, node, edge, ...).
305 """
306 return self._context
308 @readonly
309 def AttributeName(self) -> str:
310 """
311 Read-only property to access the name of the described attribute (:attr:`_attributeName`).
313 :returns: Name of the attribute.
314 """
315 return self._attributeName
317 @readonly
318 def AttributeType(self) -> AttributeTypes:
319 """
320 Read-only property to access the type of the described attribute (:attr:`_attributeType`).
322 :returns: Type of the attribute.
323 """
324 return self._attributeType
326 @readonly
327 def HasClosingTag(self) -> bool:
328 """
329 Check if this XML element is written with a separate closing tag.
331 A key is always written as a self-closing tag.
333 :returns: ``False``, because a key never has a closing tag.
334 """
335 return False
337 def Tag(self, indent: int = 2) -> str:
338 """
339 Return this key as a self-closing XML tag.
341 :param indent: Optional, indentation level of the XML element.
342 :returns: The XML tag, indented and terminated by a newline.
343 """
344 identifier = _escapeAttribute(self._id)
345 attributeName = _escapeAttribute(self._attributeName)
346 return f"""{' '*indent}<key id="{identifier}" for="{self._context}" attr.name="{attributeName}" attr.type="{self._attributeType}" />\n"""
348 @notimplemented("A key is always written as a self-closing tag.")
349 def OpeningTag(self, indent: int = 2) -> str:
350 """
351 A key has no content, so it is never written with an opening and a closing tag.
353 :param indent: Optional, indentation level of the XML element.
354 """
356 @notimplemented("A key is always written as a self-closing tag.")
357 def ClosingTag(self, indent: int = 2) -> str:
358 """
359 A key has no content, so it is never written with an opening and a closing tag.
361 :param indent: Optional, indentation level of the XML element.
362 """
364 def ToStringLines(self, indent: int = 2) -> list[str]:
365 """
366 Render this key as a list of XML lines.
368 :param indent: Optional, indentation level of the XML element.
369 :returns: List of XML lines describing this key and everything attached to it.
370 """
371 return [self.Tag(indent)]
374@export
375class Data(Base):
376 """A single attached attribute: a value and the :class:`Key` describing it."""
377 _key: Key #: Key describing name and type of this data item.
378 _data: Any #: Value of this data item.
380 def __init__(self, key: Key, data: Any) -> None:
381 """
382 Initialize a data item with the key describing it and its value.
384 :param key: Key declaring name and type of this attribute.
385 :param data: Value of this attribute.
386 """
387 super().__init__()
389 self._key = key
390 self._data = data
392 @readonly
393 def Key(self) -> Key:
394 """
395 Read-only property to access the key describing this data element (:attr:`_key`).
397 :returns: The key this data element refers to.
398 """
399 return self._key
401 @readonly
402 def Data(self) -> Any:
403 """
404 Read-only property to access the data element's value (:attr:`_data`).
406 :returns: Value of the data element.
407 """
408 return self._data
410 @readonly
411 def HasClosingTag(self) -> bool:
412 """
413 Check if this XML element is written with a separate closing tag.
415 :returns: ``False``, because a data element is written inline.
416 """
417 return False
419 def Tag(self, indent: int = 2) -> str:
420 """
421 Return this data item as a self-closing XML tag.
423 :param indent: Optional, indentation level of the XML element.
424 :returns: The XML tag, indented and terminated by a newline.
425 """
426 data = str(self._data)
427 data = data.replace("&", "&")
428 data = data.replace("<", "<")
429 data = data.replace(">", ">")
430 data = data.replace("\n", "\\n")
431 return f"""{' '*indent}<data key="{_escapeAttribute(self._key._id)}">{data}</data>\n"""
433 @notimplemented("A data item is always written in one line by Tag().")
434 def OpeningTag(self, indent: int = 2) -> str:
435 """
436 A data item holds a value only, so :meth:`Tag` writes it in one line.
438 :param indent: Optional, indentation level of the XML element.
439 """
441 @notimplemented("A data item is always written in one line by Tag().")
442 def ClosingTag(self, indent: int = 2) -> str:
443 """
444 A data item holds a value only, so :meth:`Tag` writes it in one line.
446 :param indent: Optional, indentation level of the XML element.
447 """
449 def ToStringLines(self, indent: int = 2) -> list[str]:
450 """
451 Render this data item as a list of XML lines.
453 :param indent: Optional, indentation level of the XML element.
454 :returns: List of XML lines describing this data item and everything attached to it.
455 """
456 return [self.Tag(indent)]
459@export
460class Node(BaseWithData):
461 """A node (vertex) of a GraphML graph."""
463 def __init__(self, identifier: str) -> None:
464 """
465 Initialize a node.
467 :param identifier: Unique ID of the node within the GraphML document.
468 """
469 super().__init__(identifier)
471 @readonly
472 def HasClosingTag(self) -> bool:
473 """
474 Check if this XML element is written with a separate closing tag.
476 :returns: ``True``, if the node carries data elements, otherwise ``False``.
477 """
478 return len(self._data) > 0
480 def Tag(self, indent: int = 2) -> str:
481 """
482 Return this node as a self-closing XML tag.
484 :param indent: Optional, indentation level of the XML element.
485 :returns: The XML tag, indented and terminated by a newline.
486 """
487 return f"""{' '*indent}<node id="{_escapeAttribute(self._id)}" />\n"""
489 def OpeningTag(self, indent: int = 2) -> str:
490 """
491 Return the opening XML tag of this node.
493 :param indent: Optional, indentation level of the XML element.
494 :returns: The opening XML tag, indented and terminated by a newline.
495 """
496 return f"""{' '*indent}<node id="{_escapeAttribute(self._id)}">\n"""
498 def ClosingTag(self, indent: int = 2) -> str:
499 """
500 Return the closing XML tag of this node.
502 :param indent: Optional, indentation level of the XML element.
503 :returns: The closing XML tag, indented and terminated by a newline.
504 """
505 return f"""{' ' * indent}</node>\n"""
507 def ToStringLines(self, indent: int = 2) -> list[str]:
508 """
509 Render this node as a list of XML lines.
511 :param indent: Optional, indentation level of the XML element.
512 :returns: List of XML lines describing this node and everything attached to it.
513 """
514 if not self.HasClosingTag:
515 return [self.Tag(indent)]
517 lines = [self.OpeningTag(indent)]
518 for data in self._data:
519 lines.extend(data.ToStringLines(indent + 1))
520 lines.append(self.ClosingTag(indent))
522 return lines
525@export
526class Edge(BaseWithData):
527 """An edge of a GraphML graph, connecting a source node to a target node."""
528 _source: Node #: Node the edge starts at.
529 _target: Node #: Node the edge ends at.
531 def __init__(self, identifier: Nullable[str], source: Node, target: Node) -> None:
532 """
533 Initialize an edge between two nodes.
535 :param identifier: Unique ID of the edge within the GraphML document.
536 :param source: Node the edge starts at.
537 :param target: Node the edge ends at.
538 """
539 super().__init__(identifier)
541 self._source = source
542 self._target = target
544 @readonly
545 def Source(self) -> Node:
546 """
547 Read-only property to access the edge's source node (:attr:`_source`).
549 :returns: Source node of the edge.
550 """
551 return self._source
553 @readonly
554 def Target(self) -> Node:
555 """
556 Read-only property to access the edge's target node (:attr:`_target`).
558 :returns: Target node of the edge.
559 """
560 return self._target
562 def _IDAttribute(self) -> str:
563 """
564 Return the ``id`` attribute of this edge's tag, or nothing for an edge without an ID - it is optional in GraphML.
566 :returns: The attribute with a leading space, or an empty string.
567 """
568 if self._id is None:
569 return ""
570 else:
571 return f' id="{_escapeAttribute(self._id)}"'
573 @readonly
574 def HasClosingTag(self) -> bool:
575 """
576 Check if this XML element is written with a separate closing tag.
578 :returns: ``True``, if the edge carries data elements, otherwise ``False``.
579 """
580 return len(self._data) > 0
582 def Tag(self, indent: int = 2) -> str:
583 """
584 Return this edge as a self-closing XML tag.
586 :param indent: Optional, indentation level of the XML element.
587 :returns: The XML tag, indented and terminated by a newline.
588 """
589 source = _escapeAttribute(self._source._id)
590 target = _escapeAttribute(self._target._id)
591 return f"""{' ' * indent}<edge{self._IDAttribute()} source="{source}" target="{target}" />\n"""
593 def OpeningTag(self, indent: int = 2) -> str:
594 """
595 Return the opening XML tag of this edge.
597 :param indent: Optional, indentation level of the XML element.
598 :returns: The opening XML tag, indented and terminated by a newline.
599 """
600 source = _escapeAttribute(self._source._id)
601 target = _escapeAttribute(self._target._id)
602 return f"""{' '*indent}<edge{self._IDAttribute()} source="{source}" target="{target}">\n"""
604 def ClosingTag(self, indent: int = 2) -> str:
605 """
606 Return the closing XML tag of this edge.
608 :param indent: Optional, indentation level of the XML element.
609 :returns: The closing XML tag, indented and terminated by a newline.
610 """
611 return f"""{' ' * indent}</edge>\n"""
613 def ToStringLines(self, indent: int = 2) -> list[str]:
614 """
615 Render this edge as a list of XML lines.
617 :param indent: Optional, indentation level of the XML element.
618 :returns: List of XML lines describing this edge and everything attached to it.
619 """
620 if not self.HasClosingTag:
621 return [self.Tag(indent)]
623 lines = [self.OpeningTag(indent)]
624 for data in self._data:
625 lines.extend(data.ToStringLines(indent + 1))
626 lines.append(self.ClosingTag(indent))
628 return lines
631@export
632class BaseGraph(BaseWithData, mixin=True):
633 """
634 Mixin-class for everything that contains nodes, edges and subgraphs - a graph as well as a subgraph.
636 Beside the elements themselves, it carries the document-level settings applied while writing them: the default edge
637 direction, the parsing order, and the ID styles for nodes and edges.
638 """
639 _subgraphs: dict[str, Subgraph] #: Subgraphs of this graph, by ID.
640 _nodes: dict[str, Node] #: Nodes of this graph, by ID.
641 _edges: dict[str, Edge] #: Edges of this graph with an ID, by ID.
642 _edgesWithoutID: list[Edge] #: Edges of this graph without an ID.
643 _edgeDefault: EdgeDefault #: Direction applied to edges that don't specify one.
644 _parseOrder: ParsingOrder #: Order in which nodes and edges may appear in the XML document.
645 _nodeIDStyle: IDStyle #: Whether node IDs are free-form or canonical.
646 _edgeIDStyle: IDStyle #: Whether edge IDs are free-form or canonical.
648 def __init__(self, identifier: Nullable[str] = None) -> None:
649 """
650 Initialize an empty graph with the default document settings.
652 Edges are directed, nodes are written before edges, and both ID styles are free-form until they are changed.
654 :param identifier: Optional, unique ID of the graph within the GraphML document.
655 """
656 super().__init__(identifier)
658 self._subgraphs = {}
659 self._nodes = {}
660 self._edges = {}
661 self._edgesWithoutID = []
662 self._edgeDefault = EdgeDefault.Directed
663 self._parseOrder = ParsingOrder.NodesFirst
664 self._nodeIDStyle = IDStyle.Free
665 self._edgeIDStyle = IDStyle.Free
667 @readonly
668 def Subgraphs(self) -> dict[str, Subgraph]:
669 """
670 Read-only property to access the graph's subgraphs (:attr:`_subgraphs`).
672 :returns: Dictionary of subgraph IDs and subgraphs.
673 """
674 return self._subgraphs
676 @readonly
677 def Nodes(self) -> dict[str, Node]:
678 """
679 Read-only property to access the graph's nodes (:attr:`_nodes`).
681 :returns: Dictionary of node IDs and nodes.
682 """
683 return self._nodes
685 @readonly
686 def Edges(self) -> dict[str, Edge]:
687 """
688 Read-only property to access the graph's edges with an ID (:attr:`_edges`).
690 An edge without an ID is in :attr:`EdgesWithoutID`.
692 :returns: Dictionary of edge IDs and edges.
693 """
694 return self._edges
696 @readonly
697 def EdgesWithoutID(self) -> list[Edge]:
698 """
699 Read-only property to access the graph's edges without an ID (:attr:`_edgesWithoutID`).
701 An edge with an ID is in :attr:`Edges`.
703 :returns: List of edges without an ID, in the order they were added.
704 """
705 return self._edgesWithoutID
707 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
708 """
709 Add a subgraph to this graph, which is a node of this graph as well.
711 :param subgraph: The subgraph to add.
712 :returns: The added subgraph, so it can be used in the calling expression.
713 """
714 self._subgraphs[subgraph._subgraphID] = subgraph
715 self._nodes[subgraph._id] = subgraph
716 return subgraph
718 def GetSubgraph(self, subgraphName: str) -> Subgraph:
719 """
720 Return the subgraph with the given ID.
722 :param subgraphName: ID of the subgraph.
723 :returns: The subgraph with that ID.
724 :raises KeyError: If no subgraph has that ID.
725 """
726 return self._subgraphs[subgraphName]
728 def AddNode(self, node: Node) -> Node:
729 """
730 Add a node to this graph.
732 :param node: The node to add.
733 :returns: The added node, so it can be used in the calling expression.
734 """
735 self._nodes[node._id] = node
736 return node
738 def GetNode(self, nodeName: str) -> Node:
739 """
740 Return the node with the given ID.
742 :param nodeName: ID of the node.
743 :returns: The node with that ID.
744 :raises KeyError: If no node has that ID.
745 """
746 return self._nodes[nodeName]
748 def AddEdge(self, edge: Edge) -> Edge:
749 """
750 Add an edge to this graph.
752 :param edge: The edge to add.
753 :returns: The added edge, so it can be used in the calling expression.
754 """
755 if edge._id is None:
756 self._edgesWithoutID.append(edge)
757 else:
758 self._edges[edge._id] = edge
760 return edge
762 def GetEdge(self, edgeName: str) -> Edge:
763 """
764 Return the edge with the given ID.
766 :param edgeName: ID of the edge.
767 :returns: The edge with that ID.
768 :raises KeyError: If no edge has that ID.
769 """
770 return self._edges[edgeName]
772 @notimplemented("A graph is always written with an opening and a closing tag.")
773 def Tag(self, indent: int = 1) -> str:
774 """
775 A graph is written with its nodes and edges inside, so it is never written as a self-closing tag.
777 :param indent: Optional, indentation level of the XML element.
778 """
780 def OpeningTag(self, indent: int = 1) -> str:
781 """
782 Return the opening XML tag of this graph.
784 Beside the graph's ID, the tag carries the parsing hints a reader needs: the default edge direction, the
785 number of nodes and edges, the parsing order and both ID styles.
787 :param indent: Optional, indentation level of the XML element.
788 :returns: The opening XML tag, indented and terminated by a newline.
789 """
790 return f"""\
791{' '*indent}<graph id="{_escapeAttribute(self._id)}"
792{' '*indent} edgedefault="{self._edgeDefault!s}"
793{' '*indent} parse.nodes="{len(self._nodes)}"
794{' '*indent} parse.edges="{len(self._edgesWithoutID) + len(self._edges)}"
795{' '*indent} parse.order="{self._parseOrder!s}"
796{' '*indent} parse.nodeids="{self._nodeIDStyle!s}"
797{' '*indent} parse.edgeids="{self._edgeIDStyle!s}">
798"""
800 def ClosingTag(self, indent: int = 1) -> str:
801 """
802 Return the closing XML tag of this graph.
804 :param indent: Optional, indentation level of the XML element.
805 :returns: The closing XML tag, indented and terminated by a newline.
806 """
807 return f"{' '*indent}</graph>\n"
809 def ToStringLines(self, indent: int = 1) -> list[str]:
810 """
811 Render this graph as a list of XML lines.
813 :param indent: Optional, indentation level of the XML element.
814 :returns: List of XML lines describing this graph and everything it contains.
815 """
816 lines = [self.OpeningTag(indent)]
817 for node in self._nodes.values():
818 lines.extend(node.ToStringLines(indent + 1))
820 for edge in (*self._edgesWithoutID, *self._edges.values()):
821 lines.extend(edge.ToStringLines(indent + 1))
822 # for data in self._data:
823 # lines.extend(data.ToStringLines(indent + 1))
824 lines.append(self.ClosingTag(indent))
826 return lines
829@export
830class Graph(BaseGraph):
831 """
832 The root graph of a GraphML document.
834 It owns the ID space: every node, edge and subgraph registers itself here, so an ID is used only once per document.
835 """
836 _document: GraphMLDocument #: The GraphML document this graph belongs to.
837 _ids: dict[str, Union[Node, Edge, Subgraph]] #: Every element of this graph by ID, used to keep IDs unique.
839 def __init__(self, document: GraphMLDocument, identifier: str) -> None:
840 """
841 Initialize the root graph of a GraphML document.
843 :param document: The GraphML document this graph belongs to.
844 :param identifier: Unique ID of the graph within the GraphML document.
845 """
846 super().__init__(identifier)
847 self._document = document
848 self._ids = {}
850 def GetByID(self, identifier: str) -> Union[Node, Edge, Subgraph]:
851 """
852 Return the element with the given ID, whichever kind it is.
854 :param identifier: ID of the node, edge or subgraph.
855 :returns: The element registered under that ID.
856 :raises KeyError: If no element has that ID.
857 """
858 return self._ids[identifier]
860 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph:
861 """
862 Add a subgraph to the root graph and register its ID.
864 :param subgraph: The subgraph to add.
865 :returns: The added subgraph, so it can be used in the calling expression.
866 """
867 result = super().AddSubgraph(subgraph)
868 self._ids[subgraph._subgraphID] = subgraph
869 subgraph._root = self
870 return result
872 def AddNode(self, node: Node) -> Node:
873 """
874 Add a node to the root graph and register its ID.
876 :param node: The node to add.
877 :returns: The added node, so it can be used in the calling expression.
878 """
879 result = super().AddNode(node)
880 self._ids[node._id] = node
881 return result
883 def AddEdge(self, edge: Edge) -> Edge:
884 """
885 Add an edge to the root graph and register its ID, if it has one.
887 :param edge: The edge to add.
888 :returns: The added edge, so it can be used in the calling expression.
889 """
890 result = super().AddEdge(edge)
891 if edge._id is not None:
892 self._ids[edge._id] = edge
894 return result
897@export
898class Subgraph(Node, BaseGraph):
899 """
900 A nested graph, which is a node of its parent graph and a graph of its own.
902 It therefore carries two identifiers: the node's ID it is referenced by, and :attr:`_subgraphID` for the graph it
903 contains.
904 """
905 _subgraphID: str #: ID of the subgraph, which is distinct from the node's own ID.
906 _root: Nullable[Graph] #: The graph this subgraph is nested in.
908 def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None:
909 """
910 Initialize a subgraph, which is a node in its parent graph and a graph of its own.
912 :param nodeIdentifier: Unique ID of the node representing the subgraph.
913 :param graphIdentifier: Unique ID of the graph contained in that node.
914 """
915 super().__init__(nodeIdentifier)
916 BaseGraph.__init__(self, nodeIdentifier)
918 self._subgraphID = graphIdentifier
919 self._root = None
921 @readonly
922 def RootGraph(self) -> Graph:
923 """
924 Read-only property to access the graph this subgraph is embedded in (:attr:`_root`).
926 :returns: The root graph.
927 """
928 return self._root
930 @readonly
931 def SubgraphID(self) -> str:
932 """
933 Read-only property to access the subgraph's ID (:attr:`_subgraphID`).
935 :returns: ID of the subgraph.
936 """
937 return self._subgraphID
939 @readonly
940 def HasClosingTag(self) -> bool:
941 """
942 Check if this XML element is written with a separate closing tag.
944 :returns: ``True``, because a subgraph always has a closing tag.
945 """
946 return True
948 def AddNode(self, node: Node) -> Node:
949 """
950 Add a node to this subgraph and register its ID at the root graph.
952 :param node: The node to add.
953 :returns: The added node, so it can be used in the calling expression.
954 """
955 result = super().AddNode(node)
956 self._root._ids[node._id] = node
957 return result
959 def AddEdge(self, edge: Edge) -> Edge:
960 """
961 Add an edge to this subgraph and register its ID at the root graph, if it has one.
963 :param edge: The edge to add.
964 :returns: The added edge, so it can be used in the calling expression.
965 """
966 result = super().AddEdge(edge)
967 if edge._id is not None:
968 self._root._ids[edge._id] = edge
970 return result
972 @notimplemented("A subgraph is always written with an opening and a closing tag.")
973 def Tag(self, indent: int = 2) -> str:
974 """
975 A subgraph always contains a graph, so it is never written as a self-closing tag.
977 :param indent: Optional, indentation level of the XML element.
978 """
980 def OpeningTag(self, indent: int = 1) -> str:
981 """
982 Return the opening XML tag of this subgraph.
984 :param indent: Optional, indentation level of the XML element.
985 :returns: The opening XML tag, indented and terminated by a newline.
986 """
987 return f"""\
988{' ' * indent}<graph id="{_escapeAttribute(self._subgraphID)}"
989{' ' * indent} edgedefault="{self._edgeDefault!s}"
990{' ' * indent} parse.nodes="{len(self._nodes)}"
991{' ' * indent} parse.edges="{len(self._edgesWithoutID) + len(self._edges)}"
992{' ' * indent} parse.order="{self._parseOrder!s}"
993{' ' * indent} parse.nodeids="{self._nodeIDStyle!s}"
994{' ' * indent} parse.edgeids="{self._edgeIDStyle!s}">
995"""
997 def ClosingTag(self, indent: int = 2) -> str:
998 """
999 Return the closing XML tag of this subgraph.
1001 :param indent: Optional, indentation level of the XML element.
1002 :returns: The closing XML tag, indented and terminated by a newline.
1003 """
1004 return BaseGraph.ClosingTag(self, indent)
1006 def ToStringLines(self, indent: int = 2) -> list[str]:
1007 """
1008 Render this subgraph as a list of XML lines.
1010 :param indent: Optional, indentation level of the XML element.
1011 :returns: List of XML lines describing this subgraph and everything it contains.
1012 """
1013 lines = [super().OpeningTag(indent)]
1014 for data in self._data: 1014 ↛ 1015line 1014 didn't jump to line 1015 because the loop on line 1014 never started
1015 lines.extend(data.ToStringLines(indent + 1))
1016 # lines.extend(Graph.ToStringLines(self, indent + 1))
1017 lines.append(self.OpeningTag(indent + 1))
1018 for node in self._nodes.values():
1019 lines.extend(node.ToStringLines(indent + 2))
1021 for edge in (*self._edgesWithoutID, *self._edges.values()):
1022 lines.extend(edge.ToStringLines(indent + 2))
1023 # for data in self._data:
1024 # lines.extend(data.ToStringLines(indent + 1))
1025 lines.append(self.ClosingTag(indent + 1))
1026 lines.append(super().ClosingTag(indent))
1028 return lines
1031@export
1032class GraphMLDocument(Base):
1033 """
1034 A GraphML document: the root graph, the keys it declares, and the XML boilerplate to write it out.
1035 """
1037 _XML_NS: ClassVar[dict[Nullable[str], str]] = {
1038 None: "http://graphml.graphdrawing.org/xmlns",
1039 "xsi": "http://www.w3.org/2001/XMLSchema-instance"
1040 } #: XML namespaces of a GraphML document.
1041 _XSI: ClassVar[dict[str, str]] = {
1042 "schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"
1043 } #: XML schema instance attributes of a GraphML document.
1045 _graph: Graph #: The document's root graph.
1046 _keys: dict[str, Key] #: Keys declared by this document, by ID.
1048 def __init__(self, identifier: str = "G") -> None:
1049 """
1050 Initialize a GraphML document with an empty root graph.
1052 :param identifier: Optional, unique ID of the root graph.
1053 """
1054 super().__init__()
1056 self._graph = Graph(self, identifier)
1057 self._keys = {}
1059 @readonly
1060 def Graph(self) -> BaseGraph:
1061 """
1062 Read-only property to access the document's graph (:attr:`_graph`).
1064 :returns: The graph described by this document.
1065 """
1066 return self._graph
1068 @readonly
1069 def Keys(self) -> dict[str, Key]:
1070 """
1071 Read-only property to access the attribute keys declared in this document (:attr:`_keys`).
1073 :returns: Dictionary of key IDs and keys.
1074 """
1075 return self._keys
1077 def AddKey(self, key: Key) -> Key:
1078 """
1079 Declare an attribute, so data items can refer to it.
1081 :param key: The key to declare.
1082 :returns: The declared key, so it can be used in the calling expression.
1083 """
1084 self._keys[key._id] = key
1085 return key
1087 def GetKey(self, keyName: str) -> Key:
1088 """
1089 Return the declared key with the given ID.
1091 :param keyName: ID of the key.
1092 :returns: The key with that ID.
1093 :raises KeyError: If no key has that ID.
1094 """
1095 return self._keys[keyName]
1097 def ContainsKey(self, keyName: str) -> bool:
1098 """
1099 Check if a key with the given ID was declared.
1101 :param keyName: ID of the key.
1102 :returns: ``True``, if such a key exists.
1103 """
1104 return keyName in self._keys
1106 def FromGraph(self, graph: pyToolingGraph) -> None:
1107 """
1108 Fill this document from a :class:`pyTooling.Graph.Graph`.
1110 Vertices become nodes, edges become edges, and the vertex and edge values are attached as data items,
1111 declared by two keys this method adds. A subgraph becomes a GraphML subgraph. A link connects vertices of two
1112 graphs; it becomes an edge of the root graph.
1114 A node's ID is its vertex' ID. A vertex without an ID gets a generated ID ``vertex<number>``, which no vertex' ID
1115 is, because GraphML requires one. Subgraphs are translated ordered by name, so the generated IDs don't change from
1116 one run to the next. A graph without a name keeps the document's graph ID.
1118 :param graph: The graph to translate into this document.
1119 """
1120 document = self
1121 if graph._name is not None:
1122 self._graph._id = graph._name
1124 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
1125 edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String))
1127 subgraphs = sorted(graph.Subgraphs, key=lambda subgraph: "" if subgraph._name is None else subgraph._name)
1128 vertices = [vertex for subgraph in subgraphs for vertex in subgraph.IterateVertices()]
1129 vertices += graph.IterateVertices()
1130 identifiers = GraphViz.Graph._Identifiers(vertices)
1131 nodes: dict[int, Node] = {}
1133 def getKey(keyID: str, context: AttributeContext, name: str) -> Key:
1134 """
1135 Nested function returning the key with the given ID, which is declared on first use.
1137 :param keyID: ID of the key.
1138 :param context: Kind of element the key's data items belong to.
1139 :param name: Attribute name of the key.
1140 :returns: The key.
1141 """
1142 if document.ContainsKey(keyID):
1143 return document.GetKey(keyID)
1145 return document.AddKey(Key(keyID, context, name, AttributeTypes.String))
1147 def translateEdge(edge: Union[pyToolingEdge, pyToolingLink], prefix: str) -> Edge:
1148 """
1149 Nested function translating an edge or a link, with its value and key-value pairs, into a GraphML edge.
1151 :param edge: The edge or link to translate.
1152 :param prefix: Prefix of the key IDs for its key-value pairs: ``edge`` or ``link``.
1153 :returns: The GraphML edge between the nodes of its source and destination vertex.
1154 """
1155 newEdge = Edge(edge._id, nodes[id(edge._source)], nodes[id(edge._destination)])
1156 if edge._value is not None:
1157 newEdge.AddData(Data(edgeValue, edge._value))
1159 for key, value in edge._dict.items():
1160 newEdge.AddData(Data(getKey(f"{prefix}{key!s}", AttributeContext.Edge, str(key)), value))
1162 return newEdge
1164 def translateGraph(graphMLGraph: BaseGraph, pyTGraph: Union[pyToolingGraph, pyToolingSubgraph]) -> None:
1165 """
1166 Nested function translating the vertices and edges of a graph or subgraph into GraphML nodes and edges.
1168 :param graphMLGraph: The GraphML graph or subgraph the elements are added to.
1169 :param pyTGraph: The pyTooling graph or subgraph to translate.
1170 """
1171 for vertex in pyTGraph.IterateVertices():
1172 newNode = graphMLGraph.AddNode(Node(identifiers[id(vertex)]))
1173 nodes[id(vertex)] = newNode
1174 if vertex._value is not None:
1175 newNode.AddData(Data(nodeValue, vertex._value))
1177 for key, value in vertex._dict.items():
1178 newNode.AddData(Data(getKey(f"node{key!s}", AttributeContext.Node, str(key)), value))
1180 for edge in pyTGraph.IterateEdges():
1181 graphMLGraph.AddEdge(translateEdge(edge, "edge"))
1183 for subgraph in subgraphs:
1184 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name)
1185 self._graph.AddSubgraph(nodeGraph)
1186 translateGraph(nodeGraph, subgraph)
1188 translateGraph(self._graph, graph)
1190 # A link is known to both graphs it connects: collect it from all graphs, and write it once into the root graph.
1191 translatedLinks: set[int] = set()
1192 for pyTGraph in (*subgraphs, graph):
1193 for link in pyTGraph.IterateLinks():
1194 if id(link) not in translatedLinks:
1195 translatedLinks.add(id(link))
1196 self._graph.AddEdge(translateEdge(link, "link"))
1198 def FromTree(self, tree: pyToolingNode) -> None:
1199 """
1200 Fill this document from a :class:`pyTooling.Tree.Node`.
1202 Every node of the tree becomes a GraphML node, and every parent-child relation becomes an edge.
1204 A GraphML node's ID is the tree node's ID. A tree node without an ID gets a generated ID ``vertex<number>``, which
1205 no tree node's ID is, because GraphML requires one. A root without an ID keeps the document's graph ID.
1207 :param tree: The root node of the tree to translate into this document.
1208 """
1209 if tree._id is not None:
1210 self._graph._id = tree._id
1212 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
1214 treeNodes = [tree]
1215 treeNodes += tree.GetDescendants()
1216 identifiers = GraphViz.Graph._Identifiers(treeNodes)
1217 nodes: dict[int, Node] = {}
1219 for i, treeNode in enumerate(treeNodes):
1220 newNode = self._graph.AddNode(Node(identifiers[id(treeNode)]))
1221 nodes[id(treeNode)] = newNode
1222 if treeNode._value is not None:
1223 newNode.AddData(Data(nodeValue, treeNode._value))
1225 if treeNode is not tree:
1226 self._graph.AddEdge(Edge(f"e{i - 1}", newNode, nodes[id(treeNode._parent)]))
1228 @notimplemented("A GraphML document is always written with an opening and a closing tag.")
1229 def Tag(self, indent: int = 0) -> str:
1230 """
1231 A GraphML document always contains keys and a graph, so it is never written as a self-closing tag.
1233 :param indent: Optional, indentation level of the XML element.
1234 """
1236 def OpeningTag(self, indent: int = 0) -> str:
1237 """
1238 Return the opening XML tag of this document.
1240 :param indent: Optional, indentation level of the XML element.
1241 :returns: The opening XML tag, indented and terminated by a newline.
1242 """
1243 return f"""\
1244{' '*indent}<graphml xmlns="{self._XML_NS[None]}"
1245{' '*indent} xmlns:xsi="{self._XML_NS["xsi"]}"
1246{' '*indent} xsi:schemaLocation="{self._XSI["schemaLocation"]}">
1247"""
1249 def ClosingTag(self, indent: int = 0) -> str:
1250 """
1251 Return the closing XML tag of this document.
1253 :param indent: Optional, indentation level of the XML element.
1254 :returns: The closing XML tag, indented and terminated by a newline.
1255 """
1256 return f"{' '*indent}</graphml>\n"
1258 def ToStringLines(self, indent: int = 0) -> list[str]:
1259 """
1260 Render this document as a list of XML lines.
1262 :param indent: Optional, indentation level of the XML element.
1263 :returns: List of XML lines describing this document and everything it contains.
1264 """
1265 lines = [self.OpeningTag(indent)]
1266 for key in self._keys.values():
1267 lines.extend(key.ToStringLines(indent + 1))
1268 lines.extend(self._graph.ToStringLines(indent + 1))
1269 lines.append(self.ClosingTag(indent))
1271 return lines
1273 def WriteToFile(self, file: Path) -> None:
1274 """
1275 Write this document as a GraphML file.
1277 :param file: Path of the file to write.
1278 """
1279 with file.open("w", encoding="utf-8") as f:
1280 f.write("""<?xml version="1.0" encoding="utf-8"?>""")
1281 f.writelines(self.ToStringLines())