Coverage for pyTooling/Graph/GraphML.py: 88%
412 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|_ __ __ _ _ __ | |__ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | _| '__/ _` | '_ \| '_ \ #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |_| | | | (_| | |_) | | | | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____|_| \__,_| .__/|_| |_| #
7# |_| |___/ |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32A data model to write out GraphML XML files.
34.. seealso::
36 * http://graphml.graphdrawing.org/primer/graphml-primer.html
37"""
38from enum import Enum, auto
39from pathlib import Path
40from typing import Any, List, Dict, Union, Optional as Nullable
42try:
43 from pyTooling.Decorators import export, readonly
44 from pyTooling.MetaClasses import ExtendedType
45 from pyTooling.Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph
46 from pyTooling.Tree import Node as pyToolingNode
47except (ImportError, ModuleNotFoundError): # pragma: no cover
48 print("[pyTooling.Graph.GraphML] Could not import from 'pyTooling.*'!")
50 try:
51 from Decorators import export, readonly
52 from MetaClasses import ExtendedType, mixin
53 from Graph import Graph as pyToolingGraph, Subgraph as pyToolingSubgraph
54 from Tree import Node as pyToolingNode
55 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
56 print("[pyTooling.Graph.GraphML] Could not import directly!")
57 raise ex
60@export
61class AttributeContext(Enum):
62 """
63 Enumeration of all attribute contexts.
65 An attribute context describes to what kind of GraphML node an attribute can be applied.
66 """
67 GraphML = auto()
68 Graph = auto()
69 Node = auto()
70 Edge = auto()
71 Port = auto()
73 def __str__(self) -> str:
74 return f"{self.name.lower()}"
77@export
78class AttributeTypes(Enum):
79 """
80 Enumeration of all attribute types.
82 An attribute type describes what datatype can be applied to an attribute.
83 """
84 Boolean = auto()
85 Int = auto()
86 Long = auto()
87 Float = auto()
88 Double = auto()
89 String = auto()
91 def __str__(self) -> str:
92 return f"{self.name.lower()}"
95@export
96class EdgeDefault(Enum):
97 """An enumeration describing the default edge direction."""
98 Undirected = auto()
99 Directed = auto()
101 def __str__(self) -> str:
102 return f"{self.name.lower()}"
105@export
106class ParsingOrder(Enum):
107 """An enumeration describing the parsing order of the graph's representation."""
108 NodesFirst = auto() #: First, all nodes are given, then followed by all edges.
109 AdjacencyList = auto()
110 Free = auto()
112 def __str__(self) -> str:
113 return f"{self.name.lower()}"
116@export
117class IDStyle(Enum):
118 """An enumeration describing the style of identifiers (IDs)."""
119 Canonical = auto()
120 Free = auto()
122 def __str__(self) -> str:
123 return f"{self.name.lower()}"
126@export
127class Base(metaclass=ExtendedType, slots=True):
128 """
129 Base-class for all GraphML data model classes.
130 """
131 @readonly
132 def HasClosingTag(self) -> bool:
133 return True
135 def Tag(self, indent: int = 0) -> str:
136 raise NotImplementedError()
138 def OpeningTag(self, indent: int = 0) -> str:
139 raise NotImplementedError()
141 def ClosingTag(self, indent: int = 0) -> str:
142 raise NotImplementedError()
144 def ToStringLines(self, indent: int = 0) -> List[str]:
145 raise NotImplementedError()
148@export
149class BaseWithID(Base):
150 _id: str
152 def __init__(self, identifier: str) -> None:
153 super().__init__()
154 self._id = identifier
156 @readonly
157 def ID(self) -> str:
158 return self._id
161@export
162class BaseWithData(BaseWithID):
163 _data: List['Data']
165 def __init__(self, identifier: str) -> None:
166 super().__init__(identifier)
168 self._data = []
170 @readonly
171 def Data(self) -> List['Data']:
172 return self._data
174 def AddData(self, data: Data) -> Data:
175 self._data.append(data)
176 return data
179@export
180class Key(BaseWithID):
181 _context: AttributeContext
182 _attributeName: str
183 _attributeType: AttributeTypes
185 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None:
186 super().__init__(identifier)
188 self._context = context
189 self._attributeName = name
190 self._attributeType = type
192 @readonly
193 def Context(self) -> AttributeContext:
194 return self._context
196 @readonly
197 def AttributeName(self) -> str:
198 return self._attributeName
200 @readonly
201 def AttributeType(self) -> AttributeTypes:
202 return self._attributeType
204 @readonly
205 def HasClosingTag(self) -> bool:
206 return False
208 def Tag(self, indent: int = 2) -> str:
209 return f"""{' '*indent}<key id="{self._id}" for="{self._context}" attr.name="{self._attributeName}" attr.type="{self._attributeType}" />\n"""
211 def ToStringLines(self, indent: int = 2) -> List[str]:
212 return [self.Tag(indent)]
215@export
216class Data(Base):
217 _key: Key
218 _data: Any
220 def __init__(self, key: Key, data: Any) -> None:
221 super().__init__()
223 self._key = key
224 self._data = data
226 @readonly
227 def Key(self) -> Key:
228 return self._key
230 @readonly
231 def Data(self) -> Any:
232 return self._data
234 @readonly
235 def HasClosingTag(self) -> bool:
236 return False
238 def Tag(self, indent: int = 2) -> str:
239 data = str(self._data)
240 data = data.replace("&", "&")
241 data = data.replace("<", "<")
242 data = data.replace(">", ">")
243 data = data.replace("\n", "\\n")
244 return f"""{' '*indent}<data key="{self._key._id}">{data}</data>\n"""
246 def ToStringLines(self, indent: int = 2) -> List[str]:
247 return [self.Tag(indent)]
250@export
251class Node(BaseWithData):
252 def __init__(self, identifier: str) -> None:
253 super().__init__(identifier)
255 @readonly
256 def HasClosingTag(self) -> bool:
257 return len(self._data) > 0
259 def Tag(self, indent: int = 2) -> str:
260 return f"""{' '*indent}<node id="{self._id}" />\n"""
262 def OpeningTag(self, indent: int = 2) -> str:
263 return f"""{' '*indent}<node id="{self._id}">\n"""
265 def ClosingTag(self, indent: int = 2) -> str:
266 return f"""{' ' * indent}</node>\n"""
268 def ToStringLines(self, indent: int = 2) -> List[str]:
269 if not self.HasClosingTag:
270 return [self.Tag(indent)]
272 lines = [self.OpeningTag(indent)]
273 for data in self._data:
274 lines.extend(data.ToStringLines(indent + 1))
275 lines.append(self.ClosingTag(indent))
277 return lines
280@export
281class Edge(BaseWithData):
282 _source: Node
283 _target: Node
285 def __init__(self, identifier: str, source: Node, target: Node) -> None:
286 super().__init__(identifier)
288 self._source = source
289 self._target = target
291 @readonly
292 def Source(self) -> Node:
293 return self._source
295 @readonly
296 def Target(self) -> Node:
297 return self._target
299 @readonly
300 def HasClosingTag(self) -> bool:
301 return len(self._data) > 0
303 def Tag(self, indent: int = 2) -> str:
304 return f"""{' ' * indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}" />\n"""
306 def OpeningTag(self, indent: int = 2) -> str:
307 return f"""{' '*indent}<edge id="{self._id}" source="{self._source._id}" target="{self._target._id}">\n"""
309 def ClosingTag(self, indent: int = 2) -> str:
310 return f"""{' ' * indent}</edge>\n"""
312 def ToStringLines(self, indent: int = 2) -> List[str]:
313 if not self.HasClosingTag:
314 return [self.Tag(indent)]
316 lines = [self.OpeningTag(indent)]
317 for data in self._data:
318 lines.extend(data.ToStringLines(indent + 1))
319 lines.append(self.ClosingTag(indent))
321 return lines
324@export
325class BaseGraph(BaseWithData, mixin=True):
326 _subgraphs: Dict[str, 'Subgraph']
327 _nodes: Dict[str, Node]
328 _edges: Dict[str, Edge]
329 _edgeDefault: EdgeDefault
330 _parseOrder: ParsingOrder
331 _nodeIDStyle: IDStyle
332 _edgeIDStyle: IDStyle
334 def __init__(self, identifier: Nullable[str] = None) -> None:
335 super().__init__(identifier)
337 self._subgraphs = {}
338 self._nodes = {}
339 self._edges = {}
340 self._edgeDefault = EdgeDefault.Directed
341 self._parseOrder = ParsingOrder.NodesFirst
342 self._nodeIDStyle = IDStyle.Free
343 self._edgeIDStyle = IDStyle.Free
345 @readonly
346 def Subgraphs(self) -> Dict[str, 'Subgraph']:
347 return self._subgraphs
349 @readonly
350 def Nodes(self) -> Dict[str, Node]:
351 return self._nodes
353 @readonly
354 def Edges(self) -> Dict[str, Edge]:
355 return self._edges
357 def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph':
358 self._subgraphs[subgraph._subgraphID] = subgraph
359 self._nodes[subgraph._id] = subgraph
360 return subgraph
362 def GetSubgraph(self, subgraphName: str) -> 'Subgraph':
363 return self._subgraphs[subgraphName]
365 def AddNode(self, node: Node) -> Node:
366 self._nodes[node._id] = node
367 return node
369 def GetNode(self, nodeName: str) -> Node:
370 return self._nodes[nodeName]
372 def AddEdge(self, edge: Edge) -> Edge:
373 self._edges[edge._id] = edge
374 return edge
376 def GetEdge(self, edgeName: str) -> Edge:
377 return self._edges[edgeName]
379 def OpeningTag(self, indent: int = 1) -> str:
380 return f"""\
381{' '*indent}<graph id="{self._id}"
382{' '*indent} edgedefault="{self._edgeDefault!s}"
383{' '*indent} parse.nodes="{len(self._nodes)}"
384{' '*indent} parse.edges="{len(self._edges)}"
385{' '*indent} parse.order="{self._parseOrder!s}"
386{' '*indent} parse.nodeids="{self._nodeIDStyle!s}"
387{' '*indent} parse.edgeids="{self._edgeIDStyle!s}">
388"""
390 def ClosingTag(self, indent: int = 1) -> str:
391 return f"{' '*indent}</graph>\n"
393 def ToStringLines(self, indent: int = 1) -> List[str]:
394 lines = [self.OpeningTag(indent)]
395 for node in self._nodes.values():
396 lines.extend(node.ToStringLines(indent + 1))
397 for edge in self._edges.values():
398 lines.extend(edge.ToStringLines(indent + 1))
399 # for data in self._data:
400 # lines.extend(data.ToStringLines(indent + 1))
401 lines.append(self.ClosingTag(indent))
403 return lines
406@export
407class Graph(BaseGraph):
408 _document: 'GraphMLDocument'
409 _ids: Dict[str, Union[Node, Edge, 'Subgraph']]
411 def __init__(self, document: 'GraphMLDocument', identifier: str) -> None:
412 super().__init__(identifier)
413 self._document = document
414 self._ids = {}
416 def GetByID(self, identifier: str) -> Union[Node, Edge, 'Subgraph']:
417 return self._ids[identifier]
419 def AddSubgraph(self, subgraph: 'Subgraph') -> 'Subgraph':
420 result = super().AddSubgraph(subgraph)
421 self._ids[subgraph._subgraphID] = subgraph
422 subgraph._root = self
423 return result
425 def AddNode(self, node: Node) -> Node:
426 result = super().AddNode(node)
427 self._ids[node._id] = node
428 return result
430 def AddEdge(self, edge: Edge) -> Edge:
431 result = super().AddEdge(edge)
432 self._ids[edge._id] = edge
433 return result
436@export
437class Subgraph(Node, BaseGraph):
438 _subgraphID: str
439 _root: Nullable[Graph]
441 def __init__(self, nodeIdentifier: str, graphIdentifier: str) -> None:
442 super().__init__(nodeIdentifier)
443 BaseGraph.__init__(self, nodeIdentifier)
445 self._subgraphID = graphIdentifier
446 self._root = None
448 @readonly
449 def RootGraph(self) -> Graph:
450 return self._root
452 @readonly
453 def SubgraphID(self) -> str:
454 return self._subgraphID
456 @readonly
457 def HasClosingTag(self) -> bool:
458 return True
460 def AddNode(self, node: Node) -> Node:
461 result = super().AddNode(node)
462 self._root._ids[node._id] = node
463 return result
465 def AddEdge(self, edge: Edge) -> Edge:
466 result = super().AddEdge(edge)
467 self._root._ids[edge._id] = edge
468 return result
470 def Tag(self, indent: int = 2) -> str:
471 raise NotImplementedError()
473 def OpeningTag(self, indent: int = 1) -> str:
474 return f"""\
475{' ' * indent}<graph id="{self._subgraphID}"
476{' ' * indent} edgedefault="{self._edgeDefault!s}"
477{' ' * indent} parse.nodes="{len(self._nodes)}"
478{' ' * indent} parse.edges="{len(self._edges)}"
479{' ' * indent} parse.order="{self._parseOrder!s}"
480{' ' * indent} parse.nodeids="{self._nodeIDStyle!s}"
481{' ' * indent} parse.edgeids="{self._edgeIDStyle!s}">
482"""
484 def ClosingTag(self, indent: int = 2) -> str:
485 return BaseGraph.ClosingTag(self, indent)
487 def ToStringLines(self, indent: int = 2) -> List[str]:
488 lines = [super().OpeningTag(indent)]
489 for data in self._data: 489 ↛ 490line 489 didn't jump to line 490 because the loop on line 489 never started
490 lines.extend(data.ToStringLines(indent + 1))
491 # lines.extend(Graph.ToStringLines(self, indent + 1))
492 lines.append(self.OpeningTag(indent + 1))
493 for node in self._nodes.values():
494 lines.extend(node.ToStringLines(indent + 2))
495 for edge in self._edges.values():
496 lines.extend(edge.ToStringLines(indent + 2))
497 # for data in self._data:
498 # lines.extend(data.ToStringLines(indent + 1))
499 lines.append(self.ClosingTag(indent + 1))
500 lines.append(super().ClosingTag(indent))
502 return lines
505@export
506class GraphMLDocument(Base):
507 xmlNS = {
508 None: "http://graphml.graphdrawing.org/xmlns",
509 "xsi": "http://www.w3.org/2001/XMLSchema-instance"
510 }
511 xsi = {
512 "schemaLocation": "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd"
513 }
515 _graph: Graph
516 _keys: Dict[str, Key]
518 def __init__(self, identifier: str = "G") -> None:
519 super().__init__()
521 self._graph = Graph(self, identifier)
522 self._keys = {}
524 @readonly
525 def Graph(self) -> BaseGraph:
526 return self._graph
528 @readonly
529 def Keys(self) -> Dict[str, Key]:
530 return self._keys
532 def AddKey(self, key: Key) -> Key:
533 self._keys[key._id] = key
534 return key
536 def GetKey(self, keyName: str) -> Key:
537 return self._keys[keyName]
539 def HasKey(self, keyName: str) -> bool:
540 return keyName in self._keys
542 def FromGraph(self, graph: pyToolingGraph):
543 document = self
544 self._graph._id = graph._name
546 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
547 edgeValue = self.AddKey(Key("edgeValue", AttributeContext.Edge, "value", AttributeTypes.String))
549 def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph):
550 for vertex in pyTGraph.IterateVertices():
551 newNode = Node(vertex._id)
552 newNode.AddData(Data(nodeValue, vertex._value))
553 for key, value in vertex._dict.items(): 553 ↛ 554line 553 didn't jump to line 554 because the loop on line 553 never started
554 if document.HasKey(str(key)):
555 nodeKey = document.GetKey(f"node{key!s}")
556 else:
557 nodeKey = document.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
558 newNode.AddData(Data(nodeKey, value))
560 rootGraph.AddNode(newNode)
562 for edge in pyTGraph.IterateEdges():
563 source = rootGraph.GetByID(edge._source._id)
564 target = rootGraph.GetByID(edge._destination._id)
566 newEdge = Edge(edge._id, source, target)
567 newEdge.AddData(Data(edgeValue, edge._value))
568 for key, value in edge._dict.items(): 568 ↛ 569line 568 didn't jump to line 569 because the loop on line 568 never started
569 if self.HasKey(str(key)):
570 edgeKey = self.GetBy(f"edge{key!s}")
571 else:
572 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
573 newEdge.AddData(Data(edgeKey, value))
575 rootGraph.AddEdge(newEdge)
577 for link in pyTGraph.IterateLinks():
578 source = rootGraph.GetByID(link._source._id)
579 target = rootGraph.GetByID(link._destination._id)
581 newEdge = Edge(link._id, source, target)
582 newEdge.AddData(Data(edgeValue, link._value))
583 for key, value in link._dict.items(): 583 ↛ 584line 583 didn't jump to line 584 because the loop on line 583 never started
584 if self.HasKey(str(key)):
585 edgeKey = self.GetKey(f"link{key!s}")
586 else:
587 edgeKey = self.AddKey(Key(f"link{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
588 newEdge.AddData(Data(edgeKey, value))
590 rootGraph.AddEdge(newEdge)
592 def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph):
593 rootGraph = nodeGraph.RootGraph
595 for vertex in pyTSubgraph.IterateVertices():
596 newNode = Node(vertex._id)
597 newNode.AddData(Data(nodeValue, vertex._value))
598 for key, value in vertex._dict.items(): 598 ↛ 599line 598 didn't jump to line 599 because the loop on line 598 never started
599 if self.HasKey(str(key)):
600 nodeKey = self.GetKey(f"node{key!s}")
601 else:
602 nodeKey = self.AddKey(Key(f"node{key!s}", AttributeContext.Node, str(key), AttributeTypes.String))
603 newNode.AddData(Data(nodeKey, value))
605 nodeGraph.AddNode(newNode)
607 for edge in pyTSubgraph.IterateEdges():
608 source = nodeGraph.GetNode(edge._source._id)
609 target = nodeGraph.GetNode(edge._destination._id)
611 newEdge = Edge(edge._id, source, target)
612 newEdge.AddData(Data(edgeValue, edge._value))
613 for key, value in edge._dict.items(): 613 ↛ 614line 613 didn't jump to line 614 because the loop on line 613 never started
614 if self.HasKey(str(key)):
615 edgeKey = self.GetKey(f"edge{key!s}")
616 else:
617 edgeKey = self.AddKey(Key(f"edge{key!s}", AttributeContext.Edge, str(key), AttributeTypes.String))
618 newEdge.AddData(Data(edgeKey, value))
620 nodeGraph.AddEdge(newEdge)
622 for subgraph in graph.Subgraphs:
623 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name)
624 self._graph.AddSubgraph(nodeGraph)
625 translateSubgraph(nodeGraph, subgraph)
627 translateGraph(self._graph, graph)
629 def FromTree(self, tree: pyToolingNode):
630 self._graph._id = tree._id
632 nodeValue = self.AddKey(Key("nodeValue", AttributeContext.Node, "value", AttributeTypes.String))
634 rootNode = self._graph.AddNode(Node(tree._id))
635 rootNode.AddData(Data(nodeValue, tree._value))
637 for i, node in enumerate(tree.GetDescendants()):
638 newNode = self._graph.AddNode(Node(node._id))
639 newNode.AddData(Data(nodeValue, node._value))
641 newEdge = self._graph.AddEdge(Edge(f"e{i}", newNode, self._graph.GetNode(node._parent._id)))
643 def OpeningTag(self, indent: int = 0) -> str:
644 return f"""\
645{' '*indent}<graphml xmlns="{self.xmlNS[None]}"
646{' '*indent} xmlns:xsi="{self.xmlNS["xsi"]}"
647{' '*indent} xsi:schemaLocation="{self.xsi["schemaLocation"]}">
648"""
650 def ClosingTag(self, indent: int = 0) -> str:
651 return f"{' '*indent}</graphml>\n"
653 def ToStringLines(self, indent: int = 0) -> List[str]:
654 lines = [self.OpeningTag(indent)]
655 for key in self._keys.values():
656 lines.extend(key.ToStringLines(indent + 1))
657 lines.extend(self._graph.ToStringLines(indent + 1))
658 lines.append(self.ClosingTag(indent))
660 return lines
662 def WriteToFile(self, file: Path) -> None:
663 with file.open("w", encoding="utf-8") as f:
664 f.write(f"""<?xml version="1.0" encoding="utf-8"?>""")
665 f.writelines(self.ToStringLines())