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

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. 

33 

34.. seealso:: 

35 

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 

40 

41from enum import Enum, auto 

42from pathlib import Path 

43from typing import Any, ClassVar, Union, Optional as Nullable 

44 

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 

49 

50 

51@export 

52class AttributeContext(Enum): 

53 """ 

54 Enumeration of all attribute contexts. 

55 

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() 

63 

64 def __str__(self) -> str: 

65 """ 

66 Return the enumeration value's name as it is written in a GraphML document. 

67 

68 :returns: Name of the enumeration value in lower case. 

69 """ 

70 return f"{self.name.lower()}" 

71 

72 

73@export 

74class AttributeTypes(Enum): 

75 """ 

76 Enumeration of all attribute types. 

77 

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() 

86 

87 def __str__(self) -> str: 

88 """ 

89 Return the enumeration value's name as it is written in a GraphML document. 

90 

91 :returns: Name of the enumeration value in lower case. 

92 """ 

93 return f"{self.name.lower()}" 

94 

95 

96@export 

97class EdgeDefault(Enum): 

98 """An enumeration describing the default edge direction.""" 

99 Undirected = auto() 

100 Directed = auto() 

101 

102 def __str__(self) -> str: 

103 """ 

104 Return the enumeration value's name as it is written in a GraphML document. 

105 

106 :returns: Name of the enumeration value in lower case. 

107 """ 

108 return f"{self.name.lower()}" 

109 

110 

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() 

117 

118 def __str__(self) -> str: 

119 """ 

120 Return the enumeration value's name as it is written in a GraphML document. 

121 

122 :returns: Name of the enumeration value in lower case. 

123 """ 

124 return f"{self.name.lower()}" 

125 

126 

127@export 

128class IDStyle(Enum): 

129 """An enumeration describing the style of identifiers (IDs).""" 

130 Canonical = auto() 

131 Free = auto() 

132 

133 def __str__(self) -> str: 

134 """ 

135 Return the enumeration value's name as it is written in a GraphML document. 

136 

137 :returns: Name of the enumeration value in lower case. 

138 """ 

139 return f"{self.name.lower()}" 

140 

141 

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. 

151 

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

153 """ 

154 return True 

155 

156 def Tag(self, indent: int = 0) -> str: 

157 """ 

158 Return this element as a self-closing XML tag. 

159 

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() 

165 

166 def OpeningTag(self, indent: int = 0) -> str: 

167 """ 

168 Return the opening XML tag of this element. 

169 

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() 

175 

176 def ClosingTag(self, indent: int = 0) -> str: 

177 """ 

178 Return the closing XML tag of this element. 

179 

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() 

185 

186 def ToStringLines(self, indent: int = 0) -> list[str]: 

187 """ 

188 Render this element as a list of XML lines. 

189 

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() 

195 

196 

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. 

201 

202 def __init__(self, identifier: str) -> None: 

203 """ 

204 Initialize a GraphML element with its unique ID. 

205 

206 :param identifier: Optional, unique ID of the element within the GraphML document. 

207 """ 

208 super().__init__() 

209 self._id = identifier 

210 

211 @readonly 

212 def ID(self) -> str: 

213 """ 

214 Read-only property to access the element's unique ID (:attr:`_id`). 

215 

216 :returns: Unique ID of the element. 

217 """ 

218 return self._id 

219 

220 

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. 

225 

226 def __init__(self, identifier: str) -> None: 

227 """ 

228 Initialize a GraphML element with its unique ID and an empty list of data items. 

229 

230 :param identifier: Optional, unique ID of the element within the GraphML document. 

231 """ 

232 super().__init__(identifier) 

233 

234 self._data = [] 

235 

236 @readonly 

237 def Data(self) -> list[Data]: 

238 """ 

239 Read-only property to access the data elements attached to this element (:attr:`_data`). 

240 

241 :returns: List of data elements. 

242 """ 

243 return self._data 

244 

245 def AddData(self, data: Data) -> Data: 

246 """ 

247 Attach a data item (key-value-pair) to this element. 

248 

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 

254 

255 

256@export 

257class Key(BaseWithID): 

258 """ 

259 Declares an attribute that data items can refer to. 

260 

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. 

267 

268 def __init__(self, identifier: str, context: AttributeContext, name: str, type: AttributeTypes) -> None: 

269 """ 

270 Initialize a key declaring an attribute. 

271 

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) 

278 

279 self._context = context 

280 self._attributeName = name 

281 self._attributeType = type 

282 

283 @readonly 

284 def Context(self) -> AttributeContext: 

285 """ 

286 Read-only property to access the context this key applies to (:attr:`_context`). 

287 

288 :returns: The attribute's context (graph, node, edge, ...). 

289 """ 

290 return self._context 

291 

292 @readonly 

293 def AttributeName(self) -> str: 

294 """ 

295 Read-only property to access the name of the described attribute (:attr:`_attributeName`). 

296 

297 :returns: Name of the attribute. 

298 """ 

299 return self._attributeName 

300 

301 @readonly 

302 def AttributeType(self) -> AttributeTypes: 

303 """ 

304 Read-only property to access the type of the described attribute (:attr:`_attributeType`). 

305 

306 :returns: Type of the attribute. 

307 """ 

308 return self._attributeType 

309 

310 @readonly 

311 def HasClosingTag(self) -> bool: 

312 """ 

313 Check if this XML element is written with a separate closing tag. 

314 

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

316 

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

318 """ 

319 return False 

320 

321 def Tag(self, indent: int = 2) -> str: 

322 """ 

323 Return this key as a self-closing XML tag. 

324 

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""" 

329 

330 def ToStringLines(self, indent: int = 2) -> list[str]: 

331 """ 

332 Render this key as a list of XML lines. 

333 

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)] 

338 

339 

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. 

345 

346 def __init__(self, key: Key, data: Any) -> None: 

347 """ 

348 Initialize a data item with the key describing it and its value. 

349 

350 :param key: Key declaring name and type of this attribute. 

351 :param data: Value of this attribute. 

352 """ 

353 super().__init__() 

354 

355 self._key = key 

356 self._data = data 

357 

358 @readonly 

359 def Key(self) -> Key: 

360 """ 

361 Read-only property to access the key describing this data element (:attr:`_key`). 

362 

363 :returns: The key this data element refers to. 

364 """ 

365 return self._key 

366 

367 @readonly 

368 def Data(self) -> Any: 

369 """ 

370 Read-only property to access the data element's value (:attr:`_data`). 

371 

372 :returns: Value of the data element. 

373 """ 

374 return self._data 

375 

376 @readonly 

377 def HasClosingTag(self) -> bool: 

378 """ 

379 Check if this XML element is written with a separate closing tag. 

380 

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

382 """ 

383 return False 

384 

385 def Tag(self, indent: int = 2) -> str: 

386 """ 

387 Return this data item as a self-closing XML tag. 

388 

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("&", "&amp;") 

394 data = data.replace("<", "&lt;") 

395 data = data.replace(">", "&gt;") 

396 data = data.replace("\n", "\\n") 

397 return f"""{' '*indent}<data key="{self._key._id}">{data}</data>\n""" 

398 

399 def ToStringLines(self, indent: int = 2) -> list[str]: 

400 """ 

401 Render this data item as a list of XML lines. 

402 

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)] 

407 

408 

409@export 

410class Node(BaseWithData): 

411 """A node (vertex) of a GraphML graph.""" 

412 

413 def __init__(self, identifier: str) -> None: 

414 """ 

415 Initialize a node. 

416 

417 :param identifier: Optional, unique ID of the node within the GraphML document. 

418 """ 

419 super().__init__(identifier) 

420 

421 @readonly 

422 def HasClosingTag(self) -> bool: 

423 """ 

424 Check if this XML element is written with a separate closing tag. 

425 

426 :returns: ``True``, if the node carries data elements, otherwise ``False``. 

427 """ 

428 return len(self._data) > 0 

429 

430 def Tag(self, indent: int = 2) -> str: 

431 """ 

432 Return this node as a self-closing XML tag. 

433 

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""" 

438 

439 def OpeningTag(self, indent: int = 2) -> str: 

440 """ 

441 Return the opening XML tag of this node. 

442 

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""" 

447 

448 def ClosingTag(self, indent: int = 2) -> str: 

449 """ 

450 Return the closing XML tag of this node. 

451 

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""" 

456 

457 def ToStringLines(self, indent: int = 2) -> list[str]: 

458 """ 

459 Render this node as a list of XML lines. 

460 

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)] 

466 

467 lines = [self.OpeningTag(indent)] 

468 for data in self._data: 

469 lines.extend(data.ToStringLines(indent + 1)) 

470 lines.append(self.ClosingTag(indent)) 

471 

472 return lines 

473 

474 

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. 

480 

481 def __init__(self, identifier: str, source: Node, target: Node) -> None: 

482 """ 

483 Initialize an edge between two nodes. 

484 

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) 

490 

491 self._source = source 

492 self._target = target 

493 

494 @readonly 

495 def Source(self) -> Node: 

496 """ 

497 Read-only property to access the edge's source node (:attr:`_source`). 

498 

499 :returns: Source node of the edge. 

500 """ 

501 return self._source 

502 

503 @readonly 

504 def Target(self) -> Node: 

505 """ 

506 Read-only property to access the edge's target node (:attr:`_target`). 

507 

508 :returns: Target node of the edge. 

509 """ 

510 return self._target 

511 

512 @readonly 

513 def HasClosingTag(self) -> bool: 

514 """ 

515 Check if this XML element is written with a separate closing tag. 

516 

517 :returns: ``True``, if the edge carries data elements, otherwise ``False``. 

518 """ 

519 return len(self._data) > 0 

520 

521 def Tag(self, indent: int = 2) -> str: 

522 """ 

523 Return this edge as a self-closing XML tag. 

524 

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""" 

529 

530 def OpeningTag(self, indent: int = 2) -> str: 

531 """ 

532 Return the opening XML tag of this edge. 

533 

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""" 

538 

539 def ClosingTag(self, indent: int = 2) -> str: 

540 """ 

541 Return the closing XML tag of this edge. 

542 

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""" 

547 

548 def ToStringLines(self, indent: int = 2) -> list[str]: 

549 """ 

550 Render this edge as a list of XML lines. 

551 

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)] 

557 

558 lines = [self.OpeningTag(indent)] 

559 for data in self._data: 

560 lines.extend(data.ToStringLines(indent + 1)) 

561 lines.append(self.ClosingTag(indent)) 

562 

563 return lines 

564 

565 

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. 

570 

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. 

581 

582 def __init__(self, identifier: Nullable[str] = None) -> None: 

583 """ 

584 Initialize an empty graph with the default document settings. 

585 

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

587 

588 :param identifier: Optional, unique ID of the graph within the GraphML document. 

589 """ 

590 super().__init__(identifier) 

591 

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 

599 

600 @readonly 

601 def Subgraphs(self) -> dict[str, Subgraph]: 

602 """ 

603 Read-only property to access the graph's subgraphs (:attr:`_subgraphs`). 

604 

605 :returns: Dictionary of subgraph IDs and subgraphs. 

606 """ 

607 return self._subgraphs 

608 

609 @readonly 

610 def Nodes(self) -> dict[str, Node]: 

611 """ 

612 Read-only property to access the graph's nodes (:attr:`_nodes`). 

613 

614 :returns: Dictionary of node IDs and nodes. 

615 """ 

616 return self._nodes 

617 

618 @readonly 

619 def Edges(self) -> dict[str, Edge]: 

620 """ 

621 Read-only property to access the graph's edges (:attr:`_edges`). 

622 

623 :returns: Dictionary of edge IDs and edges. 

624 """ 

625 return self._edges 

626 

627 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph: 

628 """ 

629 Add a subgraph to this graph, which is a node of this graph as well. 

630 

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 

637 

638 def GetSubgraph(self, subgraphName: str) -> Subgraph: 

639 """ 

640 Return the subgraph with the given ID. 

641 

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] 

647 

648 def AddNode(self, node: Node) -> Node: 

649 """ 

650 Add a node to this graph. 

651 

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 

657 

658 def GetNode(self, nodeName: str) -> Node: 

659 """ 

660 Return the node with the given ID. 

661 

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] 

667 

668 def AddEdge(self, edge: Edge) -> Edge: 

669 """ 

670 Add an edge to this graph. 

671 

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 

677 

678 def GetEdge(self, edgeName: str) -> Edge: 

679 """ 

680 Return the edge with the given ID. 

681 

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] 

687 

688 def OpeningTag(self, indent: int = 1) -> str: 

689 """ 

690 Return the opening XML tag of this graph. 

691 

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. 

694 

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""" 

707 

708 def ClosingTag(self, indent: int = 1) -> str: 

709 """ 

710 Return the closing XML tag of this graph. 

711 

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" 

716 

717 def ToStringLines(self, indent: int = 1) -> list[str]: 

718 """ 

719 Render this graph as a list of XML lines. 

720 

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)) 

732 

733 return lines 

734 

735 

736@export 

737class Graph(BaseGraph): 

738 """ 

739 The root graph of a GraphML document. 

740 

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. 

745 

746 def __init__(self, document: GraphMLDocument, identifier: str) -> None: 

747 """ 

748 Initialize the root graph of a GraphML document. 

749 

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 = {} 

756 

757 def GetByID(self, identifier: str) -> Union[Node, Edge, Subgraph]: 

758 """ 

759 Return the element with the given ID, whichever kind it is. 

760 

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] 

766 

767 def AddSubgraph(self, subgraph: Subgraph) -> Subgraph: 

768 """ 

769 Add a subgraph to the root graph and register its ID. 

770 

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 

778 

779 def AddNode(self, node: Node) -> Node: 

780 """ 

781 Add a node to the root graph and register its ID. 

782 

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 

789 

790 def AddEdge(self, edge: Edge) -> Edge: 

791 """ 

792 Add an edge to the root graph and register its ID. 

793 

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 

800 

801 

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. 

806 

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. 

812 

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. 

816 

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) 

822 

823 self._subgraphID = graphIdentifier 

824 self._root = None 

825 

826 @readonly 

827 def RootGraph(self) -> Graph: 

828 """ 

829 Read-only property to access the graph this subgraph is embedded in (:attr:`_root`). 

830 

831 :returns: The root graph. 

832 """ 

833 return self._root 

834 

835 @readonly 

836 def SubgraphID(self) -> str: 

837 """ 

838 Read-only property to access the subgraph's ID (:attr:`_subgraphID`). 

839 

840 :returns: ID of the subgraph. 

841 """ 

842 return self._subgraphID 

843 

844 @readonly 

845 def HasClosingTag(self) -> bool: 

846 """ 

847 Check if this XML element is written with a separate closing tag. 

848 

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

850 """ 

851 return True 

852 

853 def AddNode(self, node: Node) -> Node: 

854 """ 

855 Add a node to this subgraph and register its ID at the root graph. 

856 

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 

863 

864 def AddEdge(self, edge: Edge) -> Edge: 

865 """ 

866 Add an edge to this subgraph and register its ID at the root graph. 

867 

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 

874 

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. 

879 

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

881 """ 

882 

883 def OpeningTag(self, indent: int = 1) -> str: 

884 """ 

885 Return the opening XML tag of this subgraph. 

886 

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""" 

899 

900 def ClosingTag(self, indent: int = 2) -> str: 

901 """ 

902 Return the closing XML tag of this subgraph. 

903 

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) 

908 

909 def ToStringLines(self, indent: int = 2) -> list[str]: 

910 """ 

911 Render this subgraph as a list of XML lines. 

912 

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)) 

929 

930 return lines 

931 

932 

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 """ 

938 

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. 

946 

947 _graph: Graph #: The document's root graph. 

948 _keys: dict[str, Key] #: Keys declared by this document, by ID. 

949 

950 def __init__(self, identifier: str = "G") -> None: 

951 """ 

952 Initialize a GraphML document with an empty root graph. 

953 

954 :param identifier: Optional, unique ID of the root graph. 

955 """ 

956 super().__init__() 

957 

958 self._graph = Graph(self, identifier) 

959 self._keys = {} 

960 

961 @readonly 

962 def Graph(self) -> BaseGraph: 

963 """ 

964 Read-only property to access the document's graph (:attr:`_graph`). 

965 

966 :returns: The graph described by this document. 

967 """ 

968 return self._graph 

969 

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`). 

974 

975 :returns: Dictionary of key IDs and keys. 

976 """ 

977 return self._keys 

978 

979 def AddKey(self, key: Key) -> Key: 

980 """ 

981 Declare an attribute, so data items can refer to it. 

982 

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 

988 

989 def GetKey(self, keyName: str) -> Key: 

990 """ 

991 Return the declared key with the given ID. 

992 

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] 

998 

999 def HasKey(self, keyName: str) -> bool: 

1000 """ 

1001 Check if a key with the given ID was declared. 

1002 

1003 :param keyName: ID of the key. 

1004 :returns: ``True``, if such a key exists. 

1005 """ 

1006 return keyName in self._keys 

1007 

1008 def FromGraph(self, graph: pyToolingGraph) -> None: 

1009 """ 

1010 Fill this document from a :class:`pyTooling.Graph.Graph`. 

1011 

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. 

1014 

1015 :param graph: The graph to translate into this document. 

1016 """ 

1017 document = self 

1018 self._graph._id = graph._name 

1019 

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

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

1022 

1023 def translateGraph(rootGraph: Graph, pyTGraph: pyToolingGraph): 

1024 """ 

1025 Nested function for recursion. 

1026 

1027 It translates the vertices and edges of one pyTooling graph into GraphML nodes and edges, and recurses into the 

1028 subgraphs it finds. 

1029 

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)) 

1042 

1043 rootGraph.AddNode(newNode) 

1044 

1045 for edge in pyTGraph.IterateEdges(): 

1046 source = rootGraph.GetByID(edge._source._id) 

1047 target = rootGraph.GetByID(edge._destination._id) 

1048 

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)) 

1057 

1058 rootGraph.AddEdge(newEdge) 

1059 

1060 for link in pyTGraph.IterateLinks(): 

1061 source = rootGraph.GetByID(link._source._id) 

1062 target = rootGraph.GetByID(link._destination._id) 

1063 

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)) 

1072 

1073 rootGraph.AddEdge(newEdge) 

1074 

1075 def translateSubgraph(nodeGraph: Subgraph, pyTSubgraph: pyToolingSubgraph): 

1076 """ 

1077 Nested function for recursion. 

1078 

1079 It translates one pyTooling subgraph into a GraphML subgraph. 

1080 

1081 :param nodeGraph: The GraphML subgraph the elements are added to. 

1082 :param pyTSubgraph: The pyTooling subgraph to translate. 

1083 """ 

1084 rootGraph = nodeGraph.RootGraph 

1085 

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)) 

1095 

1096 nodeGraph.AddNode(newNode) 

1097 

1098 for edge in pyTSubgraph.IterateEdges(): 

1099 source = nodeGraph.GetNode(edge._source._id) 

1100 target = nodeGraph.GetNode(edge._destination._id) 

1101 

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)) 

1110 

1111 nodeGraph.AddEdge(newEdge) 

1112 

1113 for subgraph in graph.Subgraphs: 

1114 nodeGraph = Subgraph(subgraph.Name, "sg" + subgraph.Name) 

1115 self._graph.AddSubgraph(nodeGraph) 

1116 translateSubgraph(nodeGraph, subgraph) 

1117 

1118 translateGraph(self._graph, graph) 

1119 

1120 def FromTree(self, tree: pyToolingNode) -> None: 

1121 """ 

1122 Fill this document from a :class:`pyTooling.Tree.Node`. 

1123 

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

1125 

1126 :param tree: The root node of the tree to translate into this document. 

1127 """ 

1128 self._graph._id = tree._id 

1129 

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

1131 

1132 rootNode = self._graph.AddNode(Node(tree._id)) 

1133 rootNode.AddData(Data(nodeValue, tree._value)) 

1134 

1135 for i, node in enumerate(tree.GetDescendants()): 

1136 newNode = self._graph.AddNode(Node(node._id)) 

1137 newNode.AddData(Data(nodeValue, node._value)) 

1138 

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

1140 

1141 def OpeningTag(self, indent: int = 0) -> str: 

1142 """ 

1143 Return the opening XML tag of this document. 

1144 

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""" 

1153 

1154 def ClosingTag(self, indent: int = 0) -> str: 

1155 """ 

1156 Return the closing XML tag of this document. 

1157 

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" 

1162 

1163 def ToStringLines(self, indent: int = 0) -> list[str]: 

1164 """ 

1165 Render this document as a list of XML lines. 

1166 

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)) 

1175 

1176 return lines 

1177 

1178 def WriteToFile(self, file: Path) -> None: 

1179 """ 

1180 Write this document as a GraphML file. 

1181 

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())