Coverage for pyTooling/Warning/__init__.py: 87%

193 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 12:33 +0000

1# ==================================================================================================================== # 

2# _____ _ _ __ __ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /_ _ _ __ _ __ (_)_ __ __ _ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ /\ / / _` | '__| '_ \| | '_ \ / _` | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V V / (_| | | | | | | | | | | (_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/\_/ \__,_|_| |_| |_|_|_| |_|\__, | # 

7# |_| |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2025-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 solution to send warnings like exceptions to a handler in the upper part of the call-stack. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <WARNING>` for explanations and usage examples. 

37 

38.. seealso:: 

39 

40 :mod:`pyTooling.Exceptions` 

41 |rarr| Exceptions, which are raised instead of collected. 

42 :mod:`pyTooling.TerminalUI` 

43 |rarr| Writing the collected warnings to the terminal. 

44""" 

45from __future__ import annotations 

46 

47from threading import current_thread, local, Lock 

48from types import TracebackType 

49from typing import Callable, Optional as Nullable, Iterator, Self, Iterable, Union 

50 

51from pyTooling.Decorators import export, readonly 

52from pyTooling.Common import getFullyQualifiedName 

53from pyTooling.Exceptions import ExceptionBase 

54 

55 

56__all__ = ["_threadLocalData", "AnyWarning"] 

57 

58 

59_threadLocalData = local() 

60"""A reference to the thread local data needed by the pyTooling.Warning classes.""" 

61 

62 

63@export 

64class CriticalWarning(BaseException): 

65 """ 

66 Base-exception of all critical warnings handled by :class:`WarningCollector`. 

67 

68 .. important:: 

69 

70 A critical warning must be collected by a :class:`WarningCollector` up the call-hierarchy, otherwise 

71 :meth:`WarningCollector.Raise` raises an :exc:`UnhandledCriticalWarningError`. 

72 """ 

73 

74 @readonly 

75 def HasNotes(self) -> bool: 

76 """ 

77 Read-only property to return if the warning has attached notes. 

78 

79 :returns: True, if the warning has attached notes. 

80 """ 

81 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0 

82 

83 @readonly 

84 def Notes(self) -> tuple[str, ...]: 

85 """ 

86 Read-only property to return warning's attached notes. 

87 

88 :returns: Attached notes. 

89 """ 

90 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

91 

92 

93@export 

94class Warning(BaseException): 

95 """ 

96 Base-exception of all warnings handled by :class:`WarningCollector`. 

97 

98 .. attention:: 

99 

100 A warning doesn't need a collector: without a :class:`WarningCollector` up the call-hierarchy, it's dropped 

101 silently. 

102 """ 

103 

104 @readonly 

105 def HasNotes(self) -> bool: 

106 """ 

107 Read-only property to return if the warning has attached notes. 

108 

109 :returns: True, if the warning has attached notes. 

110 """ 

111 return hasattr(self, "__notes__") and self.__notes__ is not None and len(self.__notes__) > 0 

112 

113 @readonly 

114 def Notes(self) -> tuple[str, ...]: 

115 """ 

116 Read-only property to return warning's attached notes. 

117 

118 :returns: Attached notes. 

119 """ 

120 return tuple(self.__notes__) if hasattr(self, "__notes__") else tuple() 

121 

122 

123AnyWarning = Union[CriticalWarning, Warning] 

124 

125 

126@export 

127class UnhandledCriticalWarningError(ExceptionBase): 

128 """ 

129 This exception is raised when a critical warning isn't handled by a :class:`WarningCollector` within the 

130 call-hierarchy. 

131 

132 :meth:`WarningCollector.Raise` raises it, if no collector is current in the calling thread. The critical warning is 

133 its cause (``__cause__``), and a note says how to collect it. 

134 """ 

135 

136 

137@export 

138class UnhandledExceptionError(ExceptionBase): 

139 """ 

140 This exception is raised when an exception isn't handled by a :class:`WarningCollector` within the call-hierarchy. 

141 

142 :meth:`WarningCollector.Raise` raises it, if no collector is current in the calling thread. The exception is its 

143 cause (``__cause__``), and a note says how to collect it. 

144 """ 

145 

146 

147@export 

148class EscalatedWarningError(ExceptionBase): 

149 """ 

150 This exception is raised when a :class:`WarningCollector` decides a collected warning is escalated. 

151 

152 :meth:`WarningCollector.Raise` hands a warning to the current collector, which appends it to its list and calls its 

153 handler. A handler returning ``True`` asks for the warning to be escalated: :meth:`~WarningCollector.Raise` raises 

154 this exception, and the warning becomes its cause (``__cause__``). The warning stays in the collector's list. 

155 

156 Any function reporting a warning can raise it, but only when the caller installed a handler returning ``True`` for 

157 that warning. 

158 """ 

159 

160 

161@export 

162class WarningCollector: 

163 """ 

164 A context manager to collect warnings within the call hierarchy. 

165 

166 Entering the context makes the collector the *current* one of its thread, and :meth:`Raise` hands a warning to the 

167 current collector. Collectors are kept per thread: a warning raised in another thread doesn't reach it, see 

168 :class:`SupervisedWarningCollector`. 

169 

170 Collectors can be nested. The previous current collector becomes the :attr:`Parent`, and only the innermost 

171 collector receives a warning - the parent doesn't see it, unless it's passed on, e.g. by :meth:`AddWarning`. 

172 

173 The collected warnings are accessible by :attr:`Warnings`, by iteration, by :func:`len` and by index. 

174 """ 

175 _parent: Nullable[WarningCollector] #: Parent WarningCollector 

176 _warnings: list[BaseException] #: List of collected warnings (and exceptions). 

177 _handler: Nullable[Callable[[BaseException], bool]] #: Optional handler function, which is called per collected warning. 

178 

179 __slots__ = ("_parent", "_warnings", "_handler") 

180 

181 def __init__( 

182 self, 

183 warnings: Nullable[list[BaseException]] = None, 

184 handler: Nullable[Callable[[BaseException], bool]] = None 

185 ) -> None: 

186 """ 

187 Initializes a warning collector. 

188 

189 :param warnings: Optional, reference to a list of warnings, which can be modified (appended) by this warning 

190 collector. If ``None``, an internal list is created and can be referenced by the collector's 

191 instance. 

192 :param handler: Optional, handler function, which is called with each collected warning and decides, if it's 

193 escalated. If it returns ``True``, :meth:`Raise` raises an :exc:`EscalatedWarningError`, whose 

194 cause is the warning. An exception raised by the handler propagates through :meth:`Raise`. 

195 :raises TypeError: If optional parameter 'warnings' is not of type list. 

196 :raises TypeError: If optional parameter 'handler' is not a callable. 

197 """ 

198 if warnings is None: 

199 warnings = [] 

200 elif not isinstance(warnings, list): 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true

201 ex = TypeError("Parameter 'warnings' is not a list.") 

202 ex.add_note(f"Got type '{getFullyQualifiedName(warnings)}'.") 

203 raise ex 

204 

205 if handler is not None and not isinstance(handler, Callable): 205 ↛ 206line 205 didn't jump to line 206 because the condition on line 205 was never true

206 ex = TypeError("Parameter 'handler' is not callable.") 

207 ex.add_note(f"Got type '{getFullyQualifiedName(handler)}'.") 

208 raise ex 

209 

210 self._parent = None 

211 self._warnings = warnings 

212 self._handler = handler 

213 

214 def __len__(self) -> int: 

215 """ 

216 Returns the number of collected warnings. 

217 

218 :returns: Number of collected warnings. 

219 """ 

220 return len(self._warnings) 

221 

222 def __iter__(self) -> Iterator[Warning | CriticalWarning | Exception]: 

223 """ 

224 Return an iterator over all collected warnings. 

225 

226 :returns: Iterator over the collected warnings. 

227 """ 

228 return iter(self._warnings) 

229 

230 def __getitem__(self, index: int) -> Warning | CriticalWarning | Exception: 

231 """ 

232 Access a collected warning by index. 

233 

234 :param index: Index of the warning. 

235 :returns: Collected warning. 

236 """ 

237 return self._warnings[index] 

238 

239 def __enter__(self) -> Self: 

240 """ 

241 Enter the context and make this warning collector the current one. 

242 

243 The previous current warning collector becomes its parent. 

244 

245 :returns: The warning collector instance. 

246 """ 

247 global _threadLocalData 

248 

249 try: 

250 self._parent = _threadLocalData.warningCollector 

251 except AttributeError: 

252 pass 

253 

254 _threadLocalData.warningCollector = self 

255 

256 return self 

257 

258 def __exit__( 

259 self, 

260 exc_type: Nullable[type[BaseException]] = None, 

261 exc_val: Nullable[BaseException] = None, 

262 exc_tb: Nullable[TracebackType] = None 

263 ) -> Nullable[bool]: 

264 """ 

265 Exit the context and make the parent warning collector the current one again. 

266 

267 :param exc_type: Optional, exception type. 

268 :param exc_val: Optional, exception instance. 

269 :param exc_tb: Optional, exception's traceback. 

270 :returns: ``False``, so an exception raised in the context propagates. 

271 """ 

272 global _threadLocalData 

273 

274 _threadLocalData.warningCollector = self._parent 

275 

276 return False 

277 

278 @property 

279 def Parent(self) -> Nullable[Self]: 

280 """ 

281 Property to access the parent warning collector. 

282 

283 :returns: The parent warning collector or ``None``. 

284 """ 

285 return self._parent 

286 

287 @Parent.setter 

288 def Parent(self, value: Self) -> None: 

289 self._parent = value 

290 

291 @readonly 

292 def Warnings(self) -> list[Warning | CriticalWarning | Exception]: 

293 """ 

294 Read-only property to access the list of collected warnings. 

295 

296 :returns: A list of collected warnings. 

297 """ 

298 return self._warnings 

299 

300 def AddWarning(self, warning: Warning | CriticalWarning | Exception) -> bool: 

301 """ 

302 Add a warning to the list of warnings managed by this warning collector and call its handler. 

303 

304 An exception raised by the handler propagates. 

305 

306 :param warning: The warning to add to the collector's internal warning list. 

307 :returns: Return ``True`` if the warning collector has a local handler callback and this handler returned 

308 ``True``; otherwise ``False``. 

309 :raises ValueError: If parameter ``warning`` is None. 

310 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`, :class:`CriticalWarning` or 

311 :exc:`Exception`. 

312 """ 

313 if warning is None: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true

314 raise ValueError("Parameter 'warning' is None.") 

315 elif not isinstance(warning, (Warning, CriticalWarning, Exception)): 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 ex = TypeError("Parameter 'warning' is not of type 'Warning', 'CriticalWarning' or 'Exception'.") 

317 ex.add_note(f"Got type '{getFullyQualifiedName(warning)}'.") 

318 raise ex 

319 

320 self._warnings.append(warning) 

321 

322 return False if self._handler is None else self._handler(warning) 

323 

324 @classmethod 

325 def Raise( 

326 cls, 

327 warning: Warning | CriticalWarning | Exception, 

328 cause: Nullable[Exception] = None, 

329 *, 

330 notes: Nullable[str | Iterable[str]] = None 

331 ) -> None: 

332 """ 

333 Hand a warning to the current warning collector of the calling thread. 

334 

335 The current collector is the innermost :class:`WarningCollector` context entered in the calling thread. It appends 

336 the warning to its list (:meth:`AddWarning`) and calls its handler. Without a current collector, a :class:`Warning` 

337 is dropped, while a :class:`CriticalWarning` or an :exc:`Exception` is wrapped in an exception and raised. 

338 

339 Called inside a generator, the warning reaches the collector current when the generator is resumed, i.e. the one 

340 of the code consuming it. An exception raised by the collector's handler propagates. 

341 

342 :param warning: Warning to send to the current warning collector. 

343 :param cause: Optional, root cause to be added to the warning (``__cause__``). 

344 :param notes: Optional, a single note or a list of notes to be added to the warning. 

345 :raises ValueError: If parameter ``warning`` is None and a warning collector is current. 

346 :raises TypeError: If parameter ``warning`` is not of type :class:`Warning`, 

347 :class:`CriticalWarning` or :exc:`Exception` and a warning collector is 

348 current. 

349 :raises EscalatedWarningError: If the warning collector's handler asks for the warning to be escalated. 

350 :raises UnhandledExceptionError: If no warning collector was found along the call-hierarchy to collect and 

351 handle an exception. 

352 :raises UnhandledCriticalWarningError: If no warning collector was found along the call-hierarchy to collect 

353 and handle a critical warning. |br| 

354 Add a with-statement using :class:`WarningCollector` somewhere up the 

355 call-hierarchy to receive and collect warnings. 

356 """ 

357 global _threadLocalData 

358 

359 if cause is not None: 

360 warning.__cause__ = cause 

361 

362 if notes is not None: 

363 if isinstance(notes, str): 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true

364 warning.add_note(notes) 

365 else: 

366 for note in notes: 

367 warning.add_note(note) 

368 

369 if (warningCollector := getattr(_threadLocalData, "warningCollector", None)) is None: 

370 ex = None 

371 if isinstance(warning, Exception): 

372 ex = UnhandledExceptionError(f"Unhandled Exception: {warning}") 

373 elif isinstance(warning, CriticalWarning): 

374 ex = UnhandledCriticalWarningError(f"Unhandled Critical Warning: {warning}") 

375 

376 if ex is not None: 

377 ex.add_note(f"Add a 'with'-statement using '{cls.__name__}' somewhere up the call-hierarchy to receive and collect warnings.") 

378 raise ex from warning 

379 elif warningCollector.AddWarning(warning): 

380 raise EscalatedWarningError(f"Warning: {warning}") from warning 

381 

382 

383@export 

384class SupervisedWarningCollectorError(ExceptionBase): 

385 """ 

386 This exception is raised when a supervised warning collector is not the top-most collector in its thread. 

387 

388 A supervised collector hands its warnings to the thread supervisor, which only works if nothing else collects them 

389 first. 

390 """ 

391 

392 

393@export 

394class SupervisedWarningCollector(WarningCollector): 

395 """ 

396 A context manager to collect the warnings of a thread and hand them to a :class:`ThreadSupervisor`. 

397 

398 It's the top-most warning collector of its thread. On leaving the context, the collected warnings and an exception 

399 leaving the block are handed to the supervisor, so the thread that started this one can read the warnings and 

400 re-raise the exception (:meth:`ThreadSupervisor.ReRaise`). 

401 """ 

402 _supervisor: Nullable[ThreadSupervisor] #: Supervisor collecting warnings and exceptions of all threads. 

403 _exceptionHandler: Nullable[Callable[[BaseException], bool]] #: Handler called for an exception escaping the thread. 

404 _finallyHandler: Nullable[Callable[[], None]] #: Handler called when the thread ends, in either case. 

405 

406 __slots__ = ("_supervisor", "_exceptionHandler", "_finallyHandler") 

407 

408 def __init__( 

409 self, 

410 warnings: Nullable[list[BaseException]] = None, 

411 handler: Nullable[Callable[[BaseException], bool]] = None, 

412 /, 

413 supervisor: Nullable[ThreadSupervisor] = None, 

414 exceptionHandler: Nullable[Callable[[BaseException], bool]] = None, 

415 finallyHandler: Nullable[Callable[[], None]] = None 

416 ) -> None: 

417 """ 

418 Initializes a warning collector. 

419 

420 :param warnings: Optional, reference to a list of warnings, which can be modified (appended) by this warning 

421 collector. If ``None``, an internal list is created and can be referenced by the 

422 collector's instance. 

423 :param handler: Optional, handler function, which is called with each collected warning and decides, if 

424 it's escalated. If it returns ``True``, :meth:`Raise` raises an 

425 :exc:`EscalatedWarningError`, whose cause is the warning. 

426 :param supervisor: Optional, thread supervisor. On leaving the context, the collected warnings and an 

427 exception leaving the block are handed to it, so the thread that started this one can 

428 reraise them. Without a supervisor, an exception leaves the block unchanged. 

429 :param exceptionHandler: Optional, handler function, called with an exception leaving the block when a supervisor is 

430 set. Its result decides whether the exception is suppressed. 

431 :param finallyHandler: Optional, function called when the context is left, whether or not an exception left it. 

432 :raises TypeError: If optional parameter 'warnings' is not of type list. 

433 :raises TypeError: If optional parameter 'handler' is not a callable. 

434 """ 

435 super().__init__(warnings, handler) 

436 

437 self._supervisor = supervisor 

438 self._exceptionHandler = exceptionHandler 

439 self._finallyHandler = finallyHandler 

440 

441 def __enter__(self) -> Self: 

442 """ 

443 Enter the context and make this warning collector the current one of its thread. 

444 

445 :returns: The warning collector instance. 

446 :raises SupervisedWarningCollectorError: If this collector is not the top-most warning collector of its thread. 

447 """ 

448 global _threadLocalData 

449 

450 if hasattr(_threadLocalData, "warningCollector") and _threadLocalData.warningCollector is not None: 450 ↛ 451line 450 didn't jump to line 451 because the condition on line 450 was never true

451 raise SupervisedWarningCollectorError("This warning collector is not the top-most warning collector within the current thread.") 

452 

453 _threadLocalData.warningCollector = self 

454 

455 return self 

456 

457 def __exit__( 

458 self, 

459 exc_type: Nullable[type[BaseException]] = None, 

460 exc_val: Nullable[BaseException] = None, 

461 exc_tb: Nullable[TracebackType] = None 

462 ) -> Nullable[bool]: 

463 """ 

464 Exit the context and hand the collected warnings and a raised exception to the supervisor. 

465 

466 :param exc_type: Optional, exception type. 

467 :param exc_val: Optional, exception instance. 

468 :param exc_tb: Optional, exception's traceback. 

469 :returns: ``True`` to suppress a raised exception, if a supervisor is set; the exception handler's result 

470 instead, if it handled a raised exception; ``None`` without a supervisor. 

471 """ 

472 global _threadLocalData 

473 

474 _threadLocalData.warningCollector = None 

475 

476 if self._supervisor is not None: 476 ↛ 487line 476 didn't jump to line 487 because the condition on line 476 was always true

477 result = True 

478 if len(self._warnings) > 0: 

479 self._supervisor.AddWarnings(current_thread().name, self._warnings) 

480 

481 if exc_val is not None: 

482 self._supervisor.AddException(current_thread().name, exc_val) 

483 

484 if self._exceptionHandler is not None: 484 ↛ 489line 484 didn't jump to line 489 because the condition on line 484 was always true

485 result = self._exceptionHandler(exc_val) 

486 else: 

487 result = None 

488 

489 if self._finallyHandler is not None: 489 ↛ 490line 489 didn't jump to line 490 because the condition on line 489 was never true

490 self._finallyHandler() 

491 

492 return result 

493 

494 

495@export 

496class SupervisedThreadError(ExceptionBase): 

497 """ 

498 This exception is raised by :meth:`ThreadSupervisor.ReRaise`, if a supervised thread failed with an exception. 

499 

500 A :class:`SupervisedWarningCollector` hands the exception leaving its thread to the :class:`ThreadSupervisor`; it 

501 becomes this exception's cause, and :attr:`~SupervisedThreadError.ThreadName` names the thread. 

502 """ 

503 _threadName: str #: Name of the thread the exception was raised in. 

504 

505 def __init__( 

506 self, 

507 message: str, 

508 /, 

509 *, 

510 threadName: str, 

511 cause: Nullable[BaseException] = None 

512 ) -> None: 

513 """ 

514 Initializes the exception with the name of the thread that failed. 

515 

516 :param message: The exception's message. 

517 :param threadName: Name of the thread that raised the collected exception. 

518 :param cause: Optional, the exception collected from that thread. 

519 """ 

520 super().__init__(message) 

521 self._threadName = threadName 

522 self.__cause__ = cause 

523 

524 @readonly 

525 def ThreadName(self) -> str: 

526 """ 

527 Read-only property to access the name of the thread that raised the exception (:attr:`_threadName`). 

528 

529 :returns: Name of the thread. 

530 """ 

531 return self._threadName 

532 

533 

534@export 

535class ThreadSupervisor: 

536 """ 

537 Thread-safe collector of exceptions and warnings raised in worker threads for surfacing on another thread. 

538 

539 This thread supervisor should be used in combination with :class:`SupervisedWarningCollector` to accumulate 

540 exceptions (:class:`BaseException`) and warnings (:class:`CriticalWarning` or :class:`Warning`). 

541 

542 .. code-block:: python 

543 

544 @export 

545 class MyThread(Thread): 

546 def __init__( 

547 self, 

548 threadSupervisor: ThreadSupervisor, 

549 stopEvent: Event 

550 ) -> None: 

551 super().__init__(name="MyThread", daemon=True) 

552 

553 self._threadSupervisor = threadSupervisor 

554 self._stopEvent = stopEvent 

555 

556 def run(self) -> None: 

557 def exceptionHandler(ex: BaseException) -> bool: 

558 self._stopEvent.set() 

559 return True 

560 

561 def finallyHandler() -> None: 

562 ... # some finally code 

563 

564 with SupervisedWarningCollector( 

565 supervisor=self._threadSupervisor, 

566 exceptionHandler=exceptionHandler, 

567 finallyHandler=finallyHandler 

568 ) as warnings: 

569 ... # thread body 

570 

571 .. code-block:: python 

572 

573 def RunVivadoPipeline( 

574 self, 

575 ) -> list[AnyWarning]: 

576 stopEvent = Event() 

577 threadSupervisor = ThreadSupervisor() 

578 

579 myThread = MyThread(threadSupervisor, stopEvent) 

580 myThread.start() 

581 

582 try: 

583 myThread.join() 

584 except KeyboardInterrupt: 

585 stopEvent.set() 

586 myThread.join(timeout=2.0) 

587 raise 

588 

589 threadSupervisor.ReRaise() 

590 

591 return threadSupervisor.Warnings 

592 """ 

593 

594 _lock: Lock #: Lock serializing the collection from multiple threads. 

595 _exceptions: list[tuple[str, BaseException]] #: Exceptions of all supervised threads, as (thread name, exception). 

596 _warnings: list[tuple[str, AnyWarning]] #: Warnings of all supervised threads, as (thread name, warning). 

597 

598 __slots__ = ("_lock", "_exceptions", "_warnings") 

599 

600 def __init__(self) -> None: 

601 """ 

602 Initialize a thread supervisor with empty lists of exceptions and warnings. 

603 """ 

604 self._lock = Lock() 

605 self._exceptions = [] 

606 self._warnings = [] 

607 

608 @readonly 

609 def HasWarning(self) -> bool: 

610 """ 

611 Check if at least one warning was collected from a supervised thread. 

612 

613 :returns: ``True``, if at least one warning was collected. 

614 """ 

615 with self._lock: 

616 return len(self._warnings) > 0 

617 

618 @readonly 

619 def HasExceptions(self) -> bool: 

620 """ 

621 Check if at least one exception was collected from a supervised thread. 

622 

623 :returns: ``True``, if at least one exception was collected. 

624 """ 

625 with self._lock: 

626 return len(self._exceptions) > 0 

627 

628 @readonly 

629 def Warnings(self) -> list[AnyWarning]: 

630 """ 

631 Read-only property to return all warnings collected from supervised threads (:attr:`_warnings`). 

632 

633 :returns: List of collected warnings, without their thread names. 

634 """ 

635 with self._lock: 

636 return [warning for _, warning in self._warnings] 

637 

638 def AddWarning(self, threadName: str, warning: AnyWarning) -> None: 

639 """ 

640 Collect a warning raised in a supervised thread. 

641 

642 :param threadName: Name of the thread the warning was raised in. 

643 :param warning: The warning to collect. 

644 """ 

645 with self._lock: 

646 self._warnings.append((threadName, warning)) 

647 

648 def AddWarnings(self, threadName: str, warnings: list[AnyWarning]) -> None: 

649 """ 

650 Collect several warnings raised in a supervised thread. 

651 

652 :param threadName: Name of the thread the warnings were raised in. 

653 :param warnings: The warnings to collect. 

654 """ 

655 with self._lock: 

656 self._warnings.extend((threadName, warning) for warning in warnings) 

657 

658 def AddException(self, threadName: str, ex: BaseException) -> None: 

659 """ 

660 Collect an exception that escaped a supervised thread. 

661 

662 :param threadName: Name of the thread the exception was raised in. 

663 :param ex: The exception to collect. 

664 """ 

665 with self._lock: 

666 self._exceptions.append((threadName, ex)) 

667 

668 def ReRaise(self, unwrapped: bool = False) -> None: 

669 """ 

670 Re-raise the exceptions collected from the supervised threads in the supervising thread. 

671 

672 A single exception is re-raised as itself - wrapped in a :exc:`SupervisedThreadError` naming its thread, 

673 unless ``unwrapped`` is set. Several exceptions are raised as an :exc:`ExceptionGroup`, so none of them is lost. 

674 

675 :param unwrapped: Optional, if ``True``, a single exception is raised as it was, without naming its 

676 thread. 

677 :raises SupervisedThreadError: If exactly one thread failed. 

678 :raises ExceptionGroup: If more than one thread failed. 

679 """ 

680 with self._lock: 

681 if len(self._exceptions) == 0: 

682 return 

683 

684 exceptions = list(self._exceptions) 

685 

686 if len(exceptions) == 1: 

687 threadName, ex = exceptions[0] 

688 if unwrapped: 

689 raise ex 

690 else: 

691 raise SupervisedThreadError(f"Thread '{threadName}' failed.", threadName=threadName) from ex 

692 

693 elif unwrapped: 

694 raise ExceptionGroup( 

695 "Multiple threads failed.", 

696 [ex for _, ex in exceptions] 

697 ) 

698 else: 

699 raise ExceptionGroup( 

700 "Multiple threads failed.", 

701 [ 

702 SupervisedThreadError(f"Thread '{threadName}' failed.", threadName=threadName, cause=ex) 

703 for threadName, ex in exceptions 

704 ] 

705 )