Coverage for pyTooling/Graph/__init__.py: 79%

1253 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 powerful **graph** data structure for Python. 

33 

34Graph algorithms using all vertices are provided as methods on the graph instance. Whereas graph algorithms based on a 

35starting vertex are provided as methods on a vertex. 

36 

37.. admonition:: Example Graph 

38 

39 .. mermaid:: 

40 :caption: A directed graph with backward-edges denoted by dotted vertex relations. 

41 

42 %%{init: { "flowchart": { "nodeSpacing": 15, "rankSpacing": 30, "curve": "linear", "useMaxWidth": false } } }%% 

43 graph LR 

44 A(A); B(B); C(C); D(D); E(E); F(F) ; G(G); H(H); I(I) 

45 

46 A --> B --> E 

47 G --> F 

48 A --> C --> G --> H --> D 

49 D -.-> A 

50 D & F -.-> B 

51 I ---> E --> F --> D 

52 

53 classDef node fill:#eee,stroke:#777,font-size:smaller; 

54 

55.. seealso:: 

56 

57 :mod:`pyTooling.Graph.GraphML` 

58 |rarr| Writing a graph as a GraphML document. 

59 :mod:`pyTooling.Tree` 

60 |rarr| A tree, which is a graph without cycles and with a single root. 

61 :mod:`pyTooling.StateMachine` 

62 |rarr| A statemachine, which is a directed graph of states and transitions. 

63""" 

64from __future__ import annotations 

65 

66import heapq 

67from collections import deque 

68from itertools import chain 

69from typing import Any, TypeVar, Generic, Deque, Union, Optional as Nullable 

70from typing import Callable, Iterator as typing_Iterator, Generator, Iterable, Mapping, Hashable 

71 

72from pyTooling.Decorators import export, readonly 

73from pyTooling.MetaClasses import ExtendedType 

74from pyTooling.Exceptions import ToolingException 

75from pyTooling.Common import getFullyQualifiedName 

76from pyTooling.Tree import Node 

77 

78 

79DictKeyType = TypeVar("DictKeyType", bound=Hashable) 

80"""A type variable for dictionary keys.""" 

81 

82DictValueType = TypeVar("DictValueType") 

83"""A type variable for dictionary values.""" 

84 

85IDType = TypeVar("IDType", bound=Hashable) 

86"""A type variable for an ID.""" 

87 

88WeightType = TypeVar("WeightType", bound=Union[int, float]) 

89"""A type variable for a weight.""" 

90 

91ValueType = TypeVar("ValueType") 

92"""A type variable for a value.""" 

93 

94VertexIDType = TypeVar("VertexIDType", bound=Hashable) 

95"""A type variable for a vertex's ID.""" 

96 

97VertexWeightType = TypeVar("VertexWeightType", bound=Union[int, float]) 

98"""A type variable for a vertex's weight.""" 

99 

100VertexValueType = TypeVar("VertexValueType") 

101"""A type variable for a vertex's value.""" 

102 

103VertexDictKeyType = TypeVar("VertexDictKeyType", bound=Hashable) 

104"""A type variable for a vertex's dictionary keys.""" 

105 

106VertexDictValueType = TypeVar("VertexDictValueType") 

107"""A type variable for a vertex's dictionary values.""" 

108 

109EdgeIDType = TypeVar("EdgeIDType", bound=Hashable) 

110"""A type variable for an edge's ID.""" 

111 

112EdgeWeightType = TypeVar("EdgeWeightType", bound=Union[int, float]) 

113"""A type variable for an edge's weight.""" 

114 

115EdgeValueType = TypeVar("EdgeValueType") 

116"""A type variable for an edge's value.""" 

117 

118EdgeDictKeyType = TypeVar("EdgeDictKeyType", bound=Hashable) 

119"""A type variable for an edge's dictionary keys.""" 

120 

121EdgeDictValueType = TypeVar("EdgeDictValueType") 

122"""A type variable for an edge's dictionary values.""" 

123 

124LinkIDType = TypeVar("LinkIDType", bound=Hashable) 

125"""A type variable for an link's ID.""" 

126 

127LinkWeightType = TypeVar("LinkWeightType", bound=Union[int, float]) 

128"""A type variable for an link's weight.""" 

129 

130LinkValueType = TypeVar("LinkValueType") 

131"""A type variable for an link's value.""" 

132 

133LinkDictKeyType = TypeVar("LinkDictKeyType", bound=Hashable) 

134"""A type variable for an link's dictionary keys.""" 

135 

136LinkDictValueType = TypeVar("LinkDictValueType") 

137"""A type variable for an link's dictionary values.""" 

138 

139ComponentDictKeyType = TypeVar("ComponentDictKeyType", bound=Hashable) 

140"""A type variable for a component's dictionary keys.""" 

141 

142ComponentDictValueType = TypeVar("ComponentDictValueType") 

143"""A type variable for a component's dictionary values.""" 

144 

145SubgraphDictKeyType = TypeVar("SubgraphDictKeyType", bound=Hashable) 

146"""A type variable for a component's dictionary keys.""" 

147 

148SubgraphDictValueType = TypeVar("SubgraphDictValueType") 

149"""A type variable for a component's dictionary values.""" 

150 

151ViewDictKeyType = TypeVar("ViewDictKeyType", bound=Hashable) 

152"""A type variable for a component's dictionary keys.""" 

153 

154ViewDictValueType = TypeVar("ViewDictValueType") 

155"""A type variable for a component's dictionary values.""" 

156 

157GraphDictKeyType = TypeVar("GraphDictKeyType", bound=Hashable) 

158"""A type variable for a graph's dictionary keys.""" 

159 

160GraphDictValueType = TypeVar("GraphDictValueType") 

161"""A type variable for a graph's dictionary values.""" 

162 

163 

164@export 

165class GraphException(ToolingException): 

166 """Base exception of all exceptions raised by :mod:`pyTooling.Graph`.""" 

167 

168 

169@export 

170class InternalError(GraphException): 

171 """ 

172 The exception is raised when a data structure corruption is detected. 

173 

174 .. danger:: 

175 

176 This exception should never be raised. 

177 

178 If so, please create an issue at GitHub so the data structure corruption can be investigated and fixed. |br| 

179 `⇒ Bug Tracker at GitHub <https://github.com/pyTooling/pyTooling/issues>`__ 

180 """ 

181 

182 

183@export 

184class NotInSameGraph(GraphException): 

185 """The exception is raised when creating an edge between two vertices, but these are not in the same graph.""" 

186 

187 

188@export 

189class NotInDifferentSubgraphs(GraphException): 

190 """ 

191 The exception is raised when creating a link between two vertices, but these are in the same subgraph. 

192 

193 A link crosses subgraph boundaries. Two vertices within one subgraph are connected by an edge. 

194 """ 

195 

196 

197@export 

198class DuplicateVertexError(GraphException): 

199 """The exception is raised when the vertex already exists in the graph.""" 

200 

201 _vertexID: Nullable[VertexIDType] #: ID of the vertex that already exists in the graph. 

202 

203 def __init__(self, message: str, /, *, vertexID: Nullable[VertexIDType] = None) -> None: 

204 """ 

205 Initializes the exception with the identifier that is already taken. 

206 

207 :param message: The exception's message. 

208 :param vertexID: Optional, the vertex identifier that already exists. 

209 """ 

210 super().__init__(message) 

211 self._vertexID = vertexID 

212 

213 @readonly 

214 def VertexID(self) -> Nullable[VertexIDType]: 

215 """ 

216 Read-only property to access the identifier that already exists (:attr:`_vertexID`). 

217 

218 :returns: The duplicate vertex identifier, or ``None`` if it wasn't recorded. 

219 """ 

220 return self._vertexID 

221 

222 

223@export 

224class DuplicateEdgeError(GraphException): 

225 """The exception is raised when the edge already exists in the graph.""" 

226 

227 _edgeID: Nullable[EdgeIDType] #: ID of the edge that already exists in the graph. 

228 

229 def __init__(self, message: str, /, *, edgeID: Nullable[EdgeIDType] = None) -> None: 

230 """ 

231 Initializes the exception with the identifier that is already taken. 

232 

233 :param message: The exception's message. 

234 :param edgeID: Optional, the edge identifier that already exists. 

235 """ 

236 super().__init__(message) 

237 self._edgeID = edgeID 

238 

239 @readonly 

240 def EdgeID(self) -> Nullable[EdgeIDType]: 

241 """ 

242 Read-only property to access the identifier that already exists (:attr:`_edgeID`). 

243 

244 :returns: The duplicate edge identifier, or ``None`` if it wasn't recorded. 

245 """ 

246 return self._edgeID 

247 

248 

249@export 

250class DestinationNotReachable(GraphException): 

251 """The exception is raised when a destination vertex is not reachable.""" 

252 

253 

254@export 

255class NotATreeError(GraphException): 

256 """ 

257 The exception is raised when a subgraph is not a tree. 

258 

259 Either the subgraph has a cycle (backward edge) or links between branches (cross-edge). 

260 """ 

261 

262 

263@export 

264class CycleError(GraphException): 

265 """The exception is raised when a not permitted cycle is found.""" 

266 

267 

268@export 

269class Base( 

270 Generic[DictKeyType, DictValueType], 

271 metaclass=ExtendedType, slots=True 

272): 

273 """ 

274 Base-class for all graph elements, adding a dictionary of arbitrary key-value-pairs to them. 

275 

276 Every vertex, edge, link, component, view, subgraph and graph can carry meta information this way. 

277 """ 

278 

279 _dict: dict[DictKeyType, DictValueType] #: A dictionary to store arbitrary key-value-pairs. 

280 

281 def __init__( 

282 self, 

283 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

284 ) -> None: 

285 """ 

286 .. todo:: GRAPH::Base::init Needs documentation. 

287 

288 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

289 :raises TypeError: If parameter 'name' is not of type string. 

290 """ 

291 self._dict = {key: value for key, value in keyValuePairs.items()} if keyValuePairs is not None else {} 

292 

293 def __del__(self) -> None: 

294 """ 

295 .. todo:: GRAPH::Base::del Needs documentation. 

296 

297 """ 

298 try: 

299 del self._dict 

300 except AttributeError: 

301 pass 

302 

303 def Delete(self) -> None: 

304 """ 

305 Remove this element's attached attributes from internal dictionary. 

306 """ 

307 self._dict.clear() 

308 

309 def __getitem__(self, key: DictKeyType) -> DictValueType: 

310 """ 

311 Read a vertex's attached attributes (key-value-pairs) by key. 

312 

313 :param key: The key to look for. 

314 :returns: The value associated to the given key. 

315 """ 

316 return self._dict[key] 

317 

318 def __setitem__(self, key: DictKeyType, value: DictValueType) -> None: 

319 """ 

320 Create or update a vertex's attached attributes (key-value-pairs) by key. 

321 

322 If a key doesn't exist yet, a new key-value-pair is created. 

323 

324 :param key: The key to create or update. 

325 :param value: Optional, the value to associate to the given key. 

326 """ 

327 self._dict[key] = value 

328 

329 def __delitem__(self, key: DictKeyType) -> None: 

330 """ 

331 Remove an entry from vertex's attached attributes (key-value-pairs) by key. 

332 

333 :param key: The key to remove. 

334 :raises KeyError: If key doesn't exist in the vertex's attributes. 

335 """ 

336 del self._dict[key] 

337 

338 def __contains__(self, key: DictKeyType) -> bool: 

339 """ 

340 Checks if the key is an attached attribute (key-value-pairs) on this vertex. 

341 

342 :param key: The key to check. 

343 :returns: ``True``, if the key is an attached attribute. 

344 """ 

345 return key in self._dict 

346 

347 def __len__(self) -> int: 

348 """ 

349 Returns the number of attached attributes (key-value-pairs) on this vertex. 

350 

351 :returns: Number of attached attributes. 

352 """ 

353 return len(self._dict) 

354 

355 

356@export 

357class BaseWithIDValueAndWeight( 

358 Base[DictKeyType, DictValueType], 

359 Generic[IDType, ValueType, WeightType, DictKeyType, DictValueType] 

360): 

361 """ 

362 Base-class for graph elements identified by an ID and carrying a value and a weight - vertices, edges and links. 

363 

364 All three are optional: an element without an ID is still part of the graph, it just can't be looked up by ID. 

365 """ 

366 

367 _id: Nullable[IDType] #: Field storing the object's Identifier. 

368 _value: Nullable[ValueType] #: Field storing the object's value of any type. 

369 _weight: Nullable[WeightType] #: Field storing the object's weight. 

370 

371 def __init__( 

372 self, 

373 identifier: Nullable[IDType] = None, 

374 value: Nullable[ValueType] = None, 

375 weight: Nullable[WeightType] = None, 

376 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

377 ) -> None: 

378 """ 

379 Initialize a graph element with an optional ID, value and weight. 

380 

381 :param identifier: Optional, unique ID. 

382 :param value: Optional, value. 

383 :param weight: Optional, weight. 

384 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

385 :raises TypeError: If parameter 'name' is not of type string. 

386 """ 

387 super().__init__(keyValuePairs) 

388 

389 self._id = identifier 

390 self._value = value 

391 self._weight = weight 

392 

393 @readonly 

394 def ID(self) -> Nullable[IDType]: 

395 """ 

396 Read-only property to access the unique ID (:attr:`_id`). 

397 

398 If no ID was given at creation time, ID returns ``None``. 

399 

400 :returns: Unique ID, if ID was given at creation time, else ``None``. 

401 """ 

402 return self._id 

403 

404 @property 

405 def Value(self) -> ValueType: 

406 """ 

407 Property to get and set the value (:attr:`_value`). 

408 

409 :returns: The value. 

410 """ 

411 return self._value 

412 

413 @Value.setter 

414 def Value(self, value: ValueType) -> None: 

415 self._value = value 

416 

417 @property 

418 def Weight(self) -> Nullable[EdgeWeightType]: 

419 """ 

420 Property to get and set the weight (:attr:`_weight`) of an edge. 

421 

422 :returns: The weight of an edge. 

423 """ 

424 return self._weight 

425 

426 @Weight.setter 

427 def Weight(self, value: Nullable[EdgeWeightType]) -> None: 

428 self._weight = value 

429 

430 

431@export 

432class BaseWithName( 

433 Base[DictKeyType, DictValueType], 

434 Generic[DictKeyType, DictValueType] 

435): 

436 """Base-class for named graph elements like a graph, a subgraph, a view or a component.""" 

437 

438 _name: Nullable[str] #: Field storing the object's name. 

439 

440 def __init__( 

441 self, 

442 name: Nullable[str] = None, 

443 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, 

444 ) -> None: 

445 """ 

446 Initialize a named graph element with an optional name and optional key-value-pairs. 

447 

448 :param name: Optional, name. 

449 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

450 :raises ValueError: If parameter 'graph' is None. 

451 :raises TypeError: If parameter 'graph' is not of type :class:`Graph`. 

452 """ 

453 if name is not None and not isinstance(name, str): 

454 ex = TypeError("Parameter 'name' is not of type 'str'.") 

455 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

456 raise ex 

457 

458 super().__init__(keyValuePairs) 

459 

460 self._name = name 

461 

462 @property 

463 def Name(self) -> Nullable[str]: 

464 """ 

465 Property to access the name (:attr:`_name`). 

466 

467 :returns: The object's name, or ``None`` if it has none. 

468 :raises TypeError: If an assigned value is not of type string. 

469 """ 

470 return self._name 

471 

472 @Name.setter 

473 def Name(self, value: str) -> None: 

474 if not isinstance(value, str): 

475 ex = TypeError("Name is not of type 'str'.") 

476 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

477 raise ex 

478 

479 self._name = value 

480 

481 

482@export 

483class BaseWithVertices( 

484 BaseWithName[DictKeyType, DictValueType], 

485 Generic[ 

486 DictKeyType, DictValueType, 

487 GraphDictKeyType, GraphDictValueType, 

488 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

489 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

490 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

491 ] 

492): 

493 """Base-class for named graph elements owning a set of vertices - a subgraph, a view or a component.""" 

494 

495 _graph: Graph[ 

496 GraphDictKeyType, GraphDictValueType, 

497 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

498 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

499 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

500 ] #: Field storing a reference to the graph. 

501 _vertices: set[Vertex[ 

502 GraphDictKeyType, GraphDictValueType, 

503 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

504 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

505 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

506 ]] #: Field storing a set of vertices. 

507 

508 def __init__( 

509 self, 

510 graph: Graph, 

511 name: Nullable[str] = None, 

512 vertices: Nullable[Iterable[Vertex]] = None, 

513 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

514 ) -> None: 

515 """ 

516 Initialize a named graph element owning a set of vertices, and register it at its graph. 

517 

518 :param graph: Optional, the reference to the graph. 

519 :param name: Optional, name. 

520 :param vertices: Optional, list of vertices. 

521 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

522 :raises ValueError: If parameter 'graph' is None. 

523 :raises TypeError: If parameter 'graph' is not of type :class:`Graph`. 

524 """ 

525 if graph is None: 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true

526 raise ValueError("Parameter 'graph' is None.") 

527 elif not isinstance(graph, Graph): 527 ↛ 528line 527 didn't jump to line 528 because the condition on line 527 was never true

528 ex = TypeError("Parameter 'graph' is not of type 'Graph'.") 

529 ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") 

530 raise ex 

531 

532 super().__init__(name, keyValuePairs) 

533 

534 self._graph = graph 

535 self._vertices = set() if vertices is None else {v for v in vertices} 

536 

537 def __del__(self) -> None: 

538 """ 

539 .. todo:: GRAPH::BaseWithVertices::del Needs documentation. 

540 

541 """ 

542 try: 

543 del self._vertices 

544 except AttributeError: 

545 pass 

546 

547 super().__del__() 

548 

549 @readonly 

550 def Graph(self) -> Graph: 

551 """ 

552 Read-only property to access the graph, this object is associated to (:attr:`_graph`). 

553 

554 :returns: The graph this object is associated to. 

555 """ 

556 return self._graph 

557 

558 @readonly 

559 def Vertices(self) -> set[Vertex]: 

560 """ 

561 Read-only property to access the vertices in this component (:attr:`_vertices`). 

562 

563 :returns: The set of vertices in this component. 

564 """ 

565 return self._vertices 

566 

567 @readonly 

568 def VertexCount(self) -> int: 

569 """ 

570 Read-only property to return the number of vertices referenced by this object. 

571 

572 :returns: The number of vertices this object references. 

573 """ 

574 return len(self._vertices) 

575 

576 

577@export 

578class Vertex( 

579 BaseWithIDValueAndWeight[VertexIDType, VertexValueType, VertexWeightType, VertexDictKeyType, VertexDictValueType], 

580 Generic[ 

581 GraphDictKeyType, GraphDictValueType, 

582 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

583 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

584 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

585 ] 

586): 

587 """ 

588 A **vertex** can have a unique ID, a value and attached meta information as key-value-pairs. A vertex has references 

589 to inbound and outbound edges, thus a graph can be traversed in reverse. 

590 """ 

591 _graph: BaseGraph[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType] #: Field storing a reference to the graph. 

592 _subgraph: Subgraph[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType] #: Field storing a reference to the subgraph. 

593 _component: Component #: Field storing a reference to the component this vertex belongs to. 

594 _views: dict[Hashable, View] #: Field storing the views this vertex is part of, by view name. 

595 _inboundEdges: list[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Field storing a list of inbound edges. 

596 _outboundEdges: list[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Field storing a list of outbound edges. 

597 _inboundLinks: list[Link[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Field storing a list of inbound links. 

598 _outboundLinks: list[Link[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Field storing a list of outbound links. 

599 

600 def __init__( 

601 self, 

602 vertexID: Nullable[VertexIDType] = None, 

603 value: Nullable[VertexValueType] = None, 

604 weight: Nullable[VertexWeightType] = None, 

605 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, 

606 graph: Nullable[Graph] = None, 

607 subgraph: Nullable[Subgraph] = None 

608 ) -> None: 

609 """ 

610 Initialize a vertex and register it at its graph or subgraph. 

611 

612 :param vertexID: Optional, ID for the new vertex. 

613 :param value: Optional, value for the new vertex. 

614 :param weight: Optional, weight for the new vertex. 

615 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

616 :param graph: Optional, reference to the graph. 

617 :param subgraph: Optional, undocumented 

618 :raises TypeError: If parameter 'vertexID' is not of the graph's vertex ID type. 

619 :raises DuplicateVertexError: If the given vertex ID already exists in this graph or subgraph. 

620 """ 

621 if vertexID is not None and not isinstance(vertexID, Hashable): 621 ↛ 622line 621 didn't jump to line 622 because the condition on line 621 was never true

622 ex = TypeError("Parameter 'vertexID' is not of type 'VertexIDType'.") 

623 ex.add_note(f"Got type '{getFullyQualifiedName(vertexID)}'.") 

624 raise ex 

625 

626 super().__init__(vertexID, value, weight, keyValuePairs) 

627 

628 if subgraph is None: 

629 self._graph = graph if graph is not None else Graph() 

630 self._subgraph = None 

631 self._component = Component(self._graph, vertices=(self,)) 

632 

633 if vertexID is None: 

634 self._graph._verticesWithoutID.append(self) 

635 elif vertexID not in self._graph._verticesWithID: 

636 self._graph._verticesWithID[vertexID] = self 

637 else: 

638 raise DuplicateVertexError(f"Vertex ID '{vertexID}' already exists in this graph.", vertexID=vertexID) 

639 else: 

640 self._graph = subgraph._graph 

641 self._subgraph = subgraph 

642 self._component = Component(self._graph, vertices=(self,)) 

643 

644 if vertexID is None: 

645 subgraph._verticesWithoutID.append(self) 

646 elif vertexID not in subgraph._verticesWithID: 646 ↛ 649line 646 didn't jump to line 649 because the condition on line 646 was always true

647 subgraph._verticesWithID[vertexID] = self 

648 else: 

649 raise DuplicateVertexError(f"Vertex ID '{vertexID}' already exists in this subgraph.", vertexID=vertexID) 

650 

651 self._views = {} 

652 self._inboundEdges = [] 

653 self._outboundEdges = [] 

654 self._inboundLinks = [] 

655 self._outboundLinks = [] 

656 

657 def __del__(self) -> None: 

658 """ 

659 .. todo:: GRAPH::BaseEdge::del Needs documentation. 

660 

661 """ 

662 try: 

663 del self._views 

664 del self._inboundEdges 

665 del self._outboundEdges 

666 del self._inboundLinks 

667 del self._outboundLinks 

668 except AttributeError: 

669 pass 

670 

671 super().__del__() 

672 

673 def Delete(self) -> None: 

674 """ 

675 Delete this vertex and every edge and link connected to it. 

676 

677 The vertex is removed from its graph or subgraph, and from its views; every connected edge and link is removed 

678 from its other vertex and unregistered from the graph or subgraph it was registered on. 

679 """ 

680 for edge in self._outboundEdges: 

681 edge._destination._inboundEdges.remove(edge) 

682 edge._Unregister() 

683 edge._Delete() 

684 for edge in self._inboundEdges: 

685 edge._source._outboundEdges.remove(edge) 

686 edge._Unregister() 

687 edge._Delete() 

688 for link in self._outboundLinks: 

689 link._destination._inboundLinks.remove(link) 

690 link._Unregister() 

691 link._Delete() 

692 for link in self._inboundLinks: 692 ↛ 693line 692 didn't jump to line 693 because the loop on line 692 never started

693 link._source._outboundLinks.remove(link) 

694 link._Unregister() 

695 link._Delete() 

696 

697 # Remove from Graph or Subgraph - a vertex is registered on the subgraph it lives in, otherwise on the graph. 

698 container = self._graph if self._subgraph is None else self._subgraph 

699 if self._id is None: 

700 container._verticesWithoutID.remove(self) 

701 else: 

702 del container._verticesWithID[self._id] 

703 

704 # component 

705 

706 # views 

707 self._views.clear() 

708 self._inboundEdges.clear() 

709 self._outboundEdges.clear() 

710 self._inboundLinks.clear() 

711 self._outboundLinks.clear() 

712 

713 super().Delete() 

714 

715 @readonly 

716 def Graph(self) -> Graph: 

717 """ 

718 Read-only property to access the graph, this vertex is associated to (:attr:`_graph`). 

719 

720 :returns: The graph this vertex is associated to. 

721 """ 

722 return self._graph 

723 

724 @readonly 

725 def Component(self) -> Component: 

726 """ 

727 Read-only property to access the component, this vertex is associated to (:attr:`_component`). 

728 

729 :returns: The component this vertex is associated to. 

730 """ 

731 return self._component 

732 

733 @readonly 

734 def InboundEdges(self) -> tuple[Edge, ...]: 

735 """ 

736 Read-only property to get a tuple of inbound edges (:attr:`_inboundEdges`). 

737 

738 :returns: Tuple of inbound edges. 

739 """ 

740 return tuple(self._inboundEdges) 

741 

742 @readonly 

743 def OutboundEdges(self) -> tuple[Edge, ...]: 

744 """ 

745 Read-only property to get a tuple of outbound edges (:attr:`_outboundEdges`). 

746 

747 :returns: Tuple of outbound edges. 

748 """ 

749 return tuple(self._outboundEdges) 

750 

751 @readonly 

752 def InboundLinks(self) -> tuple[Link, ...]: 

753 """ 

754 Read-only property to get a tuple of inbound links (:attr:`_inboundLinks`). 

755 

756 :returns: Tuple of inbound links. 

757 """ 

758 return tuple(self._inboundLinks) 

759 

760 @readonly 

761 def OutboundLinks(self) -> tuple[Link, ...]: 

762 """ 

763 Read-only property to get a tuple of outbound links (:attr:`_outboundLinks`). 

764 

765 :returns: Tuple of outbound links. 

766 """ 

767 return tuple(self._outboundLinks) 

768 

769 @readonly 

770 def EdgeCount(self) -> int: 

771 """ 

772 Read-only property to get the number of all edges (inbound and outbound). 

773 

774 :returns: Number of inbound and outbound edges. 

775 """ 

776 return len(self._inboundEdges) + len(self._outboundEdges) 

777 

778 @readonly 

779 def InboundEdgeCount(self) -> int: 

780 """ 

781 Read-only property to get the number of inbound edges. 

782 

783 :returns: Number of inbound edges. 

784 """ 

785 return len(self._inboundEdges) 

786 

787 @readonly 

788 def OutboundEdgeCount(self) -> int: 

789 """ 

790 Read-only property to get the number of outbound edges. 

791 

792 :returns: Number of outbound edges. 

793 """ 

794 return len(self._outboundEdges) 

795 

796 @readonly 

797 def LinkCount(self) -> int: 

798 """ 

799 Read-only property to get the number of all links (inbound and outbound). 

800 

801 :returns: Number of inbound and outbound links. 

802 """ 

803 return len(self._inboundLinks) + len(self._outboundLinks) 

804 

805 @readonly 

806 def InboundLinkCount(self) -> int: 

807 """ 

808 Read-only property to get the number of inbound links. 

809 

810 :returns: Number of inbound links. 

811 """ 

812 return len(self._inboundLinks) 

813 

814 @readonly 

815 def OutboundLinkCount(self) -> int: 

816 """ 

817 Read-only property to get the number of outbound links. 

818 

819 :returns: Number of outbound links. 

820 """ 

821 return len(self._outboundLinks) 

822 

823 @readonly 

824 def IsRoot(self) -> bool: 

825 """ 

826 Read-only property to check if this vertex is a root vertex in the graph. 

827 

828 A root has no inbound edges (no predecessor vertices). 

829 

830 :returns: ``True``, if this vertex is a root. 

831 

832 .. seealso:: 

833 

834 :meth:`Vertex.IsLeaf <pyTooling.Graph.Vertex.IsLeaf>` 

835 |rarr| Check if a vertex is a leaf vertex in the graph. 

836 :meth:`BaseGraph.IterateRoots <pyTooling.Graph.BaseGraph.IterateRoots>` 

837 |rarr| Iterate all roots of a graph. 

838 :meth:`BaseGraph.IterateLeafs <pyTooling.Graph.BaseGraph.IterateLeafs>` 

839 |rarr| Iterate all leafs of a graph. 

840 """ 

841 return len(self._inboundEdges) == 0 

842 

843 @readonly 

844 def IsLeaf(self) -> bool: 

845 """ 

846 Read-only property to check if this vertex is a leaf vertex in the graph. 

847 

848 A leaf has no outbound edges (no successor vertices). 

849 

850 :returns: ``True``, if this vertex is a leaf. 

851 

852 .. seealso:: 

853 

854 :meth:`Vertex.IsRoot <pyTooling.Graph.Vertex.IsRoot>` 

855 |rarr| Check if a vertex is a root vertex in the graph. 

856 :meth:`BaseGraph.IterateRoots <pyTooling.Graph.BaseGraph.IterateRoots>` 

857 |rarr| Iterate all roots of a graph. 

858 :meth:`BaseGraph.IterateLeafs <pyTooling.Graph.BaseGraph.IterateLeafs>` 

859 |rarr| Iterate all leafs of a graph. 

860 """ 

861 return len(self._outboundEdges) == 0 

862 

863 @readonly 

864 def Predecessors(self) -> tuple[Vertex, ...]: 

865 """ 

866 Read-only property to get a tuple of predecessor vertices. 

867 

868 :returns: Tuple of predecessor vertices. 

869 """ 

870 return tuple([edge.Source for edge in self._inboundEdges]) 

871 

872 @readonly 

873 def Successors(self) -> tuple[Vertex, ...]: 

874 """ 

875 Read-only property to get a tuple of successor vertices. 

876 

877 :returns: Tuple of successor vertices. 

878 """ 

879 return tuple([edge.Destination for edge in self._outboundEdges]) 

880 

881 def EdgeToVertex( 

882 self, 

883 vertex: Vertex, 

884 edgeID: Nullable[EdgeIDType] = None, 

885 edgeWeight: Nullable[EdgeWeightType] = None, 

886 edgeValue: Nullable[VertexValueType] = None, 

887 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

888 ) -> Edge: 

889 """ 

890 Create an outbound edge from this vertex to the referenced vertex. 

891 

892 :param vertex: The vertex to be linked to. 

893 :param edgeID: Optional, the edge's optional ID for the new edge object. 

894 :param edgeWeight: Optional, the edge's optional weight for the new edge object. 

895 :param edgeValue: Optional, the edge's optional value for the new edge object. 

896 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new edge object. 

897 :returns: The edge object linking this vertex and the referenced vertex. 

898 :raises DuplicateEdgeError: If the given edge ID already exists in this graph or subgraph. 

899 :raises NotInSameGraph: If both vertices are not in the same graph or subgraph. |br| 

900 Use :meth:`LinkToVertex` or :meth:`LinkFromVertex` to connect vertices across 

901 subgraph boundaries. 

902 

903 .. seealso:: 

904 

905 :meth:`EdgeFromVertex` 

906 |rarr| Create an inbound edge from the referenced vertex to this vertex. 

907 :meth:`EdgeToNewVertex` 

908 |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. 

909 :meth:`EdgeFromNewVertex` 

910 |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. 

911 :meth:`LinkToVertex` 

912 |rarr| Create an outbound link from this vertex to the referenced vertex. 

913 :meth:`LinkFromVertex` 

914 |rarr| Create an inbound link from the referenced vertex to this vertex. 

915 

916 """ 

917 if self._subgraph is vertex._subgraph: 

918 edge = Edge(self, vertex, edgeID, edgeValue, edgeWeight, keyValuePairs) 

919 

920 self._outboundEdges.append(edge) 

921 vertex._inboundEdges.append(edge) 

922 

923 if self._subgraph is None: 

924 # TODO: move into Edge? 

925 # TODO: keep _graph pointer in edge and then register edge on graph? 

926 if edgeID is None: 

927 self._graph._edgesWithoutID.append(edge) 

928 elif edgeID not in self._graph._edgesWithID: 

929 self._graph._edgesWithID[edgeID] = edge 

930 else: 

931 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

932 else: 

933 # TODO: keep _graph pointer in edge and then register edge on graph? 

934 if edgeID is None: 

935 self._subgraph._edgesWithoutID.append(edge) 

936 elif edgeID not in self._subgraph._edgesWithID: 

937 self._subgraph._edgesWithID[edgeID] = edge 

938 else: 

939 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this subgraph.", edgeID=edgeID) 

940 else: 

941 ex = NotInSameGraph(f"Vertex {self!r} and vertex {vertex!r} are not in the same graph or subgraph.") 

942 ex.add_note(f"An edge can only connect vertices within the same graph or subgraph.") 

943 ex.add_note(f"Use LinkToVertex or LinkFromVertex to connect vertices across subgraph boundaries.") 

944 raise ex 

945 

946 return edge 

947 

948 def EdgeFromVertex( 

949 self, 

950 vertex: Vertex, 

951 edgeID: Nullable[EdgeIDType] = None, 

952 edgeWeight: Nullable[EdgeWeightType] = None, 

953 edgeValue: Nullable[VertexValueType] = None, 

954 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

955 ) -> Edge: 

956 """ 

957 Create an inbound edge from the referenced vertex to this vertex. 

958 

959 :param vertex: The vertex to be linked from. 

960 :param edgeID: Optional, the edge's optional ID for the new edge object. 

961 :param edgeWeight: Optional, the edge's optional weight for the new edge object. 

962 :param edgeValue: Optional, the edge's optional value for the new edge object. 

963 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new edge object. 

964 :returns: The edge object linking the referenced vertex and this vertex. 

965 :raises DuplicateEdgeError: If the given edge ID already exists in this graph or subgraph. 

966 :raises NotInSameGraph: If both vertices are not in the same graph or subgraph. |br| 

967 Use :meth:`LinkToVertex` or :meth:`LinkFromVertex` to connect vertices across 

968 subgraph boundaries. 

969 

970 .. seealso:: 

971 

972 :meth:`EdgeToVertex` 

973 |rarr| Create an outbound edge from this vertex to the referenced vertex. 

974 :meth:`EdgeToNewVertex` 

975 |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. 

976 :meth:`EdgeFromNewVertex` 

977 |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. 

978 :meth:`LinkToVertex` 

979 |rarr| Create an outbound link from this vertex to the referenced vertex. 

980 :meth:`LinkFromVertex` 

981 |rarr| Create an inbound link from the referenced vertex to this vertex. 

982 

983 """ 

984 if self._subgraph is vertex._subgraph: 

985 edge = Edge(vertex, self, edgeID, edgeValue, edgeWeight, keyValuePairs) 

986 

987 vertex._outboundEdges.append(edge) 

988 self._inboundEdges.append(edge) 

989 

990 if self._subgraph is None: 

991 # TODO: move into Edge? 

992 # TODO: keep _graph pointer in edge and then register edge on graph? 

993 if edgeID is None: 

994 self._graph._edgesWithoutID.append(edge) 

995 elif edgeID not in self._graph._edgesWithID: 

996 self._graph._edgesWithID[edgeID] = edge 

997 else: 

998 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

999 else: 

1000 # TODO: keep _graph pointer in edge and then register edge on graph? 

1001 if edgeID is None: 

1002 self._subgraph._edgesWithoutID.append(edge) 

1003 elif edgeID not in self._graph._edgesWithID: 

1004 self._subgraph._edgesWithID[edgeID] = edge 

1005 else: 

1006 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

1007 else: 

1008 ex = NotInSameGraph(f"Vertex {self!r} and vertex {vertex!r} are not in the same graph or subgraph.") 

1009 ex.add_note(f"An edge can only connect vertices within the same graph or subgraph.") 

1010 ex.add_note(f"Use LinkToVertex or LinkFromVertex to connect vertices across subgraph boundaries.") 

1011 raise ex 

1012 

1013 return edge 

1014 

1015 def EdgeToNewVertex( 

1016 self, 

1017 vertexID: Nullable[VertexIDType] = None, 

1018 vertexValue: Nullable[VertexValueType] = None, 

1019 vertexWeight: Nullable[VertexWeightType] = None, 

1020 vertexKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, 

1021 edgeID: Nullable[EdgeIDType] = None, 

1022 edgeWeight: Nullable[EdgeWeightType] = None, 

1023 edgeValue: Nullable[VertexValueType] = None, 

1024 edgeKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

1025 ) -> Edge: 

1026 """ 

1027 Create a new vertex and link that vertex by an outbound edge from this vertex. 

1028 

1029 :param vertexID: Optional, the new vertex' optional ID. 

1030 :param vertexValue: Optional, the new vertex' optional value. 

1031 :param vertexWeight: Optional, the new vertex' optional weight. 

1032 :param vertexKeyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new vertex. 

1033 :param edgeID: Optional, the edge's optional ID for the new edge object. 

1034 :param edgeWeight: Optional, the edge's optional weight for the new edge object. 

1035 :param edgeValue: Optional, the edge's optional value for the new edge object. 

1036 :param edgeKeyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new edge object. 

1037 :returns: The edge object linking this vertex and the created vertex. 

1038 :raises DuplicateEdgeError: If the given edge ID already exists in this graph or subgraph. 

1039 :raises NotInSameGraph: If both vertices are not in the same graph or subgraph. |br| 

1040 Use :meth:`LinkToVertex` or :meth:`LinkFromVertex` to connect vertices across 

1041 subgraph boundaries. 

1042 

1043 .. seealso:: 

1044 

1045 :meth:`EdgeToVertex` 

1046 |rarr| Create an outbound edge from this vertex to the referenced vertex. 

1047 :meth:`EdgeFromVertex` 

1048 |rarr| Create an inbound edge from the referenced vertex to this vertex. 

1049 :meth:`EdgeFromNewVertex` 

1050 |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. 

1051 :meth:`LinkToVertex` 

1052 |rarr| Create an outbound link from this vertex to the referenced vertex. 

1053 :meth:`LinkFromVertex` 

1054 |rarr| Create an inbound link from the referenced vertex to this vertex. 

1055 

1056 """ 

1057 vertex = Vertex(vertexID, vertexValue, vertexWeight, vertexKeyValuePairs, graph=self._graph) # , component=self._component) 

1058 

1059 if self._subgraph is vertex._subgraph: 1059 ↛ 1083line 1059 didn't jump to line 1083 because the condition on line 1059 was always true

1060 edge = Edge(self, vertex, edgeID, edgeValue, edgeWeight, edgeKeyValuePairs) 

1061 

1062 self._outboundEdges.append(edge) 

1063 vertex._inboundEdges.append(edge) 

1064 

1065 if self._subgraph is None: 1065 ↛ 1076line 1065 didn't jump to line 1076 because the condition on line 1065 was always true

1066 # TODO: move into Edge? 

1067 # TODO: keep _graph pointer in edge and then register edge on graph? 

1068 if edgeID is None: 1068 ↛ 1070line 1068 didn't jump to line 1070 because the condition on line 1068 was always true

1069 self._graph._edgesWithoutID.append(edge) 

1070 elif edgeID not in self._graph._edgesWithID: 

1071 self._graph._edgesWithID[edgeID] = edge 

1072 else: 

1073 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

1074 else: 

1075 # TODO: keep _graph pointer in edge and then register edge on graph? 

1076 if edgeID is None: 

1077 self._subgraph._edgesWithoutID.append(edge) 

1078 elif edgeID not in self._graph._edgesWithID: 

1079 self._subgraph._edgesWithID[edgeID] = edge 

1080 else: 

1081 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

1082 else: 

1083 ex = NotInSameGraph(f"Vertex {self!r} and vertex {vertex!r} are not in the same graph or subgraph.") 

1084 ex.add_note(f"An edge can only connect vertices within the same graph or subgraph.") 

1085 ex.add_note(f"Use LinkToVertex or LinkFromVertex to connect vertices across subgraph boundaries.") 

1086 raise ex 

1087 

1088 return edge 

1089 

1090 def EdgeFromNewVertex( 

1091 self, 

1092 vertexID: Nullable[VertexIDType] = None, 

1093 vertexValue: Nullable[VertexValueType] = None, 

1094 vertexWeight: Nullable[VertexWeightType] = None, 

1095 vertexKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, 

1096 edgeID: Nullable[EdgeIDType] = None, 

1097 edgeWeight: Nullable[EdgeWeightType] = None, 

1098 edgeValue: Nullable[VertexValueType] = None, 

1099 edgeKeyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

1100 ) -> Edge: 

1101 """ 

1102 Create a new vertex and link that vertex by an inbound edge to this vertex. 

1103 

1104 :param vertexID: Optional, the new vertex' optional ID. 

1105 :param vertexValue: Optional, the new vertex' optional value. 

1106 :param vertexWeight: Optional, the new vertex' optional weight. 

1107 :param vertexKeyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new vertex. 

1108 :param edgeID: Optional, the edge's optional ID for the new edge object. 

1109 :param edgeWeight: Optional, the edge's optional weight for the new edge object. 

1110 :param edgeValue: Optional, the edge's optional value for the new edge object. 

1111 :param edgeKeyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new edge object. 

1112 :returns: The edge object linking this vertex and the created vertex. 

1113 :raises DuplicateEdgeError: If the given edge ID already exists in this graph or subgraph. 

1114 :raises NotInSameGraph: If both vertices are not in the same graph or subgraph. |br| 

1115 Use :meth:`LinkToVertex` or :meth:`LinkFromVertex` to connect vertices across 

1116 subgraph boundaries. 

1117 

1118 .. seealso:: 

1119 

1120 :meth:`EdgeToVertex` 

1121 |rarr| Create an outbound edge from this vertex to the referenced vertex. 

1122 :meth:`EdgeFromVertex` 

1123 |rarr| Create an inbound edge from the referenced vertex to this vertex. 

1124 :meth:`EdgeToNewVertex` 

1125 |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. 

1126 :meth:`LinkToVertex` 

1127 |rarr| Create an outbound link from this vertex to the referenced vertex. 

1128 :meth:`LinkFromVertex` 

1129 |rarr| Create an inbound link from the referenced vertex to this vertex. 

1130 

1131 """ 

1132 vertex = Vertex(vertexID, vertexValue, vertexWeight, vertexKeyValuePairs, graph=self._graph) # , component=self._component) 

1133 

1134 if self._subgraph is vertex._subgraph: 1134 ↛ 1158line 1134 didn't jump to line 1158 because the condition on line 1134 was always true

1135 edge = Edge(vertex, self, edgeID, edgeValue, edgeWeight, edgeKeyValuePairs) 

1136 

1137 vertex._outboundEdges.append(edge) 

1138 self._inboundEdges.append(edge) 

1139 

1140 if self._subgraph is None: 1140 ↛ 1151line 1140 didn't jump to line 1151 because the condition on line 1140 was always true

1141 # TODO: move into Edge? 

1142 # TODO: keep _graph pointer in edge and then register edge on graph? 

1143 if edgeID is None: 1143 ↛ 1145line 1143 didn't jump to line 1145 because the condition on line 1143 was always true

1144 self._graph._edgesWithoutID.append(edge) 

1145 elif edgeID not in self._graph._edgesWithID: 

1146 self._graph._edgesWithID[edgeID] = edge 

1147 else: 

1148 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

1149 else: 

1150 # TODO: keep _graph pointer in edge and then register edge on graph? 

1151 if edgeID is None: 

1152 self._subgraph._edgesWithoutID.append(edge) 

1153 elif edgeID not in self._graph._edgesWithID: 

1154 self._subgraph._edgesWithID[edgeID] = edge 

1155 else: 

1156 raise DuplicateEdgeError(f"Edge ID '{edgeID}' already exists in this graph.", edgeID=edgeID) 

1157 else: 

1158 ex = NotInSameGraph(f"Vertex {self!r} and vertex {vertex!r} are not in the same graph or subgraph.") 

1159 ex.add_note(f"An edge can only connect vertices within the same graph or subgraph.") 

1160 ex.add_note(f"Use LinkToVertex or LinkFromVertex to connect vertices across subgraph boundaries.") 

1161 raise ex 

1162 

1163 return edge 

1164 

1165 def LinkToVertex( 

1166 self, 

1167 vertex: Vertex, 

1168 linkID: Nullable[EdgeIDType] = None, 

1169 linkWeight: Nullable[EdgeWeightType] = None, 

1170 linkValue: Nullable[VertexValueType] = None, 

1171 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None, 

1172 ) -> Link: 

1173 """ 

1174 Create an outbound link from this vertex to the referenced vertex. 

1175 

1176 :param vertex: The vertex to be linked to. 

1177 :param linkID: Optional, the link's optional ID for the new link object. 

1178 :param linkWeight: Optional, the link's optional weight for the new link object. 

1179 :param linkValue: Optional, the link's optional value for the new link object. 

1180 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new link object. 

1181 :returns: The link object linking this vertex and the referenced vertex. 

1182 :raises DuplicateEdgeError: If the given link ID already exists in this graph. 

1183 :raises NotInDifferentSubgraphs: If both vertices are in the same subgraph - a link connects vertices *across* 

1184 subgraph boundaries. |br| 

1185 Use :meth:`EdgeToVertex` or :meth:`EdgeFromVertex` to connect vertices within 

1186 the same subgraph. 

1187 

1188 .. seealso:: 

1189 

1190 :meth:`EdgeToVertex` 

1191 |rarr| Create an outbound edge from this vertex to the referenced vertex. 

1192 :meth:`EdgeFromVertex` 

1193 |rarr| Create an inbound edge from the referenced vertex to this vertex. 

1194 :meth:`EdgeToNewVertex` 

1195 |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. 

1196 :meth:`EdgeFromNewVertex` 

1197 |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. 

1198 :meth:`LinkFromVertex` 

1199 |rarr| Create an inbound link from the referenced vertex to this vertex. 

1200 

1201 """ 

1202 if self._subgraph is vertex._subgraph: 

1203 ex = NotInDifferentSubgraphs(f"Vertex {self!r} and vertex {vertex!r} are in the same subgraph.") 

1204 ex.add_note(f"A link can only connect vertices across subgraph boundaries.") 

1205 ex.add_note(f"Use EdgeToVertex or EdgeFromVertex to connect vertices within the same subgraph.") 

1206 raise ex 

1207 else: 

1208 link = Link(self, vertex, linkID, linkValue, linkWeight, keyValuePairs) 

1209 

1210 self._outboundLinks.append(link) 

1211 vertex._inboundLinks.append(link) 

1212 

1213 if self._subgraph is None: 

1214 # TODO: move into Edge? 

1215 # TODO: keep _graph pointer in link and then register link on graph? 

1216 if linkID is None: 1216 ↛ 1218line 1216 didn't jump to line 1218 because the condition on line 1216 was always true

1217 self._graph._linksWithoutID.append(link) 

1218 elif linkID not in self._graph._linksWithID: 

1219 self._graph._linksWithID[linkID] = link 

1220 else: 

1221 raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.", edgeID=linkID) 

1222 else: 

1223 # TODO: keep _graph pointer in link and then register link on graph? 

1224 if linkID is None: 

1225 self._subgraph._linksWithoutID.append(link) 

1226 vertex._subgraph._linksWithoutID.append(link) 

1227 elif linkID not in self._graph._linksWithID: 1227 ↛ 1231line 1227 didn't jump to line 1231 because the condition on line 1227 was always true

1228 self._subgraph._linksWithID[linkID] = link 

1229 vertex._subgraph._linksWithID[linkID] = link 

1230 else: 

1231 raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.", edgeID=linkID) 

1232 

1233 return link 

1234 

1235 def LinkFromVertex( 

1236 self, 

1237 vertex: Vertex, 

1238 linkID: Nullable[EdgeIDType] = None, 

1239 linkWeight: Nullable[EdgeWeightType] = None, 

1240 linkValue: Nullable[VertexValueType] = None, 

1241 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

1242 ) -> Edge: 

1243 """ 

1244 Create an inbound link from the referenced vertex to this vertex. 

1245 

1246 :param vertex: The vertex to be linked from. 

1247 :param linkID: Optional, the link's optional ID for the new link object. 

1248 :param linkWeight: Optional, the link's optional weight for the new link object. 

1249 :param linkValue: Optional, the link's optional value for the new link object. 

1250 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs for the new link object. 

1251 :returns: The link object linking the referenced vertex and this vertex. 

1252 :raises DuplicateEdgeError: If the given link ID already exists in this graph. 

1253 :raises NotInDifferentSubgraphs: If both vertices are in the same subgraph - a link connects vertices *across* 

1254 subgraph boundaries. |br| 

1255 Use :meth:`EdgeToVertex` or :meth:`EdgeFromVertex` to connect vertices within 

1256 the same subgraph. 

1257 

1258 .. seealso:: 

1259 

1260 :meth:`EdgeToVertex` 

1261 |rarr| Create an outbound edge from this vertex to the referenced vertex. 

1262 :meth:`EdgeFromVertex` 

1263 |rarr| Create an inbound edge from the referenced vertex to this vertex. 

1264 :meth:`EdgeToNewVertex` 

1265 |rarr| Create a new vertex and link that vertex by an outbound edge from this vertex. 

1266 :meth:`EdgeFromNewVertex` 

1267 |rarr| Create a new vertex and link that vertex by an inbound edge to this vertex. 

1268 :meth:`LinkToVertex` 

1269 |rarr| Create an outbound link from this vertex to the referenced vertex. 

1270 

1271 """ 

1272 if self._subgraph is vertex._subgraph: 

1273 ex = NotInDifferentSubgraphs(f"Vertex {self!r} and vertex {vertex!r} are in the same subgraph.") 

1274 ex.add_note(f"A link can only connect vertices across subgraph boundaries.") 

1275 ex.add_note(f"Use EdgeToVertex or EdgeFromVertex to connect vertices within the same subgraph.") 

1276 raise ex 

1277 else: 

1278 link = Link(vertex, self, linkID, linkValue, linkWeight, keyValuePairs) 

1279 

1280 vertex._outboundLinks.append(link) 

1281 self._inboundLinks.append(link) 

1282 

1283 if self._subgraph is None: 1283 ↛ 1286line 1283 didn't jump to line 1286 because the condition on line 1283 was never true

1284 # TODO: move into Edge? 

1285 # TODO: keep _graph pointer in link and then register link on graph? 

1286 if linkID is None: 

1287 self._graph._linksWithoutID.append(link) 

1288 elif linkID not in self._graph._linksWithID: 

1289 self._graph._linksWithID[linkID] = link 

1290 else: 

1291 raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.", edgeID=linkID) 

1292 else: 

1293 # TODO: keep _graph pointer in link and then register link on graph? 

1294 if linkID is None: 1294 ↛ 1297line 1294 didn't jump to line 1297 because the condition on line 1294 was always true

1295 self._subgraph._linksWithoutID.append(link) 

1296 vertex._subgraph._linksWithoutID.append(link) 

1297 elif linkID not in self._graph._linksWithID: 

1298 self._subgraph._linksWithID[linkID] = link 

1299 vertex._subgraph._linksWithID[linkID] = link 

1300 else: 

1301 raise DuplicateEdgeError(f"Link ID '{linkID}' already exists in this graph.", edgeID=linkID) 

1302 

1303 return link 

1304 

1305 def HasEdgeToDestination(self, destination: Vertex) -> bool: 

1306 """ 

1307 Check if this vertex is linked to another vertex by any outbound edge. 

1308 

1309 :param destination: Destination vertex to check. 

1310 :returns: ``True``, if the destination vertex is a destination on any outbound edge. 

1311 

1312 .. seealso:: 

1313 

1314 :meth:`HasEdgeFromSource` 

1315 |rarr| Check if this vertex is linked to another vertex by any inbound edge. 

1316 :meth:`HasLinkToDestination` 

1317 |rarr| Check if this vertex is linked to another vertex by any outbound link. 

1318 :meth:`HasLinkFromSource` 

1319 |rarr| Check if this vertex is linked to another vertex by any inbound link. 

1320 """ 

1321 for edge in self._outboundEdges: 

1322 if destination is edge.Destination: 1322 ↛ 1321line 1322 didn't jump to line 1321 because the condition on line 1322 was always true

1323 return True 

1324 

1325 return False 

1326 

1327 def HasEdgeFromSource(self, source: Vertex) -> bool: 

1328 """ 

1329 Check if this vertex is linked to another vertex by any inbound edge. 

1330 

1331 :param source: Source vertex to check. 

1332 :returns: ``True``, if the source vertex is a source on any inbound edge. 

1333 

1334 .. seealso:: 

1335 

1336 :meth:`HasEdgeToDestination` 

1337 |rarr| Check if this vertex is linked to another vertex by any outbound edge. 

1338 :meth:`HasLinkToDestination` 

1339 |rarr| Check if this vertex is linked to another vertex by any outbound link. 

1340 :meth:`HasLinkFromSource` 

1341 |rarr| Check if this vertex is linked to another vertex by any inbound link. 

1342 """ 

1343 for edge in self._inboundEdges: 

1344 if source is edge.Source: 1344 ↛ 1343line 1344 didn't jump to line 1343 because the condition on line 1344 was always true

1345 return True 

1346 

1347 return False 

1348 

1349 def HasLinkToDestination(self, destination: Vertex) -> bool: 

1350 """ 

1351 Check if this vertex is linked to another vertex by any outbound link. 

1352 

1353 :param destination: Destination vertex to check. 

1354 :returns: ``True``, if the destination vertex is a destination on any outbound link. 

1355 

1356 .. seealso:: 

1357 

1358 :meth:`HasEdgeToDestination` 

1359 |rarr| Check if this vertex is linked to another vertex by any outbound edge. 

1360 :meth:`HasEdgeFromSource` 

1361 |rarr| Check if this vertex is linked to another vertex by any inbound edge. 

1362 :meth:`HasLinkFromSource` 

1363 |rarr| Check if this vertex is linked to another vertex by any inbound link. 

1364 """ 

1365 for link in self._outboundLinks: 

1366 if destination is link.Destination: 1366 ↛ 1365line 1366 didn't jump to line 1365 because the condition on line 1366 was always true

1367 return True 

1368 

1369 return False 

1370 

1371 def HasLinkFromSource(self, source: Vertex) -> bool: 

1372 """ 

1373 Check if this vertex is linked to another vertex by any inbound link. 

1374 

1375 :param source: Source vertex to check. 

1376 :returns: ``True``, if the source vertex is a source on any inbound link. 

1377 

1378 .. seealso:: 

1379 

1380 :meth:`HasEdgeToDestination` 

1381 |rarr| Check if this vertex is linked to another vertex by any outbound edge. 

1382 :meth:`HasEdgeFromSource` 

1383 |rarr| Check if this vertex is linked to another vertex by any inbound edge. 

1384 :meth:`HasLinkToDestination` 

1385 |rarr| Check if this vertex is linked to another vertex by any outbound link. 

1386 """ 

1387 for link in self._inboundLinks: 

1388 if source is link.Source: 1388 ↛ 1387line 1388 didn't jump to line 1387 because the condition on line 1388 was always true

1389 return True 

1390 

1391 return False 

1392 

1393 def DeleteEdgeTo(self, destination: Vertex) -> None: 

1394 """ 

1395 Delete the outbound edge to the given vertex. 

1396 

1397 :param destination: The vertex the edge points to. 

1398 :raises GraphException: If no outbound edge to that vertex exists. 

1399 """ 

1400 for edge in self._outboundEdges: 1400 ↛ 1404line 1400 didn't jump to line 1404 because the loop on line 1400 didn't complete

1401 if edge._destination is destination: 1401 ↛ 1400line 1401 didn't jump to line 1400 because the condition on line 1401 was always true

1402 break 

1403 else: 

1404 raise GraphException(f"No outbound edge found to '{destination!r}'.") 

1405 

1406 edge.Delete() 

1407 

1408 def DeleteEdgeFrom(self, source: Vertex) -> None: 

1409 """ 

1410 Delete the inbound edge from the given vertex. 

1411 

1412 :param source: The vertex the edge comes from. 

1413 :raises GraphException: If no inbound edge from that vertex exists. 

1414 """ 

1415 for edge in self._inboundEdges: 

1416 if edge._source is source: 

1417 break 

1418 else: 

1419 raise GraphException(f"No inbound edge found to '{source!r}'.") 

1420 

1421 edge.Delete() 

1422 

1423 def DeleteLinkTo(self, destination: Vertex) -> None: 

1424 """ 

1425 Delete the outbound link to the given vertex. 

1426 

1427 :param destination: The vertex the link points to. 

1428 :raises GraphException: If no outbound link to that vertex exists. 

1429 """ 

1430 for link in self._outboundLinks: 

1431 if link._destination is destination: 

1432 break 

1433 else: 

1434 raise GraphException(f"No outbound link found to '{destination!r}'.") 

1435 

1436 link.Delete() 

1437 

1438 def DeleteLinkFrom(self, source: Vertex) -> None: 

1439 """ 

1440 Delete the inbound link from the given vertex. 

1441 

1442 :param source: The vertex the link comes from. 

1443 :raises GraphException: If no inbound link from that vertex exists. 

1444 """ 

1445 for link in self._inboundLinks: 

1446 if link._source is source: 

1447 break 

1448 else: 

1449 raise GraphException(f"No inbound link found to '{source!r}'.") 

1450 

1451 link.Delete() 

1452 

1453 def Copy(self, graph: Graph, copyDict: bool = False, linkingKeyToOriginalVertex: Nullable[str] = None, linkingKeyFromOriginalVertex: Nullable[str] = None) -> Vertex: 

1454 """ 

1455 Creates a copy of this vertex in another graph. 

1456 

1457 Optionally, the vertex's attached attributes (key-value-pairs) can be copied and a linkage between both vertices 

1458 can be established. 

1459 

1460 :param graph: Optional, the graph, the vertex is created in. 

1461 :param copyDict: Optional, if ``True``, copy all attached attributes into the new vertex. 

1462 :param linkingKeyToOriginalVertex: Optional, if not ``None``, add a key-value-pair using this parameter as key 

1463 from new vertex to the original vertex. 

1464 :param linkingKeyFromOriginalVertex: Optional, if not ``None``, add a key-value-pair using this parameter as key 

1465 from original vertex to the new vertex. 

1466 :returns: The newly created vertex. 

1467 :raises GraphException: If source graph and destination graph are the same. 

1468 """ 

1469 if graph is self._graph: 

1470 raise GraphException("Graph to copy this vertex to, is the same graph.") 

1471 

1472 vertex = Vertex(self._id, self._value, self._weight, graph=graph) 

1473 if copyDict: 

1474 vertex._dict = self._dict.copy() 

1475 

1476 if linkingKeyToOriginalVertex is not None: 

1477 vertex._dict[linkingKeyToOriginalVertex] = self 

1478 if linkingKeyFromOriginalVertex is not None: 

1479 self._dict[linkingKeyFromOriginalVertex] = vertex 

1480 

1481 return vertex 

1482 

1483 def IterateOutboundEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Edge, None, None]: 

1484 """ 

1485 Iterate all or selected outbound edges of this vertex. 

1486 

1487 If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. 

1488 

1489 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

1490 :returns: A generator to iterate all outbound edges. 

1491 """ 

1492 if predicate is None: 

1493 for edge in self._outboundEdges: 

1494 yield edge 

1495 else: 

1496 for edge in self._outboundEdges: 

1497 if predicate(edge): 

1498 yield edge 

1499 

1500 def IterateInboundEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Edge, None, None]: 

1501 """ 

1502 Iterate all or selected inbound edges of this vertex. 

1503 

1504 If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. 

1505 

1506 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

1507 :returns: A generator to iterate all inbound edges. 

1508 """ 

1509 if predicate is None: 

1510 for edge in self._inboundEdges: 

1511 yield edge 

1512 else: 

1513 for edge in self._inboundEdges: 

1514 if predicate(edge): 

1515 yield edge 

1516 

1517 def IterateOutboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]: 

1518 """ 

1519 Iterate all or selected outbound links of this vertex. 

1520 

1521 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. 

1522 

1523 :param predicate: Optional, filter function accepting any link and returning a boolean. 

1524 :returns: A generator to iterate all outbound links. 

1525 """ 

1526 if predicate is None: 

1527 for link in self._outboundLinks: 

1528 yield link 

1529 else: 

1530 for link in self._outboundLinks: 

1531 if predicate(link): 

1532 yield link 

1533 

1534 def IterateInboundLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link, None, None]: 

1535 """ 

1536 Iterate all or selected inbound links of this vertex. 

1537 

1538 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. 

1539 

1540 :param predicate: Optional, filter function accepting any link and returning a boolean. 

1541 :returns: A generator to iterate all inbound links. 

1542 """ 

1543 if predicate is None: 

1544 for link in self._inboundLinks: 

1545 yield link 

1546 else: 

1547 for link in self._inboundLinks: 

1548 if predicate(link): 

1549 yield link 

1550 

1551 def IterateSuccessorVertices(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Vertex, None, None]: 

1552 """ 

1553 Iterate all or selected successor vertices of this vertex. 

1554 

1555 If parameter ``predicate`` is not None, the given filter function is used to skip successors in the generator. 

1556 

1557 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

1558 :returns: A generator to iterate all successor vertices. 

1559 """ 

1560 if predicate is None: 

1561 for edge in self._outboundEdges: 

1562 yield edge.Destination 

1563 else: 

1564 for edge in self._outboundEdges: 

1565 if predicate(edge): 

1566 yield edge.Destination 

1567 

1568 def IteratePredecessorVertices(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Vertex, None, None]: 

1569 """ 

1570 Iterate all or selected predecessor vertices of this vertex. 

1571 

1572 If parameter ``predicate`` is not None, the given filter function is used to skip predecessors in the generator. 

1573 

1574 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

1575 :returns: A generator to iterate all predecessor vertices. 

1576 """ 

1577 if predicate is None: 

1578 for edge in self._inboundEdges: 

1579 yield edge.Source 

1580 else: 

1581 for edge in self._inboundEdges: 

1582 if predicate(edge): 

1583 yield edge.Source 

1584 

1585 def IterateVerticesBFS(self) -> Generator[Vertex, None, None]: 

1586 """ 

1587 A generator to iterate all reachable vertices starting from this node in breadth-first search (BFS) order. 

1588 

1589 :returns: A generator to iterate vertices traversed in BFS order. 

1590 

1591 .. seealso:: 

1592 

1593 :meth:`IterateVerticesDFS` 

1594 |rarr| Iterate all reachable vertices **depth-first search** order. 

1595 """ 

1596 visited: set[Vertex] = set() 

1597 queue: Deque[Vertex] = deque() 

1598 

1599 yield self 

1600 visited.add(self) 

1601 for edge in self._outboundEdges: 

1602 nextVertex = edge.Destination 

1603 if nextVertex is not self: 1603 ↛ 1601line 1603 didn't jump to line 1601 because the condition on line 1603 was always true

1604 queue.appendleft(nextVertex) 

1605 visited.add(nextVertex) 

1606 

1607 while queue: 

1608 vertex = queue.pop() 

1609 yield vertex 

1610 for edge in vertex._outboundEdges: 

1611 nextVertex = edge.Destination 

1612 if nextVertex not in visited: 

1613 queue.appendleft(nextVertex) 

1614 visited.add(nextVertex) 

1615 

1616 def IterateVerticesDFS(self) -> Generator[Vertex, None, None]: 

1617 """ 

1618 A generator to iterate all reachable vertices starting from this node in depth-first search (DFS) order. 

1619 

1620 :returns: A generator to iterate vertices traversed in DFS order. 

1621 

1622 .. seealso:: 

1623 

1624 :meth:`IterateVerticesBFS` 

1625 |rarr| Iterate all reachable vertices **breadth-first search** order. 

1626 

1627 Wikipedia - https://en.wikipedia.org/wiki/Depth-first_search 

1628 """ 

1629 visited: set[Vertex] = set() 

1630 stack: list[typing_Iterator[Edge]] = list() 

1631 

1632 yield self 

1633 visited.add(self) 

1634 stack.append(iter(self._outboundEdges)) 

1635 

1636 while True: 

1637 try: 

1638 edge = next(stack[-1]) 

1639 nextVertex = edge._destination 

1640 if nextVertex not in visited: 

1641 visited.add(nextVertex) 

1642 yield nextVertex 

1643 if len(nextVertex._outboundEdges) != 0: 

1644 stack.append(iter(nextVertex._outboundEdges)) 

1645 except StopIteration: 

1646 stack.pop() 

1647 

1648 if len(stack) == 0: 

1649 return 

1650 

1651 def IterateAllOutboundPathsAsVertexList(self) -> Generator[tuple[Vertex, ...], None, None]: 

1652 """ 

1653 Iterate all paths starting at this vertex, each as a tuple of vertices. 

1654 

1655 The traversal is depth-first and keeps the vertices of the current path in a set, so a cycle is detected 

1656 instead of iterated endlessly. A vertex without outbound edges yields the path containing only itself. 

1657 

1658 :returns: A generator yielding one tuple of vertices per path. 

1659 :raises CycleError: If a cycle is detected while walking a path. 

1660 """ 

1661 if len(self._outboundEdges) == 0: 

1662 yield (self, ) 

1663 return 

1664 

1665 visited: set[Vertex] = set() 

1666 vertexStack: list[Vertex] = list() 

1667 iteratorStack: list[typing_Iterator[Edge]] = list() 

1668 

1669 visited.add(self) 

1670 vertexStack.append(self) 

1671 iteratorStack.append(iter(self._outboundEdges)) 

1672 

1673 while True: 

1674 try: 

1675 edge = next(iteratorStack[-1]) 

1676 nextVertex = edge._destination 

1677 if nextVertex in visited: 

1678 ex = CycleError(f"Loop detected.") 

1679 ex.add_note(f"First loop is:") 

1680 for i, vertex in enumerate(vertexStack): 

1681 ex.add_note(f" {i}: {vertex!r}") 

1682 raise ex 

1683 

1684 vertexStack.append(nextVertex) 

1685 if len(nextVertex._outboundEdges) == 0: 

1686 yield tuple(vertexStack) 

1687 vertexStack.pop() 

1688 else: 

1689 iteratorStack.append(iter(nextVertex._outboundEdges)) 

1690 

1691 except StopIteration: 

1692 vertexStack.pop() 

1693 iteratorStack.pop() 

1694 

1695 if len(vertexStack) == 0: 

1696 return 

1697 

1698 def ShortestPathToByHops(self, destination: Vertex) -> Generator[Vertex, None, None]: 

1699 """ 

1700 Compute the shortest path (by hops) between this vertex and the destination vertex. 

1701 

1702 A generator is return to iterate all vertices along the path including source and destination vertex. 

1703 

1704 The search algorithm is breadth-first search (BFS) based. The found solution, if any, is not unique but deterministic 

1705 as long as the graph was not modified (e.g. ordering of edges on vertices). 

1706 

1707 :param destination: The destination vertex to reach. 

1708 :returns: A generator to iterate all vertices on the path found between this vertex and the 

1709 destination vertex. 

1710 :raises DestinationNotReachable: If the destination vertex cannot be reached from this vertex. 

1711 """ 

1712 # Trivial case if start is destination 

1713 if self is destination: 1713 ↛ 1714line 1713 didn't jump to line 1714 because the condition on line 1713 was never true

1714 yield self 

1715 return 

1716 

1717 # Local struct to create multiple linked-lists forming a paths from current node back to the starting point 

1718 # (actually a tree). Each node holds a reference to the vertex it represents. 

1719 # Hint: slotted classes are faster than '@dataclasses.dataclass'. 

1720 class Node(metaclass=ExtendedType, slots=True): 

1721 """A node of the search tree: the vertex it represents and its predecessor on the path.""" 

1722 parent: Node #: Predecessor on the path back to the starting point. 

1723 ref: Vertex #: The vertex this node represents. 

1724 

1725 def __init__(self, parent: Node, ref: Vertex) -> None: 

1726 """ 

1727 Initialize a search tree node. 

1728 

1729 :param parent: Predecessor on the path back to the starting point. 

1730 :param ref: The vertex this node represents. 

1731 """ 

1732 self.parent = parent 

1733 self.ref = ref 

1734 

1735 def __str__(self) -> str: 

1736 """ 

1737 Return a string representation of this search tree node. 

1738 

1739 :returns: The ID of the vertex this node represents. 

1740 """ 

1741 return f"Vertex: {self.ref.ID}" 

1742 

1743 # Initially add all reachable vertices to a queue if vertices to be processed. 

1744 startNode = Node(None, self) 

1745 visited: set[Vertex] = set() 

1746 queue: Deque[Node] = deque() 

1747 

1748 # Add starting vertex and all its children to the processing list. 

1749 # If a child is the destination, break immediately else go into 'else' branch and use BFS algorithm. 

1750 visited.add(self) 

1751 for edge in self._outboundEdges: 

1752 nextVertex = edge.Destination 

1753 if nextVertex is destination: 1753 ↛ 1755line 1753 didn't jump to line 1755 because the condition on line 1753 was never true

1754 # Child is destination, so construct the last node for path traversal and break from loop. 

1755 destinationNode = Node(startNode, nextVertex) 

1756 break 

1757 if nextVertex is not self: 1757 ↛ 1751line 1757 didn't jump to line 1751 because the condition on line 1757 was always true

1758 # Ignore backward-edges and side-edges. 

1759 # Here self-edges, because there is only the starting vertex in the list of visited edges. 

1760 visited.add(nextVertex) 

1761 queue.appendleft(Node(startNode, nextVertex)) 

1762 else: 

1763 # Process queue until destination is found or no further vertices are reachable. 

1764 while queue: 

1765 node = queue.pop() 

1766 for edge in node.ref._outboundEdges: 

1767 nextVertex = edge.Destination 

1768 # Next reachable vertex is destination, so construct the last node for path traversal and break from loop. 

1769 if nextVertex is destination: 

1770 destinationNode = Node(node, nextVertex) 

1771 break 

1772 # Ignore backward-edges and side-edges. 

1773 if nextVertex not in visited: 

1774 visited.add(nextVertex) 

1775 queue.appendleft(Node(node, nextVertex)) 

1776 # Next 3 lines realize a double-break if break was called in inner loop, otherwise continue with outer loop. 

1777 else: 

1778 continue 

1779 break 

1780 else: 

1781 # All reachable vertices have been processed, but destination was not among them. 

1782 raise DestinationNotReachable(f"Destination is not reachable.") 

1783 

1784 # Reverse order of linked list from destinationNode to startNode 

1785 currentNode = destinationNode 

1786 previousNode = destinationNode.parent 

1787 currentNode.parent = None 

1788 while previousNode is not None: 

1789 node = previousNode.parent 

1790 previousNode.parent = currentNode 

1791 currentNode = previousNode 

1792 previousNode = node 

1793 

1794 # Scan reversed linked-list and yield referenced vertices 

1795 yield startNode.ref 

1796 node = startNode.parent 

1797 while node is not None: 

1798 yield node.ref 

1799 node = node.parent 

1800 

1801 def ShortestPathToByWeight(self, destination: Vertex) -> Generator[Vertex, None, None]: 

1802 """ 

1803 Compute the shortest path (by edge weight) between this vertex and the destination vertex. 

1804 

1805 A generator is return to iterate all vertices along the path including source and destination vertex. 

1806 

1807 The search algorithm is based on Dijkstra algorithm and using :mod:`heapq`. The found solution, if any, is not 

1808 unique but deterministic as long as the graph was not modified (e.g. ordering of edges on vertices). 

1809 

1810 :param destination: The destination vertex to reach. 

1811 :returns: A generator to iterate all vertices on the path found between this vertex and the 

1812 destination vertex. 

1813 :raises DestinationNotReachable: If the destination vertex cannot be reached from this vertex. 

1814 """ 

1815 # Improvements: both-sided Dijkstra (search from start and destination to reduce discovered area. 

1816 

1817 # Trivial case if start is destination 

1818 if self is destination: 1818 ↛ 1819line 1818 didn't jump to line 1819 because the condition on line 1818 was never true

1819 yield self 

1820 return 

1821 

1822 # Local struct to create multiple-linked lists forming a paths from current node back to the starting point 

1823 # (actually a tree). Each node holds the overall weight from start to current node and a reference to the vertex it 

1824 # represents. 

1825 # Hint: slotted classes are faster than '@dataclasses.dataclass'. 

1826 class Node(metaclass=ExtendedType, slots=True): 

1827 """A node of the search tree: the vertex, its predecessor, and the accumulated weight to reach it.""" 

1828 parent: Node #: Predecessor on the path back to the starting point. 

1829 distance: EdgeWeightType #: Accumulated edge weight from the starting point to this node. 

1830 ref: Vertex #: The vertex this node represents. 

1831 

1832 def __init__(self, parent: Node, distance: EdgeWeightType, ref: Vertex) -> None: 

1833 """ 

1834 Initialize a search tree node. 

1835 

1836 :param parent: Predecessor on the path back to the starting point. 

1837 :param distance: Accumulated edge weight from the starting point to this node. 

1838 :param ref: The vertex this node represents. 

1839 """ 

1840 self.parent = parent 

1841 self.distance = distance 

1842 self.ref = ref 

1843 

1844 def __lt__(self, other: Any) -> bool: 

1845 """ 

1846 Compare two search tree nodes by their accumulated distance, so they can be kept in a priority queue. 

1847 

1848 :param other: Second operand. 

1849 :returns: ``True``, if this node is closer to the starting point than the second operand. 

1850 """ 

1851 return self.distance < other.distance 

1852 

1853 def __str__(self) -> str: 

1854 """ 

1855 Return a string representation of this search tree node. 

1856 

1857 :returns: The ID of the vertex this node represents. 

1858 """ 

1859 return f"Vertex: {self.ref.ID}" 

1860 

1861 visited: set[Vertex] = set() 

1862 startNode = Node(None, 0, self) 

1863 priorityQueue = [startNode] 

1864 

1865 # Add starting vertex and all its children to the processing list. 

1866 # If a child is the destination, break immediately else go into 'else' branch and use Dijkstra algorithm. 

1867 visited.add(self) 

1868 for edge in self._outboundEdges: 

1869 nextVertex = edge.Destination 

1870 # Child is destination, so construct the last node for path traversal and break from loop. 

1871 if nextVertex is destination: 1871 ↛ 1872line 1871 didn't jump to line 1872 because the condition on line 1871 was never true

1872 destinationNode = Node(startNode, edge._weight, nextVertex) 

1873 break 

1874 # Ignore backward-edges and side-edges. 

1875 # Here self-edges, because there is only the starting vertex in the list of visited edges. 

1876 if nextVertex is not self: 1876 ↛ 1868line 1876 didn't jump to line 1868 because the condition on line 1876 was always true

1877 visited.add(nextVertex) 

1878 heapq.heappush(priorityQueue, Node(startNode, edge._weight, nextVertex)) 

1879 else: 

1880 # Process priority queue until destination is found or no further vertices are reachable. 

1881 while priorityQueue: 1881 ↛ 1899line 1881 didn't jump to line 1899 because the condition on line 1881 was always true

1882 node = heapq.heappop(priorityQueue) 

1883 for edge in node.ref._outboundEdges: 

1884 nextVertex = edge.Destination 

1885 # Next reachable vertex is destination, so construct the last node for path traversal and break from loop. 

1886 if nextVertex is destination: 

1887 destinationNode = Node(node, node.distance + edge._weight, nextVertex) 

1888 break 

1889 # Ignore backward-edges and side-edges. 

1890 if nextVertex not in visited: 

1891 visited.add(nextVertex) 

1892 heapq.heappush(priorityQueue, Node(node, node.distance + edge._weight, nextVertex)) 

1893 # Next 3 lines realize a double-break if break was called in inner loop, otherwise continue with outer loop. 

1894 else: 

1895 continue 

1896 break 

1897 else: 

1898 # All reachable vertices have been processed, but destination was not among them. 

1899 raise DestinationNotReachable(f"Destination is not reachable.") 

1900 

1901 # Reverse order of linked-list from destinationNode to startNode 

1902 currentNode = destinationNode 

1903 previousNode = destinationNode.parent 

1904 currentNode.parent = None 

1905 while previousNode is not None: 

1906 node = previousNode.parent 

1907 previousNode.parent = currentNode 

1908 currentNode = previousNode 

1909 previousNode = node 

1910 

1911 # Scan reversed linked-list and yield referenced vertices 

1912 yield startNode.ref, startNode.distance 

1913 node = startNode.parent 

1914 while node is not None: 

1915 yield node.ref, node.distance 

1916 node = node.parent 

1917 

1918 # Other possible algorithms: 

1919 # * Bellman-Ford 

1920 # * Floyd-Warshall 

1921 

1922 # def PathExistsTo(self, destination: 'Vertex'): 

1923 # raise NotImplementedError() 

1924 # # DFS 

1925 # # Union find 

1926 # 

1927 # def MaximumFlowTo(self, destination: 'Vertex'): 

1928 # raise NotImplementedError() 

1929 # # Ford-Fulkerson algorithm 

1930 # # Edmons-Karp algorithm 

1931 # # Dinic's algorithm 

1932 

1933 def ConvertToTree(self) -> Node: 

1934 """ 

1935 Converts all reachable vertices from this starting vertex to a tree of :class:`~pyTooling.Tree.Node` instances. 

1936 

1937 The tree is traversed using depths-first-search. 

1938 

1939 :returns: Root node of the resulting tree, representing this vertex. 

1940 :raises NotATreeError: If the graph reachable from this vertex is not a tree, because a vertex has more than one 

1941 parent. 

1942 """ 

1943 visited: set[Vertex] = set() 

1944 stack: list[tuple[Node, typing_Iterator[Edge]]] = list() 

1945 

1946 root = Node(nodeID=self._id, value=self._value) 

1947 root._dict = self._dict.copy() 

1948 

1949 visited.add(self) 

1950 stack.append((root, iter(self._outboundEdges))) 

1951 

1952 while True: 

1953 try: 

1954 edge = next(stack[-1][1]) 

1955 nextVertex = edge._destination 

1956 if nextVertex not in visited: 1956 ↛ 1962line 1956 didn't jump to line 1962 because the condition on line 1956 was always true

1957 node = Node(nextVertex._id, nextVertex._value, parent=stack[-1][0]) 

1958 visited.add(nextVertex) 

1959 if len(nextVertex._outboundEdges) != 0: 

1960 stack.append((node, iter(nextVertex._outboundEdges))) 

1961 else: 

1962 raise NotATreeError(f"The directed subgraph is not a tree.") 

1963 # TODO: compute cycle: 

1964 # a) branch 1 is described in stack 

1965 # b) branch 2 can be found by walking from joint to root in the tree 

1966 except StopIteration: 

1967 stack.pop() 

1968 

1969 if len(stack) == 0: 

1970 return root 

1971 

1972 def __repr__(self) -> str: 

1973 """ 

1974 Returns a detailed string representation of the vertex. 

1975 

1976 :returns: The detailed string representation of the vertex. 

1977 """ 

1978 vertexID = value = "" 

1979 sep = ": " 

1980 if self._id is not None: 

1981 vertexID = f"{sep}vertexID='{self._id}'" 

1982 sep = "; " 

1983 if self._value is not None: 1983 ↛ 1984line 1983 didn't jump to line 1984 because the condition on line 1983 was never true

1984 value = f"{sep}value='{self._value}'" 

1985 

1986 return f"<vertex{vertexID}{value}>" 

1987 

1988 def __str__(self) -> str: 

1989 """ 

1990 Return a string representation of the vertex. 

1991 

1992 Order of resolution: 

1993 

1994 1. If :attr:`_value` is not None, return the string representation of :attr:`_value`. 

1995 2. If :attr:`_id` is not None, return the string representation of :attr:`_id`. 

1996 3. Else, return :meth:`__repr__`. 

1997 

1998 :returns: The resolved string representation of the vertex. 

1999 """ 

2000 if self._value is not None: 2000 ↛ 2001line 2000 didn't jump to line 2001 because the condition on line 2000 was never true

2001 return str(self._value) 

2002 elif self._id is not None: 2002 ↛ 2003line 2002 didn't jump to line 2003 because the condition on line 2002 was never true

2003 return str(self._id) 

2004 else: 

2005 return self.__repr__() 

2006 

2007 

2008@export 

2009class BaseEdge( 

2010 BaseWithIDValueAndWeight[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType], 

2011 Generic[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType] 

2012): 

2013 """ 

2014 An **edge** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All edges are 

2015 directed. 

2016 """ 

2017 _source: Vertex #: Vertex the edge starts at. 

2018 _destination: Vertex #: Vertex the edge ends at. 

2019 

2020 def __init__( 

2021 self, 

2022 source: Vertex, 

2023 destination: Vertex, 

2024 edgeID: Nullable[EdgeIDType] = None, 

2025 value: Nullable[EdgeValueType] = None, 

2026 weight: Nullable[EdgeWeightType] = None, 

2027 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2028 ) -> None: 

2029 """ 

2030 Initialize an edge between a source and a destination vertex. 

2031 

2032 :param source: The source of the new edge. 

2033 :param destination: The destination of the new edge. 

2034 :param edgeID: Optional, unique ID for the new edge. 

2035 :param value: Optional, value for the new edge. 

2036 :param weight: Optional, weight for the new edge. 

2037 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2038 """ 

2039 super().__init__(edgeID, value, weight, keyValuePairs) 

2040 

2041 self._source = source 

2042 self._destination = destination 

2043 

2044 component = source._component 

2045 if component is not destination._component: 

2046 # TODO: should it be divided into with/without ID? 

2047 oldComponent = destination._component 

2048 for vertex in oldComponent._vertices: 

2049 vertex._component = component 

2050 component._vertices.add(vertex) 

2051 component._graph._components.remove(oldComponent) 

2052 del oldComponent 

2053 

2054 @readonly 

2055 def Source(self) -> Vertex: 

2056 """ 

2057 Read-only property to get the source (:attr:`_source`) of an edge. 

2058 

2059 :returns: The source of an edge. 

2060 """ 

2061 return self._source 

2062 

2063 @readonly 

2064 def Destination(self) -> Vertex: 

2065 """ 

2066 Read-only property to get the destination (:attr:`_destination`) of an edge. 

2067 

2068 :returns: The destination of an edge. 

2069 """ 

2070 return self._destination 

2071 

2072 def Reverse(self) -> None: 

2073 """Reverse the direction of this edge.""" 

2074 swap = self._source 

2075 self._source = self._destination 

2076 self._destination = swap 

2077 

2078 

2079@export 

2080class Edge( 

2081 BaseEdge[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType], 

2082 Generic[EdgeIDType, EdgeValueType, EdgeWeightType, EdgeDictKeyType, EdgeDictValueType] 

2083): 

2084 """ 

2085 An **edge** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All edges are 

2086 directed. 

2087 """ 

2088 

2089 def __init__( 

2090 self, 

2091 source: Vertex, 

2092 destination: Vertex, 

2093 edgeID: Nullable[EdgeIDType] = None, 

2094 value: Nullable[EdgeValueType] = None, 

2095 weight: Nullable[EdgeWeightType] = None, 

2096 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2097 ) -> None: 

2098 """ 

2099 Initialize an edge between two vertices of the same graph or subgraph. 

2100 

2101 :param source: The source of the new edge. 

2102 :param destination: The destination of the new edge. 

2103 :param edgeID: Optional, unique ID for the new edge. 

2104 :param value: Optional, value for the new edge. 

2105 :param weight: Optional, weight for the new edge. 

2106 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2107 :raises TypeError: If parameter 'weight' is not of the graph's edge weight type. 

2108 :raises NotInSameGraph: If source and destination vertex are not in the same graph or subgraph. 

2109 """ 

2110 if not isinstance(source, Vertex): 

2111 ex = TypeError("Parameter 'source' is not of type 'Vertex'.") 

2112 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") 

2113 raise ex 

2114 if not isinstance(destination, Vertex): 

2115 ex = TypeError("Parameter 'destination' is not of type 'Vertex'.") 

2116 ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.") 

2117 raise ex 

2118 if edgeID is not None and not isinstance(edgeID, Hashable): 

2119 ex = TypeError("Parameter 'edgeID' is not of type 'EdgeIDType'.") 

2120 ex.add_note(f"Got type '{getFullyQualifiedName(edgeID)}'.") 

2121 raise ex 

2122 # if value is not None and not isinstance(value, Vertex): 

2123 # raise TypeError("Parameter 'value' is not of type 'EdgeValueType'.") 

2124 if weight is not None and not isinstance(weight, (int, float)): 

2125 ex = TypeError("Parameter 'weight' is not of type 'EdgeWeightType'.") 

2126 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") 

2127 raise ex 

2128 if source._graph is not destination._graph: 

2129 raise NotInSameGraph(f"Source vertex and destination vertex are not in same graph.") 

2130 

2131 super().__init__(source, destination, edgeID, value, weight, keyValuePairs) 

2132 

2133 def Delete(self) -> None: 

2134 """ 

2135 Delete this edge from both of its vertices and from the graph or subgraph it belongs to. 

2136 """ 

2137 # Remove from Source and Destination 

2138 self._source._outboundEdges.remove(self) 

2139 self._destination._inboundEdges.remove(self) 

2140 

2141 self._Unregister() 

2142 self._Delete() 

2143 

2144 def _Unregister(self) -> None: 

2145 """ 

2146 Remove this edge from the graph or subgraph it was registered on. 

2147 

2148 An edge is registered on the subgraph it lives in, otherwise on the graph. Called by :meth:`Delete` and by 

2149 :meth:`Vertex.Delete`, which unlinks the vertices itself. 

2150 """ 

2151 container = self._source._graph if self._source._subgraph is None else self._source._subgraph 

2152 if self._id is None: 

2153 container._edgesWithoutID.remove(self) 

2154 else: 

2155 del container._edgesWithID[self._id] 

2156 

2157 def _Delete(self) -> None: 

2158 """ 

2159 Delete the edge's attached attributes, after it was disconnected. 

2160 """ 

2161 super().Delete() 

2162 

2163 def Reverse(self) -> None: 

2164 """Reverse the direction of this edge.""" 

2165 self._source._outboundEdges.remove(self) 

2166 self._source._inboundEdges.append(self) 

2167 self._destination._inboundEdges.remove(self) 

2168 self._destination._outboundEdges.append(self) 

2169 

2170 super().Reverse() 

2171 

2172 

2173@export 

2174class Link( 

2175 BaseEdge[LinkIDType, LinkValueType, LinkWeightType, LinkDictKeyType, LinkDictValueType], 

2176 Generic[LinkIDType, LinkValueType, LinkWeightType, LinkDictKeyType, LinkDictValueType] 

2177): 

2178 """ 

2179 A **link** can have a unique ID, a value, a weight and attached meta information as key-value-pairs. All links are 

2180 directed. 

2181 """ 

2182 

2183 def __init__( 

2184 self, 

2185 source: Vertex, 

2186 destination: Vertex, 

2187 linkID: LinkIDType = None, 

2188 value: LinkValueType = None, 

2189 weight: Nullable[LinkWeightType] = None, 

2190 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2191 ) -> None: 

2192 """ 

2193 Initialize a link between two vertices of different subgraphs. 

2194 

2195 :param source: The source of the new link. 

2196 :param destination: The destination of the new link. 

2197 :param linkID: Optional, unique ID for the new link. 

2198 :param value: Optional, value for the new v. 

2199 :param weight: Optional, weight for the new link. 

2200 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2201 :raises TypeError: If parameter 'weight' is not of the graph's link weight type. 

2202 :raises NotInSameGraph: If source and destination vertex are in the same subgraph, where an edge is to be used. 

2203 """ 

2204 if not isinstance(source, Vertex): 

2205 ex = TypeError("Parameter 'source' is not of type 'Vertex'.") 

2206 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") 

2207 raise ex 

2208 if not isinstance(destination, Vertex): 

2209 ex = TypeError("Parameter 'destination' is not of type 'Vertex'.") 

2210 ex.add_note(f"Got type '{getFullyQualifiedName(destination)}'.") 

2211 raise ex 

2212 if linkID is not None and not isinstance(linkID, Hashable): 

2213 ex = TypeError("Parameter 'linkID' is not of type 'LinkIDType'.") 

2214 ex.add_note(f"Got type '{getFullyQualifiedName(linkID)}'.") 

2215 raise ex 

2216 # if value is not None and not isinstance(value, Vertex): 

2217 # raise TypeError("Parameter 'value' is not of type 'EdgeValueType'.") 

2218 if weight is not None and not isinstance(weight, (int, float)): 

2219 ex = TypeError("Parameter 'weight' is not of type 'EdgeWeightType'.") 

2220 ex.add_note(f"Got type '{getFullyQualifiedName(weight)}'.") 

2221 raise ex 

2222 if source._graph is not destination._graph: 

2223 raise NotInSameGraph(f"Source vertex and destination vertex are not in same graph.") 

2224 

2225 super().__init__(source, destination, linkID, value, weight, keyValuePairs) 

2226 

2227 def Delete(self) -> None: 

2228 """ 

2229 Delete this link from both of its vertices and from the graph and subgraphs it belongs to. 

2230 """ 

2231 self._source._outboundLinks.remove(self) 

2232 self._destination._inboundLinks.remove(self) 

2233 

2234 self._Unregister() 

2235 self._Delete() 

2236 

2237 def _Unregister(self) -> None: 

2238 """ 

2239 Remove this link from the graph or the subgraphs it was registered on. 

2240 

2241 A link crossing a subgraph boundary is registered on both subgraphs, a link between two top-level vertices on 

2242 the graph. Called by :meth:`Delete` and by :meth:`Vertex.Delete`, which unlinks the vertices itself. 

2243 """ 

2244 if self._source._subgraph is None: 2244 ↛ 2245line 2244 didn't jump to line 2245 because the condition on line 2244 was never true

2245 containers = (self._source._graph, ) 

2246 else: 

2247 containers = tuple(sg for sg in (self._source._subgraph, self._destination._subgraph) if sg is not None) 

2248 

2249 for container in containers: 

2250 if self._id is None: 

2251 container._linksWithoutID.remove(self) 

2252 else: 

2253 del container._linksWithID[self._id] 

2254 

2255 def _Delete(self) -> None: 

2256 """ 

2257 Delete the link's attached attributes, after it was disconnected. 

2258 """ 

2259 super().Delete() 

2260 

2261 def Reverse(self) -> None: 

2262 """Reverse the direction of this link.""" 

2263 self._source._outboundLinks.remove(self) 

2264 self._source._inboundLinks.append(self) 

2265 self._destination._inboundLinks.remove(self) 

2266 self._destination._outboundLinks.append(self) 

2267 

2268 super().Reverse() 

2269 

2270 

2271@export 

2272class BaseGraph( 

2273 BaseWithName[GraphDictKeyType, GraphDictValueType], 

2274 Generic[ 

2275 GraphDictKeyType, GraphDictValueType, 

2276 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2277 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2278 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2279 ] 

2280): 

2281 """ 

2282 .. todo:: GRAPH::BaseGraph Needs documentation. 

2283 

2284 """ 

2285 

2286 _verticesWithID: dict[VertexIDType, Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Vertices with an ID, by ID. 

2287 _verticesWithoutID: list[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Vertices without an ID, in insertion order. 

2288 _edgesWithID: dict[EdgeIDType, Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Edges with an ID, by ID. 

2289 _edgesWithoutID: list[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType]] #: Edges without an ID, in insertion order. 

2290 _linksWithID: dict[EdgeIDType, Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Links between subgraphs with an ID, by ID. 

2291 _linksWithoutID: list[Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Links between subgraphs without an ID, in insertion order. 

2292 

2293 def __init__( 

2294 self, 

2295 name: Nullable[str] = None, 

2296 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2297 #, vertices: Nullable[Iterable[Vertex]] = None) -> None: 

2298 ) -> None: 

2299 """ 

2300 .. todo:: GRAPH::BaseGraph::init Needs documentation. 

2301 

2302 :param name: Optional, name of the graph. 

2303 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2304 """ 

2305 super().__init__(name, keyValuePairs) 

2306 

2307 self._verticesWithoutID = [] 

2308 self._verticesWithID = {} 

2309 self._edgesWithoutID = [] 

2310 self._edgesWithID = {} 

2311 self._linksWithoutID = [] 

2312 self._linksWithID = {} 

2313 

2314 def __del__(self) -> None: 

2315 """ 

2316 .. todo:: GRAPH::BaseGraph::del Needs documentation. 

2317 

2318 """ 

2319 try: 

2320 del self._verticesWithoutID 

2321 del self._verticesWithID 

2322 del self._edgesWithoutID 

2323 del self._edgesWithID 

2324 del self._linksWithoutID 

2325 del self._linksWithID 

2326 except AttributeError: 

2327 pass 

2328 

2329 super().__del__() 

2330 

2331 @readonly 

2332 def VertexCount(self) -> int: 

2333 """Read-only property to return the number of vertices in this graph. 

2334 

2335 :returns: The number of vertices in this graph.""" 

2336 return len(self._verticesWithoutID) + len(self._verticesWithID) 

2337 

2338 @readonly 

2339 def EdgeCount(self) -> int: 

2340 """Read-only property to return the number of edges in this graph. 

2341 

2342 :returns: The number of edges in this graph.""" 

2343 return len(self._edgesWithoutID) + len(self._edgesWithID) 

2344 

2345 @readonly 

2346 def LinkCount(self) -> int: 

2347 """Read-only property to return the number of links in this graph. 

2348 

2349 :returns: The number of links in this graph.""" 

2350 return len(self._linksWithoutID) + len(self._linksWithID) 

2351 

2352 def IterateVertices(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2353 """ 

2354 Iterate all or selected vertices of a graph. 

2355 

2356 If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. 

2357 

2358 :param predicate: Optional, filter function accepting any vertex and returning a boolean. 

2359 :returns: A generator to iterate all vertices. 

2360 """ 

2361 if predicate is None: 

2362 yield from self._verticesWithoutID 

2363 yield from self._verticesWithID.values() 

2364 

2365 else: 

2366 for vertex in self._verticesWithoutID: 

2367 if predicate(vertex): 

2368 yield vertex 

2369 

2370 for vertex in self._verticesWithID.values(): 

2371 if predicate(vertex): 

2372 yield vertex 

2373 

2374 def IterateRoots(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2375 """ 

2376 Iterate all or selected roots (vertices without inbound edges / without predecessors) of a graph. 

2377 

2378 If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. 

2379 

2380 :param predicate: Optional, filter function accepting any vertex and returning a boolean. 

2381 :returns: A generator to iterate all vertices without inbound edges. 

2382 

2383 .. seealso:: 

2384 

2385 :meth:`BaseGraph.IterateLeafs <pyTooling.Graph.BaseGraph.IterateLeafs>` 

2386 |rarr| Iterate leafs of a graph. 

2387 :meth:`Vertex.IsRoot <pyTooling.Graph.Vertex.IsRoot>` 

2388 |rarr| Check if a vertex is a root vertex in the graph. 

2389 :meth:`Vertex.IsLeaf <pyTooling.Graph.Vertex.IsLeaf>` 

2390 |rarr| Check if a vertex is a leaf vertex in the graph. 

2391 """ 

2392 if predicate is None: 

2393 for vertex in self._verticesWithoutID: 

2394 if len(vertex._inboundEdges) == 0: 

2395 yield vertex 

2396 

2397 for vertex in self._verticesWithID.values(): 2397 ↛ 2398line 2397 didn't jump to line 2398 because the loop on line 2397 never started

2398 if len(vertex._inboundEdges) == 0: 

2399 yield vertex 

2400 else: 

2401 for vertex in self._verticesWithoutID: 

2402 if len(vertex._inboundEdges) == 0 and predicate(vertex): 

2403 yield vertex 

2404 

2405 for vertex in self._verticesWithID.values(): 2405 ↛ 2406line 2405 didn't jump to line 2406 because the loop on line 2405 never started

2406 if len(vertex._inboundEdges) == 0 and predicate(vertex): 

2407 yield vertex 

2408 

2409 def IterateLeafs(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2410 """ 

2411 Iterate all or selected leafs (vertices without outbound edges / without successors) of a graph. 

2412 

2413 If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. 

2414 

2415 :param predicate: Optional, filter function accepting any vertex and returning a boolean. 

2416 :returns: A generator to iterate all vertices without outbound edges. 

2417 

2418 .. seealso:: 

2419 

2420 :meth:`BaseGraph.IterateRoots <pyTooling.Graph.BaseGraph.IterateRoots>` 

2421 |rarr| Iterate roots of a graph. 

2422 :meth:`Vertex.IsRoot <pyTooling.Graph.Vertex.IsRoot>` 

2423 |rarr| Check if a vertex is a root vertex in the graph. 

2424 :meth:`Vertex.IsLeaf <pyTooling.Graph.Vertex.IsLeaf>` 

2425 |rarr| Check if a vertex is a leaf vertex in the graph. 

2426 """ 

2427 if predicate is None: 

2428 for vertex in self._verticesWithoutID: 

2429 if len(vertex._outboundEdges) == 0: 

2430 yield vertex 

2431 

2432 for vertex in self._verticesWithID.values(): 

2433 if len(vertex._outboundEdges) == 0: 

2434 yield vertex 

2435 else: 

2436 for vertex in self._verticesWithoutID: 

2437 if len(vertex._outboundEdges) == 0 and predicate(vertex): 

2438 yield vertex 

2439 

2440 for vertex in self._verticesWithID.values(): 

2441 if len(vertex._outboundEdges) == 0 and predicate(vertex): 2441 ↛ 2442line 2441 didn't jump to line 2442 because the condition on line 2441 was never true

2442 yield vertex 

2443 

2444 # def IterateBFS(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2445 # raise NotImplementedError() 

2446 # 

2447 # def IterateDFS(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2448 # raise NotImplementedError() 

2449 

2450 def IterateTopologically(self, predicate: Nullable[Callable[[Vertex], bool]] = None) -> Generator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2451 """ 

2452 Iterate all or selected vertices in topological order. 

2453 

2454 If parameter ``predicate`` is not None, the given filter function is used to skip vertices in the generator. 

2455 

2456 :param predicate: Optional, filter function accepting any vertex and returning a boolean. 

2457 :returns: A generator to iterate all vertices in topological order. 

2458 :raises CycleError: If the graph contains a cycle, so no topological order exists. 

2459 :raises InternalError: If the algorithm's internal state became inconsistent. 

2460 :except CycleError: Raised if graph is cyclic, thus topological sorting isn't possible. 

2461 """ 

2462 outboundEdgeCounts = {} 

2463 leafVertices = [] 

2464 

2465 for vertex in self._verticesWithoutID: 

2466 if (count := len(vertex._outboundEdges)) == 0: 

2467 leafVertices.append(vertex) 

2468 else: 

2469 outboundEdgeCounts[vertex] = count 

2470 

2471 for vertex in self._verticesWithID.values(): 

2472 if (count := len(vertex._outboundEdges)) == 0: 

2473 leafVertices.append(vertex) 

2474 else: 

2475 outboundEdgeCounts[vertex] = count 

2476 

2477 if not leafVertices: 2477 ↛ 2478line 2477 didn't jump to line 2478 because the condition on line 2477 was never true

2478 raise CycleError(f"Graph has no leafs. Thus, no topological sorting exists.") 

2479 

2480 overallCount = len(outboundEdgeCounts) + len(leafVertices) 

2481 

2482 def removeVertex(vertex: Vertex): 

2483 """ 

2484 Nested function removing a vertex from the counting, and queuing the vertices that became leafs. 

2485 

2486 :param vertex: The vertex that was just yielded. 

2487 """ 

2488 nonlocal overallCount 

2489 overallCount -= 1 

2490 for inboundEdge in vertex._inboundEdges: 

2491 sourceVertex = inboundEdge.Source 

2492 count = outboundEdgeCounts[sourceVertex] - 1 

2493 outboundEdgeCounts[sourceVertex] = count 

2494 if count == 0: 

2495 leafVertices.append(sourceVertex) 

2496 

2497 if predicate is None: 

2498 for vertex in leafVertices: 

2499 yield vertex 

2500 

2501 removeVertex(vertex) 

2502 else: 

2503 for vertex in leafVertices: 

2504 if predicate(vertex): 

2505 yield vertex 

2506 

2507 removeVertex(vertex) 

2508 

2509 if overallCount == 0: 2509 ↛ 2511line 2509 didn't jump to line 2511 because the condition on line 2509 was always true

2510 return 

2511 elif overallCount > 0: 

2512 raise CycleError(f"Graph has remaining vertices. Thus, the graph has at least one cycle.") 

2513 

2514 raise InternalError(f"Graph data structure is corrupted.") # pragma: no cover 

2515 

2516 def IterateEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> Generator[Edge[EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType], None, None]: 

2517 """ 

2518 Iterate all or selected edges of a graph. 

2519 

2520 If parameter ``predicate`` is not None, the given filter function is used to skip edges in the generator. 

2521 

2522 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

2523 :returns: A generator to iterate all edges. 

2524 """ 

2525 if predicate is None: 

2526 yield from self._edgesWithoutID 

2527 yield from self._edgesWithID.values() 

2528 

2529 else: 

2530 for edge in self._edgesWithoutID: 

2531 if predicate(edge): 

2532 yield edge 

2533 

2534 for edge in self._edgesWithID.values(): 

2535 if predicate(edge): 

2536 yield edge 

2537 

2538 def IterateLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> Generator[Link[LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType], None, None]: 

2539 """ 

2540 Iterate all or selected links of a graph. 

2541 

2542 If parameter ``predicate`` is not None, the given filter function is used to skip links in the generator. 

2543 

2544 :param predicate: Optional, filter function accepting any link and returning a boolean. 

2545 :returns: A generator to iterate all links. 

2546 """ 

2547 if predicate is None: 2547 ↛ 2552line 2547 didn't jump to line 2552 because the condition on line 2547 was always true

2548 yield from self._linksWithoutID 

2549 yield from self._linksWithID.values() 

2550 

2551 else: 

2552 for link in self._linksWithoutID: 

2553 if predicate(link): 

2554 yield link 

2555 

2556 for link in self._linksWithID.values(): 

2557 if predicate(link): 

2558 yield link 

2559 

2560 def ReverseEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> None: 

2561 """ 

2562 Reverse all or selected edges of a graph. 

2563 

2564 If parameter ``predicate`` is not None, the given filter function is used to skip edges. 

2565 

2566 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

2567 """ 

2568 if predicate is None: 

2569 for edge in self._edgesWithoutID: 

2570 swap = edge._source 

2571 edge._source = edge._destination 

2572 edge._destination = swap 

2573 

2574 for edge in self._edgesWithID.values(): 

2575 swap = edge._source 

2576 edge._source = edge._destination 

2577 edge._destination = swap 

2578 

2579 for vertex in self._verticesWithoutID: 

2580 swap = vertex._inboundEdges 

2581 vertex._inboundEdges = vertex._outboundEdges 

2582 vertex._outboundEdges = swap 

2583 

2584 for vertex in self._verticesWithID.values(): 

2585 swap = vertex._inboundEdges 

2586 vertex._inboundEdges = vertex._outboundEdges 

2587 vertex._outboundEdges = swap 

2588 else: 

2589 for edge in self._edgesWithoutID: 

2590 if predicate(edge): 

2591 edge.Reverse() 

2592 

2593 for edge in self._edgesWithID.values(): 

2594 if predicate(edge): 

2595 edge.Reverse() 

2596 

2597 def ReverseLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> None: 

2598 """ 

2599 Reverse all or selected links of a graph. 

2600 

2601 If parameter ``predicate`` is not None, the given filter function is used to skip links. 

2602 

2603 :param predicate: Optional, filter function accepting any link and returning a boolean. 

2604 """ 

2605 if predicate is None: 

2606 for link in self._linksWithoutID: 

2607 swap = link._source 

2608 link._source = link._destination 

2609 link._destination = swap 

2610 

2611 for link in self._linksWithID.values(): 

2612 swap = link._source 

2613 link._source = link._destination 

2614 link._destination = swap 

2615 

2616 for vertex in self._verticesWithoutID: 

2617 swap = vertex._inboundLinks 

2618 vertex._inboundLinks = vertex._outboundLinks 

2619 vertex._outboundLinks = swap 

2620 

2621 for vertex in self._verticesWithID.values(): 

2622 swap = vertex._inboundLinks 

2623 vertex._inboundLinks = vertex._outboundLinks 

2624 vertex._outboundLinks = swap 

2625 else: 

2626 for link in self._linksWithoutID: 

2627 if predicate(link): 

2628 link.Reverse() 

2629 

2630 for link in self._linksWithID.values(): 

2631 if predicate(link): 

2632 link.Reverse() 

2633 

2634 def RemoveEdges(self, predicate: Nullable[Callable[[Edge], bool]] = None) -> None: 

2635 """ 

2636 Remove all or selected edges of a graph. 

2637 

2638 If parameter ``predicate`` is not None, the given filter function is used to skip edges. 

2639 

2640 :param predicate: Optional, filter function accepting any edge and returning a boolean. 

2641 """ 

2642 if predicate is None: 

2643 for edge in self._edgesWithoutID: 

2644 edge._Delete() 

2645 

2646 for edge in self._edgesWithID.values(): 

2647 edge._Delete() 

2648 

2649 self._edgesWithoutID = [] 

2650 self._edgesWithID = {} 

2651 

2652 for vertex in self._verticesWithoutID: 

2653 vertex._inboundEdges = [] 

2654 vertex._outboundEdges = [] 

2655 

2656 for vertex in self._verticesWithID.values(): 

2657 vertex._inboundEdges = [] 

2658 vertex._outboundEdges = [] 

2659 

2660 else: 

2661 delEdges = [edge for edge in self._edgesWithID.values() if predicate(edge)] 

2662 for edge in delEdges: 

2663 del self._edgesWithID[edge._id] 

2664 

2665 edge._source._outboundEdges.remove(edge) 

2666 edge._destination._inboundEdges.remove(edge) 

2667 edge._Delete() 

2668 

2669 for edge in self._edgesWithoutID: 

2670 if predicate(edge): 

2671 self._edgesWithoutID.remove(edge) 

2672 

2673 edge._source._outboundEdges.remove(edge) 

2674 edge._destination._inboundEdges.remove(edge) 

2675 edge._Delete() 

2676 

2677 def RemoveLinks(self, predicate: Nullable[Callable[[Link], bool]] = None) -> None: 

2678 """ 

2679 Remove all or selected links of a graph. 

2680 

2681 If parameter ``predicate`` is not None, the given filter function is used to skip links. 

2682 

2683 :param predicate: Optional, filter function accepting any link and returning a boolean. 

2684 """ 

2685 if predicate is None: 

2686 for link in self._linksWithoutID: 

2687 link._Delete() 

2688 

2689 for link in self._linksWithID.values(): 

2690 link._Delete() 

2691 

2692 self._linksWithoutID = [] 

2693 self._linksWithID = {} 

2694 

2695 for vertex in self._verticesWithoutID: 

2696 vertex._inboundLinks = [] 

2697 vertex._outboundLinks = [] 

2698 

2699 for vertex in self._verticesWithID.values(): 

2700 vertex._inboundLinks = [] 

2701 vertex._outboundLinks = [] 

2702 

2703 else: 

2704 delLinks = [link for link in self._linksWithID.values() if predicate(link)] 

2705 for link in delLinks: 

2706 del self._linksWithID[link._id] 

2707 

2708 link._source._outboundLinks.remove(link) 

2709 link._destination._inboundLinks.remove(link) 

2710 link._Delete() 

2711 

2712 for link in self._linksWithoutID: 

2713 if predicate(link): 

2714 self._linksWithoutID.remove(link) 

2715 

2716 link._source._outboundLinks.remove(link) 

2717 link._destination._inboundLinks.remove(link) 

2718 link._Delete() 

2719 

2720 def HasCycle(self) -> bool: 

2721 """ 

2722 Check if the graph contains at least one cycle. 

2723 

2724 The graph is traversed depth-first from every unvisited vertex; a vertex reached again while it is still on the 

2725 current path closes a cycle. 

2726 

2727 :returns: ``True``, if the graph contains a cycle. 

2728 :raises InternalError: If the graph's data structure is corrupted. 

2729 """ 

2730 # IsAcyclic ? 

2731 

2732 # Handle trivial case if graph is empty 

2733 if len(self._verticesWithID) + len(self._verticesWithoutID) == 0: 2733 ↛ 2734line 2733 didn't jump to line 2734 because the condition on line 2733 was never true

2734 return False 

2735 

2736 outboundEdgeCounts = {} 

2737 leafVertices = [] 

2738 

2739 for vertex in self._verticesWithoutID: 

2740 if (count := len(vertex._outboundEdges)) == 0: 

2741 leafVertices.append(vertex) 

2742 else: 

2743 outboundEdgeCounts[vertex] = count 

2744 

2745 for vertex in self._verticesWithID.values(): 

2746 if (count := len(vertex._outboundEdges)) == 0: 

2747 leafVertices.append(vertex) 

2748 else: 

2749 outboundEdgeCounts[vertex] = count 

2750 

2751 # If there are no leafs, then each vertex has at least one inbound and one outbound edges. Thus, there is a cycle. 

2752 if not leafVertices: 2752 ↛ 2753line 2752 didn't jump to line 2753 because the condition on line 2752 was never true

2753 return True 

2754 

2755 overallCount = len(outboundEdgeCounts) + len(leafVertices) 

2756 

2757 for vertex in leafVertices: 

2758 overallCount -= 1 

2759 for inboundEdge in vertex._inboundEdges: 

2760 sourceVertex = inboundEdge.Source 

2761 count = outboundEdgeCounts[sourceVertex] - 1 

2762 outboundEdgeCounts[sourceVertex] = count 

2763 if count == 0: 

2764 leafVertices.append(sourceVertex) 

2765 

2766 # If all vertices were processed, no cycle exists. 

2767 if overallCount == 0: 

2768 return False 

2769 # If there are remaining vertices, then a cycle exists. 

2770 elif overallCount > 0: 

2771 return True 

2772 

2773 raise InternalError(f"Graph data structure is corrupted.") # pragma: no cover 

2774 

2775 

2776@export 

2777class Subgraph( 

2778 BaseGraph[ 

2779 SubgraphDictKeyType, SubgraphDictValueType, 

2780 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2781 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2782 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2783 ], 

2784 Generic[ 

2785 SubgraphDictKeyType, SubgraphDictValueType, 

2786 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2787 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2788 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2789 ] 

2790): 

2791 """ 

2792 .. todo:: GRAPH::Subgraph Needs documentation. 

2793 

2794 """ 

2795 

2796 _graph: Graph #: Reference to the graph this subgraph is part of. 

2797 

2798 def __init__( 

2799 self, 

2800 graph: Graph, 

2801 name: Nullable[str] = None, 

2802 # vertices: Nullable[Iterable[Vertex]] = None, 

2803 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2804 ) -> None: 

2805 """ 

2806 Initialize a subgraph and register it at its graph. 

2807 

2808 :param graph: Optional, the reference to the graph. 

2809 :param name: Optional, name of the new sub-graph. 

2810 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2811 :raises ValueError: If parameter 'graph' is None. 

2812 :raises TypeError: If parameter 'graph' is not of type :class:`Graph`. 

2813 """ 

2814 if graph is None: 2814 ↛ 2815line 2814 didn't jump to line 2815 because the condition on line 2814 was never true

2815 raise ValueError("Parameter 'graph' is None.") 

2816 if not isinstance(graph, Graph): 2816 ↛ 2817line 2816 didn't jump to line 2817 because the condition on line 2816 was never true

2817 ex = TypeError("Parameter 'graph' is not of type 'Graph'.") 

2818 ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") 

2819 raise ex 

2820 

2821 super().__init__(name, keyValuePairs) 

2822 

2823 graph._subgraphs.add(self) 

2824 

2825 self._graph = graph 

2826 

2827 def __del__(self) -> None: 

2828 """ 

2829 .. todo:: GRAPH::Subgraph::del Needs documentation. 

2830 

2831 """ 

2832 super().__del__() 

2833 

2834 @readonly 

2835 def Graph(self) -> Graph: 

2836 """ 

2837 Read-only property to access the graph, this subgraph is associated to (:attr:`_graph`). 

2838 

2839 :returns: The graph this subgraph is associated to. 

2840 """ 

2841 return self._graph 

2842 

2843 def __str__(self) -> str: 

2844 """ 

2845 Return a string representation of this subgraph. 

2846 

2847 :returns: The subgraph's name, or ``"Unnamed subgraph"`` if it has none. 

2848 """ 

2849 return self._name if self._name is not None else "Unnamed subgraph" 

2850 

2851 

2852@export 

2853class View( 

2854 BaseWithVertices[ 

2855 ViewDictKeyType, ViewDictValueType, 

2856 GraphDictKeyType, GraphDictValueType, 

2857 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2858 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2859 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2860 ], 

2861 Generic[ 

2862 ViewDictKeyType, ViewDictValueType, 

2863 GraphDictKeyType, GraphDictValueType, 

2864 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2865 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2866 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2867 ] 

2868): 

2869 """ 

2870 .. todo:: GRAPH::View Needs documentation. 

2871 

2872 """ 

2873 

2874 def __init__( 

2875 self, 

2876 graph: Graph, 

2877 name: Nullable[str] = None, 

2878 vertices: Nullable[Iterable[Vertex]] = None, 

2879 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2880 ) -> None: 

2881 """ 

2882 .. todo:: GRAPH::View::init Needs documentation. 

2883 

2884 :param graph: Optional, the reference to the graph. 

2885 :param name: Optional, name of the new view. 

2886 :param vertices: Optional, list of vertices in the new view. 

2887 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2888 """ 

2889 super().__init__(graph, name, vertices, keyValuePairs) 

2890 

2891 graph._views.add(self) 

2892 

2893 def __del__(self) -> None: 

2894 """ 

2895 .. todo:: GRAPH::View::del Needs documentation. 

2896 

2897 """ 

2898 super().__del__() 

2899 

2900 def __str__(self) -> str: 

2901 """ 

2902 Return a string representation of this view. 

2903 

2904 :returns: The view's name, or ``"Unnamed view"`` if it has none. 

2905 """ 

2906 return self._name if self._name is not None else "Unnamed view" 

2907 

2908 

2909@export 

2910class Component( 

2911 BaseWithVertices[ 

2912 ComponentDictKeyType, ComponentDictValueType, 

2913 GraphDictKeyType, GraphDictValueType, 

2914 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2915 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2916 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2917 ], 

2918 Generic[ 

2919 ComponentDictKeyType, ComponentDictValueType, 

2920 GraphDictKeyType, GraphDictValueType, 

2921 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2922 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2923 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2924 ] 

2925): 

2926 """ 

2927 .. todo:: GRAPH::Component Needs documentation. 

2928 

2929 """ 

2930 

2931 def __init__( 

2932 self, 

2933 graph: Graph, 

2934 name: Nullable[str] = None, 

2935 vertices: Nullable[Iterable[Vertex]] = None, 

2936 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2937 ) -> None: 

2938 """ 

2939 Initialize a component of a graph and register it at that graph. 

2940 

2941 :param graph: Optional, the reference to the graph. 

2942 :param name: Optional, name of the new component. 

2943 :param vertices: Optional, list of vertices in the new component. 

2944 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs. 

2945 """ 

2946 super().__init__(graph, name, vertices, keyValuePairs) 

2947 

2948 graph._components.add(self) 

2949 

2950 def __del__(self) -> None: 

2951 """ 

2952 .. todo:: GRAPH::Component::del Needs documentation. 

2953 

2954 """ 

2955 super().__del__() 

2956 

2957 def __str__(self) -> str: 

2958 """ 

2959 Return a string representation of this component. 

2960 

2961 :returns: The component's name, or ``"Unnamed component"`` if it has none. 

2962 """ 

2963 return self._name if self._name is not None else "Unnamed component" 

2964 

2965 

2966@export 

2967class Graph( 

2968 BaseGraph[ 

2969 GraphDictKeyType, GraphDictValueType, 

2970 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2971 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2972 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2973 ], 

2974 Generic[ 

2975 GraphDictKeyType, GraphDictValueType, 

2976 ComponentDictKeyType, ComponentDictValueType, 

2977 SubgraphDictKeyType, SubgraphDictValueType, 

2978 ViewDictKeyType, ViewDictValueType, 

2979 VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, 

2980 EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, 

2981 LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType 

2982 ] 

2983): 

2984 """ 

2985 A **graph** data structure is represented by an instance of :class:`~pyTooling.Graph.Graph` holding references to 

2986 all nodes. Nodes are instances of :class:`~pyTooling.Graph.Vertex` classes and directed links between nodes are 

2987 made of :class:`~pyTooling.Graph.Edge` instances. A graph can have attached meta information as key-value-pairs. 

2988 """ 

2989 _subgraphs: set[Subgraph[SubgraphDictKeyType, SubgraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Subgraphs of this graph. 

2990 _views: set[View[ViewDictKeyType, ViewDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Views defined on this graph. 

2991 _components: set[Component[ComponentDictKeyType, ComponentDictValueType, GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]] #: Connected components of this graph. 

2992 

2993 def __init__( 

2994 self, 

2995 name: Nullable[str] = None, 

2996 keyValuePairs: Nullable[Mapping[DictKeyType, DictValueType]] = None 

2997 ) -> None: 

2998 """ 

2999 .. todo:: GRAPH::Graph::init Needs documentation. 

3000 

3001 :param name: Optional, name of the new graph. 

3002 :param keyValuePairs: Optional, mapping (dictionary) of key-value-pairs.# 

3003 """ 

3004 super().__init__(name, keyValuePairs) 

3005 

3006 self._subgraphs = set() 

3007 self._views = set() 

3008 self._components = set() 

3009 

3010 def __del__(self) -> None: 

3011 """ 

3012 .. todo:: GRAPH::Graph::del Needs documentation. 

3013 

3014 """ 

3015 try: 

3016 del self._subgraphs 

3017 del self._views 

3018 del self._components 

3019 except AttributeError: 

3020 pass 

3021 

3022 super().__del__() 

3023 

3024 @readonly 

3025 def Subgraphs(self) -> set[Subgraph]: 

3026 """Read-only property to access the subgraphs in this graph (:attr:`_subgraphs`). 

3027 

3028 :returns: The set of subgraphs in this graph.""" 

3029 return self._subgraphs 

3030 

3031 @readonly 

3032 def Views(self) -> set[View]: 

3033 """Read-only property to access the views in this graph (:attr:`_views`). 

3034 

3035 :returns: The set of views in this graph.""" 

3036 return self._views 

3037 

3038 @readonly 

3039 def Components(self) -> set[Component]: 

3040 """Read-only property to access the components in this graph (:attr:`_components`). 

3041 

3042 :returns: The set of components in this graph.""" 

3043 return self._components 

3044 

3045 @readonly 

3046 def SubgraphCount(self) -> int: 

3047 """Read-only property to return the number of subgraphs in this graph. 

3048 

3049 :returns: The number of subgraphs in this graph.""" 

3050 return len(self._subgraphs) 

3051 

3052 @readonly 

3053 def ViewCount(self) -> int: 

3054 """Read-only property to return the number of views in this graph. 

3055 

3056 :returns: The number of views in this graph.""" 

3057 return len(self._views) 

3058 

3059 @readonly 

3060 def ComponentCount(self) -> int: 

3061 """Read-only property to return the number of components in this graph. 

3062 

3063 :returns: The number of components in this graph.""" 

3064 return len(self._components) 

3065 

3066 def __iter__(self) -> typing_Iterator[Vertex[GraphDictKeyType, GraphDictValueType, VertexIDType, VertexWeightType, VertexValueType, VertexDictKeyType, VertexDictValueType, EdgeIDType, EdgeWeightType, EdgeValueType, EdgeDictKeyType, EdgeDictValueType, LinkIDType, LinkWeightType, LinkValueType, LinkDictKeyType, LinkDictValueType]]: 

3067 """ 

3068 Iterate all vertices of this graph. 

3069 

3070 :returns: An iterator over the vertices without an ID, followed by those with one. 

3071 """ 

3072 def gen(): 

3073 """ 

3074 Nested generator function chaining the vertices without an ID and those with one. 

3075 

3076 :returns: A generator yielding every vertex of the graph. 

3077 """ 

3078 yield from self._verticesWithoutID 

3079 yield from self._verticesWithID 

3080 return iter(gen()) 

3081 

3082 def HasVertexByID(self, vertexID: Nullable[VertexIDType]) -> bool: 

3083 """ 

3084 Check if a vertex with the given ID exists in this graph. 

3085 

3086 :param vertexID: Optional, ID to look for, or ``None`` for a vertex without an ID. 

3087 :returns: ``True``, if such a vertex exists. 

3088 """ 

3089 if vertexID is None: 

3090 return len(self._verticesWithoutID) >= 1 

3091 else: 

3092 return vertexID in self._verticesWithID 

3093 

3094 def HasVertexByValue(self, value: Nullable[VertexValueType]) -> bool: 

3095 """ 

3096 Check if a vertex carrying the given value exists in this graph. 

3097 

3098 :param value: Optional, value to look for. 

3099 :returns: ``True``, if such a vertex exists. 

3100 """ 

3101 return any(vertex._value == value for vertex in chain(self._verticesWithoutID, self._verticesWithID.values())) 

3102 

3103 def GetVertexByID(self, vertexID: Nullable[VertexIDType]) -> Vertex: 

3104 """ 

3105 Return the vertex with the given ID. 

3106 

3107 A vertex created without an ID can be looked up with ``None``, provided it is the only such vertex. 

3108 

3109 :param vertexID: Optional, ID of the vertex to return, or ``None`` for the vertex without an ID. 

3110 :returns: The vertex with that ID. 

3111 :raises KeyError: If no vertex has that ID, or if more than one vertex matches ``None``. 

3112 """ 

3113 if vertexID is None: 

3114 if (l := len(self._verticesWithoutID)) == 1: 

3115 return self._verticesWithoutID[0] 

3116 elif l == 0: 

3117 raise KeyError(f"Found no vertex with ID `None`.") 

3118 else: 

3119 raise KeyError(f"Found multiple vertices with ID `None`.") 

3120 else: 

3121 return self._verticesWithID[vertexID] 

3122 

3123 def GetVertexByValue(self, value: Nullable[VertexValueType]) -> Vertex: 

3124 """ 

3125 Return the vertex carrying the given value. 

3126 

3127 :param value: Optional, value of the vertex to return. 

3128 :returns: The vertex with that value. 

3129 :raises KeyError: If no vertex carries that value, or if more than one vertex does. 

3130 """ 

3131 # FIXME: optimize: iterate only until first item is found and check for a second to produce error 

3132 vertices = [vertex for vertex in chain(self._verticesWithoutID, self._verticesWithID.values()) if vertex._value == value] 

3133 if (l := len(vertices)) == 1: 

3134 return vertices[0] 

3135 elif l == 0: 

3136 raise KeyError(f"Found no vertex with Value == `{value}`.") 

3137 else: 

3138 raise KeyError(f"Found multiple vertices with Value == `{value}`.") 

3139 

3140 def CopyGraph(self) -> Graph: 

3141 """ 

3142 Create a copy of this graph. 

3143 

3144 :returns: A new graph with copies of this graph's vertices and edges. 

3145 :raises NotImplementedError: Copying a whole graph is not implemented yet. 

3146 """ 

3147 raise NotImplementedError() 

3148 

3149 def CopyVertices(self, predicate: Nullable[Callable[[Vertex], bool]] = None, copyGraphDict: bool = True, copyVertexDict: bool = True) -> Graph: 

3150 """ 

3151 Create a new graph and copy all or selected vertices of the original graph. 

3152 

3153 If parameter ``predicate`` is not None, the given filter function is used to skip vertices. 

3154 

3155 :param predicate: Optional, filter function accepting any vertex and returning a boolean. 

3156 :param copyGraphDict: Optional, if ``True``, copy all graph attached attributes into the new graph. 

3157 :param copyVertexDict: Optional, if ``True``, copy all vertex attached attributes into the new vertices. 

3158 :returns: A new graph with copies of the selected vertices. 

3159 """ 

3160 graph = Graph(self._name) 

3161 if copyGraphDict: 

3162 graph._dict = self._dict.copy() 

3163 

3164 if predicate is None: 

3165 for vertex in self._verticesWithoutID: 

3166 v = Vertex(None, vertex._value, graph=graph) 

3167 if copyVertexDict: 

3168 v._dict = vertex._dict.copy() 

3169 

3170 for vertexID, vertex in self._verticesWithID.items(): 

3171 v = Vertex(vertexID, vertex._value, graph=graph) 

3172 if copyVertexDict: 

3173 v._dict = vertex._dict.copy() 

3174 else: 

3175 for vertex in self._verticesWithoutID: 

3176 if predicate(vertex): 

3177 v = Vertex(None, vertex._value, graph=graph) 

3178 if copyVertexDict: 3178 ↛ 3175line 3178 didn't jump to line 3175 because the condition on line 3178 was always true

3179 v._dict = vertex._dict.copy() 

3180 

3181 for vertexID, vertex in self._verticesWithID.items(): 

3182 if predicate(vertex): 

3183 v = Vertex(vertexID, vertex._value, graph=graph) 

3184 if copyVertexDict: 3184 ↛ 3185line 3184 didn't jump to line 3185 because the condition on line 3184 was never true

3185 v._dict = vertex._dict.copy() 

3186 

3187 return graph 

3188 

3189 # class Iterator(): 

3190 # visited = [False for _ in range(self.__len__())] 

3191 

3192 # def CheckForNegativeCycles(self): 

3193 # raise NotImplementedError() 

3194 # # Bellman-Ford 

3195 # # Floyd-Warshall 

3196 # 

3197 # def IsStronglyConnected(self): 

3198 # raise NotImplementedError() 

3199 # 

3200 # def GetStronglyConnectedComponents(self): 

3201 # raise NotImplementedError() 

3202 # # Tarjan's and Kosaraju's algorithm 

3203 # 

3204 # def TravelingSalesmanProblem(self): 

3205 # raise NotImplementedError() 

3206 # # Held-Karp 

3207 # # branch and bound 

3208 # 

3209 # def GetBridges(self): 

3210 # raise NotImplementedError() 

3211 # 

3212 # def GetArticulationPoints(self): 

3213 # raise NotImplementedError() 

3214 # 

3215 # def MinimumSpanningTree(self): 

3216 # raise NotImplementedError() 

3217 # # Kruskal 

3218 # # Prim's algorithm 

3219 # # Buruvka's algorithm 

3220 

3221 def __repr__(self) -> str: 

3222 """ 

3223 Return a detailed string representation of this graph. 

3224 

3225 :returns: The graph's name and its vertex and edge counts. 

3226 """ 

3227 statistics = f", vertices: {self.VertexCount}, edges: {self.EdgeCount}" 

3228 if self._name is None: 

3229 return f"<graph: unnamed graph{statistics}>" 

3230 else: 

3231 return f"<graph: '{self._name}'{statistics}>" 

3232 

3233 def __str__(self) -> str: 

3234 """ 

3235 Return a string representation of this graph. 

3236 

3237 :returns: The graph's name, or ``"Unnamed graph"`` if it has none. 

3238 """ 

3239 if self._name is None: 

3240 return f"Graph: unnamed graph" 

3241 else: 

3242 return f"Graph: '{self._name}'"